Skip to content

Add WebAuthn library and SignerWebAuthn #117

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

Open
wants to merge 54 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
13cc239
Add WebAuthn
ernestognw Apr 23, 2025
2d2912e
Pragma up
ernestognw Apr 23, 2025
7519e27
remove version header
ernestognw Apr 23, 2025
bb25a17
Improve docs
ernestognw Apr 23, 2025
14a54af
Nits
ernestognw Apr 23, 2025
5f6bab3
Iterate
ernestognw May 5, 2025
4866998
Partially fix tests
ernestognw May 5, 2025
8016d33
Fuzz test instead
ernestognw May 7, 2025
19daf08
Merge branch 'master' into feature/webauthn
ernestognw May 7, 2025
8573738
Add SignerWebAuthN
ernestognw May 7, 2025
ac7d01b
up
ernestognw May 7, 2025
c871241
Up
ernestognw May 7, 2025
fbab0d5
Merge branch 'master' into feature/webauthn
ernestognw May 7, 2025
0059aad
up
ernestognw May 7, 2025
ab9f79b
Remove signer
ernestognw May 7, 2025
9090f60
Add signer
ernestognw May 7, 2025
60ff832
Remove unnecessary P256 library
ernestognw May 7, 2025
9f34d45
Merge branch 'master' into feature/webauthn
ernestognw May 7, 2025
a13903d
Add to docs
ernestognw May 7, 2025
6790c6b
Add AccountWebAuthn test
ernestognw May 9, 2025
7c5f45e
Reorder webauth auth
ernestognw May 9, 2025
4482164
nit
ernestognw May 9, 2025
95f3c0a
Update SignerWebAuthn.sol
ernestognw Jun 4, 2025
3eea8a3
Apply suggestions from code review
ernestognw Jun 4, 2025
3c50fe7
Update contracts/utils/cryptography/WebAuthn.sol
ernestognw Jun 4, 2025
50aa7d0
Update contracts/utils/cryptography/WebAuthn.sol
arr00 Jun 4, 2025
d6136c4
Make use of dangling `_decodeKey` in ERC7913ZKEmailVerifier (#145)
ernestognw May 7, 2025
d19afd9
Update node modules and remove unnecessary npm dependencies (#146)
arr00 May 7, 2025
eca5513
Treat subsequent onInstall calls as no-ops in ERC7579SignatureValidat…
ernestognw May 13, 2025
3aa481c
Vendor dependencies in `vendored` branch (#149)
ernestognw May 13, 2025
e8492a4
Revert "Vendor dependencies in `vendored` branch" (#150)
ernestognw May 13, 2025
30f2f61
Add documentation for crosschain message passing (#138)
luiz-lvj May 14, 2025
4b8b87f
Move npm dependencies to git submodules (#153)
ernestognw May 15, 2025
e6be8f1
Add `view` modifier to `ERC20Allowlist.allowed` (#154)
felipelincoln May 15, 2025
4662a31
Add ERC7579 Executor modules (#121)
ernestognw May 16, 2025
c878724
Fix documentation nits and add missing Changelog entries (#155)
ernestognw May 16, 2025
8bc5b33
Move Axelar audit to audits folder (#156)
ernestognw May 16, 2025
a023191
Simplify ERC7579 Executors validation pattern (#158)
ernestognw May 17, 2025
9b73593
Simplify ERC-7579 validator modules (#159)
ernestognw May 18, 2025
c86c9ef
Add ERC-7579 modules docs and EIP-7702 note in accounts docs (#157)
ernestognw May 19, 2025
dc94c40
Add `husky` to devDependencies (#160)
ernestognw May 24, 2025
680d249
Fix bug in docs CI (#161)
arr00 May 30, 2025
63b3467
Refactor of the MultiSignerERC7913 abstract signer (#163)
Amxx Jun 3, 2025
131b611
Refactor ERC7579 Multisig modules (#165)
ernestognw Jun 3, 2025
ef537ba
Reorganise utils/cryptography folder (#164)
Amxx Jun 3, 2025
9527637
Merge branch 'master' into feature/webauthn
ernestognw Jun 4, 2025
f7a97dd
Move Signer
ernestognw Jun 4, 2025
051ae83
up
ernestognw Jun 4, 2025
797cf7a
Reapply Aryeh's suggestion
ernestognw Jun 4, 2025
0e8a700
Fix import
ernestognw Jun 4, 2025
1bfa073
Fix another import
ernestognw Jun 4, 2025
3b53c2b
Fix compilation
ernestognw Jun 4, 2025
f72340d
Revert libraries
ernestognw Jun 4, 2025
586b06d
Merge branch 'master' into feature/webauthn
ernestognw Jun 4, 2025
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
25 changes: 25 additions & 0 deletions contracts/mocks/account/AccountWebAuthnMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.24;

import {Account} from "../../account/Account.sol";
import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";
import {ERC7739} from "../../utils/cryptography/signers/ERC7739.sol";
import {ERC7821} from "../../account/extensions/ERC7821.sol";
import {SignerWebAuthn} from "../../utils/cryptography/signers/SignerWebAuthn.sol";

abstract contract AccountWebAuthnMock is Account, SignerWebAuthn, ERC7739, ERC7821, ERC721Holder, ERC1155Holder {
constructor(bytes32 qx, bytes32 qy) {
_setSigner(qx, qy);
}

/// @inheritdoc ERC7821
function _erc7821AuthorizedExecutor(
address caller,
bytes32 mode,
bytes calldata executionData
) internal view virtual override returns (bool) {
return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
}
}
6 changes: 6 additions & 0 deletions contracts/utils/cryptography/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ A collection of contracts and libraries that implement various signature validat
* {ERC7739Utils}: Utilities library that implements a defensive rehashing mechanism to prevent replayability of smart contract signatures based on ERC-7739.
* {ERC7913Utils}: Utilities library that implements ERC-1271 and ECDSA signature verification with fallback to ERC-7913.
* {ZKEmailUtils}: Library for ZKEmail signature validation utilities, enabling email-based authentication through zero-knowledge proofs.
* {WebAuthn}: Library for verifying WebAuthn Authentication Assertions.
* {AbstractSigner}: Abstract contract for internal signature validation in smart contracts.
* {ERC7739}: An abstract contract to validate signatures following the rehashing scheme from `ERC7739Utils`.
* {SignerECDSA}, {SignerP256}, {SignerRSA}: Implementations of an {AbstractSigner} with specific signature validation algorithms.
* {SignerERC7702}: Implementation of {AbstractSigner} that validates signatures using the contract's own address as the signer, useful for delegated accounts following EIP-7702.
* {SignerERC7913}, {MultiSignerERC7913}, {MultiSignerERC7913Weighted}: Implementations of {AbstractSigner} that validate signatures based on ERC-7913. Including a simple and weighted multisignature scheme.
* {SignerZKEmail}: Implementation of an {AbstractSigner} that enables email-based authentication through zero-knowledge proofs.
* {SignerWebAuthn}: Implementation of {SignerP256} that supports WebAuthn authentication assertions.
* {ERC7913P256Verifier}, {ERC7913RSAVerifier}, {ERC7913ZKEmailVerifier}: Ready to use ERC-7913 signature verifiers for P256, RSA keys, and ZKEmail.

== Utils
Expand All @@ -25,6 +27,8 @@ A collection of contracts and libraries that implement various signature validat

{{ZKEmailUtils}}

{{WebAuthn}}

== Abstract Signers

{{AbstractSigner}}
Expand All @@ -47,6 +51,8 @@ A collection of contracts and libraries that implement various signature validat

{{SignerZKEmail}}

{{SignerWebAuthn}}

== Verifiers

{{ERC7913P256Verifier}}
Expand Down
250 changes: 250 additions & 0 deletions contracts/utils/cryptography/WebAuthn.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.24;

import {Base64} from "@openzeppelin/contracts/utils/Base64.sol";
import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol";
import {P256} from "@openzeppelin/contracts/utils/cryptography/P256.sol";
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";

/**
* @dev Library for verifying WebAuthn Authentication Assertions.
*
* WebAuthn enables strong authentication for smart contracts using
* https://docs.openzeppelin.com/contracts/5.x/api/utils#P256[P256]
* as an alternative to traditional secp256k1 ECDSA signatures. This library verifies
* signatures generated during WebAuthn authentication ceremonies as specified in the
* https://www.w3.org/TR/webauthn-2/[WebAuthn Level 2 standard].
*
* For blockchain use cases, the following WebAuthn validations are intentionally omitted:
*
* * Origin validation: Origin verification in `clientDataJSON` is omitted as blockchain
* contexts rely on authenticator and dapp frontend enforcement. Standard authenticators
* implement proper origin validation.
* * RP ID hash validation: Verification of `rpIdHash` in authenticatorData against expected
* RP ID hash is omitted. This is typically handled by platform-level security measures.
* Including an expiry timestamp in signed data is recommended for enhanced security.
* * Signature counter: Verification of signature counter increments is omitted. While
* useful for detecting credential cloning, on-chain operations typically include nonce
* protection, making this check redundant.
* * Extension outputs: Extension output value verification is omitted as these are not
* essential for core authentication security in blockchain applications.
* * Attestation: Attestation object verification is omitted as this implementation
* focuses on authentication (`webauthn.get`) rather than registration ceremonies.
*
* Inspired by:
*
* * https://github.com/daimo-eth/p256-verifier/blob/master/src/WebAuthn.sol[daimo-eth implementation]
* * https://github.com/base/webauthn-sol/blob/main/src/WebAuthn.sol[base implementation]
*/
library WebAuthn {
struct WebAuthnAuth {
bytes32 r; /// The r value of secp256r1 signature
bytes32 s; /// The s value of secp256r1 signature
uint256 challengeIndex; /// The index at which "challenge":"..." occurs in `clientDataJSON`.
uint256 typeIndex; /// The index at which "type":"..." occurs in `clientDataJSON`.
/// The WebAuthn authenticator data.
/// https://www.w3.org/TR/webauthn-2/#dom-authenticatorassertionresponse-authenticatordata
bytes authenticatorData;
/// The WebAuthn client data JSON.
/// https://www.w3.org/TR/webauthn-2/#dom-authenticatorresponse-clientdatajson
string clientDataJSON;
}

/// @dev Bit 0 of the authenticator data flags: "User Present" bit.
bytes1 private constant AUTH_DATA_FLAGS_UP = 0x01;
/// @dev Bit 2 of the authenticator data flags: "User Verified" bit.
bytes1 private constant AUTH_DATA_FLAGS_UV = 0x04;
/// @dev Bit 3 of the authenticator data flags: "Backup Eligibility" bit.
bytes1 private constant AUTH_DATA_FLAGS_BE = 0x08;
/// @dev Bit 4 of the authenticator data flags: "Backup State" bit.
bytes1 private constant AUTH_DATA_FLAGS_BS = 0x10;

/// @dev The expected type string in the client data JSON when verifying assertion signatures.
/// https://www.w3.org/TR/webauthn-2/#dom-collectedclientdata-type
// solhint-disable-next-line quotes
bytes32 private constant EXPECTED_TYPE_HASH = keccak256('"type":"webauthn.get"');

/**
* @dev Performs the absolute minimal verification of a WebAuthn Authentication Assertion.
* This function includes only the essential checks required for basic WebAuthn security:
*
* 1. Type is "webauthn.get" (see {validateExpectedTypeHash})
* 2. Challenge matches the expected value (see {validateChallenge})
* 3. Cryptographic signature is valid for the given public key
*
* For most applications, use {verify} or {verifyStrict} instead.
*
* NOTE: This function intentionally omits User Presence (UP), User Verification (UV),
* and Backup State/Eligibility checks. Use this only when broader compatibility with
* authenticators is required or in constrained environments.
*/
function verifyMinimal(
bytes memory challenge,
WebAuthnAuth memory auth,
bytes32 qx,
bytes32 qy
) internal view returns (bool) {
// Verify authenticator data has sufficient length (37 bytes minimum):
// - 32 bytes for rpIdHash
// - 1 byte for flags
// - 4 bytes for signature counter
if (auth.authenticatorData.length < 37) return false;
bytes memory clientDataJSON = bytes(auth.clientDataJSON);

return
validateExpectedTypeHash(clientDataJSON, auth.typeIndex) && // 11
validateChallenge(clientDataJSON, auth.challengeIndex, challenge) && // 12
// Handles signature malleability internally
P256.verify(
sha256(
abi.encodePacked(
auth.authenticatorData,
sha256(clientDataJSON) // 19
)
),
auth.r,
auth.s,
qx,
qy
); // 20
}

/**
* @dev Performs standard verification of a WebAuthn Authentication Assertion.
*
* Same as {verifyMinimal}, but also verifies:
*
* [start=4]
* 4. {validateUserPresentBitSet} - confirming physical user presence during authentication
*
* This compliance level satisfies the core WebAuthn verification requirements while
* maintaining broad compatibility with authenticators. For higher security requirements,
* consider using {verifyStrict}.
*/
function verify(
bytes memory challenge,
WebAuthnAuth memory auth,
bytes32 qx,
bytes32 qy
) internal view returns (bool) {
// 16 && rest
return validateUserPresentBitSet(auth.authenticatorData[32]) && verifyMinimal(challenge, auth, qx, qy);
}

/**
* @dev Performs strict verification of a WebAuthn Authentication Assertion.
*
* Same as {verify}, but also also verifies:
*
* [start=5]
* 5. {validateUserVerifiedBitSet} - confirming stronger user authentication (biometrics/PIN)
* 6. {validateBackupEligibilityAndState}- Backup Eligibility (`BE`) and Backup State (BS) bits
* relationship is valid
*
* This strict verification is recommended for:
*
* * High-value transactions
* * Privileged operations
* * Account recovery or critical settings changes
* * Applications where security takes precedence over broad authenticator compatibility
*/
function verifyStrict(
bytes memory challenge,
WebAuthnAuth memory auth,
bytes32 qx,
bytes32 qy
) internal view returns (bool) {
return
validateUserVerifiedBitSet(auth.authenticatorData[32]) && // 17
validateBackupEligibilityAndState(auth.authenticatorData[32]) && // Consistency check
verify(challenge, auth, qx, qy);
}

/**
* @dev Validates that the https://www.w3.org/TR/webauthn-2/#up[User Present (UP)] bit is set.
* Step 16 in https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion[verifying an assertion].
*
* NOTE: Required by WebAuthn spec but may be skipped for platform authenticators
* (Touch ID, Windows Hello) in controlled environments. Enforce for public-facing apps.
*/
function validateUserPresentBitSet(bytes1 flags) internal pure returns (bool) {
return (flags & AUTH_DATA_FLAGS_UP) == AUTH_DATA_FLAGS_UP;
}

/**
* @dev Validates that the https://www.w3.org/TR/webauthn-2/#uv[User Verified (UV)] bit is set.
* Step 17 in https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion[verifying an assertion].
*
* The UV bit indicates whether the user was verified using a stronger identification method
* (biometrics, PIN, password). While optional, requiring UV=1 is recommended for:
*
* * High-value transactions and sensitive operations
* * Account recovery and critical settings changes
* * Privileged operations
*
* NOTE: For routine operations or when using hardware authenticators without verification capabilities,
* `UV=0` may be acceptable. The choice of whether to require UV represents a security vs. usability
* tradeoff - for blockchain applications handling valuable assets, requiring UV is generally safer.
*/
function validateUserVerifiedBitSet(bytes1 flags) internal pure returns (bool) {
return (flags & AUTH_DATA_FLAGS_UV) == AUTH_DATA_FLAGS_UV;
}

/**
* @dev Validates the relationship between Backup Eligibility (`BE`) and Backup State (`BS`) bits
* according to the WebAuthn specification.
*
* The function enforces that if a credential is backed up (`BS=1`), it must also be eligible
* for backup (`BE=1`). This prevents unauthorized credential backup and ensures compliance
* with the WebAuthn spec.
*
* Returns true in these valid states:
*
* * `BE=1`, `BS=0`: Credential is eligible but not backed up
* * `BE=1`, `BS=1`: Credential is eligible and backed up
* * `BE=0`, `BS=0`: Credential is not eligible and not backed up
*
* Returns false only when `BE=0` and `BS=1`, which is an invalid state indicating
* a credential that's backed up but not eligible for backup.
*
* NOTE: While the WebAuthn spec defines this relationship between `BE` and `BS` bits,
* validating it is not explicitly required as part of the core verification procedure.
* Some implementations may choose to skip this check for broader authenticator
* compatibility or when the application's threat model doesn't consider credential
* syncing a major risk.
*/
function validateBackupEligibilityAndState(bytes1 flags) internal pure returns (bool) {
return (flags & AUTH_DATA_FLAGS_BE) != 0 || (flags & AUTH_DATA_FLAGS_BS) == 0;
}
Comment on lines +217 to +219
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would prefer consistency on how we are checking the value of a bit

flags & AUTH_DATA_FLAGS_BE) != 0 or flags & AUTH_DATA_FLAGS_BE) == AUTH_DATA_FLAGS_BE

Otherwise looks good


/**
* @dev Validates that the https://www.w3.org/TR/webauthn-2/#type[Type] field in the client data JSON
* is set to "webauthn.get".
*/
function validateExpectedTypeHash(bytes memory clientDataJSON, uint256 typeIndex) internal pure returns (bool) {
// 21 = length of '"type":"webauthn.get"'
bytes memory typeValueBytes = Bytes.slice(clientDataJSON, typeIndex, typeIndex + 21);
return keccak256(typeValueBytes) == EXPECTED_TYPE_HASH;
}

/// @dev Validates that the challenge in the client data JSON matches the `expectedChallenge`.
function validateChallenge(
bytes memory clientDataJSON,
uint256 challengeIndex,
bytes memory expectedChallenge
) internal pure returns (bool) {
bytes memory expectedChallengeBytes = bytes(
// solhint-disable-next-line quotes
string.concat('"challenge":"', Base64.encodeURL(expectedChallenge), '"')
);
if (challengeIndex + expectedChallengeBytes.length > clientDataJSON.length) return false;
bytes memory actualChallengeBytes = Bytes.slice(
clientDataJSON,
challengeIndex,
challengeIndex + expectedChallengeBytes.length
);

return Strings.equal(string(actualChallengeBytes), string(expectedChallengeBytes));
}
}
60 changes: 60 additions & 0 deletions contracts/utils/cryptography/signers/SignerWebAuthn.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.24;

import {SignerP256} from "./SignerP256.sol";
import {WebAuthn} from "../WebAuthn.sol";

/**
* @dev Implementation of {SignerP256} that supports WebAuthn authentication assertions.
*
* This contract enables signature validation using WebAuthn authentication assertions,
* leveraging the P256 public key stored in the contract. It allows for both WebAuthn
* and raw P256 signature validation, providing compatibility with both signature types.
*
* The signature is expected to be an abi-encoded {WebAuthn-WebAuthnAuth} struct.
*
* Example usage:
*
* ```solidity
* contract MyAccountWebAuthn is Account, SignerWebAuthn, Initializable {
* function initialize(bytes32 qx, bytes32 qy) public initializer {
* _setSigner(qx, qy);
* }
* }
* ```
*
* IMPORTANT: Failing to call {_setSigner} either during construction (if used standalone)
* or during initialization (if used as a clone) may leave the signer either front-runnable or unusable.
*/
abstract contract SignerWebAuthn is SignerP256 {
/**
* @dev Validates a raw signature using the WebAuthn authentication assertion.
*
* In case the signature can't be validated, it falls back to the
* {SignerP256-_rawSignatureValidation} method for raw P256 signature validation by passing
* the raw `r` and `s` values from the signature.
*/
function _rawSignatureValidation(
bytes32 hash,
bytes calldata signature
) internal view virtual override returns (bool) {
(bytes32 qx, bytes32 qy) = signer();

return
WebAuthn.verifyMinimal(abi.encodePacked(hash), _toWebAuthnSignature(signature), qx, qy) ||
super._rawSignatureValidation(hash, signature);
}

/// @dev Non-reverting version of signature decoding.
function _toWebAuthnSignature(bytes calldata signature) private pure returns (WebAuthn.WebAuthnAuth memory auth) {
bool decodable;
assembly ("memory-safe") {
let offset := calldataload(signature.offset)
// Validate the offset is within bounds and makes sense for a WebAuthnAuth struct
// A valid offset should be 32 and point to data within the signature bounds
decodable := and(eq(offset, 32), lt(add(offset, 0x80), signature.length))
}
return decodable ? abi.decode(signature, (WebAuthn.WebAuthnAuth)) : auth;
}
}
1 change: 1 addition & 0 deletions docs/modules/ROOT/pages/accounts.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Since the minimum requirement of xref:api:account.adoc#Account[`Account`] is to

* xref:api:utils/cryptography.adoc#SignerECDSA[`SignerECDSA`]: Verifies signatures produced by regular EVM Externally Owned Accounts (EOAs).
* xref:api:utils/cryptography.adoc#SignerP256[`SignerP256`]: Validates signatures using the secp256r1 curve, common for World Wide Web Consortium (W3C) standards such as FIDO keys, passkeys or secure enclaves.
* xref:api:utils/cryptography.adoc#SignerWebAuthn[`SignerWebAuthn`]: Validates signatures using WebAuthn authentication assertions, leveraging P256 public keys for both WebAuthn and raw P256 signature validation.
* xref:api:utils/cryptography.adoc#SignerRSA[`SignerRSA`]: Verifies signatures of traditional PKI systems and X.509 certificates.
* xref:api:utils/cryptography.adoc#SignerERC7702[`SignerERC7702`]: Checks EOA signatures delegated to this signer using https://eips.ethereum.org/EIPS/eip-7702#set-code-transaction[EIP-7702 authorizations]
* xref:api:utils/cryptography.adoc#SignerERC7913[`SignerERC7913`]: Verifies generalized signatures following https://eips.ethereum.org/EIPS/eip-7913[ERC-7913].
Expand Down
Loading