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

feat: add webauthn module #189

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
2 changes: 1 addition & 1 deletion gas/modular-account/ModularAccount.gas.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ contract ModularAccountGasTest is ModularAccountBenchmarkBase("ModularAccount")
assertEq(logs.length, 5);
// Logs:
// 0: ERC1967Proxy `Upgraded`
// 1: SingleSignerValidationModule `SignerTransferred` (anonymous)
// 1: ECDSAValidationModule `SignerTransferred` (anonymous)
// 2: ModularAccount `ValidationInstalled`
// 3: ModularAccount `Initialized`
// 4: AccountFactory `ModularAccountDeployed`
Expand Down
10 changes: 5 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 {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 @@ -18,7 +18,7 @@ abstract contract ModularAccountBenchmarkBase is BenchmarkBase, ModuleSignatureU
AccountFactory public factory;
ModularAccount public accountImpl;
SemiModularAccount public semiModularImpl;
SingleSignerValidationModule public singleSignerValidationModule;
ECDSAValidationModule public ecdsaValidationModule;

ModularAccount public account1;

Expand All @@ -27,16 +27,16 @@ abstract contract ModularAccountBenchmarkBase is BenchmarkBase, ModuleSignatureU
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
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 @@ -27,15 +27,15 @@
IEntryPoint _entryPoint,
ModularAccount _accountImpl,
SemiModularAccount _semiModularImpl,
address _singleSignerValidationModule,
address _ECDSAValidationModule,

Check failure on line 30 in src/factory/AccountFactory.sol

View workflow job for this annotation

GitHub Actions / Check Format and Run Linters

Function param name must be in mixedCase

Check warning on line 30 in src/factory/AccountFactory.sol

View workflow job for this annotation

GitHub Actions / Check Format and Run Linters

Variable name must be in mixedCase
address owner
) Ownable(owner) {
ENTRY_POINT = _entryPoint;
_PROXY_BYTECODE_HASH =
keccak256(abi.encodePacked(type(ERC1967Proxy).creationCode, abi.encode(address(_accountImpl), "")));
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 @@ -25,7 +25,7 @@ import {ISingleSignerValidationModule} from "./ISingleSignerValidationModule.sol
///
/// - 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 +37,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 +113,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 ERC-6900 Authors
/// @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
/// (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 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
function validateRuntime(address, uint32, address, uint256, bytes calldata, bytes calldata)
external
pure
override
{
revert NotAuthorized();
}

/// @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)

Check warning on line 92 in src/modules/validation/WebauthnValidationModule.sol

View workflow job for this annotation

GitHub Actions / Check Format and Run Linters

Function order is incorrect, external view function can not go after external pure function (line 78)
external
view
override
returns (bytes4)
{
bytes32 _replaySafeHash = replaySafeHash(account, digest);
if (_validateSignature(entityId, account, _replaySafeHash, signature)) {
return _1271_MAGIC_VALUE;
}
return _1271_INVALID;
}

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

/// @inheritdoc IModule
function moduleId() external pure returns (string memory) {
return "erc6900.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 _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;
}

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);
}
}
4 changes: 2 additions & 2 deletions test/account/DeferredValidation.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {MockUserOpValidationModule} from "../mocks/modules/ValidationModuleMocks
import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol";
import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";

import {AccountTestBase} from "../utils/AccountTestBase.sol";
import {AccountTestBase, ECDSAValidationModule} from "../utils/AccountTestBase.sol";

contract DeferredValidationTest is AccountTestBase {
using ValidationConfigLib for ValidationConfig;
Expand Down Expand Up @@ -173,7 +173,7 @@ contract DeferredValidationTest is AccountTestBase {

bytes32 replaySafeHash = vm.envOr("SMA_TEST", false)
? _getSmaReplaySafeHash(account, typedDataHash)
: singleSignerValidationModule.replaySafeHash(address(account), typedDataHash);
: ecdsaValidationModule.replaySafeHash(address(account), typedDataHash);

bytes memory deferredInstallSig = _getDeferredInstallSig(replaySafeHash);

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
Loading