Skip to content

Commit

Permalink
Add WebAuthnSignerFactory
Browse files Browse the repository at this point in the history
  • Loading branch information
mmv08 committed Mar 5, 2024
1 parent 69ed0cf commit 72e207b
Show file tree
Hide file tree
Showing 24 changed files with 1,342 additions and 337 deletions.
11 changes: 11 additions & 0 deletions .github/workflows/ci_passkey.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,14 @@ jobs:
- run: npm run lint -w modules/passkey
- run: npm run fmt:check -w modules/passkey
- run: npm run build -w modules/passkey
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
node-version: 20.x
cache: npm
cache-dependency-path: package-lock.json
- run: npm ci
- run: npm run e2e -w modules/passkey
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ interface IWebAuthnVerifier {
* Both functions take the authenticator data, authenticator flags, challenge, client data fields, r and s components of the signature, and x and y coordinates of the public key as input.
* The `verifyWebAuthnSignature` function also checks for signature malleability by ensuring that the s component is less than the curve order n/2.
*/
contract WebAuthnVerifier is IWebAuthnVerifier {
contract WebAuthnVerifier is IWebAuthnVerifier, P256Wrapper {
constructor(address verifier) P256Wrapper(verifier) {}

/**
Expand Down
2 changes: 1 addition & 1 deletion modules/4337/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,6 @@
}
},
"dependencies": {
"@safe-global/safe-contracts": "^1.4.1"
"@safe-global/safe-contracts": "^1.4.1-build.0"
}
}
2 changes: 2 additions & 0 deletions modules/passkey/.env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Entrypoint address to use for the Safe 4337 module deployment in 4337 tests
TESTS_4337_MODULE_DEPLOYMENT_ENTRY_POINT_ADDRESS=""
277 changes: 277 additions & 0 deletions modules/passkey/contracts/4337/Safe256BitECSignerLaunchpad.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.8.0 <0.9.0;

import {IAccount} from "@account-abstraction/contracts/interfaces/IAccount.sol";
import {UserOperation} from "@account-abstraction/contracts/interfaces/UserOperation.sol";
import {_packValidationData} from "@account-abstraction/contracts/core/Helpers.sol";
import {SafeStorage} from "@safe-global/safe-contracts/contracts/libraries/SafeStorage.sol";
import {SignatureValidatorConstants} from "../SignatureValidatorConstants.sol";
import {ICustom256BitECSignerFactory} from "../interfaces/ICustomSignerFactory.sol";

/**
* @title SafeOpLaunchpad - A contract for Safe initialization with custom unique signers that would violate ERC-4337 factory rules.
* @dev The is intended to be set as a Safe proxy's implementation for ERC-4337 user operation that deploys the account.
*/
contract Safe256BitECSignerLaunchpad is IAccount, SafeStorage, SignatureValidatorConstants {
bytes32 private constant DOMAIN_SEPARATOR_TYPEHASH = keccak256("EIP712Domain(uint256 chainId,address verifyingContract)");

// keccak256("SafeSignerLaunchpad.initHash") - 1
uint256 private constant INIT_HASH_SLOT = 0x1d2f0b9dbb6ed3f829c9614e6c5d2ea2285238801394dc57e8500e0e306d8f80;

/**
* @notice The keccak256 hash of the EIP-712 SafeInit struct, representing the structure of a ERC-4337 compatible deferred Safe initialization.
* {address} singleton - The singleton to evolve into during the setup.
* {address} signerFactory - The unique signer factory to use for creating an owner.
* {bytes} signerData - The signer data to use the owner.
* {address} setupTo - The contract to delegatecall during setup.
* {bytes} setupData - The calldata for the setup delegatecall.
* {address} fallbackHandler - The fallback handler to initialize the Safe with.
*/
bytes32 private constant SAFE_INIT_TYPEHASH =
keccak256(
"SafeInit(address singleton,address signerFactory,uint256 signerX,uint256 signerY,address signerVerifier,address setupTo,bytes setupData,address fallbackHandler)"
);

/**
* @notice The keccak256 hash of the EIP-712 SafeInitOp struct, representing the user operation to execute alongside initialization.
* {bytes32} userOpHash - The user operation hash being executed.
* {uint48} validAfter - A timestamp representing from when the user operation is valid.
* {uint48} validUntil - A timestamp representing until when the user operation is valid, or 0 to indicated "forever".
* {address} entryPoint - The address of the entry point that will execute the user operation.
*/
bytes32 private constant SAFE_INIT_OP_TYPEHASH =
keccak256("SafeInitOp(bytes32 userOpHash,uint48 validAfter,uint48 validUntil,address entryPoint)");

address private immutable SELF;
address public immutable SUPPORTED_ENTRYPOINT;

constructor(address entryPoint) {
require(entryPoint != address(0), "Invalid entry point");

SELF = address(this);
SUPPORTED_ENTRYPOINT = entryPoint;
}

modifier onlyProxy() {
require(singleton == SELF, "Not called from proxy");
_;
}

modifier onlySupportedEntryPoint() {
require(msg.sender == SUPPORTED_ENTRYPOINT, "Unsupported entry point");
_;
}

receive() external payable {}

function preValidationSetup(bytes32 initHash, address to, bytes calldata preInit) external onlyProxy {
_setInitHash(initHash);
if (to != address(0)) {
(bool success, ) = to.delegatecall(preInit);
require(success, "Pre-initialization failed");
}
}

function getInitHash(
address singleton,
address signerFactory,
uint256 signerX,
uint256 signerY,
address signerVerifier,
address setupTo,
bytes memory setupData,
address fallbackHandler
) public view returns (bytes32 initHash) {
initHash = keccak256(
abi.encodePacked(
bytes1(0x19),
bytes1(0x01),
_domainSeparator(),
keccak256(
abi.encode(
SAFE_INIT_TYPEHASH,
singleton,
signerFactory,
signerX,
signerY,
signerVerifier,
setupTo,
keccak256(setupData),
fallbackHandler
)
)
)
);
}

function getOperationHash(bytes32 userOpHash, uint48 validAfter, uint48 validUntil) public view returns (bytes32 operationHash) {
operationHash = keccak256(_getOperationData(userOpHash, validAfter, validUntil));
}

function validateUserOp(
UserOperation calldata userOp,
bytes32 userOpHash,
uint256 missingAccountFunds
) external override onlyProxy onlySupportedEntryPoint returns (uint256 validationData) {
address signerFactory;
uint256 signerX;
uint256 signerY;
address signerVerifier;
{
require(this.initializeThenUserOp.selector == bytes4(userOp.callData[:4]), "invalid user operation data");

address singleton;
address setupTo;
bytes memory setupData;
address fallbackHandler;
(singleton, signerFactory, signerX, signerY, signerVerifier, setupTo, setupData, fallbackHandler, ) = abi.decode(
userOp.callData[4:],
(address, address, uint256, uint256, address, address, bytes, address, bytes)
);
bytes32 initHash = getInitHash(singleton, signerFactory, signerX, signerY, signerVerifier, setupTo, setupData, fallbackHandler);

require(initHash == _initHash(), "invalid init hash");
}

validationData = _validateSignatures(userOp, userOpHash, signerFactory, signerX, signerY, signerVerifier);
if (missingAccountFunds > 0) {
// solhint-disable-next-line no-inline-assembly
assembly ("memory-safe") {
// The `pop` is necessary here because solidity 0.5.0
// enforces "strict" assembly blocks and "statements (elements of a block) are disallowed if they return something onto the stack at the end."
// This is not well documented, the quote is taken from here:
// https://github.com/ethereum/solidity/issues/1820
// The compiler will throw an error if we keep the success value on the stack
pop(call(gas(), caller(), missingAccountFunds, 0, 0, 0, 0))
}
}
}

/**
* @dev Validates that the user operation is correctly signed and returns an ERC-4337 packed validation data
* of `validAfter || validUntil || authorizer`:
* - `authorizer`: 20-byte address, 0 for valid signature or 1 to mark signature failure (this module does not make use of signature aggregators).
* - `validUntil`: 6-byte timestamp value, or zero for "infinite". The user operation is valid only up to this time.
* - `validAfter`: 6-byte timestamp. The user operation is valid only after this time.
* @param userOp User operation struct.
* @return validationData An integer indicating the result of the validation.
*/
function _validateSignatures(
UserOperation calldata userOp,
bytes32 userOpHash,
address signerFactory,
uint256 signerX,
uint256 signerY,
address signerVerifier
) internal view returns (uint256 validationData) {
uint48 validAfter;
uint48 validUntil;
bytes calldata signature;
{
bytes calldata sig = userOp.signature;
validAfter = uint48(bytes6(sig[0:6]));
validUntil = uint48(bytes6(sig[6:12]));
signature = sig[12:];
}

bytes memory operationData = _getOperationData(userOpHash, validAfter, validUntil);
bytes32 operationHash = keccak256(operationData);
try
ICustom256BitECSignerFactory(signerFactory).isValidSignatureForSigner(
signerX,
signerY,
signerVerifier,
operationHash,
signature
)
returns (bytes4 magicValue) {
// The timestamps are validated by the entry point, therefore we will not check them again
validationData = _packValidationData(magicValue != EIP1271_MAGIC_VALUE, validUntil, validAfter);
} catch {
validationData = _packValidationData(true, validUntil, validAfter);
}
}

function initializeThenUserOp(
address singleton,
address signerFactory,
uint256 signerX,
uint256 signerY,
address signerVerifier,
address setupTo,
bytes calldata setupData,
address fallbackHandler,
bytes memory callData
) external onlySupportedEntryPoint {
SafeStorage.singleton = singleton;
{
address[] memory owners = new address[](1);
owners[0] = ICustom256BitECSignerFactory(signerFactory).createSigner(signerX, signerY, signerVerifier);

SafeSetup(address(this)).setup(owners, 1, setupTo, setupData, fallbackHandler, address(0), 0, payable(address(0)));
}

(bool success, bytes memory returnData) = address(this).delegatecall(callData);
if (!success) {
// solhint-disable-next-line no-inline-assembly
assembly ("memory-safe") {
revert(add(returnData, 0x20), mload(returnData))
}
}

_setInitHash(0);
}

function _domainSeparator() internal view returns (bytes32) {
return keccak256(abi.encode(DOMAIN_SEPARATOR_TYPEHASH, block.chainid, SELF));
}

function _getOperationData(
bytes32 userOpHash,
uint48 validAfter,
uint48 validUntil
) internal view returns (bytes memory operationData) {
operationData = abi.encodePacked(
bytes1(0x19),
bytes1(0x01),
_domainSeparator(),
keccak256(abi.encode(SAFE_INIT_OP_TYPEHASH, userOpHash, validAfter, validUntil, SUPPORTED_ENTRYPOINT))
);
}

function _initHash() public view returns (bytes32 value) {
// solhint-disable-next-line no-inline-assembly
assembly ("memory-safe") {
value := sload(INIT_HASH_SLOT)
}
}

function _setInitHash(bytes32 value) internal {
// solhint-disable-next-line no-inline-assembly
assembly ("memory-safe") {
sstore(INIT_HASH_SLOT, value)
}
}

function _isContract(address account) internal view returns (bool) {
uint256 size;
// solhint-disable-next-line no-inline-assembly
assembly ("memory-safe") {
size := extcodesize(account)
}
return size > 0;
}
}

interface SafeSetup {
function setup(
address[] calldata _owners,
uint256 _threshold,
address to,
bytes calldata data,
address fallbackHandler,
address paymentToken,
uint256 payment,
address payable paymentReceiver
) external;
}
3 changes: 1 addition & 2 deletions modules/passkey/contracts/WebAuthnSigner.sol
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ contract WebAuthnSigner is SignatureValidator {
*/
contract WebAuthnSignerFactory is ICustom256BitECSignerFactory, SignatureValidatorConstants {
// @inheritdoc ICustom256BitECSignerFactory
function getSigner(uint256 qx, uint256 qy, address verifier) public override view returns (address signer) {
function getSigner(uint256 qx, uint256 qy, address verifier) public view override returns (address signer) {
bytes32 codeHash = keccak256(abi.encodePacked(type(WebAuthnSigner).creationCode, qx, qy, uint256(uint160(verifier))));
signer = address(uint160(uint256(keccak256(abi.encodePacked(hex"ff", address(this), bytes32(0), codeHash)))));
}
Expand Down Expand Up @@ -101,7 +101,6 @@ contract WebAuthnSignerFactory is ICustom256BitECSignerFactory, SignatureValidat
}
}


/**
* @dev Checks if the provided account has no code.
* @param account The address of the account to check.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ interface ICustomECSignerFactory {
) external view returns (bytes4 magicValue);
}

/**
* @title ICustom256BitECSignerFactory
* @dev Interface for creating and verifying ECDSA signers using 256-bit elliptic curves.
*/
interface ICustom256BitECSignerFactory {
/**
* @notice Gets the unique signer address for the specified data.
Expand Down Expand Up @@ -79,4 +83,3 @@ interface ICustom256BitECSignerFactory {
bytes calldata signature
) external view returns (bytes4 magicValue);
}

5 changes: 5 additions & 0 deletions modules/passkey/contracts/test/4337ContractsImports.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// SPDX-License-Identifier: LGPL-3.0-only
/* solhint-disable no-global-import */
pragma solidity >=0.8.0;

import "@account-abstraction/contracts/interfaces/IEntryPoint.sol";
10 changes: 10 additions & 0 deletions modules/passkey/contracts/test/SafeContractsImports.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// SPDX-License-Identifier: LGPL-3.0-only
/* solhint-disable no-global-import */
pragma solidity >=0.8.0;

import "@safe-global/safe-contracts/contracts/libraries/MultiSend.sol";
import "@safe-global/safe-contracts/contracts/libraries/SafeStorage.sol";
import "@safe-global/safe-contracts/contracts/proxies/SafeProxyFactory.sol";
import "@safe-global/safe-contracts/contracts/SafeL2.sol";
import "@safe-global/safe-erc4337/contracts/Safe4337Module.sol";
import "@safe-global/safe-erc4337/contracts/SafeModuleSetup.sol";
35 changes: 35 additions & 0 deletions modules/passkey/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
version: '3.8'

services:
geth:
image: docker.io/ethereum/client-go:stable
restart: always
environment:
GETH_DEV: 'true'
GETH_HTTP: 'true'
GETH_HTTP_ADDR: '0.0.0.0'
GETH_HTTP_API: 'personal,eth,net,web3,debug'
GETH_HTTP_VHOSTS: '*'
GETH_RPC_ALLOW_UNPROTECTED_TXS: 'true'
ports:
- 8545:8545

bundler:
build:
context: .
dockerfile: docker/bundler/Dockerfile
restart: always
command: ['--auto', '--network=http://geth:8545']
ports:
- 3000:3000

bundler-upstream:
build:
context: .
dockerfile: docker/bundler/Dockerfile
args:
TAG: main
restart: always
command: ['--auto', '--network=http://geth:8545']
ports:
- 3000:3000
Loading

0 comments on commit 72e207b

Please sign in to comment.