From 32da73772f0041010698a4ab869eec32f0038729 Mon Sep 17 00:00:00 2001 From: kaczmarczyck <43844792+kaczmarczyck@users.noreply.github.com> Date: Thu, 4 Nov 2021 16:31:37 +0100 Subject: [PATCH] Upgrade tooling (#400) * upgrade tooling * adds condition on nrfjprog --- deploy.py | 38 +----- tools/deploy_partition.py | 244 ++++++++++++++++++++++++++++++++++++++ tools/perform_upgrade.sh | 31 +++++ 3 files changed, 277 insertions(+), 36 deletions(-) create mode 100755 tools/deploy_partition.py create mode 100755 tools/perform_upgrade.sh diff --git a/deploy.py b/deploy.py index 6972d1c..5aa8341 100755 --- a/deploy.py +++ b/deploy.py @@ -22,11 +22,8 @@ from __future__ import print_function import argparse import collections import copy -import datetime -import hashlib import os import shutil -import struct import subprocess import sys from typing import Dict, List, Tuple @@ -39,6 +36,8 @@ from tockloader import tbfh from tockloader import tockloader as loader from tockloader.exceptions import TockLoaderException +from tools.deploy_partition import create_metadata, pad_to + PROGRAMMERS = frozenset(("jlink", "openocd", "pyocd", "nordicdfu", "none")) # This structure allows us to support out-of-tree boards as well as (in the @@ -199,39 +198,6 @@ def assert_python_library(module: str): f"Try to run: pip3 install {module}")) -def create_metadata(firmware_image: bytes, partition_address: int) -> bytes: - """Creates the matching metadata for the given firmware. - - The metadata consists of a timestamp, the expected address and a hash of - the image and the other properties in this metadata. - - Args: - firmware_image: A byte array of kernel and app, padding to full length. - partition_address: The address to be written as a metadata property. - - Returns: - A byte array consisting of 32B hash, 4B timestamp and 4B partition address - in little endian encoding. - """ - t = datetime.datetime.utcnow().timestamp() - timestamp = struct.pack(" bytes: - """Extends the given binary to the new length with a 0xFF padding.""" - if len(binary) > length: - fatal(f"Binary size {len(binary)} exceeds flash partition {length}.") - padding = bytes([0xFF] * (length - len(binary))) - return binary + padding - - class RemoveConstAction(argparse.Action): # pylint: disable=redefined-builtin diff --git a/tools/deploy_partition.py b/tools/deploy_partition.py new file mode 100755 index 0000000..8ed6312 --- /dev/null +++ b/tools/deploy_partition.py @@ -0,0 +1,244 @@ +#!/usr/bin/env python3 +# Copyright 2020-2021 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 datetime +import hashlib +import os +import struct +from typing import Any +import uuid + +import colorama +from tqdm.auto import tqdm + +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature + +from fido2 import ctap +from fido2 import hid +from tockloader import tab +from tools.configure import fatal, error, info, get_opensk_devices, get_private_key + +OPENSK_VID_PID = (0x1915, 0x521F) +OPENSK_VENDOR_UPGRADE = 0x41 +OPENSK_VENDOR_UPGRADE_INFO = 0x42 +PAGE_SIZE = 0x1000 +KERNEL_SIZE = 0x20000 +APP_SIZE = 0x20000 +PARTITION_ADDRESS = { + "nrf52840dk_opensk_a": 0x20000, + "nrf52840dk_opensk_b": 0x60000, +} +ES256_ALGORITHM = -7 +ARCH = "thumbv7em-none-eabi" + + +def create_metadata(firmware_image: bytes, partition_address: int) -> bytes: + """Creates the matching metadata for the given firmware. + + The metadata consists of a timestamp, the expected address and a hash of + the image and the other properties in this metadata. + + Args: + firmware_image: A byte array of kernel and app, padding to full length. + partition_address: The address to be written as a metadata property. + + Returns: + A byte array consisting of 32B hash, 4B timestamp and 4B partition address + in little endian encoding. + """ + t = datetime.datetime.utcnow().timestamp() + timestamp = struct.pack(" bytes: + """Uses SHA256 to hash a message.""" + sha256_hash = hashlib.sha256() + sha256_hash.update(message) + return sha256_hash.digest() + + +def check_info(partition_address: int, authenticator: Any): + """Checks if the assumed upgrade info matches the authenticator's.""" + try: + info("Reading upgrade info...") + result = authenticator.send_cbor( + OPENSK_VENDOR_UPGRADE_INFO, + data={}, + ) + if result[0x01] != partition_address: + fatal("Identifiers do not match.") + except ctap.CtapError as ex: + error(f"Failed to read OpenSK upgrade info (error: {ex}") + + +def get_kernel(board: str) -> bytes: + """Reads the kernel binary from file.""" + kernel_file = f"third_party/tock/target/{ARCH}/release/{board}.bin" + if not os.path.exists(kernel_file): + fatal(f"File not found: {kernel_file}") + with open(kernel_file, "rb") as firmware: + binary = firmware.read() + return binary + + +def get_app(board: str) -> bytes: + """Reads the app binary for the given board from a TAB file.""" + app_tab_path = "target/tab/ctap2.tab" + if not os.path.exists(app_tab_path): + fatal(f"File not found: {app_tab_path}") + app_tab = tab.TAB(app_tab_path) + if ARCH not in app_tab.get_supported_architectures(): + fatal(f"Architecture not found: {ARCH}") + app_address = PARTITION_ADDRESS[board] + KERNEL_SIZE + return app_tab.extract_app(ARCH).get_binary(app_address) + + +def pad_to(binary: bytes, length: int) -> bytes: + """Extends the given binary to the new length with a 0xFF padding.""" + if len(binary) > length: + fatal(f"Binary size {len(binary)} exceeds flash partition {length}.") + padding = bytes([0xFF] * (length - len(binary))) + return binary + padding + + +def generate_firmware_image(board: str) -> bytes: + """Creates binaries for kernel and app, to generate a full firmware image.""" + kernel = get_kernel(board) + app = get_app(board) + return pad_to(kernel, KERNEL_SIZE) + pad_to(app, APP_SIZE) + + +def load_priv_key(priv_key_file: argparse.FileType) -> Any: + """Loads the ECDSA private key from the specified file.""" + priv_key = get_private_key(priv_key_file.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.") + return priv_key + + +def sign_firmware(data: bytes, priv_key: Any) -> bytes: + """Signs the data with the passed key and returns the signature bytes.""" + signature_der = priv_key.sign(data, ec.ECDSA(hashes.SHA256())) + (r, s) = decode_dss_signature(signature_der) + return r.to_bytes(32, "big") + s.to_bytes(32, "big") + + +def main(args): + colorama.init() + + firmware_image = generate_firmware_image(args.board) + partition_address = PARTITION_ADDRESS[args.board] + metadata = create_metadata(firmware_image, partition_address) + + if not args.priv_key: + fatal("Please pass in a private key file using --private-key.") + priv_key = load_priv_key(args.priv_key) + signed_data = firmware_image + metadata[32:40] + signature = { + "alg": ES256_ALGORITHM, + "signature": sign_firmware(signed_data, priv_key) + } + + for authenticator in tqdm(get_opensk_devices(args.batch)): + # If the device supports it, wink to show which device we upgrade. + if authenticator.device.capabilities & hid.CAPABILITY.WINK: + authenticator.device.wink() + aaguid = uuid.UUID(bytes=authenticator.get_info().aaguid) + info(f"Upgrading OpenSK device AAGUID {aaguid} ({authenticator.device}).") + + try: + check_info(partition_address, authenticator) + offset = 0 + for offset in range(0, len(firmware_image), PAGE_SIZE): + page = firmware_image[offset:][:PAGE_SIZE] + info(f"Writing at offset 0x{offset:08X}...") + cbor_data = {1: offset, 2: page, 3: hash_message(page)} + authenticator.send_cbor( + OPENSK_VENDOR_UPGRADE, + data=cbor_data, + ) + + info("Writing metadata...") + cbor_data = {2: metadata, 3: hash_message(metadata), 4: signature} + authenticator.send_cbor( + OPENSK_VENDOR_UPGRADE, + data=cbor_data, + ) + except ctap.CtapError as ex: + message = "Failed to upgrade OpenSK" + if ex.code.value == ctap.CtapError.ERR.INVALID_COMMAND: + error(f"{message} (unsupported command).") + elif ex.code.value == ctap.CtapError.ERR.INVALID_PARAMETER: + error(f"{message} (invalid parameter, maybe a wrong byte array size?).") + elif ex.code.value == ctap.CtapError.ERR_INTEGRITY_FAILURE: + error(f"{message} (hashes or signature don't match).") + elif ex.code.value == 0xF2: # VENDOR_INTERNAL_ERROR + error(f"{message} (internal conditions not met).") + elif ex.code.value == 0xF3: # VENDOR_HARDWARE_FAILURE + error(f"{message} (internal hardware error).") + else: + error(f"{message} (unexpected error: {ex}") + + +if __name__ == "__main__": + # Make sure the current working directory is the right one before running + os.chdir(os.path.realpath(os.path.dirname(__file__))) + os.chdir("..") + + 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( + "--board", + type=str, + choices=["nrf52840dk_opensk_a", "nrf52840dk_opensk_b"], + dest="board", + help=("Binary file containing the compiled firmware."), + ) + parser.add_argument( + "--private-key", + type=argparse.FileType("rb"), + default="crypto_data/opensk_upgrade.key", + dest="priv_key", + help=("PEM file for signing the firmware."), + ) + main(parser.parse_args()) diff --git a/tools/perform_upgrade.sh b/tools/perform_upgrade.sh new file mode 100755 index 0000000..dbe99d6 --- /dev/null +++ b/tools/perform_upgrade.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# Copyright 2021 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. + +# Creates a signature key and configures the public key. +# The device will not be locked down for testing purposes. +# Generates the binary and upgrades OpenSK. +# To be run from the OpenSK base path. + +set -e + +BOARD="$1" + +./deploy.py --board=$BOARD --opensk --programmer=none +python3 -m tools.deploy_partition --board=$BOARD +if nrfjprog --reset --family NRF52 ; then + echo "Upgrade finished!" +else + echo "Please replug OpenSK to reboot" +fi