Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create SafeWebAuthnSignerProxy #370

Merged
merged 53 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
5f2786a
[#312] Create SafeWebAuthnSignerProxy
akshay-ap Apr 8, 2024
2be5087
[#312] Benchmark deployment, verification cost for proxy
akshay-ap Apr 8, 2024
96aaf53
Change test name
akshay-ap Apr 9, 2024
a50bc54
[#312] Use immutables for storing values
akshay-ap Apr 9, 2024
b07adfb
[#312] Update natspec doc
akshay-ap Apr 9, 2024
da30c7f
[#312] Mark singleton as immutable
akshay-ap Apr 9, 2024
cdf98ac
[#312] Run benchmarking tests in separate job
akshay-ap Apr 10, 2024
67e2063
[#312] Make signer singleton immutable
akshay-ap Apr 10, 2024
082f588
[#312] Minor naming update in ci_passkey.yml
akshay-ap Apr 10, 2024
30ab2a3
Update modules/passkey/contracts/SafeWebAuthnSignerProxyFactory.sol
akshay-ap Apr 11, 2024
14152c5
[#312] Remove replace SafeWebAuthnSigner.sol with SafeWebAuthnSignerP…
akshay-ap Apr 11, 2024
09c87c1
[#312] Remove SafeWebAuthnSigner
akshay-ap Apr 11, 2024
4573082
[#312] Return value if staticcall to singleton succeeds
akshay-ap Apr 11, 2024
738ec46
[#312] Add unit test for SafeWebAuthnSignerProxy
akshay-ap Apr 11, 2024
ecf5c9f
[#312] Fix lint issue
akshay-ap Apr 11, 2024
b03e475
[#312] Mention use of signer proxy in README.md
akshay-ap Apr 11, 2024
7e2a6b6
[#312] Fix faling tests
akshay-ap Apr 17, 2024
5ee6d5e
[#312] Remove unused report
akshay-ap Apr 17, 2024
e6e6212
[#312] Fix failing test
akshay-ap Apr 18, 2024
0204c05
[#312] Fix lint issue, replace string with const
akshay-ap Apr 18, 2024
84a7b97
[#312] Remove singleton signer tests
akshay-ap Apr 18, 2024
f8bb9ed
[#312] Fix failing benchmarking tests
akshay-ap Apr 18, 2024
915276c
[#312] Rename Signer factory contract, update natspec doc, decode x,y…
akshay-ap Apr 19, 2024
9e542ea
[#312] Update README.md
akshay-ap Apr 19, 2024
22b4e9f
[#312] Create public getters in singleton, mark proxy variables as in…
akshay-ap Apr 19, 2024
3ef74ee
[#312] Rename variables in SafeWebAuthnSignerProxy.sol
akshay-ap Apr 19, 2024
103651c
Update modules/passkey/contracts/SafeWebAuthnSignerProxy.sol
akshay-ap Apr 19, 2024
e64e265
[#312] Update natspecdoc, rename var
akshay-ap Apr 19, 2024
8818b46
[#312] Use single getter for public key and verifier address
akshay-ap Apr 19, 2024
e9eccd5
Update modules/passkey/test/GasBenchmarkingProxy.spec.ts
akshay-ap Apr 19, 2024
75eec83
Update modules/passkey/test/GasBenchmarkingProxy.spec.ts
akshay-ap Apr 19, 2024
7e05b55
Update modules/passkey/test/userstories/OffchainPasskeyVerification.s…
akshay-ap Apr 19, 2024
9a72ab5
[#312] Use singleton so that contract points to correct abi
akshay-ap Apr 19, 2024
4e66ab6
[#312] Fix variable name in Gas benchmarking proxy tests
akshay-ap Apr 19, 2024
e45e7db
[#312] Minor test refactor
akshay-ap Apr 19, 2024
45b1eb8
[#312] Lint fixes
akshay-ap Apr 19, 2024
b1f47a9
[#312] Gas benchmarking updates
akshay-ap Apr 19, 2024
d1b713e
[#312] Fix dataOffset
akshay-ap Apr 19, 2024
37b87c1
Update modules/passkey/contracts/SafeWebAuthnSignerSingleton.sol
akshay-ap Apr 22, 2024
680ea5b
Update modules/passkey/contracts/SafeWebAuthnSignerSingleton.sol
akshay-ap Apr 22, 2024
f000c79
Update modules/passkey/test/SafeWebAuthnSignerProxy.spec.ts
akshay-ap Apr 22, 2024
4ba34fa
Update modules/passkey/contracts/SafeWebAuthnSignerFactory.sol
akshay-ap Apr 22, 2024
9427756
[#312] Fix variable name
akshay-ap Apr 22, 2024
96ccadc
[#312] Restore deleted files
akshay-ap Apr 22, 2024
f35a2eb
[#312] Use contract name instead of address in immutable type for Sin…
akshay-ap Apr 23, 2024
61822da
Update modules/passkey/contracts/SafeWebAuthnSignerProxy.sol
akshay-ap Apr 23, 2024
c154ec4
Update modules/passkey/contracts/SafeWebAuthnSignerFactory.sol
akshay-ap Apr 23, 2024
1aee6ee
Update modules/passkey/contracts/SafeWebAuthnSignerFactory.sol
akshay-ap Apr 23, 2024
3138afc
Update modules/passkey/contracts/SafeWebAuthnSignerFactory.sol
akshay-ap Apr 23, 2024
505f0d6
Revert "Update modules/passkey/contracts/SafeWebAuthnSignerFactory.sol"
akshay-ap Apr 23, 2024
a35771a
[#312] Fix build error
akshay-ap Apr 23, 2024
7ec427a
[#312] Fix compiler warning
akshay-ap Apr 23, 2024
7c6bb57
[#312] Add comment
akshay-ap Apr 23, 2024
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
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),
akshay-ap marked this conversation as resolved.
Show resolved Hide resolved
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 [email protected]
*/
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 [email protected]
*/
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
Loading