fix(aztec-nr)!: domain-separate account entrypoint authorization from generic authwits - #25166
Draft
vezenovm wants to merge 2 commits into
Draft
Conversation
…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
force-pushed
the
mv/f-844-domain-separate-entrypoint-message
branch
from
August 10, 2026 21:34
64e209a to
6ba76e3
Compare
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.
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, socreateAuthWit({ consumer: account, innerHash })mints the exact message the entrypoint validates, forging an entrypoint authorization for any calls.Exploit (pre-fix):
payload = AppPayload{ calls: [Token.transfer(victim → attacker, ALL)] }, thenX = poseidon2([payload.hash(), EXTERNAL, false], ENTRYPOINT_PAYLOAD_SEP).wallet.createAuthWit({ consumer: victimAccount, innerHash: X }). To the wallet this reads as "authorize consumer=self over opaque hash0x…" — a self-authwit, not a transaction.IntentInnerHashbranch (authwit.ts:96) the message =computeOuterAuthWitHash(victimAccount, chainId, version, X)— exactly what the pre-fix entrypoint validates.victimAccount.entrypoint(payload, EXTERNAL, false)with that witness.is_valid_implfinds the signature overmessage_hash→ passes.Fix: wrap with a dedicated
entrypoint_messagedomain 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
entrypointtx himself, so there is no second prompt to catch it. The value is making the capability boundary explicit. The dApp-facingcreateAuthWitaccepts no bare message hash (IntentInnerHash | CallIntent,wallet.ts:306), so post-fix every authwit it can request isAUTHWIT_OUTER-wrapped and disjoint from the entrypoint message; a wallet that scopescreateAuthWitbelowsendTxnow actually gets that separation. The structuredCallIntentbranch could never reach the entrypoint message anyway, since F-841 gave the payload its own inner separator, so the opaqueIntentInnerHashbranch 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
createAuthWitis meant to be grantable at a lower trust tier thansendTx. 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 inauth.nrinvited the loose reading. This PR enforces it and rewords that line.Change:
DOM_SEP__ENTRYPOINT_MESSAGE;AccountActions::entrypointusescompute_entrypoint_message_hash.DefaultAccountEntrypoint(computeEntrypointMessageHash).compute_authwit_message_hash/verify_private_authwituntouched, so no AuthRegistry re-pin.AccountActionson recompile.auth.nrlibrary doc now says a witness authorizes one action against one consumer, and points at the entrypoint separator for account origination.Exposure (ranked):
createAuthWitgated weaker thansendTx: directly exposed pre-fix; closed post-fix (structured authwits can no longer reach the entrypoint message).createAuthWit: credible.BaseWallettoday:requestCapabilitiesthrows, 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.