#!/usr/bin/env python3 # Copyright 2019 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 # pylint: disable=C0111 from __future__ import absolute_import from __future__ import division from __future__ import print_function import argparse import collections import copy import os import shutil import subprocess import sys import colorama from six.moves import input import tockloader from tockloader import tab from tockloader import tbfh from tockloader import tockloader as loader from tockloader.exceptions import TockLoaderException PROGRAMMERS = frozenset(("jlink", "openocd", "pyocd", "nordicdfu", "none")) # This structure allows us to support out-of-tree boards as well as (in the # future) more achitectures. OpenSKBoard = collections.namedtuple( "OpenSKBoard", [ # Location of the Tock board (where the Makefile file is) "path", # Target architecture (e.g. thumbv7em-none-eabi) "arch", # Size of 1 page of flash memory "page_size", # Flash address at which the kernel will be written "kernel_address", # Set to None is padding is not required for the board. # This creates a fake Tock OS application that starts at the # address specified by this parameter (must match the `prog` value # specified on the board's `layout.ld` file) and will end at # `app_address`. "padding_address", # Linker script to produce a working app for this board "app_ldscript", # Flash address at which the app should be written "app_address", # Target name for flashing the board using pyOCD "pyocd_target", # The cfg file in OpenOCD board folder "openocd_board", # Options to tell Tockloader how to work with OpenOCD # Default: [] "openocd_options", # Dictionnary specifying custom commands for OpenOCD # Default is an empty dict # Valid keys are: program, read, erase "openocd_commands", # Interface to use with JLink (e.g. swd, jtag, etc.) "jlink_if", # Device name as supported by JLinkExe "jlink_device", # Whether Nordic DFU flashing method is supported "nordic_dfu", ]) SUPPORTED_BOARDS = { "nrf52840dk": OpenSKBoard( path="third_party/tock/boards/nordic/nrf52840dk", arch="thumbv7em-none-eabi", page_size=4096, kernel_address=0, padding_address=0x30000, app_ldscript="nrf52840dk_layout.ld", app_address=0x40000, pyocd_target="nrf52840", openocd_board="nordic_nrf52840_dongle.cfg", openocd_options=[], openocd_commands={}, jlink_if="swd", jlink_device="nrf52840_xxaa", nordic_dfu=False, ), "nrf52840_dongle": OpenSKBoard( path="third_party/tock/boards/nordic/nrf52840_dongle", arch="thumbv7em-none-eabi", page_size=4096, kernel_address=0, padding_address=0x30000, app_ldscript="nrf52840dk_layout.ld", app_address=0x40000, pyocd_target="nrf52840", openocd_board="nordic_nrf52840_dongle.cfg", openocd_options=[], openocd_commands={}, jlink_if="swd", jlink_device="nrf52840_xxaa", nordic_dfu=False, ), "nrf52840_dongle_dfu": OpenSKBoard( path="third_party/tock/boards/nordic/nrf52840_dongle_dfu", arch="thumbv7em-none-eabi", page_size=4096, kernel_address=0x1000, padding_address=0x30000, app_ldscript="nrf52840dk_layout.ld", app_address=0x40000, pyocd_target="nrf52840", openocd_board="nordic_nrf52840_dongle.cfg", openocd_options=[], openocd_commands={}, jlink_if="swd", jlink_device="nrf52840_xxaa", nordic_dfu=True, ), "nrf52840_mdk_dfu": OpenSKBoard( path="third_party/tock/boards/nordic/nrf52840_mdk_dfu", arch="thumbv7em-none-eabi", page_size=4096, kernel_address=0x1000, padding_address=0x30000, app_ldscript="nrf52840dk_layout.ld", app_address=0x40000, pyocd_target="nrf52840", openocd_board="nordic_nrf52840_dongle.cfg", openocd_options=[], openocd_commands={}, jlink_if="swd", jlink_device="nrf52840_xxaa", nordic_dfu=True, ), } # The STACK_SIZE value below must match the one used in the linker script # used by the board. # e.g. for Nordic nRF52840 boards the file is `nrf52840dk_layout.ld`. STACK_SIZE = 0x4000 # The following value must match the one used in the file # `src/entry_point.rs` APP_HEAP_SIZE = 90000 def get_supported_boards(): boards = [] for name, props in SUPPORTED_BOARDS.items(): if all((os.path.exists(os.path.join(props.path, "Cargo.toml")), (props.app_ldscript and os.path.exists(props.app_ldscript)))): boards.append(name) return tuple(set(boards)) def fatal(msg): print("{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): print("{style_begin}error:{style_end} {message}".format( style_begin=colorama.Fore.RED, style_end=colorama.Style.RESET_ALL, message=msg)) def info(msg): print("{style_begin}info:{style_end} {message}".format( style_begin=colorama.Fore.GREEN + colorama.Style.BRIGHT, style_end=colorama.Style.RESET_ALL, message=msg)) def assert_mandatory_binary(binary): if not shutil.which(binary): fatal(("Couldn't find {} binary. Make sure it is installed and " "that your PATH is set correctly.").format(binary)) def assert_python_library(module): try: __import__(module) except ModuleNotFoundError: fatal(("Couldn't load python3 module {name}. " "Try to run: pip3 install {name}").format(name=module)) class RemoveConstAction(argparse.Action): # pylint: disable=redefined-builtin def __init__(self, option_strings, dest, const, default=None, required=False, help=None, metavar=None): super(RemoveConstAction, self).__init__( option_strings=option_strings, dest=dest, nargs=0, const=const, default=default, required=required, help=help, metavar=metavar) def __call__(self, parser, namespace, values, option_string=None): # Code is simply a modified version of the AppendConstAction from argparse # https://github.com/python/cpython/blob/master/Lib/argparse.py#L138-L147 # https://github.com/python/cpython/blob/master/Lib/argparse.py#L1028-L1052 items = getattr(namespace, self.dest, []) if isinstance(items, list): items = items[:] else: items = copy.copy(items) if self.const in items: items.remove(self.const) setattr(namespace, self.dest, items) class OpenSKInstaller: def __init__(self, args): colorama.init() self.args = args # Where all the TAB files should go self.tab_folder = os.path.join("target", "tab") board = SUPPORTED_BOARDS[self.args.board] self.tockloader_default_args = argparse.Namespace( arch=board.arch, board=self.args.board, bundle_apps=False, debug=False, force=False, jlink_cmd="JLinkExe", jlink=self.args.programmer == "jlink", jlink_device=board.jlink_device, jlink_if=board.jlink_if, jlink_speed=1200, openocd=self.args.programmer == "openocd", openocd_board=board.openocd_board, openocd_cmd="openocd", openocd_commands=copy.copy(board.openocd_commands), openocd_options=copy.copy(board.openocd_options), jtag=False, no_bootloader_entry=False, page_size=board.page_size, port=None, ) def checked_command(self, cmd, env=None, cwd=None): stdout = None if self.args.verbose_build else subprocess.DEVNULL try: subprocess.run( cmd, stdout=stdout, timeout=None, check=True, env=env, cwd=cwd) except subprocess.CalledProcessError as e: fatal("Failed to execute {}: {}".format(cmd[0], str(e))) def checked_command_output(self, cmd, env=None, cwd=None): cmd_output = "" try: cmd_output = subprocess.run( cmd, stdout=subprocess.PIPE, timeout=None, check=True, env=env, cwd=cwd).stdout except subprocess.CalledProcessError as e: fatal("Failed to execute {}: {}".format(cmd[0], str(e))) # Unreachable because fatal() will exit return cmd_output.decode() def update_rustc_if_needed(self): target_toolchain_fullstring = "stable" with open("rust-toolchain", "r") as f: target_toolchain_fullstring = f.readline().strip() target_toolchain = target_toolchain_fullstring.split("-", maxsplit=1) if len(target_toolchain) == 1: # If we target the stable version of rust, we won't have a date # associated to the version and split will only return 1 item. # To avoid failing later when accessing the date, we insert an # empty value. target_toolchain.append("") current_version = self.checked_command_output(["rustc", "--version"]) if not (target_toolchain[0] in current_version and target_toolchain[1] in current_version): info("Updating rust toolchain to {}".format("-".join(target_toolchain))) # Need to update rustup_install = ["rustup"] if self.args.verbose_build: rustup_install.append("--verbose") rustup_install.extend(["install", target_toolchain_fullstring]) self.checked_command(rustup_install) rustup_target = ["rustup"] if self.args.verbose_build: rustup_target.append("--verbose") rustup_target.extend( ["target", "add", SUPPORTED_BOARDS[self.args.board].arch]) self.checked_command(rustup_target) info("Rust toolchain up-to-date") def build_tockos(self): info("Building Tock OS for board {}".format(self.args.board)) props = SUPPORTED_BOARDS[self.args.board] out_directory = os.path.join("third_party", "tock", "target", props.arch, "release") os.makedirs(out_directory, exist_ok=True) env = os.environ.copy() if self.args.verbose_build: env["V"] = "1" self.checked_command(["make"], cwd=props.path, env=env) def build_example(self): info("Building example {}".format(self.args.application)) self._build_app_or_example(is_example=True) def build_opensk(self): info("Building OpenSK application") self._build_app_or_example(is_example=False) def _build_app_or_example(self, is_example): assert self.args.application # Ideally we would build a TAB file for all boards at once but depending on # the chip on the board, the link script could be totally different. # And elf2tab doesn't seem to let us set the boards a TAB file has been # created for. So at the moment we only build for the selected board. props = SUPPORTED_BOARDS[self.args.board] rust_flags = [ "-C", "link-arg=-T{}".format(props.app_ldscript), "-C", "relocation-model=static", "-D", "warnings", "--remap-path-prefix={}=".format(os.getcwd()), ] env = os.environ.copy() env["RUSTFLAGS"] = " ".join(rust_flags) command = [ "cargo", "build", "--release", "--target={}".format(props.arch), "--features={}".format(",".join(self.args.features)) ] if is_example: command.extend(["--example", self.args.application]) if self.args.verbose_build: command.append("--verbose") self.checked_command(command, env=env) app_path = os.path.join("target", props.arch, "release") if is_example: app_path = os.path.join(app_path, "examples") app_path = os.path.join(app_path, self.args.application) # Create a TAB file self.create_tab_file({props.arch: app_path}) def generate_crypto_materials(self, force_regenerate): has_error = subprocess.call([ os.path.join("tools", "gen_key_materials.sh"), "Y" if force_regenerate else "N", ]) if has_error: error(("Something went wrong while trying to generate ECC " "key and/or certificate for OpenSK")) def create_tab_file(self, binaries): assert binaries assert self.args.application info("Generating Tock TAB file for application/example {}".format( self.args.application)) package_parameter = "-n" elf2tab_ver = self.checked_command_output(["elf2tab", "--version"]).split( " ", maxsplit=1)[1] # Starting from v0.5.0-dev the parameter changed. # Current pyblished crate is 0.4.0 but we don't want developers # running the HEAD from github to be stuck if "0.5.0-dev" in elf2tab_ver: package_parameter = "--package-name" os.makedirs(self.tab_folder, exist_ok=True) tab_filename = os.path.join(self.tab_folder, "{}.tab".format(self.args.application)) elf2tab_args = [ "elf2tab", package_parameter, self.args.application, "-o", tab_filename ] if self.args.verbose_build: elf2tab_args.append("--verbose") for arch, app_file in binaries.items(): dest_file = os.path.join(self.tab_folder, "{}.elf".format(arch)) shutil.copyfile(app_file, dest_file) elf2tab_args.append(dest_file) elf2tab_args.extend([ "--stack={}".format(STACK_SIZE), "--app-heap={}".format(APP_HEAP_SIZE), "--kernel-heap=1024", "--protected-region-size=64" ]) self.checked_command(elf2tab_args) def install_tab_file(self, tab_filename): assert self.args.application info("Installing Tock application {}".format(self.args.application)) board_props = SUPPORTED_BOARDS[self.args.board] args = copy.copy(self.tockloader_default_args) setattr(args, "app_address", board_props.app_address) setattr(args, "erase", self.args.clear_apps) setattr(args, "make", False) setattr(args, "no_replace", False) tock = loader.TockLoader(args) tock.open() tabs = [tab.TAB(tab_filename)] try: tock.install(tabs, replace="yes", erase=args.erase) except TockLoaderException as e: fatal("Couldn't install Tock application {}: {}".format( self.args.application, str(e))) def get_padding(self): fake_header = tbfh.TBFHeader("") fake_header.version = 2 fake_header.fields["header_size"] = 0x10 fake_header.fields["total_size"] = ( SUPPORTED_BOARDS[self.args.board].app_address - SUPPORTED_BOARDS[self.args.board].padding_address) fake_header.fields["flags"] = 0 return fake_header.get_binary() def install_tock_os(self): board_props = SUPPORTED_BOARDS[self.args.board] kernel_file = os.path.join("third_party", "tock", "target", board_props.arch, "release", "{}.bin".format(self.args.board)) info("Flashing file {}.".format(kernel_file)) with open(kernel_file, "rb") as f: kernel = f.read() args = copy.copy(self.tockloader_default_args) setattr(args, "address", board_props.app_address) tock = loader.TockLoader(args) tock.open() try: tock.flash_binary(kernel, board_props.kernel_address) except TockLoaderException as e: fatal("Couldn't install Tock OS: {}".format(str(e))) def install_padding(self): padding = self.get_padding() board_props = SUPPORTED_BOARDS[self.args.board] info("Flashing padding application") args = copy.copy(self.tockloader_default_args) setattr(args, "address", board_props.padding_address) tock = loader.TockLoader(args) tock.open() try: tock.flash_binary(padding, args.address) except TockLoaderException as e: fatal("Couldn't install padding: {}".format(str(e))) def clear_apps(self): args = copy.copy(self.tockloader_default_args) board_props = SUPPORTED_BOARDS[self.args.board] setattr(args, "app_address", board_props.app_address) # Ensure we don't force erase all apps but only the apps starting # at `board.app_address`. This makes sure we don't erase the padding. setattr(args, "force", False) info("Erasing all installed applications") tock = loader.TockLoader(args) tock.open() try: tock.erase_apps() except TockLoaderException as e: # Erasing apps is not critical info(("A non-critical error occurred while erasing " "apps: {}".format(str(e)))) # pylint: disable=protected-access def verify_flashed_app(self, expected_app): if self.args.programmer not in ("jlink", "openocd"): return False args = copy.copy(self.tockloader_default_args) tock = loader.TockLoader(args) tock.open() app_found = False with tock._start_communication_with_board(): apps = [app.name for app in tock._extract_all_app_headers()] app_found = expected_app in apps return app_found def create_hex_file(self, dest_file): # We produce an intelhex file with everything in it # https://en.wikipedia.org/wiki/Intel_HEX # pylint: disable=g-import-not-at-top,import-outside-toplevel import intelhex board_props = SUPPORTED_BOARDS[self.args.board] final_hex = intelhex.IntelHex() if self.args.tockos: # Process kernel kernel_path = os.path.join("third_party", "tock", "target", board_props.arch, "release", "{}.bin".format(self.args.board)) with open(kernel_path, "rb") as kernel: kern_hex = intelhex.IntelHex() kern_hex.frombytes(kernel.read(), offset=board_props.kernel_address) final_hex.merge(kern_hex, overlap="error") if self.args.application: # Add padding if board_props.padding_address: padding_hex = intelhex.IntelHex() padding_hex.frombytes( self.get_padding(), offset=board_props.padding_address) final_hex.merge(padding_hex, overlap="error") # Now we can add the application from the TAB file app_tab_path = "target/tab/{}.tab".format(self.args.application) assert os.path.exists(app_tab_path) app_tab = tab.TAB(app_tab_path) if board_props.arch not in app_tab.get_supported_architectures(): fatal(("It seems that the TAB file was not produced for the " "architecture {}".format(board_props.arch))) app_hex = intelhex.IntelHex() app_hex.frombytes( app_tab.extract_app(board_props.arch).get_binary(), offset=board_props.app_address) final_hex.merge(app_hex) info("Generating all-merged HEX file: {}".format(dest_file)) final_hex.tofile(dest_file, format="hex") def check_prerequisites(self): if not tockloader.__version__.startswith("1.4."): fatal(("Your version of tockloader seems incompatible: found {}, " "expected 1.4.x.".format(tockloader.__version__))) if self.args.programmer == "jlink": assert_mandatory_binary("JLinkExe") if self.args.programmer == "openocd": assert_mandatory_binary("openocd") if self.args.programmer == "pyocd": assert_mandatory_binary("pyocd") assert_python_library("intelhex") if not SUPPORTED_BOARDS[self.args.board].pyocd_target: fatal("This board doesn't seem to support flashing through pyocd.") if self.args.programmer == "nordicdfu": assert_mandatory_binary("nrfutil") assert_python_library("intelhex") assert_python_library("nordicsemi.lister") nrfutil_version = __import__("nordicsemi.version").version.NRFUTIL_VERSION if not nrfutil_version.startswith("6."): fatal(("You need to install nrfutil python3 package v6.0 or above. " "Found: {}".format(nrfutil_version))) if not SUPPORTED_BOARDS[self.args.board].nordic_dfu: fatal("This board doesn't support flashing over DFU.") if self.args.programmer == "none": assert_python_library("intelhex") def run(self): if self.args.listing == "boards": print(os.linesep.join(get_supported_boards())) return 0 if self.args.listing == "programmers": print(os.linesep.join(PROGRAMMERS)) return 0 if self.args.listing: # Missing check? fatal("Listing {} is not implemented.".format(self.args.listing)) self.check_prerequisites() self.update_rustc_if_needed() if not self.args.tockos and not self.args.application: info("Nothing to do.") return 0 # Compile what needs to be compiled if self.args.tockos: self.build_tockos() if self.args.application == "ctap2": self.generate_crypto_materials(self.args.regenerate_keys) self.build_opensk() elif self.args.application is None: info("No application selected.") else: self.build_example() # Flashing board_props = SUPPORTED_BOARDS[self.args.board] if self.args.programmer in ("jlink", "openocd"): # We rely on Tockloader to do the job if self.args.clear_apps: self.clear_apps() if self.args.tockos: # Install Tock OS self.install_tock_os() # Install padding and application if needed if self.args.application: self.install_padding() self.install_tab_file("target/tab/{}.tab".format(self.args.application)) if self.verify_flashed_app(self.args.application): info("You're all set!") return 0 error( ("It seems that something went wrong. App/example not found " "on your board. Ensure the connections between the programmer and " "the board are correct.")) return 1 return 0 if self.args.programmer in ("pyocd", "nordicdfu", "none"): dest_file = "target/{}_merged.hex".format(self.args.board) os.makedirs("target", exist_ok=True) self.create_hex_file(dest_file) if self.args.programmer == "pyocd": info("Flashing HEX file") self.checked_command([ "pyocd", "flash", "--target={}".format(board_props.pyocd_target), "--format=hex", "--erase=auto", dest_file ]) if self.args.programmer == "nordicdfu": info("Creating DFU package") dfu_pkg_file = "target/{}_dfu.zip".format(self.args.board) self.checked_command([ "nrfutil", "pkg", "generate", "--hw-version=52", "--sd-req=0", "--application-version=1", "--application={}".format(dest_file), dfu_pkg_file ]) info( "Please insert the dongle and switch it to DFU mode by keeping the " "button pressed while inserting...") info("Press [ENTER] when ready.") _ = input() # Search for the DFU devices serial_number = [] # pylint: disable=g-import-not-at-top,import-outside-toplevel from nordicsemi.lister import device_lister for device in device_lister.DeviceLister().enumerate(): if device.vendor_id == "1915" and device.product_id == "521F": serial_number.append(device.serial_number) if not serial_number: fatal("Couldn't find any DFU device on your system.") if len(serial_number) > 1: fatal("Multiple DFU devices are detected. Please only connect one.") # Run the command without capturing stdout so that we show progress info("Flashing device using DFU...") return subprocess.run( [ "nrfutil", "dfu", "usb-serial", "--package={}".format(dfu_pkg_file), "--serial-number={}".format(serial_number[0]) ], check=False, timeout=None, ).returncode return 0 def main(args): # Make sure the current working directory is the right one before running os.chdir(os.path.realpath(os.path.dirname(__file__))) OpenSKInstaller(args).run() if __name__ == "__main__": main_parser = argparse.ArgumentParser() action_group = main_parser.add_mutually_exclusive_group(required=True) action_group.add_argument( "--list", metavar="WHAT", choices=("boards", "programmers"), default=None, dest="listing", help="List supported boards or programmers, 1 per line and then exit.", ) action_group.add_argument( "--board", metavar="BOARD_NAME", dest="board", default=None, choices=get_supported_boards(), help="Indicates which board Tock OS will be compiled for.", ) main_parser.add_argument( "--dont-clear-apps", action="store_false", default=True, dest="clear_apps", help=("When installing an application, previously installed " "applications won't be erased from the board."), ) main_parser.add_argument( "--programmer", metavar="METHOD", dest="programmer", choices=PROGRAMMERS, default="jlink", help=("Sets the method to be used to flash Tock OS or the application " "on the target board."), ) main_parser.add_argument( "--no-tockos", action="store_false", default=True, dest="tockos", help=("Only compiles and flash the application/example. " "Otherwise TockOS will also be bundled and flashed."), ) main_parser.add_argument( "--verbose-build", action="store_true", default=False, dest="verbose_build", help="Build everything in verbose mode.", ) main_parser.add_argument( "--panic-console", action="append_const", const="panic_console", dest="features", help=("In case of application panic, the console will be used to " "output messages before starting blinking the LEDs on the " "board."), ) main_parser.add_argument( "--debug", action="append_const", const="debug_ctap", dest="features", help=("Compiles and installs the OpenSK application in debug mode " "(i.e. more debug messages will be sent over the console port " "such as hexdumps of packets)."), ) main_parser.add_argument( "--debug-allocations", action="append_const", const="debug_allocations", dest="features", help=("The console will be used to output allocator statistics every " "time an allocation/deallocation happens."), ) main_parser.add_argument( "--verbose", action="append_const", const="verbose", dest="features", help=("The console will be used to output verbose information about the " "OpenSK application. This also automatically activates --debug."), ) main_parser.add_argument( "--no-u2f", action=RemoveConstAction, const="with_ctap1", dest="features", help=("Compiles the OpenSK application without backward compatible " "support for U2F/CTAP1 protocol."), ) main_parser.add_argument( "--regen-keys", action="store_true", default=False, dest="regenerate_keys", help=("Forces the generation of files (certificates and private keys) " "under the crypto_data/ directory. " "This is useful to allow flashing multiple OpenSK authenticators " "in a row without them being considered clones."), ) main_parser.add_argument( "--no-persistent-storage", action="append_const", const="ram_storage", dest="features", help=("Compiles and installs the OpenSK application without persistent " "storage (i.e. unplugging the key will reset the key)."), ) apps_group = main_parser.add_mutually_exclusive_group(required=True) apps_group.add_argument( "--no-app", dest="application", action="store_const", const=None, help=("Doesn't compile nor install any application. Useful when you only " "want to update Tock OS kernel.")) apps_group.add_argument( "--opensk", dest="application", action="store_const", const="ctap2", help="Compiles and installs the OpenSK application.") apps_group.add_argument( "--crypto_bench", dest="application", action="store_const", const="crypto_bench", help=("Compiles and installs the crypto_bench example that tests " "the performance of the cryptographic algorithms on the board.")) main_parser.set_defaults(features=["with_ctap1"]) main(main_parser.parse_args())