Skip to content

Commit

Permalink
Add configuration tool
Browse files Browse the repository at this point in the history
  • Loading branch information
jmichelp committed Dec 16, 2020
1 parent 3c93c8d commit e35c415
Show file tree
Hide file tree
Showing 4 changed files with 244 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ Cargo.lock
/reproducible/binaries.sha256sum
/reproducible/elf2tab.txt
/reproducible/reproduced.tar
__pycache__
43 changes: 43 additions & 0 deletions deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,22 @@ def run(self):
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


Expand Down Expand Up @@ -770,6 +786,33 @@ def main(args):
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",
Expand Down
3 changes: 3 additions & 0 deletions setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 197 additions & 0 deletions tools/configure.py
Original file line number Diff line number Diff line change
@@ -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())

0 comments on commit e35c415

Please sign in to comment.