Files
OpenSK/tools/configure.py
kaczmarczyck 778102712f venv for Python setup (#653)
* Python uses venv

* Small fixes to python calls
2023-11-07 05:13:21 +01:00

222 lines
7.3 KiB
Python
Executable File

#!py_virtual_env/bin/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
"""Tools that implements vendor-specific CTAP2 commands to configure OpenSK."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import argparse
import getpass
import datetime
import sys
from unittest.mock import patch
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(f"{colorama.Fore.RED + colorama.Style.BRIGHT}fatal:"
f"{colorama.Style.RESET_ALL} {msg}")
sys.exit(1)
def error(msg):
tqdm.write(f"{colorama.Fore.RED}error:{colorama.Style.RESET_ALL} {msg}")
def info(msg):
tqdm.write(f"{colorama.Fore.GREEN + colorama.Style.BRIGHT}info:"
f"{colorama.Style.RESET_ALL} {msg}")
def get_opensk_devices(batch_mode):
devices = []
for dev in hid.CtapHidDevice.list_devices():
if (dev.descriptor.vid, dev.descriptor.pid) == 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.utcnow()
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)
}
patcher = None
if args.use_vendor_hid:
patcher = patch.object(hid.base, "FIDO_USAGE_PAGE", 0xFF00)
patcher.start()
info("Using the Vendor HID interface")
devices = get_opensk_devices(args.batch)
if patcher:
patcher.stop()
responses = []
if not devices:
fatal("No devices found.")
for authenticator in tqdm(devices):
# 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(f"Programming OpenSK device AAGUID {aaguid} ({authenticator.device}).")
if args.lock or args.priv_key:
info("Please touch the device to confirm...")
try:
result = authenticator.send_cbor(
OPENSK_VENDOR_CONFIGURE,
data=cbor_data,
)
status = {"cert": result[1], "pkey": result[2]}
responses.append(status)
info(f"Certificate: {'Present' if result[1] else 'Missing'}")
info(f"Private Key: {'Present' if result[2] else 'Missing'}")
if args.lock:
info("Device is now locked down!")
except ctap.CtapError as ex:
if ex.code.value == ctap.CtapError.ERR.INVALID_COMMAND:
error("Failed to configure OpenSK (unsupported command).")
elif ex.code.value == 0xF2: # VENDOR_INTERNAL_ERROR
error(("Failed to configure OpenSK (lockdown conditions not met "
"or hardware error)."))
elif ex.code.value == ctap.CtapError.ERR.INVALID_PARAMETER:
error(
("Failed to configure OpenSK (device is partially programmed but "
"the given cert/key don't match the ones currently programmed)."))
else:
error(f"Failed to configure OpenSK (unknown error: {ex})")
return responses
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 "
"the 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."),
)
parser.add_argument(
"--vendor-hid",
default=False,
action="store_true",
dest="use_vendor_hid",
help=("Whether to configure the device using the Vendor HID interface."),
)
main(parser.parse_args())