38
deploy.py
38
deploy.py
@@ -22,11 +22,8 @@ from __future__ import print_function
|
|||||||
import argparse
|
import argparse
|
||||||
import collections
|
import collections
|
||||||
import copy
|
import copy
|
||||||
import datetime
|
|
||||||
import hashlib
|
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import struct
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from typing import Dict, List, Tuple
|
from typing import Dict, List, Tuple
|
||||||
@@ -39,6 +36,8 @@ from tockloader import tbfh
|
|||||||
from tockloader import tockloader as loader
|
from tockloader import tockloader as loader
|
||||||
from tockloader.exceptions import TockLoaderException
|
from tockloader.exceptions import TockLoaderException
|
||||||
|
|
||||||
|
from tools.deploy_partition import create_metadata, pad_to
|
||||||
|
|
||||||
PROGRAMMERS = frozenset(("jlink", "openocd", "pyocd", "nordicdfu", "none"))
|
PROGRAMMERS = frozenset(("jlink", "openocd", "pyocd", "nordicdfu", "none"))
|
||||||
|
|
||||||
# This structure allows us to support out-of-tree boards as well as (in the
|
# 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}"))
|
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("<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 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
|
|
||||||
|
|
||||||
|
|
||||||
class RemoveConstAction(argparse.Action):
|
class RemoveConstAction(argparse.Action):
|
||||||
|
|
||||||
# pylint: disable=redefined-builtin
|
# pylint: disable=redefined-builtin
|
||||||
|
|||||||
244
tools/deploy_partition.py
Executable file
244
tools/deploy_partition.py
Executable 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
31
tools/perform_upgrade.sh
Executable 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
|
||||||
Reference in New Issue
Block a user