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 sys
|
||||||
import fnmatch
|
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 args.signing_key is not None and os.path.basename(args.signing_key).lower().find("private") != -1:
|
||||||
if len(sys.argv)>1:
|
# prevent the easy mistake of using private key
|
||||||
board_pattern = sys.argv[1]
|
print("You must use the public key in the bootloader")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
os.environ['PYTHONUNBUFFERED'] = '1'
|
os.environ['PYTHONUNBUFFERED'] = '1'
|
||||||
|
|
||||||
@ -39,7 +45,12 @@ def run_program(cmd_list):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def build_board(board):
|
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
|
return False
|
||||||
if not run_program(["./waf", "clean"]):
|
if not run_program(["./waf", "clean"]):
|
||||||
return False
|
return False
|
||||||
@ -48,13 +59,19 @@ def build_board(board):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
for board in get_board_list():
|
for board in get_board_list():
|
||||||
if not fnmatch.fnmatch(board, board_pattern):
|
if not fnmatch.fnmatch(board, args.pattern):
|
||||||
continue
|
continue
|
||||||
print("Building for %s" % board)
|
print("Building for %s" % board)
|
||||||
if not build_board(board):
|
if not build_board(board):
|
||||||
failed_boards.add(board)
|
failed_boards.add(board)
|
||||||
continue
|
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]):
|
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)
|
failed_boards.add(board)
|
||||||
continue
|
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