Skip to content

Commit

Permalink
docs: update webauthn tutorial draft
Browse files Browse the repository at this point in the history
  • Loading branch information
sarahschwartz committed Sep 13, 2024
1 parent 6f40762 commit 72cf03e
Show file tree
Hide file tree
Showing 12 changed files with 533 additions and 294 deletions.
2 changes: 2 additions & 0 deletions code/webauthn/contracts/contracts/Account.sol
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,11 @@ contract Account is IAccount, IERC1271 {
_transaction.processPaymasterInput();
}

// ANCHOR: updateR1Owner
function updateR1Owner(bytes memory _r1Owner) external onlySelf {
r1Owner = _r1Owner;
}
// ANCHOR_END: updateR1Owner

// ANCHOR: validateWebAuthnSignature
function validateWebAuthnSignature(bytes memory webauthnSignature, bytes32 txHash) private view returns (bool valid) {
Expand Down
4 changes: 2 additions & 2 deletions code/webauthn/frontend/src/pages/create-account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { buttonStyles, AA_FACTORY_ADDRESS, BUTTON_COLORS } from './index';
import { containerStyles, headerStyles } from './register';
import { type Provider, utils, Wallet } from 'zksync-ethers';
import { ethers } from 'ethers';
import * as factoryAbiJSON from '../../../contracts/artifacts-zk/contracts/AAFactory.sol/AAFactory.json';
import factoryAbiJSON from '../../../contracts/artifacts-zk/contracts/AAFactory.sol/AAFactory.json';
import { getPaymasterOverrides } from '../../utils/tx';
import { useAccount, useSetAccount } from '@/hooks/useAccount';
import {
Expand Down Expand Up @@ -36,7 +36,7 @@ export default function CreateAccount({ provider }: { provider: Provider }) {
async function deployNewAccount() {
const owner = Wallet.createRandom().connect(provider);
const aaFactory = new ethers.Contract(AA_FACTORY_ADDRESS, factoryAbiJSON.abi, owner);
const salt = '0x0000000000000000000000000000000000000000000000000000000000000000';
const salt = ethers.utils.hexlify(ethers.utils.randomBytes(32));
const overrides = await getPaymasterOverrides();
const tx = await aaFactory.deployAccount(salt, owner.address, overrides);
await tx.wait();
Expand Down
2 changes: 1 addition & 1 deletion code/webauthn/frontend/src/pages/mint.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Layout } from '../components/Layout';
import { buttonStyles } from '.';
import { containerStyles } from './register';
import { ethers } from 'ethers';
import * as NFT_ABI_JSON from '../../../contracts/artifacts-zk/contracts/MyNFT.sol/MyNFT.json';
import NFT_ABI_JSON from '../../../contracts/artifacts-zk/contracts/MyNFT.sol/MyNFT.json';
import { BUTTON_COLORS, NFT_CONTRACT_ADDRESS } from '@/pages/index';
import { useAccount } from '@/hooks/useAccount';

Expand Down
17 changes: 2 additions & 15 deletions code/webauthn/frontend/utils/tx.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DEFAULT_GAS_PER_PUBDATA_LIMIT, getPaymasterParams } from 'zksync-ethers/build/utils';
import { EIP712Signer, type Wallet, type Provider, utils, type types } from 'zksync-ethers';
import { ethers } from 'ethers';
import * as accountAbiJSON from '../../contracts/artifacts-zk/contracts/Account.sol/Account.json';
import accountAbiJSON from '../../contracts/artifacts-zk/contracts/Account.sol/Account.json';
import { PAYMASTER_ADDRESS } from '@/pages/index';

export async function getTransaction(to: string, from: string, value: string, data: string, provider: Provider) {
Expand Down Expand Up @@ -42,20 +42,7 @@ export async function registerKeyInAccount(pubKey: string, account: string, prov
const contract = new ethers.Contract(account, accountAbiJSON.abi, provider);
const data = contract.interface.encodeFunctionData('updateR1Owner', [pubKey]);
const transferAmount = '0';
const overrides = await getPaymasterOverrides();

const registerTx = {
to: account,
from: account,
chainId: (await provider.getNetwork()).chainId,
nonce: await provider.getTransactionCount(account),
type: 113,
customData: overrides.customData,
value: ethers.utils.parseEther(transferAmount),
gasPrice: await provider.getGasPrice(),
gasLimit: BigInt(2000000000),
data,
};
const registerTx = await getTransaction(account, account, transferAmount, data, provider);
const signedTxHash = EIP712Signer.getSignedDigest(registerTx);
const signingKey: ethers.utils.SigningKey = new ethers.utils.SigningKey(wallet.privateKey);
const walletSignature = signingKey.signDigest(signedTxHash);
Expand Down
2 changes: 1 addition & 1 deletion code/webauthn/frontend/utils/webauthn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { defaultAbiCoder } from 'ethers/lib/utils';
import { EIP712Signer, type Provider, utils } from 'zksync-ethers';
import type { AuthenticatorAssertionResponseJSON } from '@simplewebauthn/types';
import { BigNumber } from 'ethers';
import type { TransactionRequest } from 'zksync-ethers/build/types';
import type { TransactionRequest } from 'zksync-ethers/src/types';
import * as cbor from 'cbor';

export async function authenticate(challenge: string) {
Expand Down
4 changes: 2 additions & 2 deletions content/tutorials/erc20-paymaster/10.index.md
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,7 @@ Make sure you delete the `artifacts-zk` and `cache-zk` folders before recompilin
paymasterBalance = await provider.getBalance(PAYMASTER_ADDRESS);
console.log(`Paymaster ETH balance is now ${paymasterBalance.toString()}`);
console.log(`ERC20 token balance of the the wallet after mint: ${await wallet.getBalance(TOKEN_ADDRESS)}`);
console.log(`ERC20 token balance of the wallet after mint: ${await wallet.getBalance(TOKEN_ADDRESS)}`);
}
```
Expand Down Expand Up @@ -628,7 +628,7 @@ Make sure you delete the `artifacts-zk` and `cache-zk` folders before recompilin
Minting 5 tokens for the wallet via paymaster...
Paymaster ERC20 token balance is now 1
Paymaster ETH balance is now 59650952750000000
ERC20 token balance of the the wallet after mint: 7
ERC20 token balance of the wallet after mint: 7
```
The wallet had 3 tokens after running the deployment script and, after sending the transaction to `mint` 5 more tokens,
Expand Down
17 changes: 9 additions & 8 deletions content/tutorials/signing-transactions-with-webauthn/10.index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ description: Learn how to sign transactions with WebAuthn

- Make sure your machine satisfies the [system
requirements](https://github.com/matter-labs/era-compiler-solidity/tree/main#system-requirements).
- You should have the latest version of Google Chrome installed.
- You should have [Node.js](https://nodejs.org/en/download) installed.
- You should have [`era_test_node`](https://docs.zksync.io/build/test-and-debug/in-memory-node#install-and-set-up-era_test_node) installed.
- If you aren't familiar with paymasters and smart accounts on ZKsync Era, check out the
Expand All @@ -22,21 +23,21 @@ To break this down, the app will allow users to:
- Deploy a new instance of a smart account.
- Create a new WebAuthn passkey.
- Register the WebAuthn passkey as an authorized user of the smart account.
- Use WebAuthn to Transfer ETH.
- Use WebAuthn to transfer ETH.
- Use WebAuthn to mint an NFT.
- Have gasless transactions.

<video width="100%" height="100%" controls>
<video style="clip-path:inset(2px 2px);" width="100%" height="100%" controls>
<source src="/videos/webauthn-demo.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>

You can find the completed code inside the [Community Code repo](https://github.com/zkSync-Community-Hub/community-code/code/webauthn).

## Introduction to WebAuthn

WebAuthn, short for Web Authentication API, is an API built-in to your browser that allows your app to register and authenticate users.
It allows users to authenticate using biometric data (such as Apple's TouchID) or hardware devices (such as a Yubikey).
It allows users to authenticate using [passkeys](https://passkeys.dev/docs/intro/what-are-passkeys/),
including using biometric data (such as Apple's TouchID) or portable hardware devices (such as a Yubikey).

### Ceremonies

Expand All @@ -45,14 +46,14 @@ There are two steps, or "ceremonies", for using WebAuthn: registration and authe
1. The first step, registration, is when the user creates a new key pair specific to the application they are using.
2. The second step, authentication, is used to send a challenge for WebAuthn to sign, and receive a signature in return.

In the frontend section, you will see in detail what these ceremonies look like.

### Signatures

WebAuthn uses a different signature scheme than what is typically used in blockchain.

WebAuthn uses the secp256r1 elliptic curve for signing, while standard accounts in blockchain use the secp256k1 curve.

Without a helper function to handle verifying secp256r1 signatures, it's very difficult to integrate a smart account with WebAuthn.

Luckily, we can utilize the P256Verify precompile to verify that a given WebAuthn signature matches a known public key.
Without a helper function to handle verifying secp256r1 signatures, it can be really difficult to integrate a smart account with WebAuthn.
Luckily, we can utilize the `P256Verify` precompile to verify that a given WebAuthn signature matches a known public key.

In the next section, we will create the contracts for the demo app.
Original file line number Diff line number Diff line change
Expand Up @@ -37,102 +37,129 @@ rm -rf ./test/*

## Contracts

Open the `zksync-webauthn` folder in your preferred IDE.
Open the `zksync-webauthn/contracts` folder in your preferred IDE.

We will be creating 4 contracts:

- `AAFactory.sol`: a factory contract to deploy new instances of smart accounts.
- `Account.sol`: the contract for the smart account.
- `GeneralPaymaster.sol`: a general paymaster contract to sponsor transactions.
- `MyNFT.sol`: a generic NFT contract.
- `AAFactory.sol`: a factory contract to deploy new instances of smart accounts.
- `Account.sol`: the contract for the smart account.

Create the new contract files in the `contracts/contracts` folder:
### Paymaster Contract

Create a new file in the `contracts/contracts` folder called `GeneralPaymaster.sol`.

```shell
touch {contracts/AAFactory.sol,contracts/Account.sol,contracts/GeneralPaymaster.sol,contracts/MyNFT.sol}
touch contracts/GeneralPaymaster.sol
```

Copy and paste the code below into respective files:

### AA Factory

::drop-panel
::panel{label="AAFactory.sol"}
::panel{label="GeneralPaymaster.sol"}

```solidity [contracts/AAFactory.sol]
:code-import{filePath="webauthn/contracts/contracts/AAFactory.sol"}
```
```solidity [contracts/GeneralPaymaster.sol]
:code-import{filePath="webauthn/contracts/contracts/GeneralPaymaster.sol"}
```

::
::

### Smart Contract Account
This is a basic paymaster contract that allows us to sponsor transactions for users of our app.

### NFT Contract

Create a file in the `contracts/contracts` folder called `MyNFT.sol`.

```shell
touch contracts/MyNFT.sol
```

::drop-panel
::panel{label="Account.sol"}
::panel{label="MyNFT.sol"}

```solidity [contracts/Account.sol]
:code-import{filePath="webauthn/contracts/contracts/Account.sol"}
```solidity [contracts/MyNFT.sol]
:code-import{filePath="webauthn/contracts/contracts/MyNFT.sol"}
```

::
::

### Paymaster Contract
This is a generic NFT contract that mints svg images of Zeek using different colors.
We will use this to test interacting with smart contracts using WebAuthn.

::drop-panel
::panel{label="GeneralPaymaster.sol"}
### AA Factory

```solidity [contracts/GeneralPaymaster.sol]
:code-import{filePath="webauthn/contracts/contracts/GeneralPaymaster.sol"}
Create a file in the `contracts/contracts` folder called `AAFactory.sol`.

```shell
touch contracts/AAFactory.sol
```

::drop-panel
::panel{label="AAFactory.sol"}

```solidity [contracts/AAFactory.sol]
:code-import{filePath="webauthn/contracts/contracts/AAFactory.sol"}
```

::
::

### NFT Contract
This contract is a factory contract responsbile for deploying new instances of the `Account.sol` contract.

### Smart Contract Account

Finally, create a file in the `contracts/contracts` folder called `Account.sol`.

```shell
touch contracts/Account.sol
```

::drop-panel
::panel{label="MyNFT.sol"}
::panel{label="Account.sol"}

```solidity [contracts/MyNFT.sol]
:code-import{filePath="webauthn/contracts/contracts/MyNFT.sol"}
```solidity [contracts/Account.sol]
:code-import{filePath="webauthn/contracts/contracts/Account.sol"}
```

::
::

## Understanding the WebAuthn Signature Validation
The smart account contract is where we perform the WebAuthn signature validation.

### Understanding WebAuthn Signature Validation

The smart contract account implements a way to validate signatures created with WebAuthn.

The `_validateTransaction` function first checks to see if there is a valid secp256k1 signature from the account owner,
just like the example in the TUTORIAL TODO: LINK.

just like the example in the [Native AA Multisig](https://code.zksync.io/tutorials/native-aa-multisig) tutorial.
If the transaction is not signed by the standard account owner, the next check is to see if the transaction contains a valid WebAuthn signature.

```solidity [contracts/Account.sol]
:code-import{filePath="webauthn/contracts/contracts/Account.sol:_validateTransaction"}
```

The contract contains a state variable `r1Owner` to store the the public key of the WebAuthn account that the owner can register.
The contract contains a state variable `r1Owner` to store the public key of the WebAuthn account that the owner wants to use.
The `updateR1Owner` function can update the value of the `r1Owner`.

```solidity [contracts/Account.sol]
:code-import{filePath="webauthn/contracts/contracts/Account.sol:r1Owner"}
```

```solidity [contracts/Account.sol]
:code-import{filePath="webauthn/contracts/contracts/Account.sol:validateWebAuthnSignature"}
:code-import{filePath="webauthn/contracts/contracts/Account.sol:updateR1Owner"}
```

### Validating the Signature

To validate the signature, the `_validateWebAuthnSignature` first extracts the `authenticatorData`, `clientData`, and `rs` values from the decoded signature.

It extracts the WebAuthn challenge from the `clientData`, and checks to see if it matches the transaction hash.
It's important to note that the WebAuthn authentication process doesn't just sign the transaction data we send as a challenge.
The WebAuthn authentication process returns three pieces of information that we need for the validation process:
the WebAuthn signature, the `authenticatorData`, and the `clientData`.

The `extractChallengeFromClientData` extracts the WebAuthn challenge from the `clientData`, and checks to see if it matches the transaction hash.
This means that the exact transaction data that is submitted to the contract matches the transaction data signed by the WebAuthn account.

It checks for the malleability of the signature,
Next, the `_validateWebAuthnSignature` function checks for the malleability of the signature,
and checks to see if the WebAuthn user flags are set.

Finally, it calls the `callVerifier` function to call the `P256Verify` precompile.
Expand All @@ -141,7 +168,7 @@ Finally, it calls the `callVerifier` function to call the `P256Verify` precompil
:code-import{filePath="webauthn/contracts/contracts/Account.sol:_validateWebAuthnSignature"}
```

The `callVerifier` function encodes the input for into the correct format and calls the `P256Verify` precompile.
The `callVerifier` function encodes the input into the correct format and calls the `P256Verify` precompile.

If the returned output is empty, the validation evaluated to false.
This means that the public key derived from the WebAuthn signature does not match the registered `r1Owner` public key.
Expand Down
Loading

0 comments on commit 72cf03e

Please sign in to comment.