Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
deploy.py 13.81 KiB
#!/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

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import colorama
import argparse
import copy
import os
import shutil
import subprocess
import sys

from tockloader.exceptions import TockLoaderException
from tockloader import tab, tbfh, tockloader

# This structure allows us in the future to also support out-of-tree boards.
SUPPORTED_BOARDS = {
    "nrf52840_dk": "third_party/tock/boards/nordic/nrf52840dk",
    "nrf52840_dongle": "third_party/tock/boards/nordic/nrf52840_dongle"
}

# 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, root in SUPPORTED_BOARDS.items():
    if all((os.path.exists(os.path.join(root, "Cargo.toml")),
            os.path.exists(os.path.join(root, "Makefile")))):
      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))


class RemoveConstAction(argparse.Action):

  #pylint: disable=W0622
  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")
    # This is the filename that elf2tab command expects in order
    # to create a working TAB file.
    self.target_elf_filename = os.path.join(self.tab_folder, "cortex-m4.elf")
    self.tockloader_default_args = argparse.Namespace(
        arch="cortex-m4",
        board=getattr(self.args, "board", "nrf52840"),
        debug=False,
        force=False,
        jlink=True,
        jlink_device="nrf52840_xxaa",
        jlink_if="swd",
        jlink_speed=1200,
        jtag=False,
        no_bootloader_entry=False,
        page_size=4096,
        port=None,
    )

  def checked_command_output(self, cmd):
    cmd_output = ""
    try:
      cmd_output = subprocess.check_output(cmd)
    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 all((target_toolchain[0] in current_version,
                target_toolchain[1] in current_version)):
      info("Updating rust toolchain to {}".format("-".join(target_toolchain)))
      # Need to update
      self.checked_command_output(
          ["rustup", "install", target_toolchain_fullstring])
      self.checked_command_output(
          ["rustup", "target", "add", "thumbv7em-none-eabi"])
    info("Rust toolchain up-to-date")

  def build_and_install_tockos(self):
    self.checked_command_output(
        ["make", "-C", SUPPORTED_BOARDS[self.args.board], "flash"])

  def build_and_install_example(self):
    assert self.args.application
    self.checked_command_output([
        "cargo", "build", "--release", "--target=thumbv7em-none-eabi",
        "--features={}".format(",".join(self.args.features)), "--example",
        self.args.application
    ])
    self.install_elf_file(
        os.path.join("target/thumbv7em-none-eabi/release/examples",
                     self.args.application))

  def build_and_install_opensk(self):
    assert self.args.application
    info("Building OpenSK application")
    self.checked_command_output([
        "cargo",
        "build",
        "--release",
        "--target=thumbv7em-none-eabi",
        "--features={}".format(",".join(self.args.features)),
    ])
    self.install_elf_file(
        os.path.join("target/thumbv7em-none-eabi/release",
                     self.args.application))

  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 install_elf_file(self, elf_path):
    assert 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))
    shutil.copyfile(elf_path, self.target_elf_filename)
    self.checked_command_output([
        "elf2tab", package_parameter, self.args.application, "-o", tab_filename,
        self.target_elf_filename, "--stack={}".format(STACK_SIZE),
        "--app-heap={}".format(APP_HEAP_SIZE), "--kernel-heap=1024",
        "--protected-region-size=64"
    ])
    self.install_padding()
    info("Installing Tock application {}".format(self.args.application))
    args = copy.copy(self.tockloader_default_args)
    setattr(args, "app_address", 0x40000)
    setattr(args, "erase", self.args.clear_apps)
    setattr(args, "make", False)
    setattr(args, "no_replace", False)
    tock = tockloader.TockLoader(args)
    tock.open(args)
    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 install_padding(self):
    fake_header = tbfh.TBFHeader("")
    fake_header.version = 2
    fake_header.fields["header_size"] = 0x10
    fake_header.fields["total_size"] = 0x10000
    fake_header.fields["flags"] = 0
    padding = fake_header.get_binary()
    info("Flashing padding application")
    args = copy.copy(self.tockloader_default_args)
    setattr(args, "address", 0x30000)
    tock = tockloader.TockLoader(args)
    tock.open(args)
    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)
    setattr(args, "app_address", 0x40000)
    info("Erasing all installed applications")
    tock = tockloader.TockLoader(args)
    tock.open(args)
    try:
      tock.erase_apps(False)
    except TockLoaderException as e:
      # Erasing apps is not critical
      info(("A non-critical error occured while erasing "
            "apps: {}".format(str(e))))

  def verify_flashed_app(self, expected_app):
    args = copy.copy(self.tockloader_default_args)
    tock = tockloader.TockLoader(args)
    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 run(self):
    if self.args.action is None:
      # Nothing to do
      return 0

    self.update_rustc_if_needed()

    if self.args.action == "os":
      info("Installing Tock on board {}".format(self.args.board))
      self.build_and_install_tockos()
      return 0

    if self.args.action == "app":
      if self.args.application is None:
        fatal("Unspecified application")
      if self.args.clear_apps:
        self.clear_apps()
      if self.args.application == "ctap2":
        self.generate_crypto_materials(self.args.regenerate_keys)
        self.build_and_install_opensk()
      else:
        self.build_and_install_example()
      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."))
      return 1
    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__)))
  # Check for pre-requisite executable files.
  if not shutil.which("JLinkExe"):
    fatal(("Couldn't find JLinkExe binary. Make sure Segger JLink tools "
           "are installed and correctly set up."))

  OpenSKInstaller(args).run()


if __name__ == '__main__':
  shared_parser = argparse.ArgumentParser(add_help=False)
  shared_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 = argparse.ArgumentParser()
  commands = main_parser.add_subparsers(
      dest="action",
      help=("Indicates which part of the firmware should be compiled and "
            "flashed to the connected board."))

  os_commands = commands.add_parser(
      "os",
      parents=[shared_parser],
      help=("Compiles and installs Tock OS. The target board must be "
            "specified by setting the --board argument."),
  )
  os_commands.add_argument(
      "--board",
      metavar="BOARD_NAME",
      dest="board",
      choices=get_supported_boards(),
      help="Indicates which board Tock OS will be compiled for.",
      required=True)

  app_commands = commands.add_parser(
      "app",
      parents=[shared_parser],
      help="compiles and installs an application.")
  app_commands.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."),
  )
  app_commands.add_argument(
      "--no-u2f",
      action=RemoveConstAction,
      const="with_ctap1",
      dest="features",
      help=("Compiles the OpenSK application without backward compatible "
            "support for U2F/CTAP1 protocol."),
  )
  app_commands.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."),
  )
  app_commands.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)."),
  )
  apps_group = app_commands.add_mutually_exclusive_group()
  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."))

  app_commands.set_defaults(features=["with_ctap1"])

  main(main_parser.parse_args())