Skip to content

Secure Boot and KEYROM Layout

bunnie edited this page Jan 5, 2024 · 20 revisions

Intro

Firmware signing is all the rage. Everyone's doing it, so we are too.

However, let's be clear on our objectives. Here is what we hope to achieve with firmware signing:

  • Provide a mechanism to verify that "code at rest" (that is, code stored in nonvolatile memory in between boots) is intact.
  • Empower those who have built their device from source to self-sign and provision a full-strength chain of trust which is no stronger than their protocol to keep a private key safe.
  • Enable users of third-party distros to have a degree of assurance that updates from third parties have arrived intact

Here is what we will not achieve with firmware signing:

  • A "one-stop-shop" to assist with the delegation of important trust decisions.
  • A central arbiter of what is trustworthy, or what is not.
  • Enhanced physical security.

Phrased in terms of a threat model, the primary role of code signing on Precursor/Betrusted is to complicate the insertion of root kits via remote exploits, and it can also foil attempts to tamper with firmware updates being transmitted over insecure links from an otherwise trusted source. Code signing does not do much to foil an adversary with unlimited physical access to your device -- sealing the device properly with a watermarked glue seal on the outside as well as the inside is the first line of defense; and picking good unlock passwords is the final line of defense. Code signing also doesn't solve the problem of who to trust as the vendor for your firmware updates. It gives a mechanism for a much larger meta-solution to this problem, but in and of itself, digital signatures are not a magic bullet. Trust is not a technology problem -- it's a people problem. Precursor makes it easier and safer to interact with the people you trust, but it cann't help you to pick who to trust.

Here's a diagram. It's fun to stare at these, not read the text, and draw all kinds of conclusions. They also help you understand the text better, should you care to read it.

Hardware Root of Trust

Reading How Does Precursor Get to the Reset Vector? will help make sense of this page. We're not going to recap it here.

In typical mask-defined silicon, the "root of trust" is defined by code stored in an immutable ROM plus keys burned into an eFuse.

Precursor has an FPGA-based SoC, which is mutable. However, one can burn a 256-bit "eFuse AES key" that is built into the silicon of the FPGA. This mechanism is intrinsic to the FPGA's fixed silicon, and cannot be changed or bypassed once configured correctly.

Thus, users can create an effectively immutable root of trust by burning the 256-bit AES key that secures the bitstream, and destroying any copy of the key. This bitstream defines Precursor's CPU, including its initial ROM image, which holds any public keys used to verify the downstream firmware.

Important Limitations

  1. Devices are shipped with encryption enabled, and the null key (all 0's) in the eFuse.
  2. Precursor is designed to allow self-provisioning of a fused key using its built-in TRNG. If this key is selected as the boot source to the exclusion of all other keys, if user loses the fused key, the device may be irreparably bricked. Note that as of writing, the scripts to do the burning have yet to be written, but all the hardware primitives are implemented, individually tested, and documented.
  3. If one has significant concerns that their device could be physically seized for analysis, a hardware vulnerability can be exploited to read out the FPGA bitstream via the debug port. However, this vulnerability cannot be used to change the bitstream in FLASH (immutability is preserved despite the vulnerability). In this way, it is similar to silicon: it is also possible to image secrets contained within a silicon chip, but modifying the silicon is even harder.
  4. Users may alternatively use an external device to provision a battery-backed RAM key (for complicated reasons it is only possible to self-provision eFuses; BBRAM requires an external device). If the user loses the key, all the data is lost, but the device may be re-used by removing power to the battery-backed RAM, which zero-izes the key. The risk of losing the key is, however, significant; failing to keep the device charged will eventually lead to a loss of the key. Spelling it out: one should not use this as a mechanism for long-term storage of secrets of significant value (e.g. a crypto wallet).

Roadmap

Should Precursor someday be turned into mask-defined silicon chips, the root of trust transfers to the ROM burned into the silicon itself, plus a set of public keys burned into eFuses on the silicon. This is the typical root of trust in most contemporary embedded systems, and the starting point of any substantive system architecture discussion. Therefore, one may think of the FPGA as a prototyping platform for a future mask-defined silicon implementation of a trust root that is both a hardware root of trust and a boot root of trust.

Sources of Signatures

All signatures in Precursor are ed25519 signatures, with the exception of the kernel image, which uses ed25519ph (the "pre-hash" variant, still using a SHA-512 hash).

ed25519ph is used on the kernel because no user process can observe all of kernel memory due to security restrictions and thus the kernel image cannot be measured at runtime. Instead, a pre-hash of the disk image is computed by the loader and passed to userspace as an environment variable. See #472 for discussion.

Self-Signing

Key management is hard. Self-signing is a mechanism whereby the private and public key are generated and stored inside the device. The private key is typically protected with a supplemental factor, such as a password, provided by the user. In this situation, the keys are never disclosed.

Self-signing is a tool that can provide some assurance that code, initially provisioned in a trusted environment, has not been later tampered with. Self-signing is not useful for checking that code from remote sources is authentic.

Precursor provides for a self-signing key pair, which can be used by any stage of the process to generate a signature that can be used to validate the integrity of code-at-rest.

Devkey Signing

Precursor is primarily a development platform. Code changes are expected all the time, and signing updates is hard. However, we would like to avoid providing one boot path for developers where there are no signatures, and another production path with signatures.

Thus, we provide one "well-known" signature, where the public and private key pair is baked into the root repository for everyone to use, which is used by update pacakging scripts to sign firmware artifacts so that the code verification paths are always exercised, even during development.

The devkey is not secret, and it is labelled as such:

-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIKindlyNoteThisIsADevKeyDontUseForProduction
-----END PRIVATE KEY-----

A hardware feature is added to the display of Precursor, where a permanent, single-pixel wide strikethrough is applied to the status bar on devices that are booted from code signed only by developer keys. The strikethrough is added by the hardware framebuffer logic, and cannot be turned off once activated. This provides a clear UX cue that the current firmware has not been attested to by anyone.

Of course, a malicious kernel could upload an FPGA configuration that eliminates the strike-through feature. However, this would not be possible for users that have fused their FPGAs with a unique AES key and forced boot only from images that have been encrypted with this key. This scenario is useful for developers who are primarily working at the kernel/loader level, but rarely update the FPGA, yet still desire the UX cue.

Third Party Signing

An extra key slot is provided in the key store for a third-party public key. Users can specify this key during the firmware build process for incorporation into the final update image.

We make no attempt to control or validate the security of third party keys. Caveat emptor.

General Flow

During the boot process, the Boot ROM does a signature check on the loader as follows:

  1. Verify that third party key != dev key and self-signing key != dev key. If any are identical, set the dev key UX flag.
  2. If the self-signing key is not 0, check the loader against the self-signing key. Run the loader if it checks out. If it fails, display an error condition. This failure is unrecoverable [1].
  3. (self signing key was 0): Check loader with third party key (copy loader to RAM then check signature on copied data immediately before the jump). If there is a match, flag a clean boot, and skip to the self-signing check.
  4. (third party key failure): Check that dev key boots have not been disabled; if they have been, display an error condition and idle the device in a mode that can accept a firmware update.
  5. (dev boot enabled): Check loader with the dev key. If it checks out, set the dev key UX flag, and run the loader.
  6. (nothing checks out): display an error condition and idle the device in a mode that can accept a firmware update.

[1] Self-signing key failures are unrecoverable, as at this point, the self-signing key is inaccessible; the ROM must be re-imaged using a failsafe burning procedure. However, if the device is also fused to boot using the eFuse AES key and the eFuse key is lost, then the device is effectively a brick, and unrecoverable.

Note that since the self-signing key is never disclosed to anyone, the self-signing is disabled simply by setting the key to 0. If re-enabled, the key pair is regenerated from scratch, and all of the relevant artifacts are re-signed.

Key ROM Format

The KEYROM is a 256-entry, 32-bit wide ROM that is "baked into" the FPGA bitstream. The LUTs that form the KEYROM are fixed using pcells, so that they are always located at a fixed, known, location within the FPGA bitstream. Therefore, self-provisioning keys into the FPGA takes the following flow:

  1. Given a [u32; 256] arary of data, format it into a set of patch locations for the FPGA.
  2. Decrypt the FPGA contents to RAM, and patch the keys in the bitstream.
  3. Re-compute the HMAC headers on the RAM copy.
  4. Erase the changed sectors of the old FPGA image (due to the use of CBC, one can "patch" just the changed portions of the ROM).
  5. Re-encrypt the FPGA image, and write the changed sectors to FLASH
  6. Verify the FPGA image (by decrypting and checking the HMAC)

The KEYROM has 32 slots for 256-bit keys, or 64 slots for 128-bit keys. All key data is stored in big-endian format. This is meant to be a proto-efuse implementation, but we are taking advantage of its ability to be erased and re-written for setting anti-rollback revs (normally on an eFuse you would have to "notch a belt", here we do a binary code, which is not possible on an eFuse). 0 is the default state. Note that an eFuse would also typically have an ECC code attached to every field, which further complicates versioning.

offset function type notes
0x00-0x07 eFuse key AES256 this is necessary for bitstream updates. Erasing this makes the gateware immutable. Defaults to 0
0x08-0x0F self-signing privkey ed25519 private key Defaults to 0 (not used). Erased upon disable, regenerated from TRNG when enabled. Never disclosed to user.
0x10-0x17 self-signing pubkey ed25519 public key Defaults to 0 (not used). Derived from privkey.
0x18-0x1F developer pubkey ed25519 public key Well-known key. When used, signatures are still validated, but UX is defaced with a strikethrough in the status bar.
0x20-0x27 third party pubkey ed25519 public key Reserved for users to provide a third-party public key for Boot ROM verification of loader packages.
0x28-0x2F user root key AES256 Root key for user secrets
0x30-0xF7 unallocated TBD Unallocated key store (space for ~25 additional 256-bit keys)
0xF8-0xFB pepper [2] 128 bits pepper 128 bits of pepper, unique per device, used for password hashes
0xFC anti-rollback [3] u8.u8.u8.u8 min version code for FPGA gateware (maj.min.rev.ext) (unimplemented)
0xFD anti-rollback u8.u8.u8.u8 min version code for loader firmware (maj.min.rev.ext) (unimplemented)
0xFE anti-rollback x.x.x.u8 global anti rollback code
0xFF config flags config data 32 bits for config flags. See table below for config flags.

Config flags (32-bit field, big endian)

offset function type notes
0-15 version u8.u8 version number (major.minor) of fuse table
16 devboot disable bool if 0, devboots allowed; 1, devboots disallowed
17 anti-rollback enable bool if 1, version codes can't go backward in updates
18 anti-rollforward ena bool if 1, version codes can't go forward too fast [4]
19-22 roll-forward limit 4-bit nybble limit 0-15 of fast-forward updates (rev field); 0 locks out updates (all other roll-forward limits disregarded)
23-26 roll-forward limit 4-bit nybble limit 0-15 of fast-forward updates (min field); 0 locks out updates (all other roll-forward limits disregarded
27 initialized flag bool if 1, keys have gone through an initial provisioning step
28-31 reserved reserved reserved

[2] the way this is being used is actually closer to "pepper". Salt is unique per-password, "pepper" is stored in an HSM and shared across passwords. Since the password computations are happening "in the HSM", this is "pepper". Salt is relatively "expensive" due to the limited space of the KEYROM. Any per-password salt would be stored in FLASH memory, not the KEYROM, and thus outside the scope of this table.

[3] only maj.min.rev are considered for rollback prevention; ext field is unused for version computations. When computing anti-rollback, maj > min > rev in terms of precedence. The next lower field is only considered if the higher precedence field is equal.

[4] maj updates are always limited by one rev. min/rev may skip forward by up to 15. If min changes, rev is disregarded; rev is only considered if maj.min are equal when comparing updates. Setting anti-rollforward, anti-rollback, and zero to either of the roll-forward limits effectively locks out all updates.

All secret keys (AES256 & private keys) are protected with a supplemental user password. User passwords are hashed with 128 bits of pepper (plus any salt provided) using bcrypt prior to mixing with keys.

bcrypt is chosen according to the recommendations of OWASP, as a trade-off against the limited memory available in Precursor. We can't reliably allocate 15MiB for Argon2, therefore, we "use bcrypt with a work factor of 10 or more and with a password limit of 72 bytes".