Skip to content

Commit

Permalink
feat: add webauthn module (#189)
Browse files Browse the repository at this point in the history
  • Loading branch information
howydev authored Sep 27, 2024
1 parent 164e2dc commit fe721f2
Show file tree
Hide file tree
Showing 23 changed files with 415 additions and 97 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@
[submodule "lib/forge-gas-snapshot"]
path = lib/forge-gas-snapshot
url = https://github.com/marktoda/forge-gas-snapshot
[submodule "lib/webauthn-sol"]
path = lib/webauthn-sol
url = https://github.com/base-org/webauthn-sol
5 changes: 2 additions & 3 deletions gas/modular-account/ModularAccount.gas.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ contract ModularAccountGasTest is ModularAccountBenchmarkBase("ModularAccount")

assertEq(logs.length, 4);
// Logs:
// 0: SingleSignerValidationModule `SignerTransferred` (anonymous)
// 0: ECDSAValidationModule `SignerTransferred` (anonymous)
// 1: ModularAccount `ValidationInstalled`
// 2: ModularAccount `Initialized`
// 3: AccountFactory `ModularAccountDeployed`
Expand Down Expand Up @@ -161,8 +161,7 @@ contract ModularAccountGasTest is ModularAccountBenchmarkBase("ModularAccount")

uint32 entityId = 0;
bytes memory deferredValidationInstallData = abi.encode(entityId, owner1);
ModuleEntity deferredValidation =
ModuleEntityLib.pack(address(_deploySingleSignerValidationModule()), entityId);
ModuleEntity deferredValidation = ModuleEntityLib.pack(address(_deployECDSAValidationModule()), entityId);

PackedUserOperation memory userOp = PackedUserOperation({
sender: address(account1),
Expand Down
11 changes: 6 additions & 5 deletions gas/modular-account/ModularAccountBenchmarkBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {AccountFactory} from "../../src/factory/AccountFactory.sol";
import {FALLBACK_VALIDATION} from "../../src/helpers/Constants.sol";
import {ModuleEntity, ModuleEntityLib} from "../../src/libraries/ModuleEntityLib.sol";
import {ValidationConfig, ValidationConfigLib} from "../../src/libraries/ValidationConfigLib.sol";
import {SingleSignerValidationModule} from "../../src/modules/validation/SingleSignerValidationModule.sol";
import {ECDSAValidationModule} from "../../src/modules/validation/ECDSAValidationModule.sol";
import {ModuleSignatureUtils} from "../../test/utils/ModuleSignatureUtils.sol";
import {BenchmarkBase} from "../BenchmarkBase.sol";

Expand All @@ -19,23 +19,24 @@ abstract contract ModularAccountBenchmarkBase is BenchmarkBase, ModuleSignatureU
AccountFactory public factory;
ModularAccount public accountImpl;
SemiModularAccount public semiModularImpl;
SingleSignerValidationModule public singleSignerValidationModule;
ECDSAValidationModule public ecdsaValidationModule;

ModularAccount public account1;
ModuleEntity public signerValidation;

constructor(string memory accountImplName) BenchmarkBase(accountImplName) {
accountImpl = _deployModularAccount(IEntryPoint(entryPoint));
semiModularImpl = _deploySemiModularAccount(IEntryPoint(entryPoint));
singleSignerValidationModule = _deploySingleSignerValidationModule();
ecdsaValidationModule = _deployECDSAValidationModule();

factory = new AccountFactory(
entryPoint, accountImpl, semiModularImpl, address(singleSignerValidationModule), address(this)
entryPoint, accountImpl, semiModularImpl, address(ecdsaValidationModule), address(this)
);
}

function _deployAccount1() internal {
account1 = factory.createAccount(owner1, 0, 0);
signerValidation = ModuleEntityLib.pack(address(singleSignerValidationModule), 0);
signerValidation = ModuleEntityLib.pack(address(ecdsaValidationModule), 0);
}

function _deploySemiModularAccount1() internal {
Expand Down
3 changes: 1 addition & 2 deletions gas/modular-account/SemiModularAccount.gas.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,7 @@ contract ModularAccountGasTest is ModularAccountBenchmarkBase("SemiModularAccoun

uint32 entityId = 0;
bytes memory deferredValidationInstallData = abi.encode(entityId, owner1);
ModuleEntity deferredValidation =
ModuleEntityLib.pack(address(_deploySingleSignerValidationModule()), entityId);
ModuleEntity deferredValidation = ModuleEntityLib.pack(address(_deployECDSAValidationModule()), entityId);

PackedUserOperation memory userOp = PackedUserOperation({
sender: address(account1),
Expand Down
1 change: 1 addition & 0 deletions lib/webauthn-sol
Submodule webauthn-sol added at 619f20
5 changes: 4 additions & 1 deletion remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@ account-abstraction/=node_modules/account-abstraction/contracts/
solady=node_modules/solady/src/
@erc6900/reference-implementation/=node_modules/@erc6900/reference-implementation/src/
forge-gas-snapshot/=lib/forge-gas-snapshot/src/
forge-std/=lib/forge-std/
forge-std/=lib/forge-std/
webauthn-sol/=lib/webauthn-sol/
FreshCryptoLib/=lib/webauthn-sol/lib/FreshCryptoLib/solidity/src/
openzeppelin-contracts/=lib/webauthn-sol/lib/openzeppelin-contracts/
4 changes: 2 additions & 2 deletions src/factory/AccountFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ contract AccountFactory is Ownable {
IEntryPoint _entryPoint,
ModularAccount _accountImpl,
SemiModularAccount _semiModularImpl,
address _singleSignerValidationModule,
address _ecdsaValidationModule,
address owner
) Ownable(owner) {
ENTRY_POINT = _entryPoint;
ACCOUNT_IMPL = _accountImpl;
SEMI_MODULAR_ACCOUNT_IMPL = _semiModularImpl;
SINGLE_SIGNER_VALIDATION_MODULE = _singleSignerValidationModule;
SINGLE_SIGNER_VALIDATION_MODULE = _ecdsaValidationModule;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {IValidationModule} from "@erc6900/reference-implementation/interfaces/IV

import {BaseModule} from "../BaseModule.sol";
import {ReplaySafeWrapper} from "../ReplaySafeWrapper.sol";
import {ISingleSignerValidationModule} from "./ISingleSignerValidationModule.sol";
import {IECDSAValidationModule} from "./IECDSAValidationModule.sol";

/// @title ECSDA Validation
/// @author ERC-6900 Authors
Expand All @@ -20,12 +20,11 @@ import {ISingleSignerValidationModule} from "./ISingleSignerValidationModule.sol
/// Note: Uninstallation will NOT disable all installed validation entities. None of the functions are installed on
/// the account. Account states are to be retrieved from this global singleton directly.
///
/// - This validation supports ERC-1271. The signature is valid if it is signed by the owner's private key
/// (if the owner is an EOA) or if it is a valid ERC-1271 signature from the owner (if the owner is a contract).
/// - This validation supports ERC-1271. The signature is valid if it is signed by the owner's private key.
///
/// - This validation supports composition that other validation can relay on entities in this validation
/// to validate partially or fully.
contract SingleSignerValidationModule is ISingleSignerValidationModule, ReplaySafeWrapper, BaseModule {
contract ECDSAValidationModule is IECDSAValidationModule, ReplaySafeWrapper, BaseModule {
using MessageHashUtils for bytes32;

uint256 internal constant _SIG_VALIDATION_PASSED = 0;
Expand All @@ -37,7 +36,7 @@ contract SingleSignerValidationModule is ISingleSignerValidationModule, ReplaySa

mapping(uint32 entityId => mapping(address account => address)) public signers;

/// @inheritdoc ISingleSignerValidationModule
/// @inheritdoc IECDSAValidationModule
function transferSigner(uint32 entityId, address newSigner) external {
_transferSigner(entityId, newSigner);
}
Expand Down Expand Up @@ -113,7 +112,7 @@ contract SingleSignerValidationModule is ISingleSignerValidationModule, ReplaySa

/// @inheritdoc IModule
function moduleId() external pure returns (string memory) {
return "erc6900.single-signer-validation-module.1.0.0";
return "erc6900.ecdsa-validation-module.1.0.0";
}

function supportsInterface(bytes4 interfaceId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pragma solidity ^0.8.26;

import {IValidationModule} from "@erc6900/reference-implementation/interfaces/IValidationModule.sol";

interface ISingleSignerValidationModule is IValidationModule {
interface IECDSAValidationModule is IValidationModule {
/// @notice This event is emitted when Signer of the account's validation changes.
/// @param account The account whose validation Signer changed.
/// @param entityId The entityId for the account and the signer.
Expand Down
31 changes: 31 additions & 0 deletions src/modules/validation/IWebauthnValidationModule.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.26;

import {IValidationModule} from "@erc6900/reference-implementation/interfaces/IValidationModule.sol";

interface IWebauthnValidationModule is IValidationModule {
/// @notice This event is emitted when Signer of the account's validation changes.
/// @param account The account whose validation Signer changed.
/// @param entityId The entityId for the account and the signer.
/// @param newX X coordinate of the new signer.
/// @param newY Y coordinate of the new signer.
/// @param oldX X coordinate of the old signer.
/// @param oldY Y coordinate of the old signer.
event SignerTransferred(
address indexed account,
uint32 indexed entityId,
uint256 indexed newX,
uint256 indexed newY,
uint256 oldX,
uint256 oldY
) anonymous;

error NotAuthorized();

/// @notice Updates the signer for an entityId.
/// @dev Used for key rotation or deleting a key
/// @param entityId The entityId to update the signer for.
/// @param x The x coordinate of the new signer.
/// @param y The y coordinate of the new signer.
function transferSigner(uint32 entityId, uint256 x, uint256 y) external;
}
148 changes: 148 additions & 0 deletions src/modules/validation/WebauthnValidationModule.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.26;

import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol";
import {IERC165} from "@openzeppelin/contracts/interfaces/IERC165.sol";
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
import {WebAuthn} from "webauthn-sol/src/WebAuthn.sol";

import {IModule} from "@erc6900/reference-implementation/interfaces/IModule.sol";
import {IValidationModule} from "@erc6900/reference-implementation/interfaces/IValidationModule.sol";
import {BaseModule} from "@erc6900/reference-implementation/modules/BaseModule.sol";
import {ReplaySafeWrapper} from "@erc6900/reference-implementation/modules/ReplaySafeWrapper.sol";

import {IWebauthnValidationModule} from "./IWebauthnValidationModule.sol";

/// @title Webauthn Validation
/// @author Alchemy
/// @dev Implementation referenced from Webauthn + Coinbase Smart Wallet developed by Base.
/// @notice This validation enables Webauthn (secp256r1 curve) signature validation. It handles installation by
/// each entity (entityId).
/// Note: Uninstallation will NOT disable all installed validation entities. None of the functions are installed on
/// the account. Account states are to be retrieved from this global singleton directly.
///
/// - This validation supports ERC-1271. The signature is valid if it is signed by the owner's private key.
///
/// - This validation supports composition that other validation can relay on entities in this validation
/// to validate partially or fully.
contract WebauthnValidationModule is IWebauthnValidationModule, ReplaySafeWrapper, BaseModule {
using MessageHashUtils for bytes32;
using WebAuthn for WebAuthn.WebAuthnAuth;

struct PubKey {
uint256 x;
uint256 y;
}

uint256 internal constant _SIG_VALIDATION_PASSED = 0;
uint256 internal constant _SIG_VALIDATION_FAILED = 1;

// bytes4(keccak256("isValidSignature(bytes32,bytes)"))
bytes4 internal constant _1271_MAGIC_VALUE = 0x1626ba7e;
bytes4 internal constant _1271_INVALID = 0xffffffff;

mapping(uint32 entityId => mapping(address account => PubKey)) public signers;

/// @inheritdoc IWebauthnValidationModule
function transferSigner(uint32 entityId, uint256 x, uint256 y) external {
_transferSigner(entityId, x, y);
}

/// @inheritdoc IModule
function onInstall(bytes calldata data) external override {
(uint32 entityId, uint256 x, uint256 y) = abi.decode(data, (uint32, uint256, uint256));
_transferSigner(entityId, x, y);
}

/// @inheritdoc IModule
function onUninstall(bytes calldata data) external override {
uint32 entityId = abi.decode(data, (uint32));
_transferSigner(entityId, 0, 0);
}

/// @inheritdoc IValidationModule
function validateUserOp(uint32 entityId, PackedUserOperation calldata userOp, bytes32 userOpHash)
external
view
override
returns (uint256)
{
if (_validateSignature(entityId, userOp.sender, userOpHash.toEthSignedMessageHash(), userOp.signature)) {
return _SIG_VALIDATION_PASSED;
}

return _SIG_VALIDATION_FAILED;
}

/// @inheritdoc IValidationModule
/// @dev The signature is valid if it is signed by the owner's private key
/// (if the owner is an EOA) or if it is a valid ERC-1271 signature from the
/// owner (if the owner is a contract).
/// Note that the digest is wrapped in an EIP-712 struct to prevent cross-account replay attacks. The
/// replay-safe hash may be retrieved by calling the public function `replaySafeHash`.
function validateSignature(address account, uint32 entityId, address, bytes32 digest, bytes calldata signature)
external
view
override
returns (bytes4)
{
bytes32 _replaySafeHash = replaySafeHash(account, digest);
if (_validateSignature(entityId, account, _replaySafeHash, signature)) {
return _1271_MAGIC_VALUE;
}
return _1271_INVALID;
}

/// @inheritdoc IValidationModule
function validateRuntime(address, uint32, address, uint256, bytes calldata, bytes calldata)
external
pure
override
{
revert NotAuthorized();
}

// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ Module interface functions ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

/// @inheritdoc IModule
function moduleId() external pure returns (string memory) {
return "alchemy.webauthn-validation-module.1.0.0";
}

function supportsInterface(bytes4 interfaceId)
public
view
virtual
override(BaseModule, IERC165)
returns (bool)
{
return (interfaceId == type(IValidationModule).interfaceId || super.supportsInterface(interfaceId));
}

// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ Internal / Private functions ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

function _transferSigner(uint32 entityId, uint256 newX, uint256 newY) internal {
PubKey memory oldPubKey = signers[entityId][msg.sender];
signers[entityId][msg.sender] = PubKey(newX, newY);
emit SignerTransferred(msg.sender, entityId, newX, newY, oldPubKey.x, oldPubKey.y);
}

function _validateSignature(uint32 entityId, address account, bytes32 hash, bytes calldata signature)
internal
view
returns (bool)
{
WebAuthn.WebAuthnAuth memory webAuthnAuth = abi.decode(signature, (WebAuthn.WebAuthnAuth));
PubKey storage key = signers[entityId][account];

if (WebAuthn.verify(abi.encode(hash), false, webAuthnAuth, key.x, key.y)) {
return true;
}

return false;
}
}
2 changes: 1 addition & 1 deletion test/account/DeferredValidation.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ contract DeferredValidationTest is AccountTestBase {

function setUp() external {
_encodedCall = abi.encodeCall(ModularAccount.execute, (makeAddr("dead"), 0, ""));
_deferredValidation = ModuleEntityLib.pack(address(_deploySingleSignerValidationModule()), 0);
_deferredValidation = ModuleEntityLib.pack(address(_deployECDSAValidationModule()), 0);
_isSmaTest = vm.envOr("SMA_TEST", false);

uint32 entityId = 0;
Expand Down
3 changes: 1 addition & 2 deletions test/account/GlobalValidationTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ contract GlobalValidationTest is AccountTestBase {
account2 = ModularAccount(payable(factory.getAddress(owner2, 0, TEST_DEFAULT_VALIDATION_ENTITY_ID)));
vm.deal(address(account2), 100 ether);

_signerValidation =
ModuleEntityLib.pack(address(singleSignerValidationModule), TEST_DEFAULT_VALIDATION_ENTITY_ID);
_signerValidation = ModuleEntityLib.pack(address(ecdsaValidationModule), TEST_DEFAULT_VALIDATION_ENTITY_ID);

ethRecipient = makeAddr("ethRecipient");
vm.deal(ethRecipient, 1 wei);
Expand Down
Loading

0 comments on commit fe721f2

Please sign in to comment.