feat: #76 cap-mint K10 proof-of-possession — close the broker single-point-of-compromise#223
Merged
Merged
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_hashis 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
device_crypto::cap_pop_payload+cap_pop_now/cap_pop_sig+load_device_key_from_envcap.rs::verify_cap_pop(reject forged/missing/stale →cap_pop_invalid); §22b.4 shortcut removedverify::check_client_pop, fail-closed, gated byAGENTKEYS_WORKER_REQUIRE_CAP_POP(default enforce, mirrorsAGENTKEYS_WORKER_REQUIRE_STS)BackendClient::with_device_keysigns insidecap_mint(MCP server, daemon ui-bridge, proxy);BrokerCapRequestfields + Shared broker/worker client crate — collapse the duplicated chain impls (drift fix) #203 fixtures regenerated + frozen key-setharness/scripts/heima-register-master-k10.shregisters the master's secp256k1 K10 as a CAP_MINT device (registerAdditionalMasterDevice, reusing the feat: #164 sponsored ERC-4337 register + v2-demo harness restructure #200/Migrate master authority to an ERC-4337 P-256 smart-account (resolves §11 gating findings) #164 K11-assertion machinery); wired intosetup-heima.shstep 15Verified
cargo test --workspace— 50/50 suites green (primitive round-trip; brokerverify_cap_popforged/wrong-op/stale; workercheck_client_popaccept/missing/forged/tampered/stale)cargo clippyclean on all changed cratesscripts/check-backend-fixture-drift.shgreenThe agent path is fully correct + tested (agents register
device_key_hash = keccak(K10 addr), exactly what the PoP checks).heima-register-master-k10.shrun — thecast/ABI call + the EOA-vs-Migrate master authority to an ERC-4337 P-256 smart-account (resolves §11 gating findings) #164-UserOpmsg.senderpath (flagged in the script header).cast wallet signEIP-191 of the 32-byte preimage matchingdevice_crypto::eip191_signin the 2 master-path harness demos.harness/v2-stage3-demo.sh+web-memory-bootstrap.shend-to-end withAGENTKEYS_WORKER_REQUIRE_CAP_POP=1.Follow-ups (deferred)
client_noncereplay LRU (freshness window bounds it today).client_sig→cap_pop_invalid); unit tests cover the property at the code layer.🤖 Generated with Claude Code