Upgrade tooling (#400)

* upgrade tooling

* adds condition on nrfjprog
This commit is contained in:
kaczmarczyck
2021-11-04 16:31:37 +01:00
committed by GitHub
parent 33e0d6bb74
commit 32da73772f
3 changed files with 277 additions and 36 deletions

244
tools/deploy_partition.py Executable file
View File

@@ -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("<I", int(t))
partition_start = struct.pack("<I", partition_address)
sha256_hash = hashlib.sha256()
sha256_hash.update(firmware_image)
sha256_hash.update(timestamp)
sha256_hash.update(partition_start)
checksum = sha256_hash.digest()
return checksum + timestamp + partition_start
def hash_message(message: bytes) -> 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())

31
tools/perform_upgrade.sh Executable file
View File

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