Skip to content

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

Draft
vezenovm wants to merge 2 commits 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 2 commits 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: a wallet displays and authorizes a permit-like message, but because the permit and the smart-account transaction message share a domain separator, that same message authorizes a transaction: not one scoped action against one consumer, but a whole call list executed as the account, chosen by whoever requested the permit. The wallet cannot catch this at display time: the requester supplies the inner hash as an opaque field (IntentInnerHash, authwit.ts:94) with no preimage to render. Concretely: 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(victim → 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 accepts no bare message hash (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. The structured CallIntent branch could never reach the entrypoint message anyway, since F-841 gave the payload its own inner separator, so the opaque IntentInnerHash branch was the whole exposure. That branch remains unrenderable to a wallet even post-fix; whether it belongs on the dApp-facing API at all is a separate question, left as a follow-up. 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 — scope collapse, not delegation: a structured authwit delegates one scoped action to one consumer; signing an opaque hash siloed to your own account is account authority. Pre-fix both went through the same outer hash, so requesting the narrow capability produced the bytes granting the maximal one.

The value is therefore conditional on one threat-model choice: that createAuthWit is meant to be grantable at a lower trust tier than sendTx. If we commit to that, this is required for the boundary to hold. If any authwit signer is considered fully trusted with the account, it is defense in depth. The library docs already frame authwits as scoped, consumer-checked permits and never as account origination, so the intent was there; it was just never enforced, and the one-line summary in auth.nr invited the loose reading. This PR enforces it and rewords that line.

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.
  5. Docs: the auth.nr library doc now says a witness authorizes one action against one consumer, and points at the entrypoint separator for account origination.

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