Domain-separated signatures + standalone stage0-sign signer - #8

Merged
HarryR merged 3 commits into
mainfrom
domain-separated-sigs
Jul 9, 2026
Merged

Domain-separated signatures + standalone stage0-sign signer#8
HarryR merged 3 commits into
mainfrom
domain-separated-sigs

Conversation

@HarryR

Copy link
Copy Markdown
Contributor

What

Bind every ed25519 signature stage0 admits to its exact role. Signatures are now over a fixed 64-byte preimage sha256(domain_tag) || sha256(message), where the tag is one of lockboot.v1.stage1.uki / .stage1.args / .stage1.manifest. A signature minted for one role is structurally invalid in any other, closing the last of the mix-and-match malleability (the deferred tail of #2).

Alongside: stop signing with openssl in the test Makefile and dogfood a new standalone host-side signer.

Changes

  • crates/stage0/src/sig.rs: Domain enum + tag() namespace; verify() takes a Domain and checks the preimage.
  • crates/stage0/src/main.rs: the three admission sites pass their role (stage1.uki payload, stage1.args, stage1.manifest).
  • crates/stage0-sign (new): standalone host CLI, keygen + sign --domain. Randomness is /dev/urandom via std (no getrandom/libc/C toolchain); built for musl so the image's global static RUSTFLAGS don't break proc-macro compilation. Framing duplicated from sig.rs and pinned byte-for-byte to stage1's signer by a golden known-answer test.
  • Makefile: keygen + all three sign sites now call stage0-sign (no openssl). Signer var is STAGE0_SIGN, not SIGN, to avoid clobbering by the boot matrix's SIGN=1 flag. New make sign-bin / make sign-test.

Verification

  • make sign-test (golden KAT) passes.
  • make smoke-boot-x86_64 passes 5/5 (sha256, sign, sign_args, manifest, fallback).
  • Full chain: stage0 admits the UKI that stage1's deploy signs with stage1.uki, verified against the rebuilt boot.disk.

Pairs with lockboot/stage1's domain-separation PR; the shared golden vector guarantees both repos' framing agree.

🤖 Generated with Claude Code

Every ed25519 signature stage0 admits was over raw payload bytes with no
role tag, so a signature minted for one context was structurally valid in
another wherever the bytes were accepted (a signed-args blob replayed as a
payload signature, a _stage1 manifest as anything else). Bind each signature
to its exact role by signing a fixed 64-byte preimage
sha256(domain_tag) || sha256(message).
sig.rs gains a Domain enum (stage1.uki / stage1.args / stage1.manifest, the
three roles stage0 verifies) with a tag() namespace of the form
lockboot.v1.stage1.<kind>; verify() now takes a Domain and checks the
preimage. The three admission sites pass their role.
Stop signing with openssl in the test Makefile and dogfood a new standalone
host-side signer, crates/stage0-sign (keygen + domain-separated sign), so the
repo builds and tests without stage1's deploy tool. Its framing is duplicated
from sig.rs and pinned byte-for-byte to stage1's signer by a shared golden
known-answer test (make sign-test). Randomness for keygen is /dev/urandom via
std (no getrandom/libc), and it builds for the musl target so the image's
global static RUSTFLAGS do not break proc-macro compilation. The signer var is
STAGE0_SIGN, not SIGN, to avoid clobbering by the boot matrix's SIGN=1 flag.
make smoke-boot-x86_64 passes 5/5; the full chain (stage0 admits the UKI that
stage1's deploy signs with stage1.uki) verifies against the rebuilt boot.disk.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…USTFLAGS
The Dockerfile baked a global ENV RUSTFLAGS applying the static-link flags to
every target uniformly, including the host, where +crt-static makes proc-macro
dylibs impossible to build (a standalone host-gnu build fails "cannot produce
proc-macro"). Express the flags per target in .cargo/config.toml instead, so the
host is rust-lld only and never force-static.
stage0 had no .cargo/config.toml at all and relied entirely on the global env
(and, locally, the shared workspace config). Since CI checks this repo out alone
with CARGO_HOME redirected to an empty dir, it must be self-sufficient: add a
config with the musl targets (stage0-sign), the uefi targets (the measured
bootloader + payloads), and a gnu host section (rust-lld, no crt-static).
The uefi flags reproduce the measured binaries byte-for-byte: stage0.efi,
ena.efi and payload.efi hash identically before and after this change, so PCR4
and PCR14 are unchanged. sign-test (stage0-sign KAT) and smoke-boot 5/5 pass,
and the full chain still verifies the UKI (also byte-identical).
Pairs with the shared workspace config dropping its now-redundant musl rustflags
(cargo concatenates rustflags across config files, so keeping them in both the
workspace and this repo would double them and change the compiled bytes).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Release-prep hygiene over the domain-separation + rustflags changes, no logic:
- Makefile: the stage0-sign musl-target comment still explained itself by the
global RUSTFLAGS this branch removed (now inaccurate); state the real reason
(a fully static host binary).
- Dockerfile.build: collapse the 3-line comment narrating the removed ENV
RUSTFLAGS to a one-line statement of the rule.
- .cargo/config.toml: drop the "rather than a global RUSTFLAGS" migration framing.
- README: drop the "not openssl" aside.
- sig.rs / stage0-sign: the role names are stage1.* (the domain tags), not
_stage1.* (which is the JSON metadata key) -- fix the two doc comments.
Kept the openssl-genpkey PEM-format note: it documents a live interop property
(the key is openssl-compatible), not a removed dependency. stage0.efi hashes
identically and sign-test passes (comment-only).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@HarryR
HarryR merged commit 3565fe9 into mainJul 9, 2026
3 checks passed
@HarryR
HarryR deleted the domain-separated-sigs branch July 9, 2026 18:19
HarryR added a commit to lockboot/stage1 that referenced this pull request Jul 9, 2026
## What
Bind every ed25519 signature in the chain to its exact role. Signatures
are now over a fixed 64-byte preimage `sha256(domain_tag) ||
sha256(message)`, where the tag names one of the six contexts (2 hops x
{payload, args, manifest}): `lockboot.v1.stage1.uki` / `.stage1.args` /
`.stage1.manifest` / `.stage2.payload` / `.stage2.args` /
`.stage2.manifest`. A signature minted for one context is structurally
invalid in every other.
Alongside: stop signing with `openssl` in the test Makefile and dogfood
the real `deploy` signer, folding in key generation.
## Changes
- **`crates/ed25519-sign`**: `Domain` enum (all six roles) + `tag()`;
`sign()`/`verify()` take a `Domain` and operate on the preimage (callers
still pass the raw message). `pem_from_seed` / `pubkey_b64_from_seed`
promoted to `pub`. Tests: domain-separation rejection + a **golden
known-answer vector** stage0's verifier pins against.
- **`crates/stage1`, `crates/mkuki`**: verify/sign sites pass their
role.
- **`crates/deploy`**: new `keygen` (random ed25519 key via
`/dev/urandom`, no `getrandom`/`libc`) and low-level `sign --domain`;
`create` threads the right `Domain` into each artifact.
- **`Makefile`**: release key + every signature now via
`lockboot-deploy` (no `openssl`).
## Verification
- `make test-chain-x86_64 SIGN=1` / `SIGN=1 MANIFEST=1` / `SIGN=1
SIGN_ARGS=1` all pass against the domain-aware stage0 harness: both hops
verify and all six domains round-trip across the repo boundary.
Pairs with lockboot/stage0#8; the shared golden vector guarantees both
repos' framing agree.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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

Domain-separated signatures + standalone stage0-sign signer - #8

Merged
HarryR merged 3 commits into
mainfrom
domain-separated-sigs
Jul 9, 2026
Merged

Domain-separated signatures + standalone stage0-sign signer#8
HarryR merged 3 commits into
mainfrom
domain-separated-sigs

Conversation

@HarryR

Copy link
Copy Markdown
Contributor

What

Bind every ed25519 signature stage0 admits to its exact role. Signatures are now over a fixed 64-byte preimage sha256(domain_tag) || sha256(message), where the tag is one of lockboot.v1.stage1.uki / .stage1.args / .stage1.manifest. A signature minted for one role is structurally invalid in any other, closing the last of the mix-and-match malleability (the deferred tail of #2).

Alongside: stop signing with openssl in the test Makefile and dogfood a new standalone host-side signer.

Changes

  • crates/stage0/src/sig.rs: Domain enum + tag() namespace; verify() takes a Domain and checks the preimage.
  • crates/stage0/src/main.rs: the three admission sites pass their role (stage1.uki payload, stage1.args, stage1.manifest).
  • crates/stage0-sign (new): standalone host CLI, keygen + sign --domain. Randomness is /dev/urandom via std (no getrandom/libc/C toolchain); built for musl so the image's global static RUSTFLAGS don't break proc-macro compilation. Framing duplicated from sig.rs and pinned byte-for-byte to stage1's signer by a golden known-answer test.
  • Makefile: keygen + all three sign sites now call stage0-sign (no openssl). Signer var is STAGE0_SIGN, not SIGN, to avoid clobbering by the boot matrix's SIGN=1 flag. New make sign-bin / make sign-test.

Verification

  • make sign-test (golden KAT) passes.
  • make smoke-boot-x86_64 passes 5/5 (sha256, sign, sign_args, manifest, fallback).
  • Full chain: stage0 admits the UKI that stage1's deploy signs with stage1.uki, verified against the rebuilt boot.disk.

Pairs with lockboot/stage1's domain-separation PR; the shared golden vector guarantees both repos' framing agree.

🤖 Generated with Claude Code

Every ed25519 signature stage0 admits was over raw payload bytes with no
role tag, so a signature minted for one context was structurally valid in
another wherever the bytes were accepted (a signed-args blob replayed as a
payload signature, a _stage1 manifest as anything else). Bind each signature
to its exact role by signing a fixed 64-byte preimage
sha256(domain_tag) || sha256(message).
sig.rs gains a Domain enum (stage1.uki / stage1.args / stage1.manifest, the
three roles stage0 verifies) with a tag() namespace of the form
lockboot.v1.stage1.<kind>; verify() now takes a Domain and checks the
preimage. The three admission sites pass their role.
Stop signing with openssl in the test Makefile and dogfood a new standalone
host-side signer, crates/stage0-sign (keygen + domain-separated sign), so the
repo builds and tests without stage1's deploy tool. Its framing is duplicated
from sig.rs and pinned byte-for-byte to stage1's signer by a shared golden
known-answer test (make sign-test). Randomness for keygen is /dev/urandom via
std (no getrandom/libc), and it builds for the musl target so the image's
global static RUSTFLAGS do not break proc-macro compilation. The signer var is
STAGE0_SIGN, not SIGN, to avoid clobbering by the boot matrix's SIGN=1 flag.
make smoke-boot-x86_64 passes 5/5; the full chain (stage0 admits the UKI that
stage1's deploy signs with stage1.uki) verifies against the rebuilt boot.disk.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…USTFLAGS
The Dockerfile baked a global ENV RUSTFLAGS applying the static-link flags to
every target uniformly, including the host, where +crt-static makes proc-macro
dylibs impossible to build (a standalone host-gnu build fails "cannot produce
proc-macro"). Express the flags per target in .cargo/config.toml instead, so the
host is rust-lld only and never force-static.
stage0 had no .cargo/config.toml at all and relied entirely on the global env
(and, locally, the shared workspace config). Since CI checks this repo out alone
with CARGO_HOME redirected to an empty dir, it must be self-sufficient: add a
config with the musl targets (stage0-sign), the uefi targets (the measured
bootloader + payloads), and a gnu host section (rust-lld, no crt-static).
The uefi flags reproduce the measured binaries byte-for-byte: stage0.efi,
ena.efi and payload.efi hash identically before and after this change, so PCR4
and PCR14 are unchanged. sign-test (stage0-sign KAT) and smoke-boot 5/5 pass,
and the full chain still verifies the UKI (also byte-identical).
Pairs with the shared workspace config dropping its now-redundant musl rustflags
(cargo concatenates rustflags across config files, so keeping them in both the
workspace and this repo would double them and change the compiled bytes).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Release-prep hygiene over the domain-separation + rustflags changes, no logic:
- Makefile: the stage0-sign musl-target comment still explained itself by the
global RUSTFLAGS this branch removed (now inaccurate); state the real reason
(a fully static host binary).
- Dockerfile.build: collapse the 3-line comment narrating the removed ENV
RUSTFLAGS to a one-line statement of the rule.
- .cargo/config.toml: drop the "rather than a global RUSTFLAGS" migration framing.
- README: drop the "not openssl" aside.
- sig.rs / stage0-sign: the role names are stage1.* (the domain tags), not
_stage1.* (which is the JSON metadata key) -- fix the two doc comments.
Kept the openssl-genpkey PEM-format note: it documents a live interop property
(the key is openssl-compatible), not a removed dependency. stage0.efi hashes
identically and sign-test passes (comment-only).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@HarryR
HarryR merged commit 3565fe9 into mainJul 9, 2026
3 checks passed
@HarryR
HarryR deleted the domain-separated-sigs branch July 9, 2026 18:19
HarryR added a commit to lockboot/stage1 that referenced this pull request Jul 9, 2026
## What
Bind every ed25519 signature in the chain to its exact role. Signatures
are now over a fixed 64-byte preimage `sha256(domain_tag) ||
sha256(message)`, where the tag names one of the six contexts (2 hops x
{payload, args, manifest}): `lockboot.v1.stage1.uki` / `.stage1.args` /
`.stage1.manifest` / `.stage2.payload` / `.stage2.args` /
`.stage2.manifest`. A signature minted for one context is structurally
invalid in every other.
Alongside: stop signing with `openssl` in the test Makefile and dogfood
the real `deploy` signer, folding in key generation.
## Changes
- **`crates/ed25519-sign`**: `Domain` enum (all six roles) + `tag()`;
`sign()`/`verify()` take a `Domain` and operate on the preimage (callers
still pass the raw message). `pem_from_seed` / `pubkey_b64_from_seed`
promoted to `pub`. Tests: domain-separation rejection + a **golden
known-answer vector** stage0's verifier pins against.
- **`crates/stage1`, `crates/mkuki`**: verify/sign sites pass their
role.
- **`crates/deploy`**: new `keygen` (random ed25519 key via
`/dev/urandom`, no `getrandom`/`libc`) and low-level `sign --domain`;
`create` threads the right `Domain` into each artifact.
- **`Makefile`**: release key + every signature now via
`lockboot-deploy` (no `openssl`).
## Verification
- `make test-chain-x86_64 SIGN=1` / `SIGN=1 MANIFEST=1` / `SIGN=1
SIGN_ARGS=1` all pass against the domain-aware stage0 harness: both hops
verify and all six domains round-trip across the repo boundary.
Pairs with lockboot/stage0#8; the shared golden vector guarantees both
repos' framing agree.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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

Domain-separated signatures + standalone stage0-sign signer - #8

Merged
HarryR merged 3 commits into
mainfrom
domain-separated-sigs
Jul 9, 2026
Merged

Domain-separated signatures + standalone stage0-sign signer#8
HarryR merged 3 commits into
mainfrom
domain-separated-sigs

Conversation

@HarryR

Copy link
Copy Markdown
Contributor

What

Bind every ed25519 signature stage0 admits to its exact role. Signatures are now over a fixed 64-byte preimage sha256(domain_tag) || sha256(message), where the tag is one of lockboot.v1.stage1.uki / .stage1.args / .stage1.manifest. A signature minted for one role is structurally invalid in any other, closing the last of the mix-and-match malleability (the deferred tail of #2).

Alongside: stop signing with openssl in the test Makefile and dogfood a new standalone host-side signer.

Changes

  • crates/stage0/src/sig.rs: Domain enum + tag() namespace; verify() takes a Domain and checks the preimage.
  • crates/stage0/src/main.rs: the three admission sites pass their role (stage1.uki payload, stage1.args, stage1.manifest).
  • crates/stage0-sign (new): standalone host CLI, keygen + sign --domain. Randomness is /dev/urandom via std (no getrandom/libc/C toolchain); built for musl so the image's global static RUSTFLAGS don't break proc-macro compilation. Framing duplicated from sig.rs and pinned byte-for-byte to stage1's signer by a golden known-answer test.
  • Makefile: keygen + all three sign sites now call stage0-sign (no openssl). Signer var is STAGE0_SIGN, not SIGN, to avoid clobbering by the boot matrix's SIGN=1 flag. New make sign-bin / make sign-test.

Verification

  • make sign-test (golden KAT) passes.
  • make smoke-boot-x86_64 passes 5/5 (sha256, sign, sign_args, manifest, fallback).
  • Full chain: stage0 admits the UKI that stage1's deploy signs with stage1.uki, verified against the rebuilt boot.disk.

Pairs with lockboot/stage1's domain-separation PR; the shared golden vector guarantees both repos' framing agree.

🤖 Generated with Claude Code

Every ed25519 signature stage0 admits was over raw payload bytes with no
role tag, so a signature minted for one context was structurally valid in
another wherever the bytes were accepted (a signed-args blob replayed as a
payload signature, a _stage1 manifest as anything else). Bind each signature
to its exact role by signing a fixed 64-byte preimage
sha256(domain_tag) || sha256(message).
sig.rs gains a Domain enum (stage1.uki / stage1.args / stage1.manifest, the
three roles stage0 verifies) with a tag() namespace of the form
lockboot.v1.stage1.<kind>; verify() now takes a Domain and checks the
preimage. The three admission sites pass their role.
Stop signing with openssl in the test Makefile and dogfood a new standalone
host-side signer, crates/stage0-sign (keygen + domain-separated sign), so the
repo builds and tests without stage1's deploy tool. Its framing is duplicated
from sig.rs and pinned byte-for-byte to stage1's signer by a shared golden
known-answer test (make sign-test). Randomness for keygen is /dev/urandom via
std (no getrandom/libc), and it builds for the musl target so the image's
global static RUSTFLAGS do not break proc-macro compilation. The signer var is
STAGE0_SIGN, not SIGN, to avoid clobbering by the boot matrix's SIGN=1 flag.
make smoke-boot-x86_64 passes 5/5; the full chain (stage0 admits the UKI that
stage1's deploy signs with stage1.uki) verifies against the rebuilt boot.disk.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…USTFLAGS
The Dockerfile baked a global ENV RUSTFLAGS applying the static-link flags to
every target uniformly, including the host, where +crt-static makes proc-macro
dylibs impossible to build (a standalone host-gnu build fails "cannot produce
proc-macro"). Express the flags per target in .cargo/config.toml instead, so the
host is rust-lld only and never force-static.
stage0 had no .cargo/config.toml at all and relied entirely on the global env
(and, locally, the shared workspace config). Since CI checks this repo out alone
with CARGO_HOME redirected to an empty dir, it must be self-sufficient: add a
config with the musl targets (stage0-sign), the uefi targets (the measured
bootloader + payloads), and a gnu host section (rust-lld, no crt-static).
The uefi flags reproduce the measured binaries byte-for-byte: stage0.efi,
ena.efi and payload.efi hash identically before and after this change, so PCR4
and PCR14 are unchanged. sign-test (stage0-sign KAT) and smoke-boot 5/5 pass,
and the full chain still verifies the UKI (also byte-identical).
Pairs with the shared workspace config dropping its now-redundant musl rustflags
(cargo concatenates rustflags across config files, so keeping them in both the
workspace and this repo would double them and change the compiled bytes).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Release-prep hygiene over the domain-separation + rustflags changes, no logic:
- Makefile: the stage0-sign musl-target comment still explained itself by the
global RUSTFLAGS this branch removed (now inaccurate); state the real reason
(a fully static host binary).
- Dockerfile.build: collapse the 3-line comment narrating the removed ENV
RUSTFLAGS to a one-line statement of the rule.
- .cargo/config.toml: drop the "rather than a global RUSTFLAGS" migration framing.
- README: drop the "not openssl" aside.
- sig.rs / stage0-sign: the role names are stage1.* (the domain tags), not
_stage1.* (which is the JSON metadata key) -- fix the two doc comments.
Kept the openssl-genpkey PEM-format note: it documents a live interop property
(the key is openssl-compatible), not a removed dependency. stage0.efi hashes
identically and sign-test passes (comment-only).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@HarryR
HarryR merged commit 3565fe9 into mainJul 9, 2026
3 checks passed
@HarryR
HarryR deleted the domain-separated-sigs branch July 9, 2026 18:19
HarryR added a commit to lockboot/stage1 that referenced this pull request Jul 9, 2026
## What
Bind every ed25519 signature in the chain to its exact role. Signatures
are now over a fixed 64-byte preimage `sha256(domain_tag) ||
sha256(message)`, where the tag names one of the six contexts (2 hops x
{payload, args, manifest}): `lockboot.v1.stage1.uki` / `.stage1.args` /
`.stage1.manifest` / `.stage2.payload` / `.stage2.args` /
`.stage2.manifest`. A signature minted for one context is structurally
invalid in every other.
Alongside: stop signing with `openssl` in the test Makefile and dogfood
the real `deploy` signer, folding in key generation.
## Changes
- **`crates/ed25519-sign`**: `Domain` enum (all six roles) + `tag()`;
`sign()`/`verify()` take a `Domain` and operate on the preimage (callers
still pass the raw message). `pem_from_seed` / `pubkey_b64_from_seed`
promoted to `pub`. Tests: domain-separation rejection + a **golden
known-answer vector** stage0's verifier pins against.
- **`crates/stage1`, `crates/mkuki`**: verify/sign sites pass their
role.
- **`crates/deploy`**: new `keygen` (random ed25519 key via
`/dev/urandom`, no `getrandom`/`libc`) and low-level `sign --domain`;
`create` threads the right `Domain` into each artifact.
- **`Makefile`**: release key + every signature now via
`lockboot-deploy` (no `openssl`).
## Verification
- `make test-chain-x86_64 SIGN=1` / `SIGN=1 MANIFEST=1` / `SIGN=1
SIGN_ARGS=1` all pass against the domain-aware stage0 harness: both hops
verify and all six domains round-trip across the repo boundary.
Pairs with lockboot/stage0#8; the shared golden vector guarantees both
repos' framing agree.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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

Domain-separated signatures + standalone stage0-sign signer - #8

Merged
HarryR merged 3 commits into
mainfrom
domain-separated-sigs
Jul 9, 2026
Merged

Domain-separated signatures + standalone stage0-sign signer#8
HarryR merged 3 commits into
mainfrom
domain-separated-sigs

Conversation

@HarryR

Copy link
Copy Markdown
Contributor

What

Bind every ed25519 signature stage0 admits to its exact role. Signatures are now over a fixed 64-byte preimage sha256(domain_tag) || sha256(message), where the tag is one of lockboot.v1.stage1.uki / .stage1.args / .stage1.manifest. A signature minted for one role is structurally invalid in any other, closing the last of the mix-and-match malleability (the deferred tail of #2).

Alongside: stop signing with openssl in the test Makefile and dogfood a new standalone host-side signer.

Changes

  • crates/stage0/src/sig.rs: Domain enum + tag() namespace; verify() takes a Domain and checks the preimage.
  • crates/stage0/src/main.rs: the three admission sites pass their role (stage1.uki payload, stage1.args, stage1.manifest).
  • crates/stage0-sign (new): standalone host CLI, keygen + sign --domain. Randomness is /dev/urandom via std (no getrandom/libc/C toolchain); built for musl so the image's global static RUSTFLAGS don't break proc-macro compilation. Framing duplicated from sig.rs and pinned byte-for-byte to stage1's signer by a golden known-answer test.
  • Makefile: keygen + all three sign sites now call stage0-sign (no openssl). Signer var is STAGE0_SIGN, not SIGN, to avoid clobbering by the boot matrix's SIGN=1 flag. New make sign-bin / make sign-test.

Verification

  • make sign-test (golden KAT) passes.
  • make smoke-boot-x86_64 passes 5/5 (sha256, sign, sign_args, manifest, fallback).
  • Full chain: stage0 admits the UKI that stage1's deploy signs with stage1.uki, verified against the rebuilt boot.disk.

Pairs with lockboot/stage1's domain-separation PR; the shared golden vector guarantees both repos' framing agree.

🤖 Generated with Claude Code

Every ed25519 signature stage0 admits was over raw payload bytes with no
role tag, so a signature minted for one context was structurally valid in
another wherever the bytes were accepted (a signed-args blob replayed as a
payload signature, a _stage1 manifest as anything else). Bind each signature
to its exact role by signing a fixed 64-byte preimage
sha256(domain_tag) || sha256(message).
sig.rs gains a Domain enum (stage1.uki / stage1.args / stage1.manifest, the
three roles stage0 verifies) with a tag() namespace of the form
lockboot.v1.stage1.<kind>; verify() now takes a Domain and checks the
preimage. The three admission sites pass their role.
Stop signing with openssl in the test Makefile and dogfood a new standalone
host-side signer, crates/stage0-sign (keygen + domain-separated sign), so the
repo builds and tests without stage1's deploy tool. Its framing is duplicated
from sig.rs and pinned byte-for-byte to stage1's signer by a shared golden
known-answer test (make sign-test). Randomness for keygen is /dev/urandom via
std (no getrandom/libc), and it builds for the musl target so the image's
global static RUSTFLAGS do not break proc-macro compilation. The signer var is
STAGE0_SIGN, not SIGN, to avoid clobbering by the boot matrix's SIGN=1 flag.
make smoke-boot-x86_64 passes 5/5; the full chain (stage0 admits the UKI that
stage1's deploy signs with stage1.uki) verifies against the rebuilt boot.disk.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…USTFLAGS
The Dockerfile baked a global ENV RUSTFLAGS applying the static-link flags to
every target uniformly, including the host, where +crt-static makes proc-macro
dylibs impossible to build (a standalone host-gnu build fails "cannot produce
proc-macro"). Express the flags per target in .cargo/config.toml instead, so the
host is rust-lld only and never force-static.
stage0 had no .cargo/config.toml at all and relied entirely on the global env
(and, locally, the shared workspace config). Since CI checks this repo out alone
with CARGO_HOME redirected to an empty dir, it must be self-sufficient: add a
config with the musl targets (stage0-sign), the uefi targets (the measured
bootloader + payloads), and a gnu host section (rust-lld, no crt-static).
The uefi flags reproduce the measured binaries byte-for-byte: stage0.efi,
ena.efi and payload.efi hash identically before and after this change, so PCR4
and PCR14 are unchanged. sign-test (stage0-sign KAT) and smoke-boot 5/5 pass,
and the full chain still verifies the UKI (also byte-identical).
Pairs with the shared workspace config dropping its now-redundant musl rustflags
(cargo concatenates rustflags across config files, so keeping them in both the
workspace and this repo would double them and change the compiled bytes).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Release-prep hygiene over the domain-separation + rustflags changes, no logic:
- Makefile: the stage0-sign musl-target comment still explained itself by the
global RUSTFLAGS this branch removed (now inaccurate); state the real reason
(a fully static host binary).
- Dockerfile.build: collapse the 3-line comment narrating the removed ENV
RUSTFLAGS to a one-line statement of the rule.
- .cargo/config.toml: drop the "rather than a global RUSTFLAGS" migration framing.
- README: drop the "not openssl" aside.
- sig.rs / stage0-sign: the role names are stage1.* (the domain tags), not
_stage1.* (which is the JSON metadata key) -- fix the two doc comments.
Kept the openssl-genpkey PEM-format note: it documents a live interop property
(the key is openssl-compatible), not a removed dependency. stage0.efi hashes
identically and sign-test passes (comment-only).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@HarryR
HarryR merged commit 3565fe9 into mainJul 9, 2026
3 checks passed
@HarryR
HarryR deleted the domain-separated-sigs branch July 9, 2026 18:19
HarryR added a commit to lockboot/stage1 that referenced this pull request Jul 9, 2026
## What
Bind every ed25519 signature in the chain to its exact role. Signatures
are now over a fixed 64-byte preimage `sha256(domain_tag) ||
sha256(message)`, where the tag names one of the six contexts (2 hops x
{payload, args, manifest}): `lockboot.v1.stage1.uki` / `.stage1.args` /
`.stage1.manifest` / `.stage2.payload` / `.stage2.args` /
`.stage2.manifest`. A signature minted for one context is structurally
invalid in every other.
Alongside: stop signing with `openssl` in the test Makefile and dogfood
the real `deploy` signer, folding in key generation.
## Changes
- **`crates/ed25519-sign`**: `Domain` enum (all six roles) + `tag()`;
`sign()`/`verify()` take a `Domain` and operate on the preimage (callers
still pass the raw message). `pem_from_seed` / `pubkey_b64_from_seed`
promoted to `pub`. Tests: domain-separation rejection + a **golden
known-answer vector** stage0's verifier pins against.
- **`crates/stage1`, `crates/mkuki`**: verify/sign sites pass their
role.
- **`crates/deploy`**: new `keygen` (random ed25519 key via
`/dev/urandom`, no `getrandom`/`libc`) and low-level `sign --domain`;
`create` threads the right `Domain` into each artifact.
- **`Makefile`**: release key + every signature now via
`lockboot-deploy` (no `openssl`).
## Verification
- `make test-chain-x86_64 SIGN=1` / `SIGN=1 MANIFEST=1` / `SIGN=1
SIGN_ARGS=1` all pass against the domain-aware stage0 harness: both hops
verify and all six domains round-trip across the repo boundary.
Pairs with lockboot/stage0#8; the shared golden vector guarantees both
repos' framing agree.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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

Domain-separated signatures + standalone stage0-sign signer - #8

Merged
HarryR merged 3 commits into
mainfrom
domain-separated-sigs
Jul 9, 2026
Merged

Domain-separated signatures + standalone stage0-sign signer#8
HarryR merged 3 commits into
mainfrom
domain-separated-sigs

Conversation

@HarryR

Copy link
Copy Markdown
Contributor

What

Bind every ed25519 signature stage0 admits to its exact role. Signatures are now over a fixed 64-byte preimage sha256(domain_tag) || sha256(message), where the tag is one of lockboot.v1.stage1.uki / .stage1.args / .stage1.manifest. A signature minted for one role is structurally invalid in any other, closing the last of the mix-and-match malleability (the deferred tail of #2).

Alongside: stop signing with openssl in the test Makefile and dogfood a new standalone host-side signer.

Changes

  • crates/stage0/src/sig.rs: Domain enum + tag() namespace; verify() takes a Domain and checks the preimage.
  • crates/stage0/src/main.rs: the three admission sites pass their role (stage1.uki payload, stage1.args, stage1.manifest).
  • crates/stage0-sign (new): standalone host CLI, keygen + sign --domain. Randomness is /dev/urandom via std (no getrandom/libc/C toolchain); built for musl so the image's global static RUSTFLAGS don't break proc-macro compilation. Framing duplicated from sig.rs and pinned byte-for-byte to stage1's signer by a golden known-answer test.
  • Makefile: keygen + all three sign sites now call stage0-sign (no openssl). Signer var is STAGE0_SIGN, not SIGN, to avoid clobbering by the boot matrix's SIGN=1 flag. New make sign-bin / make sign-test.

Verification

  • make sign-test (golden KAT) passes.
  • make smoke-boot-x86_64 passes 5/5 (sha256, sign, sign_args, manifest, fallback).
  • Full chain: stage0 admits the UKI that stage1's deploy signs with stage1.uki, verified against the rebuilt boot.disk.

Pairs with lockboot/stage1's domain-separation PR; the shared golden vector guarantees both repos' framing agree.

🤖 Generated with Claude Code

Every ed25519 signature stage0 admits was over raw payload bytes with no
role tag, so a signature minted for one context was structurally valid in
another wherever the bytes were accepted (a signed-args blob replayed as a
payload signature, a _stage1 manifest as anything else). Bind each signature
to its exact role by signing a fixed 64-byte preimage
sha256(domain_tag) || sha256(message).
sig.rs gains a Domain enum (stage1.uki / stage1.args / stage1.manifest, the
three roles stage0 verifies) with a tag() namespace of the form
lockboot.v1.stage1.<kind>; verify() now takes a Domain and checks the
preimage. The three admission sites pass their role.
Stop signing with openssl in the test Makefile and dogfood a new standalone
host-side signer, crates/stage0-sign (keygen + domain-separated sign), so the
repo builds and tests without stage1's deploy tool. Its framing is duplicated
from sig.rs and pinned byte-for-byte to stage1's signer by a shared golden
known-answer test (make sign-test). Randomness for keygen is /dev/urandom via
std (no getrandom/libc), and it builds for the musl target so the image's
global static RUSTFLAGS do not break proc-macro compilation. The signer var is
STAGE0_SIGN, not SIGN, to avoid clobbering by the boot matrix's SIGN=1 flag.
make smoke-boot-x86_64 passes 5/5; the full chain (stage0 admits the UKI that
stage1's deploy signs with stage1.uki) verifies against the rebuilt boot.disk.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…USTFLAGS
The Dockerfile baked a global ENV RUSTFLAGS applying the static-link flags to
every target uniformly, including the host, where +crt-static makes proc-macro
dylibs impossible to build (a standalone host-gnu build fails "cannot produce
proc-macro"). Express the flags per target in .cargo/config.toml instead, so the
host is rust-lld only and never force-static.
stage0 had no .cargo/config.toml at all and relied entirely on the global env
(and, locally, the shared workspace config). Since CI checks this repo out alone
with CARGO_HOME redirected to an empty dir, it must be self-sufficient: add a
config with the musl targets (stage0-sign), the uefi targets (the measured
bootloader + payloads), and a gnu host section (rust-lld, no crt-static).
The uefi flags reproduce the measured binaries byte-for-byte: stage0.efi,
ena.efi and payload.efi hash identically before and after this change, so PCR4
and PCR14 are unchanged. sign-test (stage0-sign KAT) and smoke-boot 5/5 pass,
and the full chain still verifies the UKI (also byte-identical).
Pairs with the shared workspace config dropping its now-redundant musl rustflags
(cargo concatenates rustflags across config files, so keeping them in both the
workspace and this repo would double them and change the compiled bytes).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Release-prep hygiene over the domain-separation + rustflags changes, no logic:
- Makefile: the stage0-sign musl-target comment still explained itself by the
global RUSTFLAGS this branch removed (now inaccurate); state the real reason
(a fully static host binary).
- Dockerfile.build: collapse the 3-line comment narrating the removed ENV
RUSTFLAGS to a one-line statement of the rule.
- .cargo/config.toml: drop the "rather than a global RUSTFLAGS" migration framing.
- README: drop the "not openssl" aside.
- sig.rs / stage0-sign: the role names are stage1.* (the domain tags), not
_stage1.* (which is the JSON metadata key) -- fix the two doc comments.
Kept the openssl-genpkey PEM-format note: it documents a live interop property
(the key is openssl-compatible), not a removed dependency. stage0.efi hashes
identically and sign-test passes (comment-only).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@HarryR
HarryR merged commit 3565fe9 into mainJul 9, 2026
3 checks passed
@HarryR
HarryR deleted the domain-separated-sigs branch July 9, 2026 18:19
HarryR added a commit to lockboot/stage1 that referenced this pull request Jul 9, 2026
## What
Bind every ed25519 signature in the chain to its exact role. Signatures
are now over a fixed 64-byte preimage `sha256(domain_tag) ||
sha256(message)`, where the tag names one of the six contexts (2 hops x
{payload, args, manifest}): `lockboot.v1.stage1.uki` / `.stage1.args` /
`.stage1.manifest` / `.stage2.payload` / `.stage2.args` /
`.stage2.manifest`. A signature minted for one context is structurally
invalid in every other.
Alongside: stop signing with `openssl` in the test Makefile and dogfood
the real `deploy` signer, folding in key generation.
## Changes
- **`crates/ed25519-sign`**: `Domain` enum (all six roles) + `tag()`;
`sign()`/`verify()` take a `Domain` and operate on the preimage (callers
still pass the raw message). `pem_from_seed` / `pubkey_b64_from_seed`
promoted to `pub`. Tests: domain-separation rejection + a **golden
known-answer vector** stage0's verifier pins against.
- **`crates/stage1`, `crates/mkuki`**: verify/sign sites pass their
role.
- **`crates/deploy`**: new `keygen` (random ed25519 key via
`/dev/urandom`, no `getrandom`/`libc`) and low-level `sign --domain`;
`create` threads the right `Domain` into each artifact.
- **`Makefile`**: release key + every signature now via
`lockboot-deploy` (no `openssl`).
## Verification
- `make test-chain-x86_64 SIGN=1` / `SIGN=1 MANIFEST=1` / `SIGN=1
SIGN_ARGS=1` all pass against the domain-aware stage0 harness: both hops
verify and all six domains round-trip across the repo boundary.
Pairs with lockboot/stage0#8; the shared golden vector guarantees both
repos' framing agree.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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

Domain-separated signatures + standalone stage0-sign signer - #8

Merged
HarryR merged 3 commits into
mainfrom
domain-separated-sigs
Jul 9, 2026
Merged

Domain-separated signatures + standalone stage0-sign signer#8
HarryR merged 3 commits into
mainfrom
domain-separated-sigs

Conversation

@HarryR

Copy link
Copy Markdown
Contributor

What

Bind every ed25519 signature stage0 admits to its exact role. Signatures are now over a fixed 64-byte preimage sha256(domain_tag) || sha256(message), where the tag is one of lockboot.v1.stage1.uki / .stage1.args / .stage1.manifest. A signature minted for one role is structurally invalid in any other, closing the last of the mix-and-match malleability (the deferred tail of #2).

Alongside: stop signing with openssl in the test Makefile and dogfood a new standalone host-side signer.

Changes

  • crates/stage0/src/sig.rs: Domain enum + tag() namespace; verify() takes a Domain and checks the preimage.
  • crates/stage0/src/main.rs: the three admission sites pass their role (stage1.uki payload, stage1.args, stage1.manifest).
  • crates/stage0-sign (new): standalone host CLI, keygen + sign --domain. Randomness is /dev/urandom via std (no getrandom/libc/C toolchain); built for musl so the image's global static RUSTFLAGS don't break proc-macro compilation. Framing duplicated from sig.rs and pinned byte-for-byte to stage1's signer by a golden known-answer test.
  • Makefile: keygen + all three sign sites now call stage0-sign (no openssl). Signer var is STAGE0_SIGN, not SIGN, to avoid clobbering by the boot matrix's SIGN=1 flag. New make sign-bin / make sign-test.

Verification

  • make sign-test (golden KAT) passes.
  • make smoke-boot-x86_64 passes 5/5 (sha256, sign, sign_args, manifest, fallback).
  • Full chain: stage0 admits the UKI that stage1's deploy signs with stage1.uki, verified against the rebuilt boot.disk.

Pairs with lockboot/stage1's domain-separation PR; the shared golden vector guarantees both repos' framing agree.

🤖 Generated with Claude Code

Every ed25519 signature stage0 admits was over raw payload bytes with no
role tag, so a signature minted for one context was structurally valid in
another wherever the bytes were accepted (a signed-args blob replayed as a
payload signature, a _stage1 manifest as anything else). Bind each signature
to its exact role by signing a fixed 64-byte preimage
sha256(domain_tag) || sha256(message).
sig.rs gains a Domain enum (stage1.uki / stage1.args / stage1.manifest, the
three roles stage0 verifies) with a tag() namespace of the form
lockboot.v1.stage1.<kind>; verify() now takes a Domain and checks the
preimage. The three admission sites pass their role.
Stop signing with openssl in the test Makefile and dogfood a new standalone
host-side signer, crates/stage0-sign (keygen + domain-separated sign), so the
repo builds and tests without stage1's deploy tool. Its framing is duplicated
from sig.rs and pinned byte-for-byte to stage1's signer by a shared golden
known-answer test (make sign-test). Randomness for keygen is /dev/urandom via
std (no getrandom/libc), and it builds for the musl target so the image's
global static RUSTFLAGS do not break proc-macro compilation. The signer var is
STAGE0_SIGN, not SIGN, to avoid clobbering by the boot matrix's SIGN=1 flag.
make smoke-boot-x86_64 passes 5/5; the full chain (stage0 admits the UKI that
stage1's deploy signs with stage1.uki) verifies against the rebuilt boot.disk.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…USTFLAGS
The Dockerfile baked a global ENV RUSTFLAGS applying the static-link flags to
every target uniformly, including the host, where +crt-static makes proc-macro
dylibs impossible to build (a standalone host-gnu build fails "cannot produce
proc-macro"). Express the flags per target in .cargo/config.toml instead, so the
host is rust-lld only and never force-static.
stage0 had no .cargo/config.toml at all and relied entirely on the global env
(and, locally, the shared workspace config). Since CI checks this repo out alone
with CARGO_HOME redirected to an empty dir, it must be self-sufficient: add a
config with the musl targets (stage0-sign), the uefi targets (the measured
bootloader + payloads), and a gnu host section (rust-lld, no crt-static).
The uefi flags reproduce the measured binaries byte-for-byte: stage0.efi,
ena.efi and payload.efi hash identically before and after this change, so PCR4
and PCR14 are unchanged. sign-test (stage0-sign KAT) and smoke-boot 5/5 pass,
and the full chain still verifies the UKI (also byte-identical).
Pairs with the shared workspace config dropping its now-redundant musl rustflags
(cargo concatenates rustflags across config files, so keeping them in both the
workspace and this repo would double them and change the compiled bytes).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Release-prep hygiene over the domain-separation + rustflags changes, no logic:
- Makefile: the stage0-sign musl-target comment still explained itself by the
global RUSTFLAGS this branch removed (now inaccurate); state the real reason
(a fully static host binary).
- Dockerfile.build: collapse the 3-line comment narrating the removed ENV
RUSTFLAGS to a one-line statement of the rule.
- .cargo/config.toml: drop the "rather than a global RUSTFLAGS" migration framing.
- README: drop the "not openssl" aside.
- sig.rs / stage0-sign: the role names are stage1.* (the domain tags), not
_stage1.* (which is the JSON metadata key) -- fix the two doc comments.
Kept the openssl-genpkey PEM-format note: it documents a live interop property
(the key is openssl-compatible), not a removed dependency. stage0.efi hashes
identically and sign-test passes (comment-only).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@HarryR
HarryR merged commit 3565fe9 into mainJul 9, 2026
3 checks passed
@HarryR
HarryR deleted the domain-separated-sigs branch July 9, 2026 18:19
HarryR added a commit to lockboot/stage1 that referenced this pull request Jul 9, 2026
## What
Bind every ed25519 signature in the chain to its exact role. Signatures
are now over a fixed 64-byte preimage `sha256(domain_tag) ||
sha256(message)`, where the tag names one of the six contexts (2 hops x
{payload, args, manifest}): `lockboot.v1.stage1.uki` / `.stage1.args` /
`.stage1.manifest` / `.stage2.payload` / `.stage2.args` /
`.stage2.manifest`. A signature minted for one context is structurally
invalid in every other.
Alongside: stop signing with `openssl` in the test Makefile and dogfood
the real `deploy` signer, folding in key generation.
## Changes
- **`crates/ed25519-sign`**: `Domain` enum (all six roles) + `tag()`;
`sign()`/`verify()` take a `Domain` and operate on the preimage (callers
still pass the raw message). `pem_from_seed` / `pubkey_b64_from_seed`
promoted to `pub`. Tests: domain-separation rejection + a **golden
known-answer vector** stage0's verifier pins against.
- **`crates/stage1`, `crates/mkuki`**: verify/sign sites pass their
role.
- **`crates/deploy`**: new `keygen` (random ed25519 key via
`/dev/urandom`, no `getrandom`/`libc`) and low-level `sign --domain`;
`create` threads the right `Domain` into each artifact.
- **`Makefile`**: release key + every signature now via
`lockboot-deploy` (no `openssl`).
## Verification
- `make test-chain-x86_64 SIGN=1` / `SIGN=1 MANIFEST=1` / `SIGN=1
SIGN_ARGS=1` all pass against the domain-aware stage0 harness: both hops
verify and all six domains round-trip across the repo boundary.
Pairs with lockboot/stage0#8; the shared golden vector guarantees both
repos' framing agree.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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

Domain-separated signatures + standalone stage0-sign signer - #8

Merged
HarryR merged 3 commits into
mainfrom
domain-separated-sigs
Jul 9, 2026
Merged

Domain-separated signatures + standalone stage0-sign signer#8
HarryR merged 3 commits into
mainfrom
domain-separated-sigs

Conversation

@HarryR

Copy link
Copy Markdown
Contributor

What

Bind every ed25519 signature stage0 admits to its exact role. Signatures are now over a fixed 64-byte preimage sha256(domain_tag) || sha256(message), where the tag is one of lockboot.v1.stage1.uki / .stage1.args / .stage1.manifest. A signature minted for one role is structurally invalid in any other, closing the last of the mix-and-match malleability (the deferred tail of #2).

Alongside: stop signing with openssl in the test Makefile and dogfood a new standalone host-side signer.

Changes

  • crates/stage0/src/sig.rs: Domain enum + tag() namespace; verify() takes a Domain and checks the preimage.
  • crates/stage0/src/main.rs: the three admission sites pass their role (stage1.uki payload, stage1.args, stage1.manifest).
  • crates/stage0-sign (new): standalone host CLI, keygen + sign --domain. Randomness is /dev/urandom via std (no getrandom/libc/C toolchain); built for musl so the image's global static RUSTFLAGS don't break proc-macro compilation. Framing duplicated from sig.rs and pinned byte-for-byte to stage1's signer by a golden known-answer test.
  • Makefile: keygen + all three sign sites now call stage0-sign (no openssl). Signer var is STAGE0_SIGN, not SIGN, to avoid clobbering by the boot matrix's SIGN=1 flag. New make sign-bin / make sign-test.

Verification

  • make sign-test (golden KAT) passes.
  • make smoke-boot-x86_64 passes 5/5 (sha256, sign, sign_args, manifest, fallback).
  • Full chain: stage0 admits the UKI that stage1's deploy signs with stage1.uki, verified against the rebuilt boot.disk.

Pairs with lockboot/stage1's domain-separation PR; the shared golden vector guarantees both repos' framing agree.

🤖 Generated with Claude Code

Every ed25519 signature stage0 admits was over raw payload bytes with no
role tag, so a signature minted for one context was structurally valid in
another wherever the bytes were accepted (a signed-args blob replayed as a
payload signature, a _stage1 manifest as anything else). Bind each signature
to its exact role by signing a fixed 64-byte preimage
sha256(domain_tag) || sha256(message).
sig.rs gains a Domain enum (stage1.uki / stage1.args / stage1.manifest, the
three roles stage0 verifies) with a tag() namespace of the form
lockboot.v1.stage1.<kind>; verify() now takes a Domain and checks the
preimage. The three admission sites pass their role.
Stop signing with openssl in the test Makefile and dogfood a new standalone
host-side signer, crates/stage0-sign (keygen + domain-separated sign), so the
repo builds and tests without stage1's deploy tool. Its framing is duplicated
from sig.rs and pinned byte-for-byte to stage1's signer by a shared golden
known-answer test (make sign-test). Randomness for keygen is /dev/urandom via
std (no getrandom/libc), and it builds for the musl target so the image's
global static RUSTFLAGS do not break proc-macro compilation. The signer var is
STAGE0_SIGN, not SIGN, to avoid clobbering by the boot matrix's SIGN=1 flag.
make smoke-boot-x86_64 passes 5/5; the full chain (stage0 admits the UKI that
stage1's deploy signs with stage1.uki) verifies against the rebuilt boot.disk.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…USTFLAGS
The Dockerfile baked a global ENV RUSTFLAGS applying the static-link flags to
every target uniformly, including the host, where +crt-static makes proc-macro
dylibs impossible to build (a standalone host-gnu build fails "cannot produce
proc-macro"). Express the flags per target in .cargo/config.toml instead, so the
host is rust-lld only and never force-static.
stage0 had no .cargo/config.toml at all and relied entirely on the global env
(and, locally, the shared workspace config). Since CI checks this repo out alone
with CARGO_HOME redirected to an empty dir, it must be self-sufficient: add a
config with the musl targets (stage0-sign), the uefi targets (the measured
bootloader + payloads), and a gnu host section (rust-lld, no crt-static).
The uefi flags reproduce the measured binaries byte-for-byte: stage0.efi,
ena.efi and payload.efi hash identically before and after this change, so PCR4
and PCR14 are unchanged. sign-test (stage0-sign KAT) and smoke-boot 5/5 pass,
and the full chain still verifies the UKI (also byte-identical).
Pairs with the shared workspace config dropping its now-redundant musl rustflags
(cargo concatenates rustflags across config files, so keeping them in both the
workspace and this repo would double them and change the compiled bytes).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Release-prep hygiene over the domain-separation + rustflags changes, no logic:
- Makefile: the stage0-sign musl-target comment still explained itself by the
global RUSTFLAGS this branch removed (now inaccurate); state the real reason
(a fully static host binary).
- Dockerfile.build: collapse the 3-line comment narrating the removed ENV
RUSTFLAGS to a one-line statement of the rule.
- .cargo/config.toml: drop the "rather than a global RUSTFLAGS" migration framing.
- README: drop the "not openssl" aside.
- sig.rs / stage0-sign: the role names are stage1.* (the domain tags), not
_stage1.* (which is the JSON metadata key) -- fix the two doc comments.
Kept the openssl-genpkey PEM-format note: it documents a live interop property
(the key is openssl-compatible), not a removed dependency. stage0.efi hashes
identically and sign-test passes (comment-only).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@HarryR
HarryR merged commit 3565fe9 into mainJul 9, 2026
3 checks passed
@HarryR
HarryR deleted the domain-separated-sigs branch July 9, 2026 18:19
HarryR added a commit to lockboot/stage1 that referenced this pull request Jul 9, 2026
## What
Bind every ed25519 signature in the chain to its exact role. Signatures
are now over a fixed 64-byte preimage `sha256(domain_tag) ||
sha256(message)`, where the tag names one of the six contexts (2 hops x
{payload, args, manifest}): `lockboot.v1.stage1.uki` / `.stage1.args` /
`.stage1.manifest` / `.stage2.payload` / `.stage2.args` /
`.stage2.manifest`. A signature minted for one context is structurally
invalid in every other.
Alongside: stop signing with `openssl` in the test Makefile and dogfood
the real `deploy` signer, folding in key generation.
## Changes
- **`crates/ed25519-sign`**: `Domain` enum (all six roles) + `tag()`;
`sign()`/`verify()` take a `Domain` and operate on the preimage (callers
still pass the raw message). `pem_from_seed` / `pubkey_b64_from_seed`
promoted to `pub`. Tests: domain-separation rejection + a **golden
known-answer vector** stage0's verifier pins against.
- **`crates/stage1`, `crates/mkuki`**: verify/sign sites pass their
role.
- **`crates/deploy`**: new `keygen` (random ed25519 key via
`/dev/urandom`, no `getrandom`/`libc`) and low-level `sign --domain`;
`create` threads the right `Domain` into each artifact.
- **`Makefile`**: release key + every signature now via
`lockboot-deploy` (no `openssl`).
## Verification
- `make test-chain-x86_64 SIGN=1` / `SIGN=1 MANIFEST=1` / `SIGN=1
SIGN_ARGS=1` all pass against the domain-aware stage0 harness: both hops
verify and all six domains round-trip across the repo boundary.
Pairs with lockboot/stage0#8; the shared golden vector guarantees both
repos' framing agree.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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

Domain-separated signatures + standalone stage0-sign signer - #8

Merged
HarryR merged 3 commits into
mainfrom
domain-separated-sigs
Jul 9, 2026
Merged

Domain-separated signatures + standalone stage0-sign signer#8
HarryR merged 3 commits into
mainfrom
domain-separated-sigs

Conversation

@HarryR

Copy link
Copy Markdown
Contributor

What

Bind every ed25519 signature stage0 admits to its exact role. Signatures are now over a fixed 64-byte preimage sha256(domain_tag) || sha256(message), where the tag is one of lockboot.v1.stage1.uki / .stage1.args / .stage1.manifest. A signature minted for one role is structurally invalid in any other, closing the last of the mix-and-match malleability (the deferred tail of #2).

Alongside: stop signing with openssl in the test Makefile and dogfood a new standalone host-side signer.

Changes

  • crates/stage0/src/sig.rs: Domain enum + tag() namespace; verify() takes a Domain and checks the preimage.
  • crates/stage0/src/main.rs: the three admission sites pass their role (stage1.uki payload, stage1.args, stage1.manifest).
  • crates/stage0-sign (new): standalone host CLI, keygen + sign --domain. Randomness is /dev/urandom via std (no getrandom/libc/C toolchain); built for musl so the image's global static RUSTFLAGS don't break proc-macro compilation. Framing duplicated from sig.rs and pinned byte-for-byte to stage1's signer by a golden known-answer test.
  • Makefile: keygen + all three sign sites now call stage0-sign (no openssl). Signer var is STAGE0_SIGN, not SIGN, to avoid clobbering by the boot matrix's SIGN=1 flag. New make sign-bin / make sign-test.

Verification

  • make sign-test (golden KAT) passes.
  • make smoke-boot-x86_64 passes 5/5 (sha256, sign, sign_args, manifest, fallback).
  • Full chain: stage0 admits the UKI that stage1's deploy signs with stage1.uki, verified against the rebuilt boot.disk.

Pairs with lockboot/stage1's domain-separation PR; the shared golden vector guarantees both repos' framing agree.

🤖 Generated with Claude Code

Every ed25519 signature stage0 admits was over raw payload bytes with no
role tag, so a signature minted for one context was structurally valid in
another wherever the bytes were accepted (a signed-args blob replayed as a
payload signature, a _stage1 manifest as anything else). Bind each signature
to its exact role by signing a fixed 64-byte preimage
sha256(domain_tag) || sha256(message).
sig.rs gains a Domain enum (stage1.uki / stage1.args / stage1.manifest, the
three roles stage0 verifies) with a tag() namespace of the form
lockboot.v1.stage1.<kind>; verify() now takes a Domain and checks the
preimage. The three admission sites pass their role.
Stop signing with openssl in the test Makefile and dogfood a new standalone
host-side signer, crates/stage0-sign (keygen + domain-separated sign), so the
repo builds and tests without stage1's deploy tool. Its framing is duplicated
from sig.rs and pinned byte-for-byte to stage1's signer by a shared golden
known-answer test (make sign-test). Randomness for keygen is /dev/urandom via
std (no getrandom/libc), and it builds for the musl target so the image's
global static RUSTFLAGS do not break proc-macro compilation. The signer var is
STAGE0_SIGN, not SIGN, to avoid clobbering by the boot matrix's SIGN=1 flag.
make smoke-boot-x86_64 passes 5/5; the full chain (stage0 admits the UKI that
stage1's deploy signs with stage1.uki) verifies against the rebuilt boot.disk.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…USTFLAGS
The Dockerfile baked a global ENV RUSTFLAGS applying the static-link flags to
every target uniformly, including the host, where +crt-static makes proc-macro
dylibs impossible to build (a standalone host-gnu build fails "cannot produce
proc-macro"). Express the flags per target in .cargo/config.toml instead, so the
host is rust-lld only and never force-static.
stage0 had no .cargo/config.toml at all and relied entirely on the global env
(and, locally, the shared workspace config). Since CI checks this repo out alone
with CARGO_HOME redirected to an empty dir, it must be self-sufficient: add a
config with the musl targets (stage0-sign), the uefi targets (the measured
bootloader + payloads), and a gnu host section (rust-lld, no crt-static).
The uefi flags reproduce the measured binaries byte-for-byte: stage0.efi,
ena.efi and payload.efi hash identically before and after this change, so PCR4
and PCR14 are unchanged. sign-test (stage0-sign KAT) and smoke-boot 5/5 pass,
and the full chain still verifies the UKI (also byte-identical).
Pairs with the shared workspace config dropping its now-redundant musl rustflags
(cargo concatenates rustflags across config files, so keeping them in both the
workspace and this repo would double them and change the compiled bytes).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Release-prep hygiene over the domain-separation + rustflags changes, no logic:
- Makefile: the stage0-sign musl-target comment still explained itself by the
global RUSTFLAGS this branch removed (now inaccurate); state the real reason
(a fully static host binary).
- Dockerfile.build: collapse the 3-line comment narrating the removed ENV
RUSTFLAGS to a one-line statement of the rule.
- .cargo/config.toml: drop the "rather than a global RUSTFLAGS" migration framing.
- README: drop the "not openssl" aside.
- sig.rs / stage0-sign: the role names are stage1.* (the domain tags), not
_stage1.* (which is the JSON metadata key) -- fix the two doc comments.
Kept the openssl-genpkey PEM-format note: it documents a live interop property
(the key is openssl-compatible), not a removed dependency. stage0.efi hashes
identically and sign-test passes (comment-only).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@HarryR
HarryR merged commit 3565fe9 into mainJul 9, 2026
3 checks passed
@HarryR
HarryR deleted the domain-separated-sigs branch July 9, 2026 18:19
HarryR added a commit to lockboot/stage1 that referenced this pull request Jul 9, 2026
## What
Bind every ed25519 signature in the chain to its exact role. Signatures
are now over a fixed 64-byte preimage `sha256(domain_tag) ||
sha256(message)`, where the tag names one of the six contexts (2 hops x
{payload, args, manifest}): `lockboot.v1.stage1.uki` / `.stage1.args` /
`.stage1.manifest` / `.stage2.payload` / `.stage2.args` /
`.stage2.manifest`. A signature minted for one context is structurally
invalid in every other.
Alongside: stop signing with `openssl` in the test Makefile and dogfood
the real `deploy` signer, folding in key generation.
## Changes
- **`crates/ed25519-sign`**: `Domain` enum (all six roles) + `tag()`;
`sign()`/`verify()` take a `Domain` and operate on the preimage (callers
still pass the raw message). `pem_from_seed` / `pubkey_b64_from_seed`
promoted to `pub`. Tests: domain-separation rejection + a **golden
known-answer vector** stage0's verifier pins against.
- **`crates/stage1`, `crates/mkuki`**: verify/sign sites pass their
role.
- **`crates/deploy`**: new `keygen` (random ed25519 key via
`/dev/urandom`, no `getrandom`/`libc`) and low-level `sign --domain`;
`create` threads the right `Domain` into each artifact.
- **`Makefile`**: release key + every signature now via
`lockboot-deploy` (no `openssl`).
## Verification
- `make test-chain-x86_64 SIGN=1` / `SIGN=1 MANIFEST=1` / `SIGN=1
SIGN_ARGS=1` all pass against the domain-aware stage0 harness: both hops
verify and all six domains round-trip across the repo boundary.
Pairs with lockboot/stage0#8; the shared golden vector guarantees both
repos' framing agree.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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