Skip to content

feat: #76 cap-mint K10 proof-of-possession — close the broker single-point-of-compromise#223

Merged
hanwencheng merged 4 commits into
mainfrom
claude/laughing-chaum-23e634
Jun 8, 2026
Merged

feat: #76 cap-mint K10 proof-of-possession — close the broker single-point-of-compromise#223
hanwencheng merged 4 commits into
mainfrom
claude/laughing-chaum-23e634

Conversation

@hanwencheng
Copy link
Copy Markdown
Member

Summary

Closes the broker single-point-of-compromise for the cap-mint data plane (issue #76, rescoped). Verified in code that the prior state had no proof-of-possession on cap-mint: the worker re-checked only the broker's own broker_sig + on-chain device registration (device_key_hash is a public identifier), never that the caller held the device key — so a compromised broker could mint a valid cap for any already-registered device + in-scope service and exfiltrate credentials/memory/config. arch.md's "broker-only compromise cannot mint a usable cap" was aspirational (deferred to #90 via §22b.4).

This adds a per-request K10 proof-of-possession: each cap-mint carries a client_sig (EIP-191 over a domain-separated, request-bound preimage) that the worker re-verifies independently of the broker against the on-chain device→omni binding. The K10 private key never reaches the broker → a compromised broker cannot forge it → cannot mint a usable cap.

What's in here

Verified

  • cargo test --workspace — 50/50 suites green (primitive round-trip; broker verify_cap_pop forged/wrong-op/stale; worker check_client_pop accept/missing/forged/tampered/stale)
  • cargo clippy clean on all changed crates
  • scripts/check-backend-fixture-drift.sh green

The agent path is fully correct + tested (agents register device_key_hash = keccak(K10 addr), exactly what the PoP checks).

⚠️ NEEDS LIVE VERIFICATION (no chain / Touch ID in this environment)

Follow-ups (deferred)

🤖 Generated with Claude Code

…point-of-compromise

Every cap-mint now carries a K10 device-key signature the worker re-verifies
INDEPENDENTLY of the broker, so a compromised broker (which holds no K10 private
key) cannot mint a usable cap. Closes the §22b.4 stage-1 gap, where the worker
re-checked only the broker's own broker_sig + on-chain device *registration*
(device_key_hash is a public identifier), never possession.

- core: device_crypto::cap_pop_payload (domain-separated, request-bound) +
  cap_pop_now/cap_pop_sig + load_device_key_from_env
- broker: handlers/cap.rs::verify_cap_pop rejects forged/missing/stale client_sig
  (cap_pop_invalid 4xx); §22b.4 shortcut removed
- workers (cred/memory/config/classify): verify::check_client_pop, fail-closed,
  gated by AGENTKEYS_WORKER_REQUIRE_CAP_POP (default enforce, mirrors REQUIRE_STS)
- clients: BackendClient::with_device_key signs the PoP inside cap_mint (MCP,
  daemon ui-bridge, proxy); BrokerCapRequest fields + #203 fixtures regenerated
- master K10/K11 split: harness/scripts/heima-register-master-k10.sh registers the
  master's secp256k1 K10 as a CAP_MINT device (registerAdditionalMasterDevice,
  reusing the #200/#164 K11-assertion machinery), wired into setup-heima step 15
- docs: arch.md §22b.4 resolved + headline guarantee; CLAUDE.md isolation table

Agent path verified: 50 Rust test suites green, clippy clean, backend-fixture gate
green. NEEDS-LIVE-VERIFICATION (no chain / Touch ID here): the on-chain master-K10
registration (cast/ABI + EOA-vs-#164-UserOp msg.sender) and cast EIP-191 matching
device_crypto::eip191_sign in the 2 master-path harness demos.
The harness on test infra caught that hard-requiring the K10 cap-PoP broke the
master-self path: the master registers device_key_hash=keccak(operator_omni)
(the #164 passkey account) and has no secp256k1 K10 registered yet, so master
cap-mints (phase 4 memory-plant, phase 6 web-parity) failed 'master K10 not found'.

Make the PoP optional + verify-when-present (the correct non-breaking staged rollout):
- protocol/broker/worker: client_sig/nonce/ts are Option; a supplied PoP is always
  validated (broker verify_cap_pop + worker), a MISSING PoP is rejected ONLY under
  AGENTKEYS_WORKER_REQUIRE_CAP_POP=1 (default OFF). New verify::enforce_client_pop
  centralizes the gate across the 4 workers.
- clients (BackendClient/ui_bridge/proxy): sign when a K10 is available, else mint
  with no PoP + the caller's device_key_hash — no hard-fail.
- harness master demos: revert to no-PoP bodies (master mints without PoP until its
  K10 is registered); fixture cap_mint_request back to the minimal no-PoP key-set.
- docs (arch §22b.4 + headline, CLAUDE.md): enforcement is a staged flag-flip after
  every actor's K10 (incl. the master's) is registered — that's when the SPOF closes.

The agent path still carries a verified PoP (agents register keccak(K10 addr)).
fmt + clippy + full test suite + fixture gate green locally.
…rker-smoke

The funded harness run cleared the gas failures; the only remaining red was
phase 1 step 15 (worker-smoke email-inbox) returning HTTP 502 — the SAME known
's3:ListBucket IAM not wired on the broker EC2' follow-up the soft-warn already
tolerates as 500, but surfacing via nginx (502) when the worker errors on
ListObjects. The toleration only matched 500, so the 502 variant fell through to
die. Broaden to the 5xx class (500|502|503). Not a #76/code issue — the email
worker /healthz passes; inbox LIST IAM is a separate deploy follow-up.

All code gates + harness phases 2-6 (incl. the #76 cap-PoP path: phase 3 negatives,
phase 4 plant, phase 6 web-parity) already pass on the funded run.
@hanwencheng hanwencheng merged commit 27c833b into main Jun 8, 2026
8 checks passed
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