Skip to content

fix(aztec-nr)!: domain-separate account entrypoint authorization from generic authwits - #25166

Draft
vezenovm wants to merge 1 commit into
mv/f-841-bind-entrypoint-fee-payloadfrom
mv/f-844-domain-separate-entrypoint-message
Draft

fix(aztec-nr)!: domain-separate account entrypoint authorization from generic authwits#25166
vezenovm wants to merge 1 commit into
mv/f-841-bind-entrypoint-fee-payloadfrom
mv/f-844-domain-separate-entrypoint-message

Conversation

@vezenovm

@vezenovm vezenovm commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Stacked on #25157 (F-841). Breaking: changes the entrypoint authorization preimage.

Bug: the entrypoint wrapped its payload hash with the generic authwit outer hash, so createAuthWit({ consumer: account, innerHash }) mints the exact message the entrypoint validates, forging an entrypoint authorization for any calls.

Exploit (pre-fix):

  1. dApp precomputes offline (all public): payload = AppPayload{calls: [Token.transfer(attacker, ALL)]}, then X = poseidon2([payload.hash(), EXTERNAL, false], ENTRYPOINT_PAYLOAD_SEP).
  2. dApp calls wallet.createAuthWit({ consumer: victimAccount, innerHash: X }). To the wallet this reads as "authorize consumer=self over opaque hash 0x…": a self-authwit, not a transaction.
  3. Wallet signs. Via the IntentInnerHash branch (authwit.ts:96) the message = computeOuterAuthWitHash(victimAccount, chainId, version, X): exactly what the pre-fix entrypoint validates.
  4. Attacker submits victimAccount.entrypoint(payload, EXTERNAL, false) with that witness. is_valid_impl finds the signature over message_hash → passes.
  5. Victim drained. No dApp authwit reused, no signature forged — the wallet legitimately signed one ambiguous message.

Fix: wrap with a dedicated entrypoint_message domain separator, so the generic authwit path can no longer produce the entrypoint message.

Likelihood & design: Low likelihood — a wallet talking to a dApp usually already trusts it, and the only gate is the sign step (step 2): the attacker submits the entrypoint tx himself, so there is no second prompt to catch it. The value is making the capability boundary explicit. The dApp-facing createAuthWit is structured-only (IntentInnerHash | CallIntent, wallet.ts:306), so post-fix every authwit it can request is AUTHWIT_OUTER-wrapped and disjoint from the entrypoint message; a wallet that scopes createAuthWit below sendTx now actually gets that separation. Posture: keep structured authwits narrowly scoped, treat raw opaque signing as high privilege (the fix cannot help a bare-field signer), domain-separate entrypoint authorization so the scopes cannot collapse cryptographically. If authwits instead stayed opaque and unscoped, equating the two capabilities would be the conservative alternative.

Root cause: at the protocol layer the entrypoint authorization is an authwit (consumer = the account). Authwits being separate from tx authority was a wallet-layer intention the protocol never enforced, so a generic app approval could carry the weight of a whole-account tx. Now enforced cryptographically.

Change:

  1. Noir: new DOM_SEP__ENTRYPOINT_MESSAGE; AccountActions::entrypoint uses compute_entrypoint_message_hash.
  2. TS: mirror in DefaultAccountEntrypoint (computeEntrypointMessageHash).
  3. compute_authwit_message_hash / verify_private_authwit untouched, so no AuthRegistry re-pin.
  4. Account contracts inherit it via AccountActions on recompile.

Exposure (ranked):

  1. Session-wide / unattended createAuthWit gated weaker than sendTx: directly exposed pre-fix; closed post-fix (structured authwits can no longer reach the entrypoint message).
  2. Compromised or malicious frontend with direct createAuthWit: credible.
  3. Default BaseWallet today: requestCapabilities throws, no capability boundary, so little added authority yet. Latent until capabilities ship.

Tests: red/green (entrypoint message == new hash, != generic hash for the same payload); TS drift test + Noir collision test.

@vezenovm vezenovm added the ci-draft Run CI on draft PRs. label Aug 10, 2026
@vezenovm vezenovm changed the title fix!: domain-separate account entrypoint authorization from generic authwits fix(aztec-nr)!: domain-separate account entrypoint authorization from generic authwits Aug 10, 2026
…uthwits

The account entrypoint wrapped its payload hash with the generic authwit outer hash
(compute_authwit_message_hash / computeOuterAuthWitHash). That put the authorized
message in the image of the generic authwit path: a createAuthWit over
{ consumer: account, innerHash: <entrypoint payload hash> } produces exactly the
message the entrypoint validates, so a party able to request a generic authwit from
the account could mint an entrypoint authorization for a call list of its choosing.

The entrypoint now wraps the payload hash with a dedicated entrypoint_message domain
separator (compute_entrypoint_message_hash), moving the authorized message out of the
generic authwit image. compute_authwit_message_hash and verify_private_authwit are left
untouched, so the pinned AuthRegistry is unaffected (no standard-contract re-pin).
@vezenovm
vezenovm force-pushed the mv/f-844-domain-separate-entrypoint-message branch from 64e209a to 6ba76e3 Compare August 10, 2026 21:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-draft Run CI on draft PRs.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant