Skip to content

feat: persist + expose proven kernel signing address at registration (evidence #52) - #230

Draft
LamaSu wants to merge 4 commits into
feat/evidence-vocab-industrialfrom
feat/kernel-signing-key-registry
Draft

feat: persist + expose proven kernel signing address at registration (evidence #52)#230
LamaSu wants to merge 4 commits into
feat/evidence-vocab-industrialfrom
feat/kernel-signing-key-registry

Conversation

@LamaSu

@LamaSu LamaSu commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Why

The oracle authenticates kernel-signed machine logs (evidence primitive #52, machine.execution_log settlement) against a kernel's real secp256k1 signing address. Today registration threw that address away and stored a random value (kernel.facade.ts:313 publicKey: 0x${randomBytes(32)}), so the oracle had nothing to verify against — the root blocker of the machine-log money path.

What

Persist the kernel's REAL signing address at registration, with proof-of-possession, and expose it on the kernel DTO.

  • DB (@pcc/store): new nullable signing_address column on shop_kernels (drizzle schema + idempotent safeAddColumn for existing DBs). $inferInsert auto-derives — no repo change.
  • Gateway (@pcc/gateway) register(): recovers the signer from signingProof (viem recoverMessageAddress over a kernelId-bound message) and persists signingAddress only if it matches the claimed address. Absent / mismatched / malformed / replayed proof → null. Fail closed — never persist an unproven signer (a wrong one could clear settlement and release escrow).
  • Set-once hardening: a generic upsert never overwrites an already-proven signer (key rotation is a dedicated owner-authenticated op — follow-up).
  • DTO: KernelDTO.signingAddress: string | null (checksummed when proven, else null) via the populator → flows to GET /api/kernels/:id (oracle fetch) and GET /api/kernels.
  • Kernel (@pcc/kernel): KernelKeychain.buildRegistrationProof(kernelId) returns { signingAddress, signingProof }; kernelSigningProofMessage() is the single source of truth for the message format, imported by the gateway verifier so signer and verifier never drift.

Pinned contract (honored)

GET /api/kernels/:kernelIdKernelDTO includes signingAddress: string | null.

Security invariant

  • Proof bound to pcc-kernel-signing-key:${kernelId} → a signature captured for one kernel cannot register another kernel's address.
  • Only cryptographically-proven addresses are stored; everything else is null.

Tests (all watched pass)

  • gateway kernel-signing-address.test.ts7/7: valid → persisted + returned by /:id and list; mismatch / absent / garbage / cross-kernel replay → null; no-fields → null; set-once blocks overwrite. Proofs from a real viem test account.
  • kernel kernel-registration-proof.test.ts4/4: round-trip recover, kernelId binding, per-keychain distinctness.
  • No regression: existing register tests 10/10, @pcc/store store+device-registry 61/61. @pcc/store + @pcc/kernel build clean.

Follow-ups (out of scope)

  1. Wire buildRegistrationProof into the live log-signing process's POST /api/kernels call (no existing TS daemon self-registers; pcc-node is Python/Ed25519, not the secp256k1 kernel key).
  2. Owner-authenticated key rotation endpoint (this PR is set-once via the generic upsert).
  3. Operator-ownership authz on who may bind a signer to a given kernelId.

🤖 Generated with Claude Code

LamaSu and others added 4 commits July 9, 2026 20:45
Kernel's real secp256k1 signing address, proven at registration via
EIP-191 proof-of-possession. Same address that signs machine-log
entries, so the oracle can authenticate kernel-signed logs for
settlement (evidence primitive #52). Nullable/fail-closed; added to
the drizzle schema and via idempotent safeAddColumn for existing DBs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMoShvxX49Xyt4Mi5evDnB
KernelKeychain.buildRegistrationProof(kernelId) returns the
{ signingAddress, signingProof } pair a kernel sends at registration:
an EIP-191 signature by the kernel signing key over the kernelId-bound
message. kernelSigningProofMessage() is the single source of truth for
the message format, imported by the gateway verifier so signer and
verifier never drift. Binding to kernelId blocks cross-kernel replay.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMoShvxX49Xyt4Mi5evDnB
…ster

register() now recovers the signer from signingProof (viem
recoverMessageAddress over the kernelId-bound message) and persists
signingAddress ONLY when it matches the claimed address; absent /
mismatched / malformed / replayed proof -> null (fail closed). Update
path is set-once: a generic upsert never overwrites an already-proven
signer. KernelDTO exposes signingAddress: string | null via the
populator, flowing to GET /api/kernels/:id (oracle fetch) and
GET /api/kernels. 7/7 proof-of-possession tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMoShvxX49Xyt4Mi5evDnB
Assert the proof emits the real signing address, recovers to it under
the bound message (gateway acceptance path), fails to recover under a
different kernelId (replay binding), and differs per keychain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMoShvxX49Xyt4Mi5evDnB
@LamaSu

LamaSu commented Jul 10, 2026

Copy link
Copy Markdown
Owner Author

Superseded by feat/optionc-machinelog (Option C tagged Ed25519-default keys, bulletin #306) — do not merge standalone.

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