Skip to content

Commit

Permalink
Document the Safe Signer Launchpad As Experimental
Browse files Browse the repository at this point in the history
This PR moves the special 4337 integration contracts to an experimental
folder and adds documentation specific to those contracts there (as well
the rationale for the contracts being experimental in the first place).

To follow up this PR, I will add unit tests for how we intend passkeys
to be used over ERC-4337 (i.e. the "non-experimental" way of using
them).
  • Loading branch information
nlordell committed May 8, 2024
1 parent a85650a commit 84f84e0
Show file tree
Hide file tree
Showing 9 changed files with 244 additions and 81 deletions.
80 changes: 16 additions & 64 deletions modules/passkey/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@

This package contains a passkey signature verifier, that can be used as an owner for a Safe, compatible with versions 1.3.0+.

## Contracts overview
Passkey support with the Safe is provided by implementing [`SignatureValidator`s](./contracts/base/SignatureValidator.sol) that can verify WebAuthn signatures on-chain, the underlying standard used by passkeys, and be used as owners for Safe accounts. At a high level, this works by:

1. Deploying a signer instance using the [SafeWebAuthnSignerFactory](./contracts/SafeWebAuthnSignerFactory.sol), this will create a contract instance at a deterministic address using `CREATE2` based the parameters of the WebAuthn credential: the public key coordinates and which `ecverify` implementations to use.
2. Set the deployed signer as an owner for a Safe.

## Contracts Overview

Safe account being standard agnostic, new user flows such as custom signature verification logic can be added/removed as and when required. By leveraging this flexibility to support customizing Safe account, Passkeys-based execution flow can be enabled on a Safe. The contracts in this package use [ERC-1271](https://eips.ethereum.org/EIPS/eip-1271) standard and [WebAuthn](https://w3c.github.io/webauthn/) standard to allow signature verification for WebAuthn credentials using the secp256r1 curve. The contracts in this package are designed to be used with precompiles for signature verification in the supported networks or use any verifier contract as a fallback mechanism. In their current state, the contracts are tested with [Fresh Crypto Lib (FCL)](https://github.com/rdubois-crypto/FreshCryptoLib) and [daimo-eth](https://github.com/daimo-eth/p256-verifier).

Expand Down Expand Up @@ -39,71 +44,18 @@ bytes memory signature = abi.encode(authenticatorData, clientDataFields, r, s);

### [P256](./contracts/libraries/P256.sol)

`P256` is a library for P256 signature verification with contracts that follows the EIP-7212 EC verify precompile interface. This library defines a custom type `Verifiers`, which encodes two addresses into a single `uint176`. The first address (2 bytes) is a precompile address dedicated to verification, and the second (20 bytes) is a fallback address. This setup allows the library to support networks where the precompile is not yet available, seamlessly transitioning to the precompile when it becomes active, while relying on a fallback contract address in the meantime.

## Setup and Execution flow

```mermaid
sequenceDiagram
actor U as User
participant CS as CredentialStore
actor B as Bundler
participant EP as EntryPoint
participant SPF as SafeProxyFactory
participant SWASPF as SafeWebAuthnSignerFactory
participant SP as SafeProxy
participant SSL as SafeSignerLaunchpad
participant S as Singleton
participant M as Module
participant SWASS as SafeWebAuthnSignerSingleton
participant WAV as WebAuthn library
participant PV as P256Verifier
actor T as Target
U->>+CS: Create Credential (User calls `create(...)`)
CS->>U: Decode public key from the return value
U->>+SWASPF: Get signer address (signer might not be deployed yet)
SWASPF->>U: Signer address
U->>+B: Submit UserOp payload that deploys SafeProxy address with SafeSignerLaunchpad as singleton in initCode and corresponding call data that calls `initializeThenUserOp(...)` ands sets implementation to Safe Singleton
B->>+EP: Submit User Operations
EP->>+SP: Validate UserOp
SP-->>SSL: Load SignerLaunchpad logic
SSL-->>SWASPF: Forward validation
SWASPF-->>SWASS: call isValidSignature(bytes32,bytes) with x,y values and verifier address added to the call data
SWASS-->>WAV: call verifyWebAuthnSignatureAllowMalleability
WAV->>+PV: Verify signature
PV->>WAV: Signature verification result
WAV->>SWASS: Signature verification result
SWASS->>SWASPF: Signature verification result
SWASPF-->>SSL: Return magic value
opt Pay required fee
SP->>EP: Perform fee payment
end
SP-->>-EP: Validation response
EP->>+SP: Execute User Operation with call to `initializeThenUserOp(...)`
SP-->>SSL: Load SignerLaunchpad logic
SP->>+SWASPF: Create Signer
SWASPF-->>SP: Return owner address
SP->>SP: Setup Safe
SP-->>SP: delegatecall with calldata received in `initializeThenUserOp(...)`
SP-->>S: Load Safe logic
SP->>+M: Forward execution
M->>SP: Execute From Module
SP-->>S: Load Safe logic
SP->>+T: Perform transaction
opt Bubble up return data
T-->>-SP: Call Return Data
SP-->>M: Call Return Data
M-->>-SP: Call return data
SP-->>-EP: Call return data
end
```
`P256` is a library for P256 signature verification with contracts that follows the EIP-7212 EC verify precompile interface. This library defines a custom type `Verifiers`, which encodes two addresses into a single `uint176`. The first address (2 bytes) is a precompile address dedicated to verification, and the second (20 bytes) is a fallback address. This setup allows the library to support networks where the precompile is not yet available, seamlessly transitioning to the precompile when it becomes active, while relying on a fallback contract address in the meantime. Note that only 2 bytes are needed to represent the precompile address, as the reserved range for precompile contracts is between address 0x0000 and 0xffff which fits into 2 bytes.

ERC-4337 outlines specific storage access rules for the validation phase, which limits the deployment of SafeProxy for use with the passkey flow. To navigate this restriction, in the `initCode` of UserOp, a `SafeProxy` is deployed with `SafeSignerLaunchpad` as a singleton. The `SafeSignerLaunchpad` is used to validate the signature of the UserOp. The `SafeSignerLaunchpad` forwards the signature validation to the `SafeWebAuthnSignerSingleton`, which in turn forwards the signature validation to the `WebAuthn` library. `WebAuthn` forwards the call to `P256Verifier`. The `P256Verifier` is used to validate the signature. In the validation, phase the launchpad stores the Safe's setup hash (owners, threshold, modules, etc) which is then verified during the execution phase.
The `verifiers` value can be computed with the following code:

During the execution phase, the implementation of the `SafeProxy` is set to the Safe Singleton along with the owner as signer contract deployed by SafeSignerLaunchpad.
```solidity
uint16 precompile = ...;
address fallbackVerifier = ...;
P256.Verifiers = P256.Verifiers.wrap(
(uint176(precompile) << 160) + uint176(uint160(fallbackVerifier))
);
```

## Usage

Expand Down
211 changes: 211 additions & 0 deletions modules/passkey/contracts/4337/experimental/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
# Passkey 4337 Support

This directory contains additional support contracts for using passkeys with Safes over ERC-4337.

> These contracts are marked as experimental as they are only needed when deploying Safes with initial passkey owners that are required for verifying the very first ERC-4337 user operation with `initCode`. We do not, however, recommend this as it would tie your Safe account's address to a passkey which may not be always available. In particular: the WebAuthn authenticator that stores a device-bound credential that does not allow for backups may be lost, the domain the credential is tied to may no longer be available, you lose access to the passkey provider where your WebAuthn credentials are stored (for example, you no longer have an iPhone or MacBook with access to your iCloud keychain passkeys).
>
> As such, for the moment, we recommend that Safes be created with an ownership structure or recovery mechanism that allows passkey owners to be rotated in case access to the WebAuthn credential is lost.
## Overview

The core contract provided by the `passkey` module is the `SafeWebAuthnSignerFactory` contract which can be used to create smart contract signers that can be used as owners of Safes. These WebAuthn signers are fully supported both with traditional Safe transactions and infrastructure, as well as ERC-4337. In fact, they are designed such that no storage is read during signature verification (all configuration - the WebAuthn credential public key coordinates and the P-256 verifier contract to use) are stored in contract code instead of account storage for compatibility with ERC-4337.

There is one notable caveat when using the `passkey` module with ERC-4337 specifically, which is that ERC-4337 user operations can only deploy exactly one `CREATE2` contract whose address matches the `sender` of the user operation. This means that deploying both the Safe account and its WebAuthn credential owner in a user operation's `initCode` is not possible.

In order to work around this limitation, there are two possible workarounds that can be used:

1. Using a "launchpad" contract, the `SafeSignerLaunchpad`: this implementation provides an alternative `singleton` implementation for the account that is used **only** for the first user operation and makes use of a `ISafeSignerFactory` to validate the WebAuthn signature without deploying the owner in the validation phase of the ERC-4337 user operation (i.e. `validateUserOp`). The WebAuthn owner is then deployed in the execution phase of the user operation, once there are no more restrictions on what is allowed to execute. This implementation allows for 1/1 Safes with a single passkey owner to be able to execute transactions over ERC-4337.
2. Using a shared signer, the `SafeWebAuthnSharedSigner`: this implementation provides a shared signer that can be used as a Safe owner. The shared signer uses account storage for its configuration in order to circumvent any ERC-4337 restrictions on storage during `initCode`. This implementation allows for n/m (including 1/1) Safes with a single passkey owner to be able to execute transactions over ERC-4337. Note that since the signer is a single contract, it can only be used to represent a single passkey owner for any given Safe; however, additional passkey owners can still be added by using the `SafeWebAuthnSignerFactory` to deploy additional WebAuthn signer contracts and adding them as owners to the Safe.

Note that this restriction only applies if **you want to use the passkey module with a Safe over 4337 without any additional EOA owners**. If _any_ of the following applies to you, then the contracts provided in this directory are **not** required:

- You want to deploy a Safe that is also owned by more than `threshold` additional EOA signers, in this case you can use the EOAs to sign the first ERC-4337 user operation that deploys the account and include in the execution phase a call to the `SafeWebAuthnSignerFactory` to deploy the passkey owner.
- You want to deploy the Safe outside of ERC-4337, the WebAuthn signer instance as well as the Safe account can be deployed permissionlessly, so their creation can be batched together in a single transaction when deploying the Safe. Once the Safe and the WebAuthn signer are deployed, they can be used regularly over ERC-4337.
- The passkey owner is already deployed, in this case, the standard ERC-4337 deployment process would apply where you would simply add the already created WebAuthn signer instance as an owner to the Safe.

## [Safe Signer Launchpad](./SafeSignerLaunchpad.sol)

The Safe signer launchpad is used as a `singleton` implementation for the very first user operation. The signature check in the ERC-4337 validation phase is done **without** deploying the WebAuthn signer for the passkey credential using a well-defined `ISafeSignerFactory` interface. In the case the user operation is validated and then executed, the launchpad will:

1. Set the `singleton` to the actual Safe implementation to use
2. Deploy the WebAuthn signer, so that it can be used normally over ERC-4337 for the following user operations

Note that for the very first user operation, an EIP-712 `SafeInitOp` is signed instead of the usual `SafeOp` that is used by the canonical ERC-4337 module. This is done in order to distinguish signatures of a launchpad Safe's user operation from a standard ERC-4337 Safe user operation.

### Init Code Example

The `initCode` should be generated with:

```solitidy
SafeProxyFactory proxyFactory = ...;
SafeSignerLaunchpad launchpad = ...;
Safe singleton = ...;
SafeWebAuthnSignerFactory signerFactory = ...;
uint256 signerX = ...;
uint256 signerY = ...;
uint256 signerVerifiers = ...;
address initializer = ...;
bytes memory initializerData = ...;
address fallbackHandler = ...;
bytes memory initCode = abi.encodePacked(
proxyFactory,
abi.encodeCall(
launchpad.setup,
(
address(singleton),
address(signerFactory),
signerX,
signerY,
signerVerifiers,
initializer,
initializerData,
fallbackHandler
)
)
);
```

### User Operation Execution Flow

```mermaid
sequenceDiagram
participant CS as Browser
actor U as User
actor B as Bundler
participant EP as EntryPoint
participant SPF as SafeProxyFactory
participant SP as SafeProxy
participant SSL as SafeSignerLaunchpad
participant S as Safe
participant SWASF as SafeWebAuthnSignerFactory
participant SWASP as SafeWebAuthnSignerProxy
participant SWASS as SafeWebAuthnSignerSingleton
participant WAV as WebAuthn library
participant PV as P256Verifier
actor T as Target
U->>+CS: Create Credential (User calls `navigator.credentials.create(...)`)
CS->>U: Decode public key from the return value
U->>SWASF: Get signer address (signer might not be deployed yet)
SWASF->>U: Signer address
U->>+B: Submit UserOp with `initCode` to deploy a SafeProxy with SafeSignerLaunchpad as singleton
note over EP: account creation
B->>+EP: handleOps([userOp], ...)
EP->>+SPF: createProxyWithNonce(launchpad, setup, ...)
SPF->>SP: CREATE2
SPF->>SP: setup(...)
SP-->>SSL: DELEGATECALL
SP->>S: DELEGATECALL setup(...)
S-->>SPF: ok
SPF-->>-EP: Proxy address
note over EP: validation phase
EP->>+SP: validateUserOp(...)
SP-->>SSL: DELEGATECALL
SP->>SWASF: isValidSignatureForSigner(...)
SWASF->>SWASS: isValidSignature(...) || configuration
SWASS->>WAV: verifyWebAuthnSignatureAllowMalleability(...)
WAV->>PV: ecverify(...)
PV-->>WAV: Signature verification result
WAV-->>SWASS: Signature verification result
SWASS-->>SWASF: Signature verification result
SWASF-->>SP: ERC-1271 magic value
opt Pay required fee
SP->>EP: Perform fee payment
end
SP-->>-EP: Validation response
note over EP: execution phase
EP->>+SP: promoteAccountAndExecuteUserOp(...)
SP-->>SSL: DELEGATECALL
SP->>SP: set Safe as singleton
SP->>SWASF: createSigner(...)
SP->>T: Execute user operation
T-->>SP: Result
SP-->>-EP: Result
```

## [Safe WebAuthn Shared Signer](./SafeWebAuthnSharedSigner.sol)

Alternatively, the shared signer can be used in order as an owner for the Safe. This method is simpler than the launchpad, in that there is no special setup step, and the standard Safe implementation and canonical ERC-4337 module can be used. The only additional requirement is that the Safe `setup` must delegate call into the `SafeWebAuthnSharedSigner` instance in order for it to set its configuration. When paired with the `Safe4337Module`, the `MultiSend` contract can be used to both enable the ERC-4337 support in the Safe as well as configure the WebAuthn credential in the WebAuthn shared signer.

Because the shared signer is a single contract address, it can only ever represent a single passkey owner for Safe. However, additional passkey owners can be added for sWebAuthn signers created with the canonical `SafeWebAuthnSignerFactory` contract, and can even co-exist with the `SafeWebAuthnSharedSigner` owner (so there is no need to re-deploy a signer for the original WebAuthn credential represented by the shared signer).

### Init Code Example

The `initCode` should be generated with:

```solitidy
SafeProxyFactory proxyFactory = ...;
Safe singleton = ...;
SafeWebAuthnSharedSigner sharedSigner = ...;
Safe4337Module safe4337Module = ...;
SafeModuleSetup moduleSetup = ...;
MultiSend multiSend = ...;
uint256 signerX = ...;
uint256 signerY = ...;
uint256 signerVerifiers = ...;
address initializer = ...;
bytes memory initializerData = ...;
address fallbackHandler = ...;
address[] owners memory = [..., address(sharedSigner), ...];
uint256 threshold = ...;
bytes memory configureData = abi.encodeCall(
sharedSigner.configure,
(
signerX,
signerY,
signerVerifiers
)
);
address[] memory modules = new address[](1);
{
modules[0] = safe4337Module;
}
bytes memory enableModulesData = abi.encodeCall(
moduleSetup.enableModules,
(modules)
);
bytes memory initCode = abi.encodePacked(
proxyFactory,
abi.encodeCall(
launchpad.setup,
(
owners,
threshold,
address(multiSend),
abi.encodeCall(
multiSend.multiSend,
abi.encodePacked(
// configure signer
uint8(1), // operation
address(sharedSigner),
uint256(0), // value
configureData.length,
configureData
// enable modules
uint8(1), // operation
address(moduleSetup),
uint256(0), // value
enableModulesData.length,
enableModulesData
)
),
address(safe4337Module),
address(0),
0,
address(0)
)
)
);
```
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import {PackedUserOperation} from "@account-abstraction/contracts/interfaces/Pac
import {_packValidationData} from "@account-abstraction/contracts/core/Helpers.sol";
import {SafeStorage} from "@safe-global/safe-contracts/contracts/libraries/SafeStorage.sol";

import {ISafeSignerFactory, P256} from "../interfaces/ISafeSignerFactory.sol";
import {ISafe} from "../interfaces/ISafe.sol";
import {ERC1271} from "../libraries/ERC1271.sol";
import {ISafeSignerFactory, P256} from "../../interfaces/ISafeSignerFactory.sol";
import {ISafe} from "../../interfaces/ISafe.sol";
import {ERC1271} from "../../libraries/ERC1271.sol";

/**
* @title Safe Launchpad for Custom ECDSA Signing Schemes.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.8.0;

import {SignatureValidator} from "../base/SignatureValidator.sol";
import {ISafe} from "../interfaces/ISafe.sol";
import {P256, WebAuthn} from "../libraries/WebAuthn.sol";
import {SignatureValidator} from "../../base/SignatureValidator.sol";
import {ISafe} from "../../interfaces/ISafe.sol";
import {P256, WebAuthn} from "../../libraries/WebAuthn.sol";

/**
* @title Safe WebAuthn Shared Signer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import {
} from '@safe-global/safe-4337/src/utils/userOp'
import { chainId, encodeMultiSendTransactions } from '@safe-global/safe-4337/test/utils/encoding'
import { Safe4337 } from '@safe-global/safe-4337/src/utils/safe'
import { WebAuthnCredentials } from '../utils/webauthnShim'
import { decodePublicKey, encodeWebAuthnSignature } from '../../src/utils/webauthn'
import { WebAuthnCredentials } from '../../utils/webauthnShim'
import { decodePublicKey, encodeWebAuthnSignature } from '../../../src/utils/webauthn'

describe('Safe4337Module', () => {
const setupTests = deployments.createFixture(async ({ deployments }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { setBalance } from '@nomicfoundation/hardhat-network-helpers'
import { expect } from 'chai'
import { deployments, ethers } from 'hardhat'

import { SafeSignerLaunchpad, PackedUserOperationStruct } from '../../typechain-types/contracts/4337/SafeSignerLaunchpad'
import * as ERC1271 from '../utils/erc1271'
import { SafeSignerLaunchpad, PackedUserOperationStruct } from '../../../typechain-types/contracts/4337/experimental/SafeSignerLaunchpad'
import * as ERC1271 from '../../utils/erc1271'

describe('SafeSignerLaunchpad', () => {
const setupTests = deployments.createFixture(async () => {
Expand Down
Loading

0 comments on commit 84f84e0

Please sign in to comment.