Skip to content

stage2: discriminated union (payload | manifest) with signed-manifest resolution#16

Merged
HarryR merged 1 commit into
mainfrom
manifest-resolve
Jul 8, 2026
Merged

stage2: discriminated union (payload | manifest) with signed-manifest resolution#16
HarryR merged 1 commit into
mainfrom
manifest-resolve

Conversation

@HarryR

@HarryR HarryR commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

What

Reworks _stage2 admission so each per-arch entry is a discriminated union, exactly one of:

  • payload -- admit a binary now: sha256 pin or ed25519-signed (sig_url), with inline args or signed remote args_url. All the flexible fields from main are kept, just nested under payload.
  • manifest -- fetch a signed manifest (itself a _stage2 fragment) from url, verify its detached signature (sig_url, else <url>.sig) against the pinned ed25519 key, 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 fresh manifest (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's resolved_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_url paths intact.

Changes

  • metadata: Entry / Payload / ManifestRef types (externally-tagged union via #[serde(flatten)]), Payload::admission / ManifestRef::validate. ManifestRef.sha256 optionally pins the manifest's own bytes.
  • stage1: the resolution loop (replacing the single admit in stage2()), deep_merge, fetch_manifest; the merged doc is serialized to stage2's stdin (sha256-only passes the received bytes through unchanged).
  • deploy: --manifest emits a signed manifest fragment per payload and pins {"manifest":{url,ed25519}}; direct sha256 / ed25519 / --sign-args modes unchanged.
  • Makefile: MANIFEST=1 chain-test mode. README + JSON schema updated.

Verification

  • cargo test --workspace: 25 tests (union round-trip incl. the serde-flatten case, deep_merge top-level union + manifest-wins, cycle detection, resolved_manifests provenance, deploy manifest round-trip).
  • make test-chain-x86_64 across all modes under QEMU: sha256, ed25519 detached-sig, signed manifest (SIGN=1 MANIFEST=1), signed remote args, inline args, mirror fallback -- all boot stage0 -> UKI -> stage1 -> stage2; the manifest run shows the merged _stage2 + resolved_manifests on stage2's stdin.
  • lockboot-deploy exercised end-to-end (create sha256/ed25519/manifest, validate, modify).

Rebuilt from main as a purely additive change; supersedes #15 (which had deleted the flexible ed25519 paths).

Pairs with lockboot/stage0 manifest-resolve.

🤖 Generated with Claude Code

…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)
@HarryR HarryR merged commit 63e532d into main Jul 8, 2026
4 checks passed
@HarryR HarryR deleted the manifest-resolve branch July 8, 2026 17:03
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