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

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 mainJul 8, 2026
4 checks passed
@HarryR
HarryR deleted the manifest-resolve branch July 8, 2026 17:03
Sign up for freeto 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

@HarryR
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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

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 mainJul 8, 2026
4 checks passed
@HarryR
HarryR deleted the manifest-resolve branch July 8, 2026 17:03
Sign up for freeto 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

@HarryR
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

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 mainJul 8, 2026
4 checks passed
@HarryR
HarryR deleted the manifest-resolve branch July 8, 2026 17:03
Sign up for freeto 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

@HarryR
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

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 mainJul 8, 2026
4 checks passed
@HarryR
HarryR deleted the manifest-resolve branch July 8, 2026 17:03
Sign up for freeto 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

@HarryR
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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

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 mainJul 8, 2026
4 checks passed
@HarryR
HarryR deleted the manifest-resolve branch July 8, 2026 17:03
Sign up for freeto 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

@HarryR
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

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 mainJul 8, 2026
4 checks passed
@HarryR
HarryR deleted the manifest-resolve branch July 8, 2026 17:03
Sign up for freeto 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

@HarryR
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

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 mainJul 8, 2026
4 checks passed
@HarryR
HarryR deleted the manifest-resolve branch July 8, 2026 17:03
Sign up for freeto 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

@HarryR
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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

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 mainJul 8, 2026
4 checks passed
@HarryR
HarryR deleted the manifest-resolve branch July 8, 2026 17:03
Sign up for freeto 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

@HarryR