fix(chain): require K11 self-attestation at first-master bootstrap (closes #165)#166
Merged
Merged
Conversation
, anti-front-run) registerFirstMasterDevice was unauthenticated first-call-wins: an attacker could copy the victim's operatorOmni from the mempool and front-run with their own msg.sender, permanently locking the operator out (operatorMasterWallet[omni] = attacker). The 'attestation' bytes param was accepted but ignored. Fix (Approach A from #165 / the security review): require a K11 P-256 SELF-attestation verified against the to-be-registered pubkey (k11PubX/k11PubY) over a challenge that binds msg.sender + operator/actor omni + deviceKeyHash + k11Pub + chainid + contract. A captured assertion is non-transferable: a different sender → different challenge → the embedded clientDataJSON challenge no longer matches → verifyAssertion rejects; and an attacker cannot forge the operator's K11 signature. No chicken-and-egg — the attesting key IS the key being registered (a self-attestation), enrolled by bootstrap time (arch.md §9 stage 2). The binding is to msg.sender, so it survives the ERC-4337 migration unchanged (EOA today, smart-account address later). - SidecarRegistry.sol: add OP_REGISTER_1ST_MASTER; replace the ignored `bytes attestation` with a `K11Assertion` self-attestation; verify + store the read signCount. - AgentKeysV1.t.sol: +RejectsBogusSelfAttestation (unauthenticated path closed) and +RejectsFrontRunWithDifferentSender (captured assertion non-transferable; legit operator still bootstraps); update happy-path + helper to mock the verifier (real P-256 is covered in K11Verifier.t.sol / P256Verifier.t.sol). 41 forge tests pass. - heima-register-first-master.sh: header note — the live cast call is the old ABI; flip it to the new ABI + self-attestation IN THE SAME CHANGE that redeploys SidecarRegistry (coordinated; needs a live authenticator). Harness skips first-master. Activation = redeploy SidecarRegistry on Heima + update deployed-contracts.md + operator-workstation.env + verify-heima-contracts.sh (documented in #165).
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.
The vulnerability (CRITICAL, #165)
SidecarRegistry.registerFirstMasterDevicewas unauthenticated first-call-wins: it setoperatorMasterWallet[operatorOmni] = msg.senderwith no binding betweenoperatorOmniand the sender, and itsattestationparam was accepted but ignored (SidecarRegistry.sol:143, pre-fix). An attacker watching the mempool could copy the victim'soperatorOmniand front-run with their own sender → permanent operator lockout. Surfaced by the codex adversarial review on #162.This is a live vuln in the current EOA/
castbootstrap, independent of the web flow (#163) and the ERC-4337 migration (#164).The fix — Approach A: K11 self-attestation bound to
msg.senderregisterFirstMasterDevicenow requires a K11 P-256 self-attestation, verified against the to-be-registered pubkey (k11PubX/k11PubY— there's no prior device at bootstrap) over:Why it defeats the front-run: the challenge commits
msg.sender. A front-runner replaying the victim's captured assertion with their own sender → the contract recomputes a different challenge → the embeddedclientDataJSONchallenge no longer matches →verifyAssertionrejects. The attacker can't forge the operator's K11 signature, and using their own K11 key yields a differentoperatorOmni. No chicken-and-egg: the attesting key is the key being registered (a self-attestation), enrolled by bootstrap time (arch.md §9 stage 2).Survives ERC-4337 (#164) unchanged — the binding is to
msg.sender(an EOA today, the smart-account address later).What's in this PR
SidecarRegistry.sol— addOP_REGISTER_1ST_MASTER; replace the ignoredbytes attestationwith aK11Assertion selfAttestation; verify via the existingK11Verifier+ store the readsignCount.AgentKeysV1.t.sol— newtest_RegisterFirstMaster_RejectsBogusSelfAttestation(unauthenticated path closed) andtest_RegisterFirstMaster_RejectsFrontRunWithDifferentSender(captured assertion is non-transferable to another sender; the legit operator still bootstraps); existing happy-path + helper updated to mock the verifier (real P-256 is covered inK11Verifier.t.sol/P256Verifier.t.sol).forge test: 41 passed, 0 failed.heima-register-first-master.sh— header note documenting the new ABI + the self-attestation challenge + the coordinated-redeploy activation (see below). The livecastcall is left on the old ABI so it keeps working against the currently-deployed registry.Activation (coordinated redeploy — follow-up, documented in #165)
The contract isn't redeployed by this PR. To activate: redeploy
SidecarRegistryon Heima + flipheima-register-first-master.sh'scastcall to the new ABI + generate the self-attestation (mirrorscripts/heima-scope-set.sh --webauthn) + updatedocs/spec/deployed-contracts.md+scripts/operator-workstation.env+ re-runverify-heima-contracts.sh. This step needs a live authenticator, so it's a deliberate operator action, not part of this code PR. The harness skips first-master in CI, so nothing breaks meanwhile.Scope notes
forge fmtis not CI-gated here; this PR keeps the repo's existing brace style (no spurious reformat).Closes #165.
🤖 Generated with Claude Code