From 0c2594d04b7a030c6acd321d5899042bee5b409d Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Mon, 29 Aug 2022 14:05:47 +1000 Subject: [PATCH] Tools: added signing tools for secure boot --- Tools/scripts/build_bootloaders.py | 31 ++++-- Tools/scripts/signing/README.md | 120 ++++++++++++++++++++++++ Tools/scripts/signing/generate_keys.py | 35 +++++++ Tools/scripts/signing/make_secure_bl.py | 74 +++++++++++++++ Tools/scripts/signing/make_secure_fw.py | 91 ++++++++++++++++++ 5 files changed, 344 insertions(+), 7 deletions(-) create mode 100644 Tools/scripts/signing/README.md create mode 100755 Tools/scripts/signing/generate_keys.py create mode 100755 Tools/scripts/signing/make_secure_bl.py create mode 100755 Tools/scripts/signing/make_secure_fw.py diff --git a/Tools/scripts/build_bootloaders.py b/Tools/scripts/build_bootloaders.py index 5d5648fee5..bf6e5c3461 100755 --- a/Tools/scripts/build_bootloaders.py +++ b/Tools/scripts/build_bootloaders.py @@ -10,11 +10,17 @@ import subprocess import sys import fnmatch -board_pattern = '*' +# get command line arguments +from argparse import ArgumentParser +parser = ArgumentParser(description='make_secure_bl') +parser.add_argument("--signing-key", type=str, default=None, help="signing key for secure bootloader") +parser.add_argument("pattern", type=str, default='*', help="board wildcard pattern") +args = parser.parse_args() -# allow argument for pattern of boards to build -if len(sys.argv)>1: - board_pattern = sys.argv[1] +if args.signing_key is not None and os.path.basename(args.signing_key).lower().find("private") != -1: + # prevent the easy mistake of using private key + print("You must use the public key in the bootloader") + sys.exit(1) os.environ['PYTHONUNBUFFERED'] = '1' @@ -39,7 +45,12 @@ def run_program(cmd_list): return True def build_board(board): - if not run_program(["./waf", "configure", "--board", board, "--bootloader", "--no-submodule-update", "--Werror"]): + configure_args = "--board %s --bootloader --no-submodule-update --Werror" % board + configure_args = configure_args.split() + if args.signing_key is not None: + print("Building secure bootloader") + configure_args.append("--signed-fw") + if not run_program(["./waf", "configure"] + configure_args): return False if not run_program(["./waf", "clean"]): return False @@ -48,13 +59,19 @@ def build_board(board): return True for board in get_board_list(): - if not fnmatch.fnmatch(board, board_pattern): + if not fnmatch.fnmatch(board, args.pattern): continue print("Building for %s" % board) if not build_board(board): failed_boards.add(board) continue - shutil.copy('build/%s/bin/AP_Bootloader.bin' % board, 'Tools/bootloaders/%s_bl.bin' % board) + bl_file = 'Tools/bootloaders/%s_bl.bin' % board + shutil.copy('build/%s/bin/AP_Bootloader.bin' % board, bl_file) + if args.signing_key is not None: + print("Signing bootloader with %s" % args.signing_key) + if not run_program(["./Tools/scripts/signing/make_secure_bl.py", bl_file, args.signing_key]): + print("Failed to sign bootloader for %s" % board) + sys.exit(1) if not run_program([sys.executable, "Tools/scripts/bin2hex.py", "--offset", "0x08000000", 'Tools/bootloaders/%s_bl.bin' % board, 'Tools/bootloaders/%s_bl.hex' % board]): failed_boards.add(board) continue diff --git a/Tools/scripts/signing/README.md b/Tools/scripts/signing/README.md new file mode 100644 index 0000000000..6240161133 --- /dev/null +++ b/Tools/scripts/signing/README.md @@ -0,0 +1,120 @@ +# Secure Boot Support + +To assist with vendors needing high levels of tamper resistance with +RemoteID, you can optionally use secure boot with ArduPilot. This +involves installing a bootloader with up to 10 public keys included +and signing the ArduPilot vehicle firmware with one secret key. The +bootloader will refuse to boot the firmware if the signature on the +firmware doesn't match any of the public keys in the bootloader. + +## Generating Keys + +To generate a public/private key pair, run the following command: + +``` + python3 -m pip install pymonocypher + Tools/scripts/signing/generate_keys.py NAME +``` + +That will create two files: + - NAME_private_key.dat + - NAME_public_key.dat + +NAME can be any string, but would usually be your vendor name. It is +only used for the local filenames. + +The generated private key should be kept in a secure location. The +public key will be used to create a secure bootloader that will only +accept firmwares signed with one of the public keys in the bootloader. + +## Building secure bootloader + +To build a secure bootloader run this command: + +``` + Tools/scripts/build_bootloaders.py BOARDNAME --signing-key=NAME_public.key +``` + +That will update the bootloader in Tools/bootloaders/BOARDNAME_bl.bin +to enable secure boot with the specified public key. Next time you +build a firmware for this board then that bootloader will be included +in ROMFS. + +Note that this will include the 3 ArduPilot signing keys by default as +well as your key. This is done so that your users can update to a +standard ArduPilot firmware release and also prevents issues with +vendors who can no longer provide firmware updates to users. If you +have a very good reason for not including the ArduPilot signing keys +then you can pass the option --omit-ardupilot-keys to the +make_secure_bl.py script. + +## Building Signed Firmware + +To build a signed firmware run this command (example is for a copter build): + +``` + ./waf configure --board BOARDNAME --signed-fw + ./waf copter + ./Tools/scripts/signing/make_secure_fw.py build/BOARDNAME/bin/arducopter.apj NAME_private_key.dat +``` + +The final step signs the apj firmware with your private key. You can +then load that secure firmware as usual with your ground station, for +example using load custom firmware in MissionPlanner or +Tools/scripts/uploader.py on Linux. + +## Flashing the secure bootloader + +There are two methods of getting the secure bootloader onto the +board. The simplest is to follow the above steps and then follow the +usual method of updating the bootloader, which involves sending a +MAVLink command to ask the firmware to flash the embedded bootloader +from ROMFS. The firmware generated using the above steps will have +your secure bootloader included in ROMFS, so when the users asks for +the bootloader to update it will flash the secure bootloader. + +The second method is to put the board into DFU mode. If your hwdef.dat +and hwdef-bl.dat include the ENABLE_DFU_BOOT options and your board is +based on a STM32H7 then your ground station should be able to put the +board into DFU mode. You can then flash the bootloader bin file to +address 0x08000000 using any DFU capable client. + +Note that the flight controller will refuse a switch to DFU mode if it +is running a secure bootloader already. + +## How to tell you are using secure boot + +When using a secure bootloader the USB ID presented by the bootloader +will have a "Secure" string added. For example, you would see this in +"dmesg" in Linux: + +``` + Product: BOARDNAME-Secure-BL-v10 +``` + +On Windows you can look at the device properties in device manager +when the bootloader is running and look for the "Bus reported device +description". It will have the above "Secure" string. Note that this +string only appears when in the bootloader. To ensure the board stays +in the bootloader for long enough to see this string just flash a +normal unsigned firmware. With a secure bootloader and an unsigned +firmware the board will stay in the bootloader forever as it will be +failing the secure boot checks. + +## Reverting to normal boot + +If you have installed secure boot on a board then to revert to normal +boot you would need to flash a new bootloader that does not have +secure boot enabled. To do that you should replace +Tools/bootloaders/BOARDNAME_bl.bin with the normal bootloader for your +board then build and sign a firmware as above. Then ask the flight +controller to flash the updated bootloader using the GCS interface and +you will then be running a normal bootloader. + +## Supported Boards + +Secure boot is only supported on boards with at least 32k of flash +space for the bootloader. This includes all boards based on the +STM32H7 and STM32F7. You can use secure boot on older other boards if +you change the hwdef.dat and hwdef-bl.dat to add more space for the +bootloader. diff --git a/Tools/scripts/signing/generate_keys.py b/Tools/scripts/signing/generate_keys.py new file mode 100755 index 0000000000..5da4388c30 --- /dev/null +++ b/Tools/scripts/signing/generate_keys.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +''' +generate a public/private key pair using Monocypher +''' + +import sys +import base64 + +try: + import monocypher +except ImportError: + print("Please install monocypher with: python3 -m pip install pymonocypher") + sys.exit(1) + + +if len(sys.argv) != 2: + print("Usage: generate_keys.py BASENAME") + sys.exit(1) + +bname = sys.argv[1] + +def encode_key(ktype, key): + return ktype + "_KEYV1:" + base64.b64encode(key).decode('utf-8') + +private_key = monocypher.generate_key() +public_key = monocypher.compute_signing_public_key(private_key) + +public_fname = "%s_public_key.dat" % bname +private_fname = "%s_private_key.dat" % bname + +open(private_fname, "w").write(encode_key("PRIVATE", private_key)) +print("Generated %s" % private_fname) + +open(public_fname, "w").write(encode_key("PUBLIC", public_key)) +print("Generated %s" % public_fname) diff --git a/Tools/scripts/signing/make_secure_bl.py b/Tools/scripts/signing/make_secure_bl.py new file mode 100755 index 0000000000..1f539dfc0f --- /dev/null +++ b/Tools/scripts/signing/make_secure_bl.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +''' +add a set of up to 10 public keys to an ArduPilot bootloader bin file +''' + +import sys +import os +import base64 + +try: + import monocypher +except ImportError: + print("Please install monocypher with: python3 -m pip install pymonocypher") + sys.exit(1) + +# get command line arguments +from argparse import ArgumentParser +parser = ArgumentParser(description='make_secure_bl') +parser.add_argument("--omit-ardupilot-keys", action='store_true', default=False, help="omit ArduPilot signing keys") +parser.add_argument("bootloader", type=str, default=None, help="bootloader") +parser.add_argument("keys", type=str, nargs='+', help="keys") +args = parser.parse_args() + +descriptor = b'\x4e\xcf\x4e\xa5\xa6\xb6\xf7\x29' +max_keys = 10 +key_len = 32 + +if len(args.keys) <= 0: + print("At least one key file required") + sys.exit(1) + +img = open(args.bootloader, 'rb').read() + +offset = img.find(descriptor) +if offset == -1: + print("Failed to find descriptor") + sys.exit(1) + +offset += 8 +desc = b'' +desc_len = 0 + +keys = args.keys[:] + +if not args.omit_ardupilot_keys: + print("Adding ArduPilot keys") + signing_dir = os.path.dirname(os.path.realpath(__file__)) + keydir = os.path.join(signing_dir,"ArduPilotKeys") + for root, dirs, files in os.walk(keydir): + for f in files: + if f.endswith(".dat"): + keys.append(os.path.relpath(os.path.join(keydir, f))) + +if len(keys) > max_keys: + print("Too many key files %u, max is %u" % (len(keys), max_keys)) + sys.exit(1) + +def decode_key(ktype, key): + ktype += "_KEYV1:" + if not key.startswith(ktype): + print("Invalid key type") + sys.exit(1) + return base64.b64decode(key[len(ktype):]) + +for kfile in keys: + key = decode_key("PUBLIC", open(kfile, "r").read()) + print("Applying Public Key %s" % kfile) + if len(key) != key_len: + print("Bad key length %u in %s" % (len(key), kfile)) + sys.exit(1) + desc += key + desc_len += key_len +img = img[:offset] + desc + img[offset+desc_len:] +open(sys.argv[1], 'wb').write(img) diff --git a/Tools/scripts/signing/make_secure_fw.py b/Tools/scripts/signing/make_secure_fw.py new file mode 100755 index 0000000000..4e916090fc --- /dev/null +++ b/Tools/scripts/signing/make_secure_fw.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +''' +sign an ArduPilot APJ firmware with a private key +''' + +import sys +import struct +import json, base64, zlib + +try: + import monocypher +except ImportError: + print("Please install monocypher with: python3 -m pip install pymonocypher") + sys.exit(1) + +key_len = 32 +sig_len = 64 +sig_version = 30437 +descriptor = b'\x41\xa3\xe5\xf2\x65\x69\x92\x07' + +if len(sys.argv) < 3: + print("Usage: make_secure_fw.py APJ_FILE PRIVATE_KEYFILE") + sys.exit(1) + +def to_unsigned(i): + '''convert a possibly signed integer to unsigned''' + if i < 0: + i += 2**32 + return i + +apj_file = sys.argv[1] +key_file = sys.argv[2] + +# open apj file +apj = open(apj_file, 'r').read() + +# decode json in apj +d = json.loads(apj) + +# get image data +img = zlib.decompress(base64.b64decode(d['image'])) +img_len = len(img) + +def decode_key(ktype, key): + ktype += "_KEYV1:" + if not key.startswith(ktype): + print("Invalid key type") + sys.exit(1) + return base64.b64decode(key[len(ktype):]) + +key = decode_key("PRIVATE", open(key_file, 'r').read()) +if len(key) != key_len: + print("Bad key length %u" % len(key)) + sys.exit(1) + +offset = img.find(descriptor) +if offset == -1: + print("No APP_DESCRIPTOR found") + sys.exit(1) + +offset += 8 +desc_len = 92 + +flash1 = img[:offset] +flash2 = img[offset+desc_len:] +flash12 = flash1 + flash2 + +signature = monocypher.signature_sign(key, flash12) +if len(signature) != sig_len: + print("Bad signature length %u should be %u" % (len(signature), sig_len)) + sys.exit(1) + +# pack signature in 4 bytes length, 8 byte signature version and 64 byte +# signature. We have a signature version to allow for changes to signature +# system in the future +desc = struct.pack("