Skip to content

feat(signing): sign the boot FIT from the key registry via signing.fit_key - #224

Open
mobileoverlord wants to merge 6 commits into
mainfrom
jschneck/fit-sign-config
Open

feat(signing): sign the boot FIT from the key registry via signing.fit_key#224
mobileoverlord wants to merge 6 commits into
mainfrom
jschneck/fit-sign-config

Conversation

@mobileoverlord

Copy link
Copy Markdown
Contributor

The FIT signing key for FIT-booting machines came in through AVOCADO_FIT_KEY_DIR / AVOCADO_FIT_UNSIGNED, so a signed boot image was not reproducible from avocado.yaml.

What

runtimes:
  prod:
    signing:
      fit_key: product-fit        # registry name or key id of an RSA PEM key
      # fit_unsigned: true        # explicit opt-out; mutually exclusive
avocado signing-keys import product-fit --key FIT.key --cert FIT.crt
avocado signing-keys create product-fit --algorithm rsa2048   # via host openssl

How

  • The registry gains RSA PEM entries (rsa2048/rsa4096) stored as <keyid>.key (0600) / <keyid>.crt, key id = SHA-256 of the certificate DER.
  • The runtime build resolves signing.fit_key, stages the pair as FIT.key/FIT.crt in a private temp dir and mounts it read-only at /tmp/fit-keys, where the FIT assembly already looks. ed25519 and PKCS#11 entries are refused for FIT signing with a message saying why.
  • The environment variables are no longer read; a hint names the config keys.

Tests: config accessor, key id derivation, algorithm gating; 3,665 pass. Hardware check on imx8mp-evk to follow (same signed FIT as the env-var path produced).

https://claude.ai/code/session_01S75qGgqVA2cQUsZdW46qUz

…t_key

The FIT signing key came in through AVOCADO_FIT_KEY_DIR / AVOCADO_FIT_UNSIGNED,
so a signed boot image was not reproducible from avocado.yaml. Move it into
config and the signing-key registry:

- `runtimes.<name>.signing.fit_key` names an RSA PEM key in the registry (by
  name or key id); the runtime build materializes it as FIT.key/FIT.crt in a
  private temp dir mounted read-only at /tmp/fit-keys, where the FIT assembly
  already looks. `signing.fit_unsigned: true` is the explicit opt-out; both
  set is an error. The env variables are no longer read (a hint is printed).
- The registry gains RSA PEM entries (rsa2048/rsa4096): `signing-keys import
  <name> --key --cert` stores an existing pair, `signing-keys create <name>
  --algorithm rsa2048` generates one with the host's openssl. Key id is the
  SHA-256 of the certificate DER. PKCS#11 and ed25519 entries are refused for
  FIT signing with a message saying why.
Copilot AI lite review requested due to automatic review settings August 27, 2026 22:46
@mobileoverlord

Copy link
Copy Markdown
Contributor Author

End-to-end on the imx8mp-evk sample project: key imported with signing-keys import product-fit --key --cert, runtimes.dev.signing.fit_key: product-fit, avocado build with AVOCADO_FIT_KEY_DIR/AVOCADO_FIT_UNSIGNED unset → Built boot FIT: ... fitImage (signed) (rootfs root hash embedded). Boot on the board (project-keyed U-Boot) follows with the next deploy.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR moves boot FIT signing configuration from ad-hoc environment variables into avocado.yaml, by resolving an RSA PEM key from the signing-key registry and staging it for mkimage during runtime builds. This makes signed boot images reproducible from configuration alone and adds CLI support for creating/importing the required RSA key material.

Changes:

  • Added runtimes.<name>.signing.fit_key / fit_unsigned to runtime signing config, plus an accessor to read the per-runtime choice.
  • Extended the signing-key registry to support file-backed RSA PEM keypairs (key id derived from cert DER) and added signing-keys import + signing-keys create --algorithm rsa2048/rsa4096.
  • Updated runtime build to materialize FIT.key/FIT.crt from the registry and mount them into the SDK for mkimage FIT signing; updated docs/changelog accordingly.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/pkcs11_integration_test.rs Updates integration test calls to include the new algorithm parameter for signing key creation.
src/utils/signing_keys.rs Adds PEM RSA helpers (algorithm gating, cert-derived key id, save/load PEM keypair files) and unit tests.
src/utils/config.rs Introduces fit_key / fit_unsigned fields and a per-runtime accessor with tests.
src/main.rs Adds CLI flags for signing key algorithm selection and wires in the new signing-keys import command.
src/commands/signing_keys/mod.rs Exposes the new import subcommand module.
src/commands/signing_keys/import.rs Implements importing an existing RSA PEM key+cert into the signing key registry.
src/commands/signing_keys/create.rs Extends key creation to support RSA PEM generation via host openssl and stores into the registry.
src/commands/runtime/build.rs Switches FIT signing to registry-backed fit_key/fit_unsigned, stages FIT.key/FIT.crt in a temp dir, and updates script messages.
docs/signing-keys.md Documents Boot-FIT RSA key workflows and runtime configuration.
CHANGELOG.md Records the new config keys and deprecation of the interim env var path.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/utils/signing_keys.rs
Comment on lines +649 to +655
pub fn keyid_for_pem_cert(cert_pem: &str) -> Result<String> {
let body: String = cert_pem
.lines()
.filter(|l| !l.starts_with("-----"))
.map(str::trim)
.collect();
let der = BASE64_STANDARD
Comment thread src/utils/signing_keys.rs
Comment on lines +683 to +690
fs::write(&key_path, key_pem)
.with_context(|| format!("Failed to write private key: {}", key_path.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o600))
.with_context(|| format!("Failed to set permissions on {}", key_path.display()))?;
}
Comment thread src/commands/signing_keys/import.rs Dismissed
…IT is actually signed

- signing.fit_key_in_bootloader (default true with fit_key): after assembling
  the FIT, run the feed's imx-boot-tools/rekey-imx-boot.sh so U-Boot enforces
  the project key, then prove it with fit_check_sign against the keyed control
  DTB. A feed without the tooling fails the build rather than shipping a
  bootloader that ignores the key.
- A feed built without verified-boot ships a FIT template whose configuration
  nodes carry no signature-* subnode, and `mkimage -r` then signs nothing
  without complaint - the previous "(signed)" was not. Inject a signature-1
  node (algo from the registry key, key-name-hint FIT, sign-images from the
  configuration's own image properties) when the template lacks them, and
  fail the build unless `mkimage -l` shows a signature afterwards.
@mobileoverlord

Copy link
Copy Markdown
Contributor Author

End-to-end on the imx8mp-evk sample with the merged edge build: signing.fit_key: product-fit (registry) and no env vars → signature nodes injected into all 37 configurations, mkimage -l: Sign algo: sha256,rsa2048:FIT; rekey-imx-boot.sh (meta-avocado #335) re-packed both flash_evk targets and fit_check_sign verified the FIT against the keyed control DTB inside the build. Board flash with the re-keyed bootloader is next.

… first boot

The var partition is sized from the image (stone --partition-size) and only
grown to the disk later, on the device. A runtime that encrypts /var on first
boot therefore found a partition 3 MiB larger than the filesystem filling it,
and cryptsetup-var's in-place reencrypt (which needs 32 MiB in front of the
data) could neither find the room nor shrink a 285 MiB btrfs to make it -
observed on a freshly provisioned imx8mp-evk. Add 64 MiB to the partition
when the runtime opts into var.encrypt; plaintext runtimes keep the exact
image size.
Not only when the runtime encrypts: a device flashed plaintext must be able to
turn var.encrypt on over an update later, and without the dev extension its
partition is never grown, so the headroom has to be there from the first
flash. var is the last partition and the image is sparse-flashed, so the only
cost is 64 MiB of zeros; the layout no longer depends on a runtime flag.
stone now adds it to every override-sized partition, for the runtime bundle
and for provisioning alike; carrying a second copy here would double it.
…S2 header

The first boot converts /var to LUKS2 in place, which needs 32 MiB in front of
the data that cryptsetup-var obtains by shrinking the filesystem. mkfs.btrfs -r
packs its chunks to the content, so the flashed image had nothing to shrink
into and the conversion failed on a freshly provisioned board. When the runtime
declares var.encrypt, rebuild the image at its tight size plus 64 MiB so the
room is inside the filesystem that asked for it. Partition sizing is untouched
everywhere (still exactly the image size), and a plaintext runtime's image is
unchanged.
@mobileoverlord

Copy link
Copy Markdown
Contributor Author

Board result (imx8mp-evk, fresh provision from a build with signing.fit_key: product-fit, var.encrypt: true, rootfs verity, no env vars): U-Boot from the re-keyed imx-boot verified the FIT (sha256,rsa2048:FIT+ OK x3), root on /dev/mapper/root, and the var image built with room to shrink let cryptsetup-var convert /var in place on first boot (resizing it (-29M), --device-size 320M, /dev/mapper/var). 0 failed units.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants