Skip to content

feat(spec): kernel signing-key registry snapshot + ident.registered_key PrimitiveVerifier (N19) - #416

Draft
LamaSu wants to merge 10 commits into
masterfrom
feat/evidence-registered-key-verifier
Draft

LamaSu wants to merge 10 commits into
masterfrom
feat/evidence-registered-key-verifier

Conversation

@LamaSu

@LamaSu LamaSu commented Sep 24, 2026

Copy link
Copy Markdown
Owner

Summary

This is N19, the tier-1 unblocker. ident.registered_key (#47) is the dependency of every signed primitive, so no CSD reaches tier 1 until /settle verifies it. Verifying it needs a registry state the accepted deal can pin, and none was published: the gateway's registry-snapshot route serves only the CSD registry.

  • Kernel signing-key registry (pcc.registry.kernel-signing-keys.v1): a map-shaped RegistrySnapshot, using spec's existing registry model.
  • verifyRegisteredKey requires all of:
    • the pinned registry;
    • inline entries that re-hash to both the snapshot's own hash and the pinned snapshotHash;
    • the kernel's row;
    • a signer that is exactly that key. The signer may be raw hex, a RegisteredSigner or a principal id.
  • makeRegisteredKeyVerifier is the fail-closed PrimitiveVerifier. params = {registryId, snapshotHash}, as the deal pins them.

Stacked on #399 (it uses the pinned principal forms).

Cross-lane, to follow:

  • gateway publishes snapshots of the registry it keeps;
  • composition pins one in the accepted deal;
  • the oracle registers the verifier.

verifierStatus stays stub until then.

Test plan

  • registered-key 8/8
  • spec 819/819; tsc clean
  • 8 mutants killed
  • secret scan: 0 findings

🤖 Generated with Claude Code

https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS

…gistered_key PrimitiveVerifier (N19)

ident.registered_key is the dependency of every signed primitive
(receipt.kernel_signed, machine.execution_log, ...), so no CSD reaches tier 1
until it is verified at /settle. It needs a registry state the accepted deal
can pin, and none was published: the gateway's registry-snapshot route
serves only the CSD registry.

- The kernel signing-key registry, pcc.registry.kernel-signing-keys.v1, is a
  map-shaped RegistrySnapshot (spec's existing registry model):
  - key = kernelId (lowercase, refused otherwise);
  - value = the kernel's device principal id, ed25519:0x<key>, never a key
    whose secret is public.
  computeKernelSigningKeySnapshotHash reuses computeMapSnapshotHash and is
  pinned by a vector.
- verifyRegisteredKey requires:
  - the pinned registry;
  - inline entries that re-hash to both the snapshot's and the pinned
    snapshotHash;
  - the kernel's row, with the signer (raw hex, RegisteredSigner or principal
    id) equal to exactly that key.
- makeRegisteredKeyVerifier is the fail-closed PrimitiveVerifier. params are
  {registryId, snapshotHash}, as the deal pins them; no instance is pending;
  garbage is not met, and it never throws.
- It is stacked on #399 for the pinned principal-id form. The gateway must
  publish snapshots of the registry it keeps, and composition must pin one in
  the accepted deal. verifierStatus stays "stub" until the oracle runs it.

Tests: registered-key 8/8; spec 819/819; tsc clean. 8 mutants killed:
pinned-hash, recompute, registry id, signer, mixed-case ids, leaked key,
pinned registry id, and the other-registry case.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS
LamaSu and others added 9 commits September 28, 2026 20:27
…tered_key

computeMapSnapshotHash now sorts keys by UTF-16 code unit, never locale
collation, so a non-JS publisher of the kernel signing-key registry
computes the same snapshotHash (oracle #3348). #416's pinned vector
0x38ff407c..28db is unchanged: kernel-a and kernel-b sort the same way
under both orders.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…oracle #3348)

Six kernel ids that ICU collation and code units order differently hash
to a pinned snapshotHash 0xdc7e4100..dd58, reproduced in Python over
byte-sorted keys. The localeCompare mutant fails it. This is the vector
the oracle asked for before mirroring ident.registered_key.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…finding 2)

fixer-echo2: verifyRegisteredKey now reads its runtime input only inside one
exception boundary and returns { ok: false, reason: "malformed input: ..." }
instead of throwing. The catch formats the thrown value without trusting it.

Reproduced at cd5988f (10 of 11 new cases threw out of the exported helper):
- snapshot { registryId } with no entriesLocator: TypeError reading 'kind'
- null snapshot, no pinned, null input: TypeError (property read / destructure)
- inline entries 5: TypeError entries is not iterable
- non-string snapshotHash / pinned.snapshotHash: toLowerCase is not a function
- bigint kernelId: TypeError Do not know how to serialize a BigInt
- entry getter throwing Error("boom"), or an unprintable value
The verdict's literal case (snapshot {}) did NOT throw at cd5988f: the
registryId check returns "snapshot is from another registry" before
entriesLocator is read. It stays in the table as a guard.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
fixer-echo2: verifyRegisteredKey copies the inline entries into plain frozen
rows, reading each entry and its key and value exactly once inside the
exception boundary, and validates, hashes, searches and compares only those
rows. signerPrincipal refuses a principal-id signer whose key is compromised
(isCompromisedDevicePublicKey), as the raw-hex and object paths already did.
Non-array entries are refused up front, as cd5988f refused them (via the
entries.map TypeError), so the copy loosens nothing.

Reproduced at cd5988f:
- value getter (KEY_A for 2 reads, then an unregistered attacker key) with
  the kernel-a -> KEY_A pin: helper {"ok":true}, wrapper {"met":true}
- same through a key getter on another kernel's row, and a proxied entries array
- the leaked N35 key as the evil value, signer "ed25519:<leaked>": helper
  {"ok":true}, wrapper {"met":true}
- read counts for a 2-row snapshot: [0] 3, [0].key 4, [0].value 3,
  [1] 2, [1].key 3, [1].value 2 (each must be 1)
- "ed25519:<leaked>" on a plain snapshot: reason "signer is not the key the
  pinned registry holds for this kernel", not the compromised-key refusal

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…E2 finding 3)

fixer-echo2: the PrimitiveVerifier validates the pinned params first (both
strings, the kernel signing-key registry id, a 0x + 64 hex snapshotHash in any
case) and only then lets a missing instance mean pending. A deal that pins no
kernel signing-key snapshot can never be met, so it is met:false, not pending.
Every params value that still reaches pending is one verifyRegisteredKey could
accept; for a present instance only the detail text changes.

Reproduced at cd5988f: verify(null, {}, ctx) returned
{"met":"pending","detail":["no registry resolution yet"]}; so did undefined,
null, {} and partial, non-string, other-registry ("pcc.registry.other.v1")
and non-hash ("0x1234") params, for both a null and an undefined instance
(8 cases, all "pending").
Guard: well-formed params with the hash in lower, upper and 0X-upper case stay
pending with no instance and met with the kernel's key (passes at cd5988f too).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e thrown value (E2 finding 2)

Found in the E2 fix round and reproduced at cd5988f and b5d0f56: when
a params or instance getter throws a value whose toString also throws,
the wrapper's own catch threw while formatting it (String(err)), so
verify() rejected, although its contract says it never throws. The test
failed with "Error: toString ran". The catch now formats with errorText,
which cannot throw.

registered-key 37/37; spec 853/853; tsc clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…c: F1 still open)

Cross-family confirmation E2c on c3118d2: F2, F3 and the catch fix are
CLOSED; F1 (HIGH) stayed OPEN. The entries were read once, but
pinned.registryId was read twice (the pin check, then the snapshot
check) and snapshot.entriesLocator twice (kind, then entries). A
registryId getter could name the kernel registry for the first check and
another registry for the second, and verify against that registry's
snapshot.

Reproduced at c3118d2 first (2 failing tests): the flipping getter gave
{ok: true} for a snapshot of pcc.registry.other.v1, and a read counter
saw pinned.registryId read twice.

verifyRegisteredKey now copies every field of the pin (registryId,
snapshotHash), the snapshot (registryId, snapshotHash, entriesLocator)
and the locator (kind, entries) exactly once into inert locals before
any check, and reads only those; both hashes must be strings. The
wrapper already passed copied params.

spec 855/855, tsc clean; 3 mutants (a second read of registryId, of the
locator, of the pinned hash) killed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…(E2d: F1 still open)

Cross-family review E2d on c11e2bd: F1 (HIGH) still OPEN. The helper
read every field once, but the PrimitiveVerifier wrapper read
inst.kernelId twice: once as the value verified, again to render the
success detail. A getter could verify "kernel-a" and report "kernel-b".

Reproduced at c11e2bd first (2 failing tests): the getter was read
twice and the detail named kernel-b; a read counter saw kernelId: 2.

The wrapper now copies inst.snapshot, inst.kernelId and inst.signer into
locals once, verifies those, and reports the verified kernelId.

spec 857/857, tsc clean; reading kernelId again for the detail fails
both tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…nce-principal-ids) has merged

Clean merge, no conflicts. Retargeted to master; E2e (SHIP @525b6851) reviewed #416's own changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0116fiVRS9gDE1HrCECyjVR9
@LamaSu
LamaSu changed the base branch from feat/evidence-principal-ids to master October 7, 2026 00:32

This branch had an error being deployed

1 failed deployment
trusted-checks — c07fd588 Deployed Oct 7, 2026 by LamaSu via post-verdicts #211
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant