Skip to content

stage0: admit _stage1 via a signed manifest (fix mix-and-match, #2)#5

Closed
HarryR wants to merge 1 commit into
mainfrom
manifest-admission
Closed

stage0: admit _stage1 via a signed manifest (fix mix-and-match, #2)#5
HarryR wants to merge 1 commit into
mainfrom
manifest-admission

Conversation

@HarryR

@HarryR HarryR commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Part 1 of the fix for signed-mode malleability (#2). stage1 side follows in lockboot/stage1.

What

ed25519 signed mode used to pin a release key inline and sign each artifact separately (sig_url for the payload, args_url/args_sig_url for args). A hostile mirror (metadata is trusted; the payload/args/sig URLs are not) could mix-and-match validly-signed payload-from-A with args-from-B, or roll back to an old signed payload.

Now the arch entry pins { ed25519 pubkey, manifest_url }. stage0 fetches a signed manifest { url, sha256, args, version }, verifies its detached signature against the pinned key, then admits the payload by the manifest's exact sha256. Binding the payload + args under one signature closes mix-and-match; the release still rolls forward by re-signing a new manifest (user-data unchanged).

  • config.rs: drop sig_url/args_url/args_sig_url; add manifest_url/manifest_sig_url + a Manifest type. validate() enforces the per-mode shape.
  • main.rs: admit_payload dispatches by mode — sha256 downloads from the inline url; ed25519 fetches+verifies the manifest (manifest_sig_url, {sha256}manifest hash, else <manifest_url>.sig) then admits by manifest.sha256. fetch_signed_args removed.
  • Makefile: the test payload is no longer self-signed; a signed manifest.json is built + served, and boot-% serves a directory.

Verification

  • make boot-x86_64 → manifest fetched + signature verified, payload downloaded from the manifest's URL, hash matched, PCR 14 extended, payload ran.
  • Negative (direct harness run with a byte-flipped manifest sig) → manifest signature invalidSECURITY_VIOLATION → fail-closed power-off, payload never ran.

Deferred (tracked on #2)

  • Anti-rollback enforcement (a version field is carried now; TPM-NV floor later).
  • Domain-separated (namespaced) signatures.

🤖 Generated with Claude Code

ed25519 signed mode now pins { ed25519 pubkey, manifest_url } in the (trusted)
user-data instead of signing each artifact separately. stage0 fetches a signed
manifest ({ url, sha256, args, version }), verifies its detached signature against
the pinned key, then admits the payload by the manifest's exact sha256. Binding the
payload + args under one signature closes the mix-and-match / downgrade hole (#2): a
hostile mirror can neither recombine independently-signed pieces nor swap the
payload, while the release still rolls forward by re-signing a new manifest.

- config.rs: drop sig_url/args_url/args_sig_url; add manifest_url/manifest_sig_url
  plus a Manifest type; url/sha256 are now the sha256-mode fields. validate()
  enforces the per-mode shape.
- main.rs: admit_payload dispatches by mode -- sha256 downloads from the inline url;
  ed25519 fetches+verifies the manifest (manifest_sig_url, {sha256} -> manifest hash,
  else <manifest_url>.sig) then admits by manifest.sha256. Drop fetch_signed_args.
- Makefile: the test payload is no longer self-signed; a signed manifest.json is
  built + served, and boot-% serves a directory.

Verified: make boot-x86_64 admits via the manifest and runs the payload; a corrupted
manifest signature is rejected (SECURITY_VIOLATION) and fails closed with the payload
never running.

Anti-rollback (version enforcement via TPM NV) and domain-separated signatures are
deferred follow-ups on #2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@HarryR HarryR closed this Jul 8, 2026
HarryR added a commit that referenced this pull request Jul 8, 2026
…ible boot-test matrix (#6)

## What

Mirrors the stage1 rework on stage0's `_stage1` admission. Each per-arch
entry is a **discriminated union**, exactly one of:

- **`payload`** -- admit a UEFI binary now: `sha256` pin or
`ed25519`-signed (`sig_url`), with inline or signed LoadOptions (`args`
/ `args_url`).
- **`manifest`** -- fetch a signed manifest (a `_stage1` fragment),
verify its detached signature against the pinned `ed25519` key,
deep-merge it, and re-evaluate. Resolution loops through a chain of
signed manifests (per-hop key delegation) until a payload is reached; a
repeated `(url, sha256)` is a cycle and fails closed.

stage0 forwards no document (the UKI re-fetches its own metadata), so
the merged doc drives only re-evaluation. Closes the ed25519
mix-and-match malleability (#2) without dropping the
flexible `url`/`sha256`/`ed25519`/`sig_url`/`args_url` paths.
`config.rs` mirrors stage1's `metadata` types (http-only,
Deserialize-only).

## Reproducible boot-test suite (Makefile-driven, no manual docker)

- **`boot-%`** -- mode-aware (`SIGN` / `SIGN_ARGS` / `MANIFEST` /
`FALLBACK` / `ARGS`), serves a staging dir so payload, signed args, and
a signed manifest are all served uniformly at
`http://SERVE_HOST/<file>`.
- **`smoke-boot-%`** -- boots every admission mode `stage0 ->
test-payload` and asserts the payload actually chain-loaded (fetch ->
admit -> PCR-measure -> chain-load), with a per-mode PASS/FAIL summary.
- **Fast by default**: the test payload powers off at the end instead of
returning to stage0 (so stage0's ~90s fail-closed drain is never hit),
and skips its EC2-only ~60s serial-flush hold when its LoadOptions carry
`--nosleep` (a runtime flag the harness passes -- no separate build; a
real EC2 deploy keeps the full hold). The whole matrix runs in ~2 min.

## Verification

- Compiles clean for `x86_64-unknown-uefi` via `make`.
- `make smoke-boot-x86_64`: **all 5 modes PASS** (sha256, ed25519 sig,
signed args, **signed manifest**, mirror fallback) in ~2 min.

Rebuilt from `main` as a purely additive change; **supersedes #5**.
Pairs with lockboot/stage1#16.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
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.

1 participant