Add configuration tool

This commit is contained in:
Jean-Michel Picod
2020-12-10 16:17:09 +01:00
parent 3c93c8ddc6
commit e35c41578e
4 changed files with 244 additions and 0 deletions

1
.gitignore vendored
View File

@@ -11,3 +11,4 @@ Cargo.lock
/reproducible/binaries.sha256sum
/reproducible/elf2tab.txt
/reproducible/reproduced.tar
__pycache__

View File

@@ -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",

View File

@@ -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

197
tools/configure.py Executable file
View File

@@ -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())