Tools: added signing tools for secure boot
This commit is contained in:
parent
5cd0105971
commit
0c2594d04b
@ -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
|
||||
|
120
Tools/scripts/signing/README.md
Normal file
120
Tools/scripts/signing/README.md
Normal file
@ -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.
|
35
Tools/scripts/signing/generate_keys.py
Executable file
35
Tools/scripts/signing/generate_keys.py
Executable file
@ -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)
|
74
Tools/scripts/signing/make_secure_bl.py
Executable file
74
Tools/scripts/signing/make_secure_bl.py
Executable file
@ -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)
|
91
Tools/scripts/signing/make_secure_fw.py
Executable file
91
Tools/scripts/signing/make_secure_fw.py
Executable file
@ -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("<IQ64s", sig_len+8, sig_version, signature)
|
||||
img = img[:(offset + 16)] + desc + img[(offset + desc_len):]
|
||||
|
||||
if len(img) != img_len:
|
||||
print("Error: Image length changed")
|
||||
sys.exit(1)
|
||||
|
||||
print("Applying signature")
|
||||
|
||||
d["image"] = base64.b64encode(zlib.compress(img,9)).decode('utf-8')
|
||||
d["signed_firmware"] = True
|
||||
|
||||
f = open(sys.argv[1], "w")
|
||||
f.write(json.dumps(d, indent=4))
|
||||
f.close()
|
||||
print("Wrote %s" % apj_file)
|
Loading…
Reference in New Issue
Block a user