mirror of
https://github.com/raspberrypi/rpi-eeprom.git
synced 2026-01-20 21:13:36 +08:00
rpi-eeprom-config: Update to the same version as raspberrypi/usbboot
Update rpi-eeprom-config to include the secure-boot changes.
This commit is contained in:
@@ -8,6 +8,7 @@ import argparse
|
|||||||
import atexit
|
import atexit
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import string
|
||||||
import struct
|
import struct
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -15,7 +16,12 @@ import time
|
|||||||
|
|
||||||
IMAGE_SIZE = 512 * 1024
|
IMAGE_SIZE = 512 * 1024
|
||||||
|
|
||||||
MAX_BOOTCONF_SIZE = 2024
|
# Larger files won't with with "vcgencmd bootloader_config"
|
||||||
|
MAX_FILE_SIZE = 2024
|
||||||
|
ALIGN_SIZE = 4096
|
||||||
|
BOOTCONF_TXT = 'bootconf.txt'
|
||||||
|
BOOTCONF_SIG = 'bootconf.sig'
|
||||||
|
PUBKEY_BIN = 'pubkey.bin'
|
||||||
|
|
||||||
# Each section starts with a magic number followed by a 32 bit offset to the
|
# Each section starts with a magic number followed by a 32 bit offset to the
|
||||||
# next section (big-endian).
|
# next section (big-endian).
|
||||||
@@ -26,12 +32,18 @@ MAX_BOOTCONF_SIZE = 2024
|
|||||||
# The last 4KB of the EEPROM image is reserved for internal use by the
|
# The last 4KB of the EEPROM image is reserved for internal use by the
|
||||||
# bootloader and may be overwritten during the update process.
|
# bootloader and may be overwritten during the update process.
|
||||||
MAGIC = 0x55aaf00f
|
MAGIC = 0x55aaf00f
|
||||||
|
PAD_MAGIC = 0x55aafeef
|
||||||
MAGIC_MASK = 0xfffff00f
|
MAGIC_MASK = 0xfffff00f
|
||||||
FILE_MAGIC = 0x55aaf11f # id for modifiable file, currently only bootconf.txt
|
FILE_MAGIC = 0x55aaf11f # id for modifiable files
|
||||||
FILE_HDR_LEN = 20
|
FILE_HDR_LEN = 20
|
||||||
FILENAME_LEN = 12
|
FILENAME_LEN = 12
|
||||||
TEMP_DIR = None
|
TEMP_DIR = None
|
||||||
|
|
||||||
|
DEBUG = False
|
||||||
|
def debug(s):
|
||||||
|
if DEBUG:
|
||||||
|
sys.stderr.write(s + '\n')
|
||||||
|
|
||||||
def rpi4():
|
def rpi4():
|
||||||
compatible_path = "/sys/firmware/devicetree/base/compatible"
|
compatible_path = "/sys/firmware/devicetree/base/compatible"
|
||||||
if os.path.exists(compatible_path):
|
if os.path.exists(compatible_path):
|
||||||
@@ -59,6 +71,25 @@ def create_tempdir():
|
|||||||
if TEMP_DIR is None:
|
if TEMP_DIR is None:
|
||||||
TEMP_DIR = tempfile.mkdtemp()
|
TEMP_DIR = tempfile.mkdtemp()
|
||||||
|
|
||||||
|
def pemtobin(infile):
|
||||||
|
"""
|
||||||
|
Converts an RSA public key into the format expected by the bootloader.
|
||||||
|
"""
|
||||||
|
# Import the package here to make this a weak dependency.
|
||||||
|
from Cryptodome.PublicKey import RSA
|
||||||
|
|
||||||
|
arr = bytearray()
|
||||||
|
f = open(infile,'r')
|
||||||
|
key = RSA.importKey(f.read())
|
||||||
|
|
||||||
|
if key.size_in_bits() != 2048:
|
||||||
|
raise Exception("RSA key size must be 2048")
|
||||||
|
|
||||||
|
# Export N and E in little endian format
|
||||||
|
arr.extend(key.n.to_bytes(256, byteorder='little'))
|
||||||
|
arr.extend(key.e.to_bytes(8, byteorder='little'))
|
||||||
|
return arr
|
||||||
|
|
||||||
def exit_error(msg):
|
def exit_error(msg):
|
||||||
"""
|
"""
|
||||||
Trapped a fatal error, output message to stderr and exit with non-zero
|
Trapped a fatal error, output message to stderr and exit with non-zero
|
||||||
@@ -118,6 +149,8 @@ def apply_update(config, eeprom=None, config_src=None):
|
|||||||
sys.stdout.write("Updating bootloader EEPROM\n image: %s\nconfig_src: %s\nconfig: %s\n%s\n%s\n%s\n" %
|
sys.stdout.write("Updating bootloader EEPROM\n image: %s\nconfig_src: %s\nconfig: %s\n%s\n%s\n%s\n" %
|
||||||
(eeprom_image, config_src, config, '#' * 80, config_str, '#' * 80))
|
(eeprom_image, config_src, config, '#' * 80, config_str, '#' * 80))
|
||||||
|
|
||||||
|
sys.stdout.write("\n*** To cancel this update run 'sudo rpi-eeprom-update -r' ***\n\n")
|
||||||
|
|
||||||
# Ignore APT package checksums so that this doesn't fail when used
|
# Ignore APT package checksums so that this doesn't fail when used
|
||||||
# with EEPROMs with configs delivered outside of APT.
|
# with EEPROMs with configs delivered outside of APT.
|
||||||
# The checksums are really just a safety check for automatic updates.
|
# The checksums are really just a safety check for automatic updates.
|
||||||
@@ -178,6 +211,14 @@ def read_current_config():
|
|||||||
|
|
||||||
return (shell_cmd(['vcgencmd', 'bootloader_config']), "vcgencmd bootloader_config")
|
return (shell_cmd(['vcgencmd', 'bootloader_config']), "vcgencmd bootloader_config")
|
||||||
|
|
||||||
|
class ImageSection:
|
||||||
|
def __init__(self, magic, offset, length, filename=''):
|
||||||
|
self.magic = magic
|
||||||
|
self.offset = offset
|
||||||
|
self.length = length
|
||||||
|
self.filename = filename
|
||||||
|
debug("ImageSection %x %x %x %s" % (magic, offset, length, filename))
|
||||||
|
|
||||||
class BootloaderImage(object):
|
class BootloaderImage(object):
|
||||||
def __init__(self, filename, output=None):
|
def __init__(self, filename, output=None):
|
||||||
"""
|
"""
|
||||||
@@ -185,6 +226,7 @@ class BootloaderImage(object):
|
|||||||
and optionally an output filename.
|
and optionally an output filename.
|
||||||
"""
|
"""
|
||||||
self._filename = filename
|
self._filename = filename
|
||||||
|
self._sections = []
|
||||||
try:
|
try:
|
||||||
self._bytes = bytearray(open(filename, 'rb').read())
|
self._bytes = bytearray(open(filename, 'rb').read())
|
||||||
except IOError as err:
|
except IOError as err:
|
||||||
@@ -196,47 +238,112 @@ class BootloaderImage(object):
|
|||||||
if len(self._bytes) != IMAGE_SIZE:
|
if len(self._bytes) != IMAGE_SIZE:
|
||||||
exit_error("%s: Expected size %d bytes actual size %d bytes" %
|
exit_error("%s: Expected size %d bytes actual size %d bytes" %
|
||||||
(filename, IMAGE_SIZE, len(self._bytes)))
|
(filename, IMAGE_SIZE, len(self._bytes)))
|
||||||
|
self.parse()
|
||||||
|
|
||||||
def find_config(self):
|
def parse(self):
|
||||||
|
"""
|
||||||
|
Builds a table of offsets to the different sections in the EEPROM.
|
||||||
|
"""
|
||||||
offset = 0
|
offset = 0
|
||||||
magic = 0
|
magic = 0
|
||||||
|
found = False
|
||||||
while offset < IMAGE_SIZE:
|
while offset < IMAGE_SIZE:
|
||||||
magic, length = struct.unpack_from('>LL', self._bytes, offset)
|
magic, length = struct.unpack_from('>LL', self._bytes, offset)
|
||||||
if (magic & MAGIC_MASK) != MAGIC:
|
if magic == 0x0 or magic == 0xffffffff:
|
||||||
raise Exception('EEPROM is corrupted')
|
break # EOF
|
||||||
|
elif (magic & MAGIC_MASK) != MAGIC:
|
||||||
|
raise Exception('EEPROM is corrupted %x %x %x' % (magic, magic & MAGIC_MASK, MAGIC))
|
||||||
|
|
||||||
|
filename = ''
|
||||||
if magic == FILE_MAGIC: # Found a file
|
if magic == FILE_MAGIC: # Found a file
|
||||||
name = self._bytes[offset + 8: offset + FILE_HDR_LEN]
|
# Discard trailing null characters used to pad filename
|
||||||
if name.decode('utf-8') == 'bootconf.txt':
|
filename = self._bytes[offset + 8: offset + FILE_HDR_LEN].decode('utf-8').replace('\0', '')
|
||||||
return (offset, length)
|
self._sections.append(ImageSection(magic, offset, length, filename))
|
||||||
|
|
||||||
offset += 8 + length # length + type
|
offset += 8 + length # length + type
|
||||||
offset = (offset + 7) & ~7
|
offset = (offset + 7) & ~7
|
||||||
|
|
||||||
raise Exception('EEPROM parse error: Bootloader config not found')
|
def find_file(self, filename):
|
||||||
|
"""
|
||||||
|
Returns the offset, length and whether this is the last section in the
|
||||||
|
EEPROM for a modifiable file within the image.
|
||||||
|
"""
|
||||||
|
ret = (-1, -1, False)
|
||||||
|
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)
|
||||||
|
ret = (s.offset, s.length, is_last)
|
||||||
|
break
|
||||||
|
debug('%s offset %d length %d last %s' % (filename, ret[0], ret[1], ret[2]))
|
||||||
|
return ret
|
||||||
|
|
||||||
def write(self, new_config):
|
def update(self, src_bytes, dst_filename):
|
||||||
hdr_offset, length = self.find_config()
|
"""
|
||||||
new_config_bytes = open(new_config, 'rb').read()
|
Replaces a modifiable file with specified byte array.
|
||||||
new_len = len(new_config_bytes) + FILENAME_LEN + 4
|
"""
|
||||||
if len(new_config_bytes) > MAX_BOOTCONF_SIZE:
|
hdr_offset, length, is_last = self.find_file(dst_filename)
|
||||||
raise Exception("Config is too large (%d bytes). The maximum size is %d bytes."
|
if hdr_offset < 0:
|
||||||
% (len(new_config_bytes), MAX_BOOTCONF_SIZE))
|
raise Exception('Update target %s not found' % dst_filename)
|
||||||
if hdr_offset + len(new_config_bytes) + FILE_HDR_LEN > IMAGE_SIZE:
|
|
||||||
|
if hdr_offset + len(src_bytes) + FILE_HDR_LEN > IMAGE_SIZE:
|
||||||
raise Exception('EEPROM image size exceeded')
|
raise Exception('EEPROM image size exceeded')
|
||||||
|
|
||||||
|
new_len = len(src_bytes) + FILENAME_LEN + 4
|
||||||
struct.pack_into('>L', self._bytes, hdr_offset + 4, new_len)
|
struct.pack_into('>L', self._bytes, hdr_offset + 4, new_len)
|
||||||
struct.pack_into(("%ds" % len(new_config_bytes)), self._bytes,
|
struct.pack_into(("%ds" % len(src_bytes)), self._bytes,
|
||||||
hdr_offset + 4 + FILE_HDR_LEN, new_config_bytes)
|
hdr_offset + 4 + FILE_HDR_LEN, src_bytes)
|
||||||
|
|
||||||
# If the new config is smaller than the old config then set any old
|
# If the new file is smaller than the old file then set any old
|
||||||
# data which is now unused to all ones (erase value)
|
# data which is now unused to all ones (erase value)
|
||||||
pad_start = hdr_offset + 4 + FILE_HDR_LEN + len(new_config_bytes)
|
pad_start = hdr_offset + 4 + FILE_HDR_LEN + len(src_bytes)
|
||||||
|
|
||||||
|
# Add padding up to 8-byte boundary
|
||||||
|
while pad_start % 8 != 0:
|
||||||
|
struct.pack_into('B', self._bytes, pad_start, 0xff)
|
||||||
|
pad_start += 1
|
||||||
|
|
||||||
|
# Create a padding section unless the padding size is smaller than the
|
||||||
|
# size of a section head. Padding is allowed in the last section but
|
||||||
|
# by convention bootconf.txt is the last section and there's no need to
|
||||||
|
# pad to the end of the sector. This also ensures that the loopback
|
||||||
|
# config read/write tests produce identical binaries.
|
||||||
|
pad_bytes = ALIGN_SIZE - (pad_start % ALIGN_SIZE)
|
||||||
|
if pad_bytes > 8 and not is_last:
|
||||||
|
pad_bytes -= 8
|
||||||
|
struct.pack_into('>i', self._bytes, pad_start, PAD_MAGIC)
|
||||||
|
pad_start += 4
|
||||||
|
struct.pack_into('>i', self._bytes, pad_start, pad_bytes)
|
||||||
|
pad_start += 4
|
||||||
|
|
||||||
|
debug("pad %d" % pad_bytes)
|
||||||
pad = 0
|
pad = 0
|
||||||
while pad < (length - len(new_config_bytes)):
|
while pad < pad_bytes:
|
||||||
struct.pack_into('B', self._bytes, pad_start + pad, 0xff)
|
struct.pack_into('B', self._bytes, pad_start + pad, 0xff)
|
||||||
pad = pad + 1
|
pad = pad + 1
|
||||||
|
|
||||||
|
def update_key(self, src_pem, dst_filename):
|
||||||
|
"""
|
||||||
|
Replaces the specified public key entry with the public key values extracted
|
||||||
|
from the source PEM file.
|
||||||
|
"""
|
||||||
|
pubkey_bytes = pemtobin(src_pem)
|
||||||
|
self.update(pubkey_bytes, dst_filename)
|
||||||
|
|
||||||
|
def update_file(self, src_filename, dst_filename):
|
||||||
|
"""
|
||||||
|
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:
|
||||||
|
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)
|
||||||
|
|
||||||
|
def write(self):
|
||||||
|
"""
|
||||||
|
Writes the updated EEPROM image to stdout or the specified output file.
|
||||||
|
"""
|
||||||
if self._out is not None:
|
if self._out is not None:
|
||||||
self._out.write(self._bytes)
|
self._out.write(self._bytes)
|
||||||
self._out.close()
|
self._out.close()
|
||||||
@@ -246,14 +353,14 @@ class BootloaderImage(object):
|
|||||||
else:
|
else:
|
||||||
sys.stdout.write(self._bytes)
|
sys.stdout.write(self._bytes)
|
||||||
|
|
||||||
def get_config(self):
|
def get_file(self, filename):
|
||||||
hdr_offset, length = self.find_config()
|
hdr_offset, length, is_last = self.find_file(filename)
|
||||||
offset = hdr_offset + 4 + FILE_HDR_LEN
|
offset = hdr_offset + 4 + FILE_HDR_LEN
|
||||||
config_bytes = self._bytes[offset:offset+length-FILENAME_LEN-4]
|
config_bytes = self._bytes[offset:offset+length-FILENAME_LEN-4]
|
||||||
return config_bytes
|
return config_bytes
|
||||||
|
|
||||||
def read(self):
|
def read(self):
|
||||||
config_bytes = self.get_config()
|
config_bytes = self.get_file('bootconf.txt')
|
||||||
if self._out is not None:
|
if self._out is not None:
|
||||||
self._out.write(config_bytes)
|
self._out.write(config_bytes)
|
||||||
self._out.close()
|
self._out.close()
|
||||||
@@ -320,8 +427,21 @@ Operating modes:
|
|||||||
The default text editor is nano and may be overridden by setting the 'EDITOR'
|
The default text editor is nano and may be overridden by setting the 'EDITOR'
|
||||||
environment variable and passing '-E' to 'sudo' to preserve the environment.
|
environment variable and passing '-E' to 'sudo' to preserve the environment.
|
||||||
|
|
||||||
See 'rpi-eeprom-update -h' for more information about the available EEPROM
|
6. Signing the bootloader config file.
|
||||||
images.
|
Updates an EEPROM binary with a signed config file (created by rpi-eeprom-digest) plus
|
||||||
|
the corresponding RSA public key.
|
||||||
|
|
||||||
|
Requires Python Cryptodomex libraries and OpenSSL. To install on Raspberry Pi OS run:-
|
||||||
|
sudo apt install openssl python-pip
|
||||||
|
sudo python3 -m pip install cryptodomex
|
||||||
|
|
||||||
|
rpi-eeprom-digest -k private.pem -i bootconf.txt -o bootconf.sig
|
||||||
|
rpi-eeprom-config --config bootconf.txt --digest bootconf.sig --pubkey public.pem --out pieeprom-signed.bin pieeprom.bin
|
||||||
|
|
||||||
|
Currently, the signing process is a separate step so can't be used with the --edit or --apply modes.
|
||||||
|
|
||||||
|
|
||||||
|
See 'rpi-eeprom-update -h' for more information about the available EEPROM images.
|
||||||
"""
|
"""
|
||||||
parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter,
|
parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
description=description)
|
description=description)
|
||||||
@@ -331,6 +451,8 @@ images.
|
|||||||
parser.add_argument('-c', '--config', help='Name of bootloader configuration file', required=False)
|
parser.add_argument('-c', '--config', help='Name of bootloader configuration file', required=False)
|
||||||
parser.add_argument('-e', '--edit', action='store_true', default=False, help='Edit the current EEPROM config')
|
parser.add_argument('-e', '--edit', action='store_true', default=False, help='Edit the current EEPROM config')
|
||||||
parser.add_argument('-o', '--out', help='Name of output file', required=False)
|
parser.add_argument('-o', '--out', help='Name of output file', required=False)
|
||||||
|
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('eeprom', nargs='?', help='Name of EEPROM file to use as input')
|
parser.add_argument('eeprom', nargs='?', help='Name of EEPROM file to use as input')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
@@ -351,7 +473,12 @@ images.
|
|||||||
if args.config is not None:
|
if args.config is not None:
|
||||||
if not os.path.exists(args.config):
|
if not os.path.exists(args.config):
|
||||||
exit_error("config file '%s' not found" % args.config)
|
exit_error("config file '%s' not found" % args.config)
|
||||||
image.write(args.config)
|
image.update_file(args.config, BOOTCONF_TXT)
|
||||||
|
if args.digest is not None:
|
||||||
|
image.update_file(args.digest, BOOTCONF_SIG)
|
||||||
|
if args.pubkey is not None:
|
||||||
|
image.update_key(args.pubkey, PUBKEY_BIN)
|
||||||
|
image.write()
|
||||||
else:
|
else:
|
||||||
image.read()
|
image.read()
|
||||||
elif args.config is None and args.eeprom is None:
|
elif args.config is None and args.eeprom is None:
|
||||||
|
|||||||
Reference in New Issue
Block a user