Skip to content

Commit

Permalink
Create SafeWebAuthnSignerProxy (#370)
Browse files Browse the repository at this point in the history
Fixes #312 

Changes in PR:
- [x] Create `SafeWebAuthnSignerSingleton`. This contract has no
storage, no immutables, and no constants.
- [x] Create `SafeWebAuthnSignerProxy` and corresponding proxy factory
i.e. `SafeWebAuthnSignerProxyFactory`

- Create immutable `singleton` contract when deploying proxy factory
- `SafeWebAuthnSignerProxyFactory.isValidSignatureForSigner(...)`
forwards to singleton contract instead directly calling `WebAuthn`
library
- `SafeWebAuthnSignerProxy` uses no storage to avoid 4337 storage access
restriction violation.
- Add unit test to verify that `SafeWebAuthnSignerProxy` forwards every
call to singleton address
- [x] Report gas usage metrics
- [x] Update github workflow to execute benchmarking tests separately.
(This is required to get accurate gas usage numbers)
- [x] Update documentation
- [x] Replace `SafeWebAuthnSigner` with `SafeWebAuthnSignerProxy`, same
for factory contract
- [x] Update all tests to use `SafeWebAuthnSignerProxy`. Hence, the
large diff in this PR.


Benchmark number from this PR:

```
Gas Benchmarking Proxy [@bench]
    SafeWebAuthnSignerProxy
      ⛽ deployment: 164012
      ✔ Benchmark signer deployment cost (10[38](https://github.com/safe-global/safe-modules/actions/runs/8739938588/job/23982409583?pr=370#step:4:39)ms)
      ⛽ verification (FreshCryptoLib): 216248
      ✔ Benchmark signer verification cost with FreshCryptoLib verifier (201ms)
      ⛽ verification (daimo-eth): 346151
      ✔ Benchmark signer verification cost with daimo-eth verifier (186ms)
      ⛽ verification (Dummy): 14[40](https://github.com/safe-global/safe-modules/actions/runs/8739938588/job/23982409583?pr=370#step:4:41)2
      ✔ Benchmark signer verification cost with Dummy verifier
```

Benchmark number from main:
```
  Gas Benchmarking [@bench]
    SafeWebAuthnSigner
      ⛽ deployment: 491288
      ✔ Benchmark signer deployment cost (654ms)
      ⛽ verification (FreshCryptoLib): 209630
      ✔ Benchmark signer verification cost with FreshCryptoLib verifier (138ms)
      ⛽ verification (daimo-eth): 336881
      ✔ Benchmark signer verification cost with daimo-eth verifier (108ms)
      ⛽ verification (Dummy): 11266
      ✔ Benchmark signer verification cost with Dummy verifier
```

---------

Co-authored-by: Nicholas Rodrigues Lordello <nick@safe.global>
  • Loading branch information
akshay-ap and nlordell authored Apr 23, 2024
1 parent f289a22 commit fbce8f1
Show file tree
Hide file tree
Showing 14 changed files with 231 additions and 96 deletions.
12 changes: 12 additions & 0 deletions .github/workflows/ci_passkey.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,15 @@ jobs:
- run: |
npm ci
npm run test:4337 -w modules/passkey
benchmark:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20.x
cache: npm
cache-dependency-path: package-lock.json
- run: |
npm ci
npm run bench -w modules/passkey
41 changes: 29 additions & 12 deletions modules/passkey/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

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

## SafeWebAuthnSignerProxy

Use of `SafeWebAuthnSignerProxy` provides gas savings compared to the complete bytecode contract for each signer creation. The `SafeWebAuthnSignerProxy` contract is a proxy contract that forwards calls to the `SafeWebAuthnSignerSingleton` contract which is a singleton contract. Both `SafeWebAuthnSignerProxy` and `SafeWebAuthnSignerSingleton` use no storage slots to avoid storage access violations defined in ERC-4337. The details on gas savings can be found in [this PR](https://github.com/safe-global/safe-modules/pull/370).

## Setup and Execution flow

```mermaid
Expand All @@ -11,39 +15,42 @@ participant CS as CredentialStore
actor B as Bundler
participant EP as EntryPoint
participant SPF as SafeProxyFactory
participant WASF as WebAuthnSignerFactory
participant SWASPF as SafeWebAuthnSignerFactory
participant SP as SafeProxy
participant SSL as SafeSignerLaunchpad
participant S as Singleton
participant M as Module
participant WAV as WebAuthnVerifier
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->>+WASF: Get signer address (signer might not be deployed yet)
WASF->>U: Signer address
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-->>WASF: Forward validation
WASF-->>WAV: call verifyWebAuthnSignatureAllowMalleability
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->>WASF: Signature verification result
WASF-->>SSL: Return magic value
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->>+WASF: Create Signer
WASF-->>SP: Return owner address
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
Expand All @@ -59,9 +66,9 @@ SP->>+T: Perform transaction
end
```

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 WebAuthnVerifier, which in turn forwards the signature validation to the 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.
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.

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.
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.

## Usage

Expand Down Expand Up @@ -128,10 +135,20 @@ This command will upload the contract source to Etherscan.
npx hardhat --network <network> etherscan-verify
```

### Run benchmark tests

```bash
npm run bench
```

## Security and Liability

All contracts are WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

## User stories

The test cases in [userstories](./test/userstories) directory demonstrates the usage of the passkey module in different scenarios like deploying a Safe account with passkey module enabled, executing a `userOp` with a Safe using Passkey signer, etc.

## License

All smart contracts are released under LGPL-3.0.
49 changes: 0 additions & 49 deletions modules/passkey/contracts/SafeWebAuthnSigner.sol

This file was deleted.

46 changes: 36 additions & 10 deletions modules/passkey/contracts/SafeWebAuthnSignerFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,33 @@
pragma solidity >=0.8.0;

import {ISafeSignerFactory} from "./interfaces/ISafeSignerFactory.sol";
import {ERC1271} from "./libraries/ERC1271.sol";
import {P256, WebAuthn} from "./libraries/WebAuthn.sol";
import {SafeWebAuthnSigner} from "./SafeWebAuthnSigner.sol";
import {SafeWebAuthnSignerProxy} from "./SafeWebAuthnSignerProxy.sol";
import {SafeWebAuthnSignerSingleton} from "./SafeWebAuthnSignerSingleton.sol";
import {P256} from "./libraries/P256.sol";

/**
* @title WebAuthnSignerFactory
* @dev A factory contract for creating and managing WebAuthn signers.
* @title SafeWebAuthnSignerFactory
* @dev A factory contract for creating and managing WebAuthn proxy signers.
*/
contract SafeWebAuthnSignerFactory is ISafeSignerFactory {
SafeWebAuthnSignerSingleton public immutable SINGLETON;

constructor() {
SINGLETON = new SafeWebAuthnSignerSingleton();
}

/**
* @inheritdoc ISafeSignerFactory
*/
function getSigner(uint256 x, uint256 y, P256.Verifiers verifiers) public view override returns (address signer) {
bytes32 codeHash = keccak256(
abi.encodePacked(type(SafeWebAuthnSigner).creationCode, x, y, uint256(P256.Verifiers.unwrap(verifiers)))
abi.encodePacked(
type(SafeWebAuthnSignerProxy).creationCode,
uint256(uint160(address(SINGLETON))),
x,
y,
uint256(P256.Verifiers.unwrap(verifiers))
)
);
signer = address(uint160(uint256(keccak256(abi.encodePacked(hex"ff", address(this), bytes32(0), codeHash)))));
}
Expand All @@ -28,8 +40,8 @@ contract SafeWebAuthnSignerFactory is ISafeSignerFactory {
signer = getSigner(x, y, verifiers);

if (_hasNoCode(signer)) {
SafeWebAuthnSigner created = new SafeWebAuthnSigner{salt: bytes32(0)}(x, y, verifiers);
assert(address(created) == signer);
SafeWebAuthnSignerProxy created = new SafeWebAuthnSignerProxy{salt: bytes32(0)}(address(SINGLETON), x, y, verifiers);
require(address(created) == signer);
}
}

Expand All @@ -43,8 +55,22 @@ contract SafeWebAuthnSignerFactory is ISafeSignerFactory {
uint256 y,
P256.Verifiers verifiers
) external view override returns (bytes4 magicValue) {
if (WebAuthn.verifySignature(message, signature, WebAuthn.USER_VERIFICATION, x, y, verifiers)) {
magicValue = ERC1271.MAGIC_VALUE;
address singleton = address(SINGLETON);
bytes memory data = abi.encodePacked(
abi.encodeWithSignature("isValidSignature(bytes32,bytes)", message, signature),
x,
y,
verifiers
);

// solhint-disable-next-line no-inline-assembly
assembly {
let dataSize := mload(data)
let dataLocation := add(data, 0x20)
// staticcall to the singleton contract with return size given as 32 bytes. The singleton contract is known and immutable so, it is safe to specify return size.
if staticcall(gas(), singleton, dataLocation, dataSize, 0, 32) {
magicValue := mload(0)
}
}
}

Expand Down
64 changes: 64 additions & 0 deletions modules/passkey/contracts/SafeWebAuthnSignerProxy.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.8.0;
import {P256} from "./libraries/WebAuthn.sol";

/**
* @title WebAuthn Safe Signature Validator
* @dev A proxy contracy that points to Safe signature validator implementation for a WebAuthn P-256 credential.
* @custom:security-contact bounty@safe.global
*/
contract SafeWebAuthnSignerProxy {
/**
* @notice The x coordinate of the P-256 public key of the WebAuthn credential.
*/
uint256 internal immutable _X;
/**
* @notice The y coordinate of the P-256 public key of the WebAuthn credential.
*/
uint256 internal immutable _Y;
/**
* @notice The P-256 verifiers used for ECDSA signature validation.
*/
P256.Verifiers internal immutable _VERIFIERS;

/**
* @notice The contract address to which proxy contract forwards the call via delegatecall.
*/
address internal immutable _SINGLETON;

/**
* @notice Creates a new WebAuthn Safe Signer Proxy.
* @param singleton Address of the singleton contract to which the proxy forwards the call via delegatecall.
* @param x The x coordinate of the P-256 public key of the WebAuthn credential.
* @param y The y coordinate of the P-256 public key of the WebAuthn credential.
* @param verifiers The P-256 verifiers used for ECDSA signature validation.
*/
constructor(address singleton, uint256 x, uint256 y, P256.Verifiers verifiers) {
_SINGLETON = singleton;
_X = x;
_Y = y;
_VERIFIERS = verifiers;
}

/**
* @dev Fallback function forwards all transactions and returns all received return data.
*/
// solhint-disable-next-line no-complex-fallback
fallback() external payable {
bytes memory data = abi.encodePacked(msg.data, _X, _Y, _VERIFIERS);
address singleton = _SINGLETON;

// solhint-disable-next-line no-inline-assembly
assembly {
let dataSize := mload(data)
let dataLocation := add(data, 0x20)

let success := delegatecall(gas(), singleton, dataLocation, dataSize, 0, 0)
returndatacopy(0, 0, returndatasize())
if iszero(success) {
revert(0, returndatasize())
}
return(0, returndatasize())
}
}
}
35 changes: 35 additions & 0 deletions modules/passkey/contracts/SafeWebAuthnSignerSingleton.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.8.0;

import {SignatureValidator} from "./base/SignatureValidator.sol";
import {P256, WebAuthn} from "./libraries/WebAuthn.sol";
/**
* @title WebAuthn Safe Signature Validator Singleton
* @dev A contract that represents a WebAuthn signer.
* @custom:security-contact bounty@safe.global
*/
contract SafeWebAuthnSignerSingleton is SignatureValidator {
/**
* @inheritdoc SignatureValidator
*/
function _verifySignature(bytes32 message, bytes calldata signature) internal view virtual override returns (bool success) {
(uint256 x, uint256 y, P256.Verifiers verifiers) = getConfiguration();

success = WebAuthn.verifySignature(message, signature, WebAuthn.USER_VERIFICATION, x, y, verifiers);
}

/**
* @notice Returns the x coordinate, y coordinate, and P-256 verifiers used for ECDSA signature validation. The values are expected to be passed by the SafeWebAuthnSignerProxy contract in msg.data.
* @return x The x coordinate of the P-256 public key.
* @return y The y coordinate of the P-256 public key.
* @return verifiers The P-256 verifiers.
*/
function getConfiguration() public pure returns (uint256 x, uint256 y, P256.Verifiers verifiers) {
// solhint-disable-next-line no-inline-assembly
assembly ("memory-safe") {
x := calldataload(sub(calldatasize(), 88))
y := calldataload(sub(calldatasize(), 56))
verifiers := shr(64, calldataload(sub(calldatasize(), 24)))
}
}
}
1 change: 0 additions & 1 deletion modules/passkey/contracts/libraries/WebAuthn.sol
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,6 @@ library WebAuthn {
// need to encode the signing message if the expected authenticator flags are missing).
// However, ordering things this way helps the Solidity compiler generate meaningfully more
// optimal code for the "happy path" when Yul optimizations are turned on.

bytes memory message = encodeSigningMessage(challenge, signature.authenticatorData, signature.clientDataFields);
if (checkAuthenticatorFlags(signature.authenticatorData, authenticatorFlags)) {
success = verifiers.verifySignatureAllowMalleability(_sha256(message), signature.r, signature.s, x, y);
Expand Down
Loading

0 comments on commit fbce8f1

Please sign in to comment.