Skip to content

Commit 742a9bb

Browse files
ernestognwarr00Amxx
authored
Add WebAuthn library and SignerWebAuthn (#117)
Co-authored-by: Arr00 <[email protected]> Co-authored-by: Hadrien Croubois <[email protected]>
1 parent bf3da91 commit 742a9bb

File tree

8 files changed

+839
-1
lines changed

8 files changed

+839
-1
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.24;
4+
5+
import {Account} from "../../account/Account.sol";
6+
import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
7+
import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";
8+
import {ERC7739} from "../../utils/cryptography/signers/ERC7739.sol";
9+
import {ERC7821} from "../../account/extensions/ERC7821.sol";
10+
import {SignerWebAuthn} from "../../utils/cryptography/signers/SignerWebAuthn.sol";
11+
12+
abstract contract AccountWebAuthnMock is Account, SignerWebAuthn, ERC7739, ERC7821, ERC721Holder, ERC1155Holder {
13+
constructor(bytes32 qx, bytes32 qy) {
14+
_setSigner(qx, qy);
15+
}
16+
17+
/// @inheritdoc ERC7821
18+
function _erc7821AuthorizedExecutor(
19+
address caller,
20+
bytes32 mode,
21+
bytes calldata executionData
22+
) internal view virtual override returns (bool) {
23+
return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
24+
}
25+
}

contracts/utils/cryptography/README.adoc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ A collection of contracts and libraries that implement various signature validat
88
* {ERC7739Utils}: Utilities library that implements a defensive rehashing mechanism to prevent replayability of smart contract signatures based on ERC-7739.
99
* {ERC7913Utils}: Utilities library that implements ERC-1271 and ECDSA signature verification with fallback to ERC-7913.
1010
* {ZKEmailUtils}: Library for ZKEmail signature validation utilities, enabling email-based authentication through zero-knowledge proofs.
11+
* {WebAuthn}: Library for verifying WebAuthn Authentication Assertions.
1112
* {AbstractSigner}: Abstract contract for internal signature validation in smart contracts.
1213
* {ERC7739}: An abstract contract to validate signatures following the rehashing scheme from `ERC7739Utils`.
1314
* {SignerECDSA}, {SignerP256}, {SignerRSA}: Implementations of an {AbstractSigner} with specific signature validation algorithms.
1415
* {SignerERC7702}: Implementation of {AbstractSigner} that validates signatures using the contract's own address as the signer, useful for delegated accounts following EIP-7702.
1516
* {SignerERC7913}, {MultiSignerERC7913}, {MultiSignerERC7913Weighted}: Implementations of {AbstractSigner} that validate signatures based on ERC-7913. Including a simple and weighted multisignature scheme.
1617
* {SignerZKEmail}: Implementation of an {AbstractSigner} that enables email-based authentication through zero-knowledge proofs.
18+
* {SignerWebAuthn}: Implementation of {SignerP256} that supports WebAuthn authentication assertions.
1719
* {ERC7913P256Verifier}, {ERC7913RSAVerifier}, {ERC7913ZKEmailVerifier}: Ready to use ERC-7913 signature verifiers for P256, RSA keys, and ZKEmail.
1820

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

2628
{{ZKEmailUtils}}
2729

30+
{{WebAuthn}}
31+
2832
== Abstract Signers
2933

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

4852
{{SignerZKEmail}}
4953

54+
{{SignerWebAuthn}}
55+
5056
== Verifiers
5157

5258
{{ERC7913P256Verifier}}
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.24;
4+
5+
import {Base64} from "@openzeppelin/contracts/utils/Base64.sol";
6+
import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol";
7+
import {P256} from "@openzeppelin/contracts/utils/cryptography/P256.sol";
8+
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
9+
10+
/**
11+
* @dev Library for verifying WebAuthn Authentication Assertions.
12+
*
13+
* WebAuthn enables strong authentication for smart contracts using
14+
* https://docs.openzeppelin.com/contracts/5.x/api/utils#P256[P256]
15+
* as an alternative to traditional secp256k1 ECDSA signatures. This library verifies
16+
* signatures generated during WebAuthn authentication ceremonies as specified in the
17+
* https://www.w3.org/TR/webauthn-2/[WebAuthn Level 2 standard].
18+
*
19+
* For blockchain use cases, the following WebAuthn validations are intentionally omitted:
20+
*
21+
* * Origin validation: Origin verification in `clientDataJSON` is omitted as blockchain
22+
* contexts rely on authenticator and dapp frontend enforcement. Standard authenticators
23+
* implement proper origin validation.
24+
* * RP ID hash validation: Verification of `rpIdHash` in authenticatorData against expected
25+
* RP ID hash is omitted. This is typically handled by platform-level security measures.
26+
* Including an expiry timestamp in signed data is recommended for enhanced security.
27+
* * Signature counter: Verification of signature counter increments is omitted. While
28+
* useful for detecting credential cloning, on-chain operations typically include nonce
29+
* protection, making this check redundant.
30+
* * Extension outputs: Extension output value verification is omitted as these are not
31+
* essential for core authentication security in blockchain applications.
32+
* * Attestation: Attestation object verification is omitted as this implementation
33+
* focuses on authentication (`webauthn.get`) rather than registration ceremonies.
34+
*
35+
* Inspired by:
36+
*
37+
* * https://github.com/daimo-eth/p256-verifier/blob/master/src/WebAuthn.sol[daimo-eth implementation]
38+
* * https://github.com/base/webauthn-sol/blob/main/src/WebAuthn.sol[base implementation]
39+
*/
40+
library WebAuthn {
41+
struct WebAuthnAuth {
42+
bytes32 r; /// The r value of secp256r1 signature
43+
bytes32 s; /// The s value of secp256r1 signature
44+
uint256 challengeIndex; /// The index at which "challenge":"..." occurs in `clientDataJSON`.
45+
uint256 typeIndex; /// The index at which "type":"..." occurs in `clientDataJSON`.
46+
/// The WebAuthn authenticator data.
47+
/// https://www.w3.org/TR/webauthn-2/#dom-authenticatorassertionresponse-authenticatordata
48+
bytes authenticatorData;
49+
/// The WebAuthn client data JSON.
50+
/// https://www.w3.org/TR/webauthn-2/#dom-authenticatorresponse-clientdatajson
51+
string clientDataJSON;
52+
}
53+
54+
/// @dev Bit 0 of the authenticator data flags: "User Present" bit.
55+
bytes1 private constant AUTH_DATA_FLAGS_UP = 0x01;
56+
/// @dev Bit 2 of the authenticator data flags: "User Verified" bit.
57+
bytes1 private constant AUTH_DATA_FLAGS_UV = 0x04;
58+
/// @dev Bit 3 of the authenticator data flags: "Backup Eligibility" bit.
59+
bytes1 private constant AUTH_DATA_FLAGS_BE = 0x08;
60+
/// @dev Bit 4 of the authenticator data flags: "Backup State" bit.
61+
bytes1 private constant AUTH_DATA_FLAGS_BS = 0x10;
62+
63+
/// @dev The expected type string in the client data JSON when verifying assertion signatures.
64+
/// https://www.w3.org/TR/webauthn-2/#dom-collectedclientdata-type
65+
// solhint-disable-next-line quotes
66+
bytes32 private constant EXPECTED_TYPE_HASH = keccak256('"type":"webauthn.get"');
67+
68+
/**
69+
* @dev Performs the absolute minimal verification of a WebAuthn Authentication Assertion.
70+
* This function includes only the essential checks required for basic WebAuthn security:
71+
*
72+
* 1. Type is "webauthn.get" (see {validateExpectedTypeHash})
73+
* 2. Challenge matches the expected value (see {validateChallenge})
74+
* 3. Cryptographic signature is valid for the given public key
75+
*
76+
* For most applications, use {verify} or {verifyStrict} instead.
77+
*
78+
* NOTE: This function intentionally omits User Presence (UP), User Verification (UV),
79+
* and Backup State/Eligibility checks. Use this only when broader compatibility with
80+
* authenticators is required or in constrained environments.
81+
*/
82+
function verifyMinimal(
83+
bytes memory challenge,
84+
WebAuthnAuth memory auth,
85+
bytes32 qx,
86+
bytes32 qy
87+
) internal view returns (bool) {
88+
// Verify authenticator data has sufficient length (37 bytes minimum):
89+
// - 32 bytes for rpIdHash
90+
// - 1 byte for flags
91+
// - 4 bytes for signature counter
92+
if (auth.authenticatorData.length < 37) return false;
93+
bytes memory clientDataJSON = bytes(auth.clientDataJSON);
94+
95+
return
96+
validateExpectedTypeHash(clientDataJSON, auth.typeIndex) && // 11
97+
validateChallenge(clientDataJSON, auth.challengeIndex, challenge) && // 12
98+
// Handles signature malleability internally
99+
P256.verify(
100+
sha256(
101+
abi.encodePacked(
102+
auth.authenticatorData,
103+
sha256(clientDataJSON) // 19
104+
)
105+
),
106+
auth.r,
107+
auth.s,
108+
qx,
109+
qy
110+
); // 20
111+
}
112+
113+
/**
114+
* @dev Performs standard verification of a WebAuthn Authentication Assertion.
115+
*
116+
* Same as {verifyMinimal}, but also verifies:
117+
*
118+
* [start=4]
119+
* 4. {validateUserPresentBitSet} - confirming physical user presence during authentication
120+
*
121+
* This compliance level satisfies the core WebAuthn verification requirements while
122+
* maintaining broad compatibility with authenticators. For higher security requirements,
123+
* consider using {verifyStrict}.
124+
*/
125+
function verify(
126+
bytes memory challenge,
127+
WebAuthnAuth memory auth,
128+
bytes32 qx,
129+
bytes32 qy
130+
) internal view returns (bool) {
131+
// 16 && rest
132+
return validateUserPresentBitSet(auth.authenticatorData[32]) && verifyMinimal(challenge, auth, qx, qy);
133+
}
134+
135+
/**
136+
* @dev Performs strict verification of a WebAuthn Authentication Assertion.
137+
*
138+
* Same as {verify}, but also also verifies:
139+
*
140+
* [start=5]
141+
* 5. {validateUserVerifiedBitSet} - confirming stronger user authentication (biometrics/PIN)
142+
* 6. {validateBackupEligibilityAndState}- Backup Eligibility (`BE`) and Backup State (BS) bits
143+
* relationship is valid
144+
*
145+
* This strict verification is recommended for:
146+
*
147+
* * High-value transactions
148+
* * Privileged operations
149+
* * Account recovery or critical settings changes
150+
* * Applications where security takes precedence over broad authenticator compatibility
151+
*/
152+
function verifyStrict(
153+
bytes memory challenge,
154+
WebAuthnAuth memory auth,
155+
bytes32 qx,
156+
bytes32 qy
157+
) internal view returns (bool) {
158+
return
159+
validateUserVerifiedBitSet(auth.authenticatorData[32]) && // 17
160+
validateBackupEligibilityAndState(auth.authenticatorData[32]) && // Consistency check
161+
verify(challenge, auth, qx, qy);
162+
}
163+
164+
/**
165+
* @dev Validates that the https://www.w3.org/TR/webauthn-2/#up[User Present (UP)] bit is set.
166+
* Step 16 in https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion[verifying an assertion].
167+
*
168+
* NOTE: Required by WebAuthn spec but may be skipped for platform authenticators
169+
* (Touch ID, Windows Hello) in controlled environments. Enforce for public-facing apps.
170+
*/
171+
function validateUserPresentBitSet(bytes1 flags) internal pure returns (bool) {
172+
return (flags & AUTH_DATA_FLAGS_UP) == AUTH_DATA_FLAGS_UP;
173+
}
174+
175+
/**
176+
* @dev Validates that the https://www.w3.org/TR/webauthn-2/#uv[User Verified (UV)] bit is set.
177+
* Step 17 in https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion[verifying an assertion].
178+
*
179+
* The UV bit indicates whether the user was verified using a stronger identification method
180+
* (biometrics, PIN, password). While optional, requiring UV=1 is recommended for:
181+
*
182+
* * High-value transactions and sensitive operations
183+
* * Account recovery and critical settings changes
184+
* * Privileged operations
185+
*
186+
* NOTE: For routine operations or when using hardware authenticators without verification capabilities,
187+
* `UV=0` may be acceptable. The choice of whether to require UV represents a security vs. usability
188+
* tradeoff - for blockchain applications handling valuable assets, requiring UV is generally safer.
189+
*/
190+
function validateUserVerifiedBitSet(bytes1 flags) internal pure returns (bool) {
191+
return (flags & AUTH_DATA_FLAGS_UV) == AUTH_DATA_FLAGS_UV;
192+
}
193+
194+
/**
195+
* @dev Validates the relationship between Backup Eligibility (`BE`) and Backup State (`BS`) bits
196+
* according to the WebAuthn specification.
197+
*
198+
* The function enforces that if a credential is backed up (`BS=1`), it must also be eligible
199+
* for backup (`BE=1`). This prevents unauthorized credential backup and ensures compliance
200+
* with the WebAuthn spec.
201+
*
202+
* Returns true in these valid states:
203+
*
204+
* * `BE=1`, `BS=0`: Credential is eligible but not backed up
205+
* * `BE=1`, `BS=1`: Credential is eligible and backed up
206+
* * `BE=0`, `BS=0`: Credential is not eligible and not backed up
207+
*
208+
* Returns false only when `BE=0` and `BS=1`, which is an invalid state indicating
209+
* a credential that's backed up but not eligible for backup.
210+
*
211+
* NOTE: While the WebAuthn spec defines this relationship between `BE` and `BS` bits,
212+
* validating it is not explicitly required as part of the core verification procedure.
213+
* Some implementations may choose to skip this check for broader authenticator
214+
* compatibility or when the application's threat model doesn't consider credential
215+
* syncing a major risk.
216+
*/
217+
function validateBackupEligibilityAndState(bytes1 flags) internal pure returns (bool) {
218+
return (flags & AUTH_DATA_FLAGS_BE) != 0 || (flags & AUTH_DATA_FLAGS_BS) == 0;
219+
}
220+
221+
/**
222+
* @dev Validates that the https://www.w3.org/TR/webauthn-2/#type[Type] field in the client data JSON
223+
* is set to "webauthn.get".
224+
*/
225+
function validateExpectedTypeHash(bytes memory clientDataJSON, uint256 typeIndex) internal pure returns (bool) {
226+
// 21 = length of '"type":"webauthn.get"'
227+
bytes memory typeValueBytes = Bytes.slice(clientDataJSON, typeIndex, typeIndex + 21);
228+
return keccak256(typeValueBytes) == EXPECTED_TYPE_HASH;
229+
}
230+
231+
/// @dev Validates that the challenge in the client data JSON matches the `expectedChallenge`.
232+
function validateChallenge(
233+
bytes memory clientDataJSON,
234+
uint256 challengeIndex,
235+
bytes memory expectedChallenge
236+
) internal pure returns (bool) {
237+
bytes memory expectedChallengeBytes = bytes(
238+
// solhint-disable-next-line quotes
239+
string.concat('"challenge":"', Base64.encodeURL(expectedChallenge), '"')
240+
);
241+
if (challengeIndex + expectedChallengeBytes.length > clientDataJSON.length) return false;
242+
bytes memory actualChallengeBytes = Bytes.slice(
243+
clientDataJSON,
244+
challengeIndex,
245+
challengeIndex + expectedChallengeBytes.length
246+
);
247+
248+
return Strings.equal(string(actualChallengeBytes), string(expectedChallengeBytes));
249+
}
250+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.24;
4+
5+
import {SignerP256} from "./SignerP256.sol";
6+
import {WebAuthn} from "../WebAuthn.sol";
7+
8+
/**
9+
* @dev Implementation of {SignerP256} that supports WebAuthn authentication assertions.
10+
*
11+
* This contract enables signature validation using WebAuthn authentication assertions,
12+
* leveraging the P256 public key stored in the contract. It allows for both WebAuthn
13+
* and raw P256 signature validation, providing compatibility with both signature types.
14+
*
15+
* The signature is expected to be an abi-encoded {WebAuthn-WebAuthnAuth} struct.
16+
*
17+
* Example usage:
18+
*
19+
* ```solidity
20+
* contract MyAccountWebAuthn is Account, SignerWebAuthn, Initializable {
21+
* function initialize(bytes32 qx, bytes32 qy) public initializer {
22+
* _setSigner(qx, qy);
23+
* }
24+
* }
25+
* ```
26+
*
27+
* IMPORTANT: Failing to call {_setSigner} either during construction (if used standalone)
28+
* or during initialization (if used as a clone) may leave the signer either front-runnable or unusable.
29+
*/
30+
abstract contract SignerWebAuthn is SignerP256 {
31+
/**
32+
* @dev Validates a raw signature using the WebAuthn authentication assertion.
33+
*
34+
* In case the signature can't be validated, it falls back to the
35+
* {SignerP256-_rawSignatureValidation} method for raw P256 signature validation by passing
36+
* the raw `r` and `s` values from the signature.
37+
*/
38+
function _rawSignatureValidation(
39+
bytes32 hash,
40+
bytes calldata signature
41+
) internal view virtual override returns (bool) {
42+
(bytes32 qx, bytes32 qy) = signer();
43+
44+
return
45+
WebAuthn.verifyMinimal(abi.encodePacked(hash), _toWebAuthnSignature(signature), qx, qy) ||
46+
super._rawSignatureValidation(hash, signature);
47+
}
48+
49+
/// @dev Non-reverting version of signature decoding.
50+
function _toWebAuthnSignature(bytes calldata signature) private pure returns (WebAuthn.WebAuthnAuth memory auth) {
51+
bool decodable;
52+
assembly ("memory-safe") {
53+
let offset := calldataload(signature.offset)
54+
// Validate the offset is within bounds and makes sense for a WebAuthnAuth struct
55+
// A valid offset should be 32 and point to data within the signature bounds
56+
decodable := and(eq(offset, 32), lt(add(offset, 0x80), signature.length))
57+
}
58+
return decodable ? abi.decode(signature, (WebAuthn.WebAuthnAuth)) : auth;
59+
}
60+
}

docs/modules/ROOT/pages/accounts.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Since the minimum requirement of xref:api:account.adoc#Account[`Account`] is to
2222

2323
* xref:api:utils/cryptography.adoc#SignerECDSA[`SignerECDSA`]: Verifies signatures produced by regular EVM Externally Owned Accounts (EOAs).
2424
* 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.
25+
* 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.
2526
* xref:api:utils/cryptography.adoc#SignerRSA[`SignerRSA`]: Verifies signatures of traditional PKI systems and X.509 certificates.
2627
* 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]
2728
* xref:api:utils/cryptography.adoc#SignerERC7913[`SignerERC7913`]: Verifies generalized signatures following https://eips.ethereum.org/EIPS/eip-7913[ERC-7913].

0 commit comments

Comments
 (0)