Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/docs-developers/docs/resources/migration_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@ Aztec is in active development. Each version may introduce breaking changes that

## TBD

### [aztec-nr / Aztec.js] Account entrypoint authorization is now domain-separated from generic authwits

The account entrypoint previously wrapped its payload hash with the generic authwit outer hash
(`compute_authwit_message_hash` / `computeOuterAuthWitHash`). That placed 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 anyone able to request a generic authwit from the account could mint
an entrypoint authorization for a call list of their choosing. The entrypoint now wraps the payload hash with a
dedicated `entrypoint_message` domain separator, so the message can no longer be produced through the generic path.

This changes the authorized message preimage again, so it is a breaking change with the same upgrade requirements as
above: client and account bytecode must be upgraded together. **Third-party wallets or tooling** that build the
entrypoint authorization witness themselves must wrap the payload hash with the new `entrypoint_message` domain
separator instead of the generic outer authwit hash.

### [aztec-nr / Aztec.js] Account entrypoint authorization now binds the fee-payment method and cancellation flag

The account entrypoint (`AccountActions::entrypoint`) previously authorized only the app payload hash. It now
Expand Down
33 changes: 30 additions & 3 deletions noir-projects/labs/aztec-nr/aztec/src/authwit/account.nr
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::context::PrivateContext;

use crate::protocol::{hash::poseidon2_hash_with_separator, traits::Hash};
use crate::protocol::{address::AztecAddress, hash::poseidon2_hash_with_separator, traits::{Hash, ToField}};

use crate::authwit::auth::{compute_authwit_message_hash, IS_VALID_SELECTOR};
use crate::authwit::entrypoint::app::AppPayload;
Expand All @@ -16,6 +16,15 @@ pub(crate) global DOM_SEP__TX_NULLIFIER: u32 = 1025801951;
/// them together.
pub(crate) global DOM_SEP__ENTRYPOINT_PAYLOAD: u32 = 3045079954;

/// Domain separator for the account entrypoint authorization message.
///
/// The entrypoint authorizes `compute_entrypoint_message_hash`, which wraps the payload hash with this separator
/// rather than the generic authwit outer separator. Sharing the generic separator would place the entrypoint
/// message in the image of the generic authwit path (`compute_authwit_message_hash` over an attacker-chosen inner
/// hash), letting anyone able to request a generic authwit from the account mint an entrypoint authorization for a
/// call list of their choosing.
pub(crate) global DOM_SEP__ENTRYPOINT_MESSAGE: u32 = 3858756027;

/// Computes the inner hash the account authorizes when invoked through its entrypoint.
///
/// Binds the app payload together with the fee-payment selector and cancellation flag. Both drive fee-payer and
Expand All @@ -30,6 +39,22 @@ fn compute_entrypoint_payload_hash(app_payload: AppPayload, fee_payment_method:
)
}

/// Computes the message the account authorizes when invoked through its entrypoint.
///
/// Mirrors `compute_authwit_message_hash` but uses `DOM_SEP__ENTRYPOINT_MESSAGE`, so reproducing the entrypoint
/// message through the generic authwit path would require a Poseidon2 collision.
fn compute_entrypoint_message_hash(
consumer: AztecAddress,
chain_id: Field,
version: Field,
inner_hash: Field,
) -> Field {
poseidon2_hash_with_separator(
[consumer.to_field(), chain_id, version, inner_hash],
DOM_SEP__ENTRYPOINT_MESSAGE,
)
}

pub struct AccountActions<Context> {
context: Context,
is_valid_impl: fn(&mut PrivateContext, Field) -> bool,
Expand Down Expand Up @@ -62,7 +87,9 @@ impl AccountActions<&mut PrivateContext> {
/// Verifies that the entrypoint payload is authorized and executes the `app_payload`.
///
/// The authorized message binds the app payload together with `fee_payment_method` and `cancellable`, so the
/// account's approval covers the fee-payer and cancellation side effects rather than the call list alone.
/// account's approval covers the fee-payer and cancellation side effects rather than the call list alone. It is
/// domain-separated from generic authwits (see `DOM_SEP__ENTRYPOINT_MESSAGE`) so it cannot be minted through
/// the generic authwit path.
///
/// @param app_payload The payload that contains the calls to be executed in the app phase.
///
Expand All @@ -84,7 +111,7 @@ impl AccountActions<&mut PrivateContext> {
let valid_fn = self.is_valid_impl;

let inner_hash = compute_entrypoint_payload_hash(app_payload, fee_payment_method, cancellable);
let message_hash = compute_authwit_message_hash(
let message_hash = compute_entrypoint_message_hash(
self.context.this_address(),
self.context.chain_id(),
self.context.version(),
Expand Down
8 changes: 6 additions & 2 deletions noir-projects/labs/aztec-nr/aztec/src/authwit/auth.nr
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@ pub(crate) global DOM_SEP__AUTHWIT_NULLIFIER: u32 = 1239150694;

/// Authentication witness helper library
///
/// Authentication Witness is a scheme for authenticating actions on Aztec, so users can allow third-parties (e.g.
/// protocols or other users) to execute an action on their behalf.
/// Authentication Witness is a scheme for authorizing application-level actions on Aztec, so users can allow
/// third-parties (e.g. protocols or other users) to perform an action that a consuming contract checks.
///
/// A witness authorizes one action against one consumer. It does not authorize originating a transaction as the
/// account: an account's entrypoint message uses its own domain separator (see `DOM_SEP__ENTRYPOINT_MESSAGE` in
/// `crate::authwit::account`).
///
/// This library provides helper functions to manage such witnesses. The authentication witness, is some "witness"
/// (data) that authenticates a `message_hash`. The simplest example of an authentication witness, is a signature. The
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
//! that the whole set (every aztec-nr separator plus every protocol separator) is collision-free. The protocol enforces
//! the same for its own separators in its own tests.

use crate::authwit::account::{DOM_SEP__ENTRYPOINT_PAYLOAD, DOM_SEP__TX_NULLIFIER};
use crate::authwit::account::{DOM_SEP__ENTRYPOINT_MESSAGE, DOM_SEP__ENTRYPOINT_PAYLOAD, DOM_SEP__TX_NULLIFIER};
use crate::authwit::auth::DOM_SEP__AUTHWIT_NULLIFIER;
use crate::keys::ecdh_shared_secret::{DOM_SEP__ECDH_FIELD_MASK, DOM_SEP__ECDH_SUBKEY};
use crate::macros::functions::initialization_utils::DOM_SEP__INITIALIZATION_NULLIFIER;
Expand Down Expand Up @@ -124,6 +124,7 @@ unconstrained fn domain_separators_are_valid() {
);
all = all.push_back(derived(DOM_SEP__TX_NULLIFIER, "tx_nullifier"));
all = all.push_back(derived(DOM_SEP__ENTRYPOINT_PAYLOAD, "entrypoint_payload"));
all = all.push_back(derived(DOM_SEP__ENTRYPOINT_MESSAGE, "entrypoint_message"));
all = all.push_back(derived(DOM_SEP__AUTHWIT_NULLIFIER, "authwit_nullifier"));
all = all.push_back(derived(DOM_SEP__ECDH_SUBKEY, "ecdh_subkey"));
all = all.push_back(derived(DOM_SEP__ECDH_FIELD_MASK, "ecdh_field_mask"));
Expand Down
38 changes: 37 additions & 1 deletion yarn-project/entrypoints/src/account_entrypoint.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { poseidon2HashBytes } from '@aztec/foundation/crypto/poseidon';
import { Fr } from '@aztec/foundation/curves/bn254';
import { AuthWitness } from '@aztec/stdlib/auth-witness';
import { AuthWitness, computeOuterAuthWitHash } from '@aztec/stdlib/auth-witness';
import { AztecAddress } from '@aztec/stdlib/aztec-address';
import { GasSettings } from '@aztec/stdlib/gas';
import { ExecutionPayload } from '@aztec/stdlib/tx';
Expand All @@ -9,8 +9,12 @@ import {
AccountFeePaymentMethodOptions,
DefaultAccountEntrypoint,
type DefaultAccountEntrypointOptions,
ENTRYPOINT_MESSAGE_DOMAIN_SEPARATOR,
ENTRYPOINT_PAYLOAD_DOMAIN_SEPARATOR,
computeEntrypointMessageHash,
computeEntrypointPayloadHash,
} from './account_entrypoint.js';
import { EncodedAppEntrypointCalls } from './encoding.js';
import type { AuthWitnessProvider, ChainInfo } from './interfaces.js';

describe('DefaultAccountEntrypoint', () => {
Expand Down Expand Up @@ -81,4 +85,36 @@ describe('DefaultAccountEntrypoint', () => {
);
expect(ENTRYPOINT_PAYLOAD_DOMAIN_SEPARATOR).toEqual(derived);
});

it('mirrors the Noir entrypoint message domain separator', async () => {
const derived = Number(
(await poseidon2HashBytes(Buffer.from('az_dom_sep__entrypoint_message'))).toBigInt() & 0xffffffffn,
);
expect(ENTRYPOINT_MESSAGE_DOMAIN_SEPARATOR).toEqual(derived);
});

// The entrypoint must authorize a message the generic authwit path cannot reproduce. Otherwise a party able to
// request a generic authwit from the account (a createAuthWit over { consumer: account, innerHash }) could mint an
// entrypoint authorization for a call list of its choosing.
it('domain-separates the entrypoint message from the generic authwit hash', async () => {
const hash = await getPayloadAuthWitnessHash(baseOptions);

const encodedCalls = await EncodedAppEntrypointCalls.create(ExecutionPayload.empty().calls, baseOptions.txNonce);
const payloadHash = await computeEntrypointPayloadHash(
encodedCalls,
baseOptions.feePaymentMethodOptions,
!!baseOptions.cancellable,
);

const entrypointMessage = await computeEntrypointMessageHash(
address,
chainInfo.chainId,
chainInfo.version,
payloadHash,
);
const genericAuthwit = await computeOuterAuthWitHash(address, chainInfo.chainId, chainInfo.version, payloadHash);

expect(hash.equals(entrypointMessage)).toBe(true);
expect(hash.equals(genericAuthwit)).toBe(false);
});
});
33 changes: 31 additions & 2 deletions yarn-project/entrypoints/src/account_entrypoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
encodeArguments,
getFunctionReturnType,
} from '@aztec/stdlib/abi';
import { computeOuterAuthWitHash } from '@aztec/stdlib/auth-witness';
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
import type { GasSettings } from '@aztec/stdlib/gas';
import { ExecutionPayload, HashedValues, TxContext, TxExecutionRequest } from '@aztec/stdlib/tx';
Expand Down Expand Up @@ -39,6 +38,31 @@ export async function computeEntrypointPayloadHash(
);
}

/**
* Domain separator for the account entrypoint authorization message. Mirrors DOM_SEP__ENTRYPOINT_MESSAGE in
* noir-projects/labs/aztec-nr/aztec/src/authwit/account.nr. Derived from the poseidon hash of
* "az_dom_sep__entrypoint_message" truncated to a u32; kept in TypeScript by hand because the generated constants
* package only carries protocol-circuit constants, and pinned by a drift test that re-derives it.
*/
export const ENTRYPOINT_MESSAGE_DOMAIN_SEPARATOR = 3858756027;

/**
* Computes the message the account authorizes when invoked through its entrypoint. Wraps the entrypoint payload hash
* with a dedicated domain separator rather than the generic authwit outer hash, so the message cannot be reproduced
* through the generic authwit path (a `createAuthWit` over `{ consumer: account, innerHash }`).
*/
export function computeEntrypointMessageHash(
consumer: AztecAddress,
chainId: Fr,
version: Fr,
payloadHash: Fr,
): Promise<Fr> {
return poseidon2HashWithSeparator(
[consumer.toField(), chainId, version, payloadHash],
ENTRYPOINT_MESSAGE_DOMAIN_SEPARATOR,
);
}

/**
* The mechanism via which an account contract will pay for a transaction in which it gets invoked.
*/
Expand Down Expand Up @@ -168,7 +192,12 @@ export class DefaultAccountEntrypoint implements EntrypointInterface {
const functionSelector = await FunctionSelector.fromNameAndParameters(abi.name, abi.parameters);

const payloadHash = await computeEntrypointPayloadHash(encodedCalls, feePaymentMethodOptions, !!cancellable);
const messageHash = await computeOuterAuthWitHash(this.address, chainInfo.chainId, chainInfo.version, payloadHash);
const messageHash = await computeEntrypointMessageHash(
this.address,
chainInfo.chainId,
chainInfo.version,
payloadHash,
);
const payloadAuthWitness = await this.auth.createAuthWit(messageHash);

return {
Expand Down
Loading