From 07bf72a91980cf1fb916f097d786dffbcbc15dab Mon Sep 17 00:00:00 2001 From: Tim Gover Date: Wed, 27 Mar 2024 10:23:12 +0000 Subject: [PATCH] tools: Preliminary tool support for signed-boot on 2712 Update rpi-eeprom-config to support replacement of bootcode.bin with a customer counter-signed version. Add a new rpi-sign-bootcode script which enables bootcode.bin to be counter-signed with the customer key. N.B. Signed boot on 2712 requires newer firmware which is currently under development and has not been released. --- rpi-eeprom-config | 93 +++++++++++----- tools/rpi-sign-bootcode | 229 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 293 insertions(+), 29 deletions(-) create mode 100755 tools/rpi-sign-bootcode diff --git a/rpi-eeprom-config b/rpi-eeprom-config index a4c768e..8ba5fb3 100755 --- a/rpi-eeprom-config +++ b/rpi-eeprom-config @@ -20,6 +20,7 @@ BOOTCONF_TXT = 'bootconf.txt' BOOTCONF_SIG = 'bootconf.sig' PUBKEY_BIN = 'pubkey.bin' CACERT_DER = 'cacert.der' +BOOTCODE_BIN = 'bootcode.bin' # Each section starts with a magic number followed by a 32 bit offset to the # next section (big-endian). @@ -297,14 +298,22 @@ class BootloaderImage(object): length = -1 is_last = False - next_offset = self._image_size - ERASE_ALIGN_SIZE # Don't create padding inside the bootloader scratch page - for i in range(0, len(self._sections)): + if filename == BOOTCODE_BIN: + next_offset = 0 + dst_filename = filename + i = 0 s = self._sections[i] - if s.magic == FILE_MAGIC and s.filename == filename: - is_last = (i == len(self._sections) - 1) - offset = s.offset - length = s.length - break + offset = s.offset + length = s.length + else: + next_offset = self._image_size - ERASE_ALIGN_SIZE # Don't create padding inside the bootloader scratch page + for i in range(0, len(self._sections)): + s = self._sections[i] + if s.magic == FILE_MAGIC and s.filename == filename: + is_last = (i == len(self._sections) - 1) + offset = s.offset + length = s.length + break # Find the start of the next non padding section i += 1 @@ -318,30 +327,43 @@ class BootloaderImage(object): debug('%s offset %d length %d is-last %d next %d' % (filename, ret[0], ret[1], ret[2], ret[3])) return ret - def update(self, src_bytes, dst_filename): + def update(self, src_bytes, dst_filename, bootcode = False): """ Replaces a modifiable file with specified byte array. """ - hdr_offset, length, is_last, next_offset = self.find_file(dst_filename) - update_len = len(src_bytes) + FILE_HDR_LEN + if bootcode: + hdr_offset, length, is_last, next_offset = self.find_file(dst_filename) + struct.pack_into('>L', self._bytes, hdr_offset + 4, len(src_bytes)) + struct.pack_into(("%ds" % len(src_bytes)), self._bytes, hdr_offset + 8, src_bytes) + pad_start = hdr_offset + len(src_bytes) + 8 + is_last = False + debug("bootcode padded to %d" % next_offset); + if next_offset < 128 * 1024: + raise Exception("update-bootcode: Can't update image - 128K must be reserved for bootcode") + if next_offset < 0: + raise Exception("update-bootcode: Failed to find next section") - if hdr_offset + update_len > self._image_size - ERASE_ALIGN_SIZE: - raise Exception('No space available - image past EOF.') + else: + hdr_offset, length, is_last, next_offset = self.find_file(dst_filename) + update_len = len(src_bytes) + FILE_HDR_LEN - if hdr_offset < 0: - raise Exception('Update target %s not found' % dst_filename) + if hdr_offset + update_len > self._image_size - ERASE_ALIGN_SIZE: + raise Exception('No space available - image past EOF.') - if hdr_offset + update_len > next_offset: - raise Exception('Update %d bytes is larger than section size %d' % (update_len, next_offset - hdr_offset)) + if hdr_offset < 0: + raise Exception('Update target %s not found' % dst_filename) - new_len = len(src_bytes) + FILENAME_LEN + 4 - struct.pack_into('>L', self._bytes, hdr_offset + 4, new_len) - struct.pack_into(("%ds" % len(src_bytes)), self._bytes, - hdr_offset + 4 + FILE_HDR_LEN, src_bytes) + if hdr_offset + update_len > next_offset: + raise Exception('Update %d bytes is larger than section size %d' % (update_len, next_offset - hdr_offset)) - # If the new file is smaller than the old file then set any old - # data which is now unused to all ones (erase value) - pad_start = hdr_offset + 4 + FILE_HDR_LEN + len(src_bytes) + new_len = len(src_bytes) + FILENAME_LEN + 4 + struct.pack_into('>L', self._bytes, hdr_offset + 4, new_len) + struct.pack_into(("%ds" % len(src_bytes)), self._bytes, + hdr_offset + 4 + FILE_HDR_LEN, src_bytes) + + # If the new file is smaller than the old file then set any old + # data which is now unused to all ones (erase value) + pad_start = hdr_offset + 4 + FILE_HDR_LEN + len(src_bytes) # Add padding up to 8-byte boundary while pad_start % 8 != 0: @@ -380,10 +402,11 @@ class BootloaderImage(object): Replaces the contents of dst_filename in the EEPROM with the contents of src_file. """ src_bytes = open(src_filename, 'rb').read() - if len(src_bytes) > MAX_FILE_SIZE: + bootcode = dst_filename == BOOTCODE_BIN + if not bootcode and len(src_bytes) > MAX_FILE_SIZE: raise Exception("src file %s is too large (%d bytes). The maximum size is %d bytes." % (src_filename, len(src_bytes), MAX_FILE_SIZE)) - self.update(src_bytes, dst_filename) + self.update(src_bytes, dst_filename, bootcode) def set_timestamp(self, timestamp): """ @@ -409,14 +432,22 @@ class BootloaderImage(object): def get_file(self, filename): hdr_offset, length, is_last, next_offset = self.find_file(filename) - offset = hdr_offset + 4 + FILE_HDR_LEN - file_bytes = self._bytes[offset:offset+length-FILENAME_LEN-4] + if filename == BOOTCODE_BIN: + offset = hdr_offset + 8 + file_bytes = self._bytes[offset:offset+length] + else: + offset = hdr_offset + 4 + FILE_HDR_LEN + file_bytes = self._bytes[offset:offset+length-FILENAME_LEN-4] + return file_bytes def extract_files(self): for i in range(0, len(self._sections)): s = self._sections[i] - if s.magic == FILE_MAGIC: + if s.magic == MAGIC and s.offset == 0: + file_bytes = self.get_file(BOOTCODE_BIN) + open(BOOTCODE_BIN, 'wb').write(file_bytes) + elif s.magic == FILE_MAGIC: file_bytes = self.get_file(s.filename) open(s.filename, 'wb').write(file_bytes) @@ -515,6 +546,7 @@ See 'rpi-eeprom-update -h' for more information about the available EEPROM image parser.add_argument('-d', '--digest', help='Signed boot only. The name of the .sig file generated by rpi-eeprom-dgst for config.txt ', required=False) parser.add_argument('-p', '--pubkey', help='Signed boot only. The name of the RSA public key file to store in the EEPROM', required=False) parser.add_argument('-x', '--extract', action='store_true', default=False, help='Extract the modifiable files (boot.conf, pubkey, signature)', required=False) + parser.add_argument('-b', '--bootcode', help='Signed boot 2712 only. The name of the customer signed bootcode.bin file to store in the EEPROM', required=False) parser.add_argument('-t', '--timestamp', help='Set the timestamp in the EEPROM image file', required=False) parser.add_argument('--cacertder', help='The name of a CA Certificate DER encoded file to store in the EEPROM', required=False) parser.add_argument('eeprom', nargs='?', help='Name of EEPROM file to use as input') @@ -539,7 +571,10 @@ See 'rpi-eeprom-update -h' for more information about the available EEPROM image image = BootloaderImage(args.eeprom, args.out) if args.timestamp is not None: image.set_timestamp(args.timestamp) - if args.config is not None: + if args.bootcode is not None: + image.update_file(args.bootcode, BOOTCODE_BIN) + image.write() + elif args.config is not None: if not os.path.exists(args.config): exit_error("config file '%s' not found" % args.config) image.update_file(args.config, BOOTCONF_TXT) diff --git a/tools/rpi-sign-bootcode b/tools/rpi-sign-bootcode new file mode 100755 index 0000000..af87243 --- /dev/null +++ b/tools/rpi-sign-bootcode @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 + +import argparse +import base64 +import struct +import sys + +# python3 -m pip install pycryptodomex +from Cryptodome.Hash import HMAC, SHA1, SHA256 +from Cryptodome.PublicKey import RSA +from Cryptodome.Signature import pkcs1_15 + +_CONFIG = {'DEBUG': False} +MAX_BIN_SIZE = 110 * 1024 + +def debug(msg): + """ + Outputs the msg string to stdout if DEBUG is enabled (via -d) + """ + if _CONFIG['DEBUG']: + sys.stderr.write(str(msg) + '\n') + +class ImageFile: + """ + Signed binary image + """ + def __init__(self, filename, max_size_kb): + self._filename = filename + self._bytes_written = 0 + if self._filename is None: + self._of = sys.stdout + else: + self._of = open(self._filename, "wb") + self._max_size_kb = max_size_kb + self._bytes = bytearray() + + debug("%8s %20s: [%6s] %s" % ('OFFSET', 'TYPE', 'SIZE', 'DESCRIPTION')) + debug("") + + def append(self, data): + """ + Appends a blob of binary data to the image + """ + self._bytes.extend(data) + + def append_file(self, source_file): + """ + Appends the binary contents of source_file to the current image. If + source_file is None then a base64 encoded blob is read from stdin. + """ + if source_file is None: + b64 = "" + for l in sys.stdin.readlines(): + b64 += l + file_bytes = base64.b64decode(b64) + else: + file_bytes = bytearray(open(source_file, 'rb').read()) + size = len(file_bytes) + debug("%08x %20s: [%6d] %s" % (self.pos(), 'FILE', size, source_file)) + self.append(file_bytes) + + def append_keynum(self, keynum): + """ + Appends a given key number as a 32-bit LE integer. + """ + if (keynum < 0 or keynum > 4) and keynum != 16: + raise Exception("Bad key number %d" % keynum) + debug("%08x %20s: [%6d] %d" % (self.pos(), "KEYNUM", 4, keynum)) + self.append(struct.pack(' 32: + raise Exception("Bad version number %d must be between 0-32" % version) + debug("%08x %20s: [%6d] %d" % (self.pos(), "VERSION", 4, version)) + self.append(struct.pack(' self._max_size_kb: + raise Exception("Signed binary size %d is too large. Max size %d" % (len(self._bytes), MAX_BIN_SIZE)) + debug("Image size %d" % len(self._bytes)) + if self._filename is None: + self._of.buffer.write(base64.b64encode(self._bytes)) + else: + self._of.write(self._bytes) + + def close(self): + self._of.close() + +def create_2711_image(output, bootcode, private_key, private_keynum, hmac): + """ + Create a 2711 C0 secure-boot compatible seconds stage signed binary. + """ + image = ImageFile(output, MAX_BIN_SIZE) + image.append_file(bootcode) + image.append_length() + image.append_keynum(private_keynum) + image.append_rsa_signature('sha1', private_key) + image.append_digest('hmac-sha1', hmac) + image.write() + image.close() + +def create_2712_image(output, bootcode, private_key, private_keynum, private_version): + """ + Create 2712 signed bootloader. The HMAC is removed and the full public key is appended. + """ + image = ImageFile(output, MAX_BIN_SIZE) + image.append_file(bootcode) + image.append_length() + image.append_keynum(private_keynum) + image.append_version(private_version) + image.append_rsa_signature('sha256', private_key) + image.append_public_key(private_key) + image.write() + image.close() + +def main(): + help_text = """ + Signs a second stage bootloader image. + + Examples: + 2711 mode: + rpi-sign-bootcode --debug -c 2711 -i bootcode.bin.clr -o bootcode.bin -k 2711_rsa_priv_0.pem -n 0 -m bootcode-production.key + + 2712 C1 and D0 mode: + * HMAC not included on 2712 + * RSA public key included - ROM just contains the hashes of the RPi public keys. + + Customer counter-signed signed: + * Exactly the same as Raspberry Pi signing but the input is the Raspberry Pi signed bootcode.bin + * The key number will probably always be 16 to indicate a customer signing + + rpi-sign-bootcode --debug -c 2712 -i bootcode.bin.sign2 -o bootcode.bin -k customer.pem + """ + parser = argparse.ArgumentParser(help_text) + parser.add_argument('-o', '--output', required=False, help='Output filename . If not specified the signed images is written to stdout in base64 format') + parser.add_argument('-c', '--chip', required=True, type=int, help='Chip number') + parser.add_argument('-i', '--input', required=False, help='Path of the unsigned bootcode.bin file OR RPi signed bootcode file sign with the customer key. If NULLL the binary is read from stdin in base64 format') + parser.add_argument('-m', '--hmac', required=False, help='Path of the HMAC key file') + parser.add_argument('-k', '--private-key', dest='private_key', required=True, help='Path of RSA private key (PEM format)') + parser.add_argument('-n', '--private-keynum', dest='private_keynum', required=False, default=0, type=int, help='ROM key index for RPi signing stage') + parser.add_argument('-d', '--debug', action='store_true') + parser.add_argument('-v', '--private-version', dest='private_version', required=True, type=int, help='Version of firmware, stops firmware rollback, only valid 0-31') + + args = parser.parse_args() + _CONFIG['DEBUG'] = args.debug + if args.chip == 2711: + if args.hmac is None: + raise Exception("HMAC key requried for 2711") + create_2711_image(args.output, args.input, args.private_key, args.private_keynum, args.hmac) + elif args.chip == 2712: + create_2712_image(args.output, args.input, args.private_key, args.private_keynum, args.private_version) + +if __name__ == '__main__': + main()