stage2: discriminated union (payload | manifest) with signed-manifest resolution#16
Merged
Conversation
…d-manifest resolution Rework _stage2 admission so each per-arch entry is a discriminated union: a `payload` (admit a binary now, by sha256 pin or ed25519-signed, with inline or signed remote args) or a `manifest` (fetch a signed manifest, verify it against the pinned key, deep-merge it into the doc at the top level, and re-evaluate). Resolution is a loop: a manifest resolves to a payload, or delegates to another signed manifest (per-hop key delegation), until a payload is reached. A repeated (url, sha256) is a cycle and fails closed. Each hop is recorded in the arch entry's resolved_manifests (verifier-authoritative, carrying the resolved hash), and the merged document is what stage2 receives on stdin. Binding the payload and its args under one manifest signature closes the mix-and-match / rollback malleability in ed25519 mode (lockboot/stage0#2) while keeping the existing flexible url/sha256/ed25519/sig_url/args_url paths intact. - metadata: Entry/Payload/ManifestRef types + Payload::admission / ManifestRef::validate - stage1: resolution loop, deep_merge, fetch_manifest; merged doc handed to stdin - deploy: --manifest emits a signed manifest fragment and pins {manifest:{url,ed25519}} - Makefile: MANIFEST=1 chain-test mode; README + JSON schema updated Supersedes #15 (rebuilt from main as an additive change). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
HarryR
added a commit
to lockboot/stage0
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Reworks
_stage2admission so each per-arch entry is a discriminated union, exactly one of:payload-- admit a binary now:sha256pin ored25519-signed (sig_url), with inlineargsor signed remoteargs_url. All the flexible fields frommainare kept, just nested underpayload.manifest-- fetch a signed manifest (itself a_stage2fragment) fromurl, verify its detached signature (sig_url, else<url>.sig) against the pinneded25519key, deep-merge the whole document into the received user-data at the top level (manifest wins on conflict), and re-evaluate the entry.Resolution is a loop: a manifest resolves to a
payload, or delegates to a freshmanifest(per-hop key delegation), until a payload is reached. A repeated(url, sha256)is a cycle and fails closed. Every hop is recorded in the arch entry'sresolved_manifests(verifier-authoritative, carrying the resolved hash), and the merged doc is what stage2 receives on stdin.Binding the payload and its args under one manifest signature closes the mix-and-match / rollback malleability in ed25519 mode (lockboot/stage0#2), while keeping the existing direct
url/sha256/ed25519/sig_url/args_urlpaths intact.Changes
metadata:Entry/Payload/ManifestReftypes (externally-tagged union via#[serde(flatten)]),Payload::admission/ManifestRef::validate.ManifestRef.sha256optionally pins the manifest's own bytes.stage1: the resolution loop (replacing the single admit instage2()),deep_merge,fetch_manifest; the merged doc is serialized to stage2's stdin (sha256-only passes the received bytes through unchanged).deploy:--manifestemits a signed manifest fragment per payload and pins{"manifest":{url,ed25519}}; direct sha256 / ed25519 /--sign-argsmodes unchanged.Makefile:MANIFEST=1chain-test mode. README + JSON schema updated.Verification
cargo test --workspace: 25 tests (union round-trip incl. the serde-flatten case,deep_mergetop-level union + manifest-wins, cycle detection,resolved_manifestsprovenance, deploy manifest round-trip).make test-chain-x86_64across all modes under QEMU: sha256, ed25519 detached-sig, signed manifest (SIGN=1 MANIFEST=1), signed remote args, inline args, mirror fallback -- all bootstage0 -> UKI -> stage1 -> stage2; the manifest run shows the merged_stage2+resolved_manifestson stage2's stdin.lockboot-deployexercised end-to-end (create sha256/ed25519/manifest, validate, modify).Rebuilt from
mainas a purely additive change; supersedes #15 (which had deleted the flexible ed25519 paths).Pairs with lockboot/stage0
manifest-resolve.🤖 Generated with Claude Code