diff --git a/.gitignore b/.gitignore index 1b32046..611b278 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ Cargo.lock /reproducible/binaries.sha256sum /reproducible/elf2tab.txt /reproducible/reproduced.tar +__pycache__ diff --git a/deploy.py b/deploy.py index 42579a2..b3daa6f 100755 --- a/deploy.py +++ b/deploy.py @@ -710,6 +710,22 @@ class OpenSKInstaller: check=False, timeout=None, ).returncode + + # Configure OpenSK through vendor specific command if needed + if any([ + self.args.lock_device, + self.args.config_cert, + self.args.config_pkey, + ]): + # pylint: disable=g-import-not-at-top,import-outside-toplevel + import tools.configure + tools.configure.main( + argparse.Namespace( + batch=False, + certificate=self.args.config_cert, + priv_key=self.args.config_pkey, + lock=self.args.lock_device, + )) return 0 @@ -770,6 +786,33 @@ if __name__ == "__main__": help=("Erases the persistent storage when installing an application. " "All stored data will be permanently lost."), ) + main_parser.add_argument( + "--lock-device", + action="store_true", + default=False, + dest="lock_device", + help=("Try to disable JTAG at the end of the operations. This " + "operation may fail if the device is already locked or if " + "the certificate/private key are not programmed."), + ) + main_parser.add_argument( + "--inject-certificate", + default=None, + metavar="PEM_FILE", + type=argparse.FileType("rb"), + dest="config_cert", + help=("If this option is set, the corresponding certificate " + "will be programmed into the key as the last operation."), + ) + main_parser.add_argument( + "--inject-private-key", + default=None, + metavar="PEM_FILE", + type=argparse.FileType("rb"), + dest="config_pkey", + help=("If this option is set, the corresponding private key " + "will be programmed into the key as the last operation."), + ) main_parser.add_argument( "--programmer", metavar="METHOD", diff --git a/setup.sh b/setup.sh index 903e0e3..13ad9b0 100755 --- a/setup.sh +++ b/setup.sh @@ -44,3 +44,6 @@ rustup target add thumbv7em-none-eabi # Install dependency to create applications. mkdir -p elf2tab cargo install elf2tab --version 0.6.0 --root elf2tab/ + +# Install python dependencies to factory configure OpenSK (crypto, JTAG lockdown) +pip3 install --user --upgrade colorama tqdm cryptography fido2 diff --git a/tools/configure.py b/tools/configure.py new file mode 100755 index 0000000..eff1166 --- /dev/null +++ b/tools/configure.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Lint as: python3 + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import argparse +import getpass +import datetime +import sys +import uuid + +import colorama +from tqdm.auto import tqdm + +from cryptography import x509 +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import ec + +from fido2 import ctap +from fido2 import ctap2 +from fido2 import hid + +OPENSK_VID_PID = (0x1915, 0x521F) +OPENSK_VENDOR_CONFIGURE = 0x40 + + +def fatal(msg): + tqdm.write("{style_begin}fatal:{style_end} {message}".format( + style_begin=colorama.Fore.RED + colorama.Style.BRIGHT, + style_end=colorama.Style.RESET_ALL, + message=msg)) + sys.exit(1) + + +def error(msg): + tqdm.write("{style_begin}error:{style_end} {message}".format( + style_begin=colorama.Fore.RED, + style_end=colorama.Style.RESET_ALL, + message=msg)) + + +def info(msg): + tqdm.write("{style_begin}info:{style_end} {message}".format( + style_begin=colorama.Fore.GREEN + colorama.Style.BRIGHT, + style_end=colorama.Style.RESET_ALL, + message=msg)) + + +def get_opensk_devices(batch_mode): + devices = [] + for dev in hid.CtapHidDevice.list_devices(): + if (dev.descriptor["vendor_id"], + dev.descriptor["product_id"]) == OPENSK_VID_PID: + if dev.capabilities & hid.CAPABILITY.CBOR: + if batch_mode: + devices.append(ctap2.CTAP2(dev)) + else: + return [ctap2.CTAP2(dev)] + return devices + + +def get_private_key(data, password=None): + # First we try without password + try: + return serialization.load_pem_private_key(data, password=None) + except TypeError: + # Maybe we need a password then + if sys.stdin.isatty(): + password = getpass.getpass(prompt="Private key password: ") + else: + password = sys.stdin.readline().rstrip() + return get_private_key(data, password=password.encode(sys.stdin.encoding)) + + +def main(args): + colorama.init() + # We need either both the certificate and the key or none + if bool(args.priv_key) ^ bool(args.certificate): + fatal("Certificate and private key must be set together or both omitted.") + + cbor_data = {1: args.lock} + + if args.priv_key: + cbor_data[1] = args.lock + priv_key = get_private_key(args.priv_key.read()) + if not isinstance(priv_key, ec.EllipticCurvePrivateKey): + fatal("Private key must be an Elliptic Curve one.") + if not isinstance(priv_key.curve, ec.SECP256R1): + fatal("Private key must use Secp256r1 curve.") + if priv_key.key_size != 256: + fatal("Private key must be 256 bits long.") + info("Private key is valid.") + + cert = x509.load_pem_x509_certificate(args.certificate.read()) + # Some sanity/validity checks + now = datetime.datetime.now() + if cert.not_valid_before > now: + fatal("Certificate validity starts in the future.") + if cert.not_valid_after <= now: + fatal("Certificate expired.") + pub_key = cert.public_key() + if not isinstance(pub_key, ec.EllipticCurvePublicKey): + fatal("Certificate public key must be an Elliptic Curve one.") + if not isinstance(pub_key.curve, ec.SECP256R1): + fatal("Certificate public key must use Secp256r1 curve.") + if pub_key.key_size != 256: + fatal("Certificate public key must be 256 bits long.") + if pub_key.public_numbers() != priv_key.public_key().public_numbers(): + fatal("Certificate public doesn't match with the private key.") + info("Certificate is valid.") + + cbor_data[2] = { + 1: + cert.public_bytes(serialization.Encoding.DER), + 2: + priv_key.private_numbers().private_value.to_bytes( + length=32, byteorder='big', signed=False) + } + + for authenticator in tqdm(get_opensk_devices(args.batch)): + # If the device supports it, wink to show which device + # we're going to program + if authenticator.device.capabilities & hid.CAPABILITY.WINK: + authenticator.device.wink() + aaguid = uuid.UUID(bytes=authenticator.get_info().aaguid) + info(("Programming device {} AAGUID {} ({}). " + "Please touch the device to confirm...").format( + authenticator.device.descriptor.get("product_string", "Unknown"), + aaguid, authenticator.device)) + try: + result = authenticator.send_cbor( + OPENSK_VENDOR_CONFIGURE, + data=cbor_data, + ) + info("Certificate: {}".format("Present" if result[1] else "Missing")) + info("Private Key: {}".format("Present" if result[2] else "Missing")) + if result[3]: + info("Device locked down!") + except ctap.CtapError as ex: + if ex.code.value == ctap.CtapError.ERR.INVALID_COMMAND: + error("Failed to configure OpenSK (unsupported command).") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "--batch", + default=False, + action="store_true", + help=( + "When batch processing is used, all plugged OpenSK devices will " + "be programmed the same way. Otherwise (default) only the first seen " + "device will be programmed."), + ) + parser.add_argument( + "--certificate", + type=argparse.FileType("rb"), + default=None, + metavar="PEM_FILE", + dest="certificate", + help=("PEM file containing the certificate to inject into " + "OpenSK authenticator."), + ) + parser.add_argument( + "--private-key", + type=argparse.FileType("rb"), + default=None, + metavar="PEM_FILE", + dest="priv_key", + help=("PEM file containing the private key associated " + "with the certificate."), + ) + parser.add_argument( + "--lock-device", + default=False, + action="store_true", + dest="lock", + help=("Locks the device (i.e. bootloader and JTAG access). " + "This command can fail if the certificate or the private key " + "haven't been both programmed yet."), + ) + main(parser.parse_args())