Skip to content
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
@@ -0,0 +1,3 @@
[submodule "lib/webauthn-sol"]
path = lib/webauthn-sol
url = https://github.com/base/webauthn-sol
127 changes: 127 additions & 0 deletions contracts/external/wc-cosigner/MultiKeySigner.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.25;

// External Dependencies
import { ECDSA } from "solady/utils/ECDSA.sol";

// Local Project Files
import { ERC7579_MODULE_TYPE_STATELESS_VALIDATOR } from "../../DataTypes.sol";
import { PasskeyHelper, WebAuthnValidatorData } from "./libs/PasskeyHelper.sol";
import { ISessionValidator } from "contracts/interfaces/ISessionValidator.sol";
import { SignerEncode, Signer, SignerType } from "./libs/SignerEncode.sol";

/// @title MultiKeySigner - Stateless Session Validator for ERC-7579
/// @author [alphabetically] Filipp Makarov (Biconomy), rplusq (Reown) & zeroknots.eth (Rhinestone)

/// @notice This contract is an ERC-7579 compliant stateless session validator module,
/// designed for use with the Smart Sessions framework. It enables signature validation
/// for sessions using multiple signer types, such as EOA and WebAuthn Passkeys.
/// The configuration for signers is provided via the `data` parameter in `validateSignatureWithData`,
/// making the validator stateless in its operation for each validation.
///
/// @dev Implements `ISessionValidator`. This module is intended to be a secure
/// and verifiable component, attestable via an EIP-7484 compliant module registry.
contract MultiKeySigner is ISessionValidator {
using PasskeyHelper for *;
using SignerEncode for *;

// --- Errors ---

/// @notice Reverted if the provided signature array's length does not match the signers array's length.
error InvalidSignatureLength();
/// @notice Reverted if an unsupported signer type is encountered during validation.
error InvalidSignatureType();
/// @notice Reverted if installation data is provided, as this module does not support it.
error InstallationDataNotSupported();
/// @notice Reverted if an unknown signer type is encountered during decoding.
error UnknownSignerType();

// --- External Functions ---

/// @notice Hook called by the account when this module is installed.
/// @dev As per ERC-7579, modules can use `data` for initialization. This stateless module does not
/// support initialization data via this function. If `data` is provided, the call will revert.
/// The actual signer configuration is passed dynamically during validation.
/// @param data Arbitrary data passed by the account during installation. Must be empty for this module.
function onInstall(bytes calldata data) external {
if (data.length > 0) {
revert InstallationDataNotSupported();
}
}

/// @notice Hook called by the account when this module is uninstalled.
/// @dev As per ERC-7579, modules can use `data` for cleanup or other actions upon uninstallation.
/// This stateless module currently has no specific state to clean up.
function onUninstall(bytes calldata /* data */ ) external { }

/// @notice Checks if this module conforms to a given module type ID, as per ERC-7579.
/// @dev Returns true if the `id` matches the ERC-7579 Stateless Validator Module type ID.
/// @param id The module type ID to check.
/// @return isType True if this module is of the specified type, false otherwise.
function isModuleType(uint256 id) external pure returns (bool) {
return (id == ERC7579_MODULE_TYPE_STATELESS_VALIDATOR);
}

/// @notice Checks if the contract supports a given interface ID.
/// @dev Specifically checks for `ISessionValidator` interface support, often used in conjunction
/// with ERC-7579 modules.
/// @param sig The interface ID (bytes4) to check.
/// @return supported True if the interface is supported, false otherwise.
function supportsInterface(bytes4 sig) external view returns (bool) {
return (sig == type(ISessionValidator).interfaceId);
}

function isInitialized(address /* smartAccount */ ) external view returns (bool) {
return true; // This module is stateless and considered always initialized
}

/// @notice Validates a signature against a given user operation hash, using the module's configured signers.
/// @dev This is a core function for an ERC-7579 Stateless Validator Module. It decodes the signers from `data`
/// and the corresponding signatures from `sig`, then validates each signature against the `userOpHash`.
/// Reverts with `InvalidSignatureLength` if `sig` and `signers` (decoded from `data`) lengths mismatch.
/// Reverts with `InvalidSignatureType` if an unsupported signer type is encountered.
/// @param userOpHash The hash of the UserOperation to be validated.
/// @param sig An ABI encoded byte array of signatures, one for each signer defined in `data`.
/// @param data An ABI encoded byte array representing the `Signer[]` array configuration for this validation.
/// @return validSig True if all signatures are valid, false otherwise.
function validateSignatureWithData(
bytes32 userOpHash,
bytes calldata sig,
bytes calldata data
)
external
view
returns (bool validSig)
{
bytes32 ethHash = ECDSA.toEthSignedMessageHash(userOpHash);
Signer[] memory signers = data.decodeSigners();
bytes[] memory sigs = abi.decode(sig, (bytes[]));

uint256 length = signers.length;
if (sigs.length != length) revert InvalidSignatureLength();

for (uint256 i = 0; i < length; i++) {
if (signers[i].signerType == SignerType.EOA) {
address eoa = signers[i].decodeEOA();
address recovered = ECDSA.recover(ethHash, sigs[i]);
if (recovered != eoa) return false;
} else if (signers[i].signerType == SignerType.PASSKEY) {
WebAuthnValidatorData memory passkeyData = signers[i].decodePasskey();
bool passkeyValid = passkeyData.verifyPasskey(userOpHash, sigs[i]);
if (!passkeyValid) return false;
} else {
revert InvalidSignatureType();
}
}
return true;
}

/// @notice Encodes an array of `Signer` structs into a byte array.
/// @dev This helper function is used to prepare the `data` parameter for `validateSignatureWithData`.
/// @param signers An array of `Signer` structs to encode.
/// @return encoded The ABI encoded byte array of signers.
function encodeSigners(Signer[] memory signers) external pure returns (bytes memory) {
return signers.encodeSignersInternal();
}
}
61 changes: 61 additions & 0 deletions contracts/external/wc-cosigner/libs/PasskeyHelper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import { WebAuthn } from "webauthn-sol/WebAuthn.sol";

/// @notice Holds the public key components for WebAuthn validation.
struct WebAuthnValidatorData {
/// @dev The x-coordinate of the public key.
uint256 pubKeyX;
/// @dev The y-coordinate of the public key.
uint256 pubKeyY;
}

/// @notice Helper library for Passkey (WebAuthn) signature verification.
/// @dev Provides utility functions to verify signatures generated by passkeys.
library PasskeyHelper {
/**
* @notice Verify a signature generated by a passkey.
*
* @dev Decodes the signature blob and then verifies it against the provided hash and public key
* using the WebAuthn library. The hash is used as the challenge value and user verification
* is required by default.
*
* @param webAuthnData The WebAuthn validator data containing the public key coordinates:
* - pubKeyX: The x-coordinate of the public key
* - pubKeyY: The y-coordinate of the public key
* @param hash The hash of the message that was signed, used as the challenge value.
* @param signature The encoded WebAuthn.WebAuthnAuth struct containing:
* - authenticatorData: Raw authenticator data including flags
* - clientDataJSON: JSON string with type, challenge, origin
* - challengeIndex: Index of challenge in clientDataJSON
* - typeIndex: Index of type in clientDataJSON
* - r: ECDSA signature r value
* - s: ECDSA signature s value
*
* @return True if the signature is valid and user verification flag is set, false otherwise.
*/
function verifyPasskey(
WebAuthnValidatorData memory webAuthnData,
bytes32 hash,
bytes memory signature
)
internal
view
returns (bool)
{
// Decode the signature blob directly into WebAuthn.WebAuthnAuth struct
WebAuthn.WebAuthnAuth memory authStruct = abi.decode(signature, (WebAuthn.WebAuthnAuth));

// The `hash` parameter (userOpHash) is the challenge.
// WebAuthn.verify expects `bytes memory challenge`.
bytes memory challengeBytes = abi.encode(hash);

// Require User Verification (UV) by default for passkey signatures.
// The WebAuthn.verify function will check the UV flag in authStruct.authenticatorData.
bool requireUV = true;

// Call the updated WebAuthn.verify function
return WebAuthn.verify(challengeBytes, requireUV, authStruct, webAuthnData.pubKeyX, webAuthnData.pubKeyY);
}
}
81 changes: 81 additions & 0 deletions contracts/external/wc-cosigner/libs/SignerEncode.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import { WebAuthnValidatorData } from "./PasskeyHelper.sol";

/// @notice Defines the type of signer.
enum SignerType {
EOA, // External Owned Account
PASSKEY // WebAuthn Passkey

}

/// @notice Represents a signer with its type and associated data.
struct Signer {
/// @dev The type of the signer (EOA or PASSKEY).
SignerType signerType;
/// @dev The data associated with the signer (e.g., address for EOA, WebAuthn data for Passkey).
bytes data;
}

/// @title SignerEncode
/// @author [alphabetically] Filipp Makarov (Biconomy) & zeroknots.eth (Rhinestone)
///
/// @notice Library for encoding and decoding Signer structs.
library SignerEncode {
error UnknownSignerType();

/// @notice Decodes EOA signer data to an address.
/// @param signer The Signer struct containing EOA data.
/// @return eoa The decoded EOA address.
function decodeEOA(Signer memory signer) internal pure returns (address) {
return address(bytes20(signer.data));
}

/// @notice Decodes Passkey signer data to WebAuthnValidatorData.
/// @param signer The Signer struct containing Passkey data.
/// @return passkey The decoded WebAuthnValidatorData.
function decodePasskey(Signer memory signer) internal pure returns (WebAuthnValidatorData memory) {
return abi.decode(signer.data, (WebAuthnValidatorData));
}

/// @notice Encodes an array of Signer structs into a byte array.
/// @dev Internal function to handle the encoding logic.
/// @param signers Array of Signer structs to encode.
/// @return encoded The abi encoded byte array of signers.
function encodeSignersInternal(Signer[] memory signers) internal pure returns (bytes memory) {
uint256 length = signers.length;
bytes memory encoded = abi.encodePacked(uint8(length));
for (uint256 i = 0; i < length; i++) {
encoded = abi.encodePacked(encoded, uint8(signers[i].signerType));
encoded = abi.encodePacked(encoded, signers[i].data);
}
return encoded;
}

/// @notice Decodes a byte array into an array of Signer structs.
/// @dev Reverts with UnknownSignerType if an invalid signerType is encountered.
/// @param data The abi encoded byte array of signers.
/// @return signers Array of decoded Signer structs.
function decodeSigners(bytes calldata data) internal pure returns (Signer[] memory signers) {
uint256 length = uint256(uint8(bytes1(data[0])));
signers = new Signer[](length);
uint256 offset = 1;
for (uint256 i = 0; i < length; i++) {
uint8 signerTypeByte = uint8(bytes1(data[offset]));
offset++;
uint256 dataLength;
if (signerTypeByte == uint8(SignerType.EOA)) {
dataLength = 20;
} else if (signerTypeByte == uint8(SignerType.PASSKEY)) {
dataLength = 64;
} else {
revert UnknownSignerType();
}
bytes memory signerData = data[offset:offset + dataLength];
offset += dataLength;
signers[i] = Signer(SignerType(signerTypeByte), signerData);
}
return signers;
}
}
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
cache_path = "cache_forge"
libs = ["node_modules", "lib"]
gas_reports_ignore = ["LockTest"]
fs_permissions = [{ access = "read", path = "test/fixtures" }]

[profile.ci]
fuzz = { runs = 10_000 }
Expand Down
1 change: 1 addition & 0 deletions lib/webauthn-sol
Submodule webauthn-sol added at 619f20
3 changes: 2 additions & 1 deletion remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ kernel/=node_modules/@zerodev/kernel/src/
ExcessivelySafeCall/=node_modules/excessively-safe-call/src/
excessively-safe-call/=node_modules/excessively-safe-call/src/
flatbytes/=node_modules/@rhinestone/flatbytes/src/
stringutils/=node_modules/stringutils/src/
stringutils/=node_modules/stringutils/src/
webauthn-sol/=lib/webauthn-sol/src/
Loading