Skip to content

Commit

Permalink
feat: AUTHWIT cancellations (#4799)
Browse files Browse the repository at this point in the history
Fixes #4786 and #3007
  • Loading branch information
LHerskind authored Feb 28, 2024
1 parent 9e246c1 commit b7c2bc0
Show file tree
Hide file tree
Showing 22 changed files with 417 additions and 145 deletions.
15 changes: 15 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,19 @@ jobs:
command: cond_spot_run_compose end-to-end 4 ./scripts/docker-compose.yml TEST=e2e_token_contract.test.ts
aztec_manifest_key: end-to-end

e2e-authwit-test:
docker:
- image: aztecprotocol/alpine-build-image
resource_class: small
steps:
- *checkout
- *setup_env
- run:
name: "Test"
command: cond_spot_run_compose end-to-end 4 ./scripts/docker-compose.yml TEST=e2e_authwit.test.ts
aztec_manifest_key: end-to-end


e2e-blacklist-token-contract:
docker:
- image: aztecprotocol/alpine-build-image
Expand Down Expand Up @@ -1432,6 +1445,7 @@ workflows:
- e2e-deploy-contract: *e2e_test
- e2e-lending-contract: *e2e_test
- e2e-token-contract: *e2e_test
- e2e-authwit-test: *e2e_test
- e2e-blacklist-token-contract: *e2e_test
# TODO(3458): Investigate intermittent failure
# - e2e-slow-tree: *e2e_test
Expand Down Expand Up @@ -1479,6 +1493,7 @@ workflows:
- e2e-deploy-contract
- e2e-lending-contract
- e2e-token-contract
- e2e-authwit-test
- e2e-blacklist-token-contract
- e2e-sandbox-example
- e2e-state-vars
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,13 @@ As part of `AuthWit` we are assuming that the `on_behalf_of` implements the priv

```rust
#[aztec(private)]
fn is_valid(message_hash: Field) -> Field;
fn spend_private_authwit(inner_hash: Field) -> Field;

#[aztec(public)]
fn is_valid_public(message_hash: Field) -> Field;
fn spend_public_authwit(inner_hash: Field) -> Field;
```

Both return the value `0xe86ab4ff` (`is_valid` selector) for a successful authentication, and `0x00000000` for a failed authentication. You might be wondering why we are expecting the return value to be a selector instead of a boolean. This is mainly to account for a case of selector collisions where the same selector is used for different functions, and we don't want an account to mistakenly allow a different function to be called on its behalf - it is hard to return the selector by mistake, but you might have other functions returning a bool.
Both return the value `0xabf64ad4` (`IS_VALID` selector) for a successful authentication, and `0x00000000` for a failed authentication. You might be wondering why we are expecting the return value to be a selector instead of a boolean. This is mainly to account for a case of selector collisions where the same selector is used for different functions, and we don't want an account to mistakenly allow a different function to be called on its behalf - it is hard to return the selector by mistake, but you might have other functions returning a bool.

## The `AuthWit` library.

Expand All @@ -102,11 +102,10 @@ As you can see above, this function takes a `caller` and a `request`. The `reque

For private calls where we allow execution on behalf of others, we generally want to check if the current call is authenticated by `on_behalf_of`. To easily do so, we can use the `assert_current_call_valid_authwit` which fetches information from the current context without us needing to provide much beyond the `on_behalf_of`.

This function computes the message hash, and then forwards the call to the more generic `assert_valid_authwit`. This validating function will then:

- make a call to `on_behalf_of` to validate that the call is authenticated
- emit a nullifier for the action to prevent replay attacks
- throw if the action is not authenticated by `on_behalf_of`
This function will then make a to `on_behalf_of` to execute the `spend_private_authwit` function which validates that the call is authenticated.
The `on_behalf_of` should assert that we are indeed authenticated and then emit a nullifier when we are spending the authwit to prevent replay attacks.
If the return value is not as expected, we throw an error.
This is to cover the case where the `on_behalf_of` might implemented some function with the same selector as the `spend_private_authwit` that could be used to authenticate unintentionally.

#### Example

Expand Down Expand Up @@ -176,7 +175,7 @@ In the snippet below, this is done as a separate contract call, but can also be

We have cases where we need a non-wallet contract to approve an action to be executed by another contract. One of the cases could be when making more complex defi where funds are passed along. When doing so, we need the intermediate contracts to support approving of actions on their behalf.

To support this, we must implement the `is_valid_public` function as seen in the snippet below.
To support this, we must implement the `spend_public_authwit` function as seen in the snippet below.

#include_code authwit_uniswap_get /noir-projects/noir-contracts/contracts/uniswap_contract/src/main.nr rust

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ E.g. you don't want a user to subscribe once they have subscribed already. Or yo

Emit a nullifier in your function. By adding this nullifier into the tree, you prevent another nullifier from being added again. This is also why in authwit, we emit a nullifier, to prevent someone from reusing their approval.

#include_code assert_valid_authwit_public /noir-projects/aztec-nr/authwit/src/auth.nr rust
#include_code spend_private_authwit /noir-projects/aztec-nr/authwit/src/account.nr rust

Note be careful to ensure that the nullifier is not deterministic and that no one could do a preimage analysis attack. More in [the anti pattern section on deterministic nullifiers](#deterministic-nullifiers)

Expand Down
5 changes: 4 additions & 1 deletion docs/docs/developers/tutorials/uniswap/l2_contract_setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ Next, paste this function:

#include_code authwit_uniswap_get noir-projects/noir-contracts/contracts/uniswap_contract/src/main.nr rust

In this function, the token contract calls the Uniswap contract to check if Uniswap has indeed done the approval. The token contract expects a `is_valid()` function to exit for private approvals and `is_valid_public()` for public approvals. If the action is indeed approved, it expects that the contract would return the function selector for `is_valid()`  in both cases. The Aztec.nr library exposes this constant for ease of use. The token contract also emits a nullifier for this message so that this approval (with the nonce) can’t be used again.
In this function, the token contract calls the Uniswap contract to check if Uniswap has indeed done the approval.
The token contract expects a `spend_private_authwit()` function to exit for private approvals and `spend_public_authwit()` for public approvals.
If the action is indeed approved, it expects that the contract will emit a nullifier and return the function selector for `IS_VALID()`  in both cases.
The Aztec.nr library exposes this constant for ease of use.

This is similar to the [Authwit flow](../../contracts/resources/common_patterns/authwit.md).

Expand Down
41 changes: 29 additions & 12 deletions noir-projects/aztec-nr/authwit/src/account.nr
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use dep::aztec::context::{PrivateContext, PublicContext, Context};
use dep::aztec::state_vars::{Map, PublicMutable};
use dep::aztec::protocol_types::{address::AztecAddress, abis::function_selector::FunctionSelector, hash::{pedersen_hash}};

use crate::entrypoint::{app::AppPayload, fee::FeePayload};
use crate::auth::IS_VALID_SELECTOR;
use crate::auth::{IS_VALID_SELECTOR, compute_outer_authwit_hash};

struct AccountActions {
context: Context,
Expand Down Expand Up @@ -69,21 +70,37 @@ impl AccountActions {
}
// docs:end:entrypoint

pub fn is_valid(self, message_hash: Field) -> Field {
// docs:start:spend_private_authwit
pub fn spend_private_authwit(self, inner_hash: Field) -> Field {
let context = self.context.private.unwrap();
// The `inner_hash` is "siloed" with the `msg_sender` to ensure that only it can
// consume the message.
// This ensures that contracts cannot consume messages that are not intended for them.
let message_hash = compute_outer_authwit_hash(context.msg_sender(), inner_hash);
let valid_fn = self.is_valid_impl;
if (valid_fn(self.context.private.unwrap(), message_hash)) {
IS_VALID_SELECTOR
} else {
0
}
assert(valid_fn(context, message_hash) == true, "Message not authorized by account");
context.push_new_nullifier(message_hash, 0);
IS_VALID_SELECTOR
}
// docs:end:spend_private_authwit

pub fn is_valid_public(self, message_hash: Field) -> Field {
let value = self.approved_action.at(message_hash).read();
if (value) { IS_VALID_SELECTOR } else { 0 }
// docs:start:spend_public_authwit
pub fn spend_public_authwit(self, inner_hash: Field) -> Field {
let context = self.context.public.unwrap();
// The `inner_hash` is "siloed" with the `msg_sender` to ensure that only it can
// consume the message.
// This ensures that contracts cannot consume messages that are not intended for them.
let message_hash = compute_outer_authwit_hash(context.msg_sender(), inner_hash);
let is_valid = self.approved_action.at(message_hash).read();
assert(is_valid == true, "Message not authorized by account");
context.push_new_nullifier(message_hash, 0);
IS_VALID_SELECTOR
}
// docs:end:spend_public_authwit

pub fn internal_set_is_valid_storage(self, message_hash: Field, value: bool) {
self.approved_action.at(message_hash).write(value);
// docs:start:approve_public_authwit
pub fn approve_public_authwit(self, message_hash: Field) {
self.approved_action.at(message_hash).write(true);
}
// docs:end:approve_public_authwit
}
78 changes: 24 additions & 54 deletions noir-projects/aztec-nr/authwit/src/auth.nr
Original file line number Diff line number Diff line change
@@ -1,78 +1,48 @@
use dep::aztec::protocol_types::{
abis::function_selector::FunctionSelector, address::AztecAddress,
constants::{GENERATOR_INDEX__AUTHWIT}, hash::{hash_args, pedersen_hash}
constants::{GENERATOR_INDEX__AUTHWIT_INNER, GENERATOR_INDEX__AUTHWIT_OUTER},
hash::{hash_args, pedersen_hash}
};
use dep::aztec::context::{PrivateContext, PublicContext, Context};

global IS_VALID_SELECTOR = 0xe86ab4ff;
global IS_VALID_PUBLIC_SELECTOR = 0xf3661153;

// @todo #2676 Should use different generator than the payload to limit probability of collisions.

// docs:start:assert_valid_authwit
// Assert that `on_behalf_of` have authorized `message_hash` with a valid authentication witness
pub fn assert_valid_authwit(
context: &mut PrivateContext,
on_behalf_of: AztecAddress,
message_hash: Field
) {
let is_valid_selector = FunctionSelector::from_field(IS_VALID_SELECTOR);
let result = context.call_private_function(on_behalf_of, is_valid_selector, [message_hash])[0];
context.push_new_nullifier(message_hash, 0);
assert(result == IS_VALID_SELECTOR, "Message not authorized by account");
}
// docs:end:assert_valid_authwit
global IS_VALID_SELECTOR = 0xabf64ad4; // 4 first bytes of keccak256("IS_VALID()")

// docs:start:assert_current_call_valid_authwit
// Assert that `on_behalf_of` have authorized the current call with a valid authentication witness
pub fn assert_current_call_valid_authwit(context: &mut PrivateContext, on_behalf_of: AztecAddress) {
// message_hash = H(caller, contract_this, selector, args_hash)
let message_hash = pedersen_hash(
[
context.msg_sender().to_field(), context.this_address().to_field(), context.selector().to_field(), context.args_hash
],
GENERATOR_INDEX__AUTHWIT
);
assert_valid_authwit(context, on_behalf_of, message_hash);
}
// docs:end:assert_current_call_valid_authwit

// docs:start:assert_valid_authwit_public
// Assert that `on_behalf_of` have authorized `message_hash` in a public context
pub fn assert_valid_authwit_public(context: &mut PublicContext, on_behalf_of: AztecAddress, message_hash: Field) {
let is_valid_public_selector = FunctionSelector::from_field(IS_VALID_PUBLIC_SELECTOR);
let result = context.call_public_function(on_behalf_of, is_valid_public_selector, [message_hash])[0];
context.push_new_nullifier(message_hash, 0);
let function_selector = FunctionSelector::from_signature("spend_private_authwit(Field)");
let inner_hash = compute_inner_authwit_hash([context.msg_sender().to_field(), context.selector().to_field(), context.args_hash]);
let result = context.call_private_function(on_behalf_of, function_selector, [inner_hash])[0];
assert(result == IS_VALID_SELECTOR, "Message not authorized by account");
}
// docs:end:assert_valid_authwit_public
// docs:end:assert_current_call_valid_authwit

// docs:start:assert_current_call_valid_authwit_public
// Assert that `on_behalf_of` have authorized the current call in a public context
pub fn assert_current_call_valid_authwit_public(context: &mut PublicContext, on_behalf_of: AztecAddress) {
// message_hash = H(caller, contract_this, selector, args_hash)
let message_hash = pedersen_hash(
[
context.msg_sender().to_field(), context.this_address().to_field(), context.selector().to_field(), context.args_hash
],
GENERATOR_INDEX__AUTHWIT
);
assert_valid_authwit_public(context, on_behalf_of, message_hash);
let function_selector = FunctionSelector::from_signature("spend_public_authwit(Field)");
let inner_hash = compute_inner_authwit_hash([context.msg_sender().to_field(), context.selector().to_field(), context.args_hash]);
let result = context.call_public_function(on_behalf_of, function_selector, [inner_hash])[0];
assert(result == IS_VALID_SELECTOR, "Message not authorized by account");
}
// docs:end:assert_current_call_valid_authwit_public

// docs:start:compute_authwit_message_hash
// Compute the message hash to be used by an authentication witness
pub fn compute_authwit_message_hash<N>(
caller: AztecAddress,
target: AztecAddress,
selector: FunctionSelector,
args: [Field; N]
) -> Field {
pub fn compute_call_authwit_hash<N>(caller: AztecAddress, consumer: AztecAddress, selector: FunctionSelector, args: [Field; N]) -> Field {
let args_hash = hash_args(args);
let inner_hash = compute_inner_authwit_hash([caller.to_field(), selector.to_field(), args_hash]);
compute_outer_authwit_hash(consumer, inner_hash)
}
// docs:end:compute_authwit_message_hash

pub fn compute_inner_authwit_hash<N>(args: [Field; N]) -> Field {
pedersen_hash(args, GENERATOR_INDEX__AUTHWIT_INNER)
}

pub fn compute_outer_authwit_hash(consumer: AztecAddress, inner_hash: Field) -> Field {
pedersen_hash(
[caller.to_field(), target.to_field(), selector.to_field(), args_hash],
GENERATOR_INDEX__AUTHWIT
[consumer.to_field(), inner_hash],
GENERATOR_INDEX__AUTHWIT_OUTER
)
}
// docs:end:compute_authwit_message_hash
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,16 @@ contract DocsExample {
);
}

#[aztec(private)]
fn spend_private_authwit(inner_hash: Field) -> Field {
1
}

#[aztec(public)]
fn spend_public_authwit(inner_hash: Field) -> Field {
1
}

#[aztec(public)]
internal fn update_leader(account: AztecAddress, points: u8) {
let new_leader = Leader { account, points };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ mod ecdsa_public_key_note;
// Account contract that uses ECDSA signatures for authentication on the same curve as Ethereum.
// The signing key is stored in an immutable private note and should be different from the signing key.
contract EcdsaAccount {
use dep::aztec::protocol_types::{abis::call_context::CallContext, address::AztecAddress};
use dep::aztec::protocol_types::{abis::{call_context::CallContext, function_selector::FunctionSelector}, address::AztecAddress};
use dep::std;
use dep::std::option::Option;
use dep::aztec::{
Expand Down Expand Up @@ -40,39 +40,44 @@ contract EcdsaAccount {
}

#[aztec(private)]
fn is_valid(message_hash: Field) -> Field {
fn spend_private_authwit(inner_hash: Field) -> Field {
let actions = AccountActions::private(&mut context, ACCOUNT_ACTIONS_STORAGE_SLOT, is_valid_impl);
actions.is_valid(message_hash)
actions.spend_private_authwit(inner_hash)
}

#[aztec(public)]
fn is_valid_public(message_hash: Field) -> Field {
fn spend_public_authwit(inner_hash: Field) -> Field {
let actions = AccountActions::public(&mut context, ACCOUNT_ACTIONS_STORAGE_SLOT, is_valid_impl);
actions.is_valid_public(message_hash)
actions.spend_public_authwit(inner_hash)
}

#[aztec(private)]
internal fn cancel_authwit(outer_hash: Field) {
context.push_new_nullifier(outer_hash, 0);
}

#[aztec(public)]
internal fn set_is_valid_storage(message_hash: Field, value: bool) {
internal fn approve_public_authwit(outer_hash: Field) {
let actions = AccountActions::public(&mut context, ACCOUNT_ACTIONS_STORAGE_SLOT, is_valid_impl);
actions.internal_set_is_valid_storage(message_hash, value)
actions.approve_public_authwit(outer_hash)
}

#[contract_library_method]
fn is_valid_impl(context: &mut PrivateContext, message_field: Field) -> pub bool {
fn is_valid_impl(context: &mut PrivateContext, outer_hash: Field) -> bool {
// Load public key from storage
let storage = Storage::init(Context::private(context));
let public_key = storage.public_key.get_note();

// Load auth witness
let witness: [Field; 64] = get_auth_witness(message_field);
let witness: [Field; 64] = get_auth_witness(outer_hash);
let mut signature: [u8; 64] = [0; 64];
for i in 0..64 {
signature[i] = witness[i] as u8;
}

// Verify payload signature using Ethereum's signing scheme
// Note that noir expects the hash of the message/challenge as input to the ECDSA verification.
let hashed_message: [u8; 32] = std::hash::sha256(message_field.to_be_bytes(32));
let hashed_message: [u8; 32] = std::hash::sha256(outer_hash.to_be_bytes(32));
let verification = std::ecdsa_secp256k1::verify_signature(public_key.x, public_key.y, signature, hashed_message);
assert(verification == true);

Expand Down
Loading

0 comments on commit b7c2bc0

Please sign in to comment.