Skip to content

Commit 0f578d2

Browse files
Amxxernestognw
andauthored
Migrate WebAuthn library, signer and verifier from community (#5809)
Co-authored-by: ernestognw <[email protected]>
1 parent a3c6ada commit 0f578d2

File tree

15 files changed

+873
-53
lines changed

15 files changed

+873
-53
lines changed

.changeset/angry-waves-film.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`WebAuthn`: Add a library for verifying WebAuthn Authentication Assertions.

.changeset/petite-seas-shake.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`SignerWebAuthn`: Add an abstract signer that verifies WebAuthn signatures, with a P256 fallback.

.changeset/tender-dolls-nail.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`ERC7913WebAuthnVerifier`: Add an ERC-7913 verifier that verifies WebAuthn Authentication Assertions for P256 identities.

contracts/mocks/account/AccountMock.sol

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {AbstractSigner} from "../../utils/cryptography/signers/AbstractSigner.so
1616
import {SignerECDSA} from "../../utils/cryptography/signers/SignerECDSA.sol";
1717
import {SignerP256} from "../../utils/cryptography/signers/SignerP256.sol";
1818
import {SignerRSA} from "../../utils/cryptography/signers/SignerRSA.sol";
19+
import {SignerWebAuthn} from "../../utils/cryptography/signers/SignerWebAuthn.sol";
1920
import {SignerERC7702} from "../../utils/cryptography/signers/SignerERC7702.sol";
2021
import {SignerERC7913} from "../../utils/cryptography/signers/SignerERC7913.sol";
2122
import {MultiSignerERC7913} from "../../utils/cryptography/signers/MultiSignerERC7913.sol";
@@ -70,6 +71,17 @@ abstract contract AccountRSAMock is Account, SignerRSA, ERC7739, ERC7821, ERC721
7071
}
7172
}
7273

74+
abstract contract AccountWebAuthnMock is Account, SignerWebAuthn, ERC7739, ERC7821, ERC721Holder, ERC1155Holder {
75+
/// @inheritdoc ERC7821
76+
function _erc7821AuthorizedExecutor(
77+
address caller,
78+
bytes32 mode,
79+
bytes calldata executionData
80+
) internal view virtual override returns (bool) {
81+
return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
82+
}
83+
}
84+
7385
abstract contract AccountERC7702Mock is Account, SignerERC7702, ERC7739, ERC7821, ERC721Holder, ERC1155Holder {
7486
/// @inheritdoc ERC7821
7587
function _erc7821AuthorizedExecutor(

contracts/utils/cryptography/README.adoc

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ A collection of contracts and libraries that implement various signature validat
1313
* {MerkleProof}: Functions for verifying https://en.wikipedia.org/wiki/Merkle_tree[Merkle Tree] proofs.
1414
* {EIP712}: Contract with functions to allow processing signed typed structure data according to https://eips.ethereum.org/EIPS/eip-712[EIP-712].
1515
* {ERC7739Utils}: Utilities library that implements a defensive rehashing mechanism to prevent replayability of smart contract signatures based on ERC-7739.
16+
* {WebAuthn}: Library for verifying WebAuthn Authentication Assertions.
1617
* {AbstractSigner}: Abstract contract for internal signature validation in smart contracts.
1718
* {ERC7739}: An abstract contract to validate signatures following the rehashing scheme from {ERC7739Utils}.
1819
* {SignerECDSA}, {SignerP256}, {SignerRSA}: Implementations of an {AbstractSigner} with specific signature validation algorithms.
1920
* {SignerERC7702}: Implementation of {AbstractSigner} that validates signatures using the contract's own address as the signer, useful for delegated accounts following EIP-7702.
21+
* {SignerWebAuthn}: Implementation of {SignerP256} that supports WebAuthn
2022
* {SignerERC7913}, {MultiSignerERC7913}, {MultiSignerERC7913Weighted}: Implementations of {AbstractSigner} that validate signatures based on ERC-7913. Including a simple and weighted multisignature scheme.
21-
* {ERC7913P256Verifier}, {ERC7913RSAVerifier}: Ready to use ERC-7913 signature verifiers for P256 and RSA keys.
23+
* {ERC7913P256Verifier}, {ERC7913RSAVerifier}, {ERC7913WebAuthnVerifier}: Ready to use ERC-7913 signature verifiers for P256, RSA keys and WebAuthn.
2224

2325
== Utils
2426

@@ -40,6 +42,8 @@ A collection of contracts and libraries that implement various signature validat
4042

4143
{{ERC7739Utils}}
4244

45+
{{WebAuthn}}
46+
4347
== Abstract Signers
4448

4549
{{AbstractSigner}}
@@ -65,3 +69,5 @@ A collection of contracts and libraries that implement various signature validat
6569
{{ERC7913P256Verifier}}
6670

6771
{{ERC7913RSAVerifier}}
72+
73+
{{ERC7913WebAuthnVerifier}}

contracts/utils/cryptography/SignatureChecker.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ pragma solidity ^0.8.24;
66
import {ECDSA} from "./ECDSA.sol";
77
import {IERC1271} from "../../interfaces/IERC1271.sol";
88
import {IERC7913SignatureVerifier} from "../../interfaces/IERC7913.sol";
9-
import {Bytes} from "../../utils/Bytes.sol";
9+
import {Bytes} from "../Bytes.sol";
1010

1111
/**
1212
* @dev Signature verification helper that can be used instead of `ECDSA.recover` to seamlessly support:
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.24;
4+
5+
import {P256} from "./P256.sol";
6+
import {Base64} from "../Base64.sol";
7+
import {Bytes} from "../Bytes.sol";
8+
import {Strings} from "../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 internal constant AUTH_DATA_FLAGS_UP = 0x01;
56+
/// @dev Bit 2 of the authenticator data flags: "User Verified" bit.
57+
bytes1 internal constant AUTH_DATA_FLAGS_UV = 0x04;
58+
/// @dev Bit 3 of the authenticator data flags: "Backup Eligibility" bit.
59+
bytes1 internal constant AUTH_DATA_FLAGS_BE = 0x08;
60+
/// @dev Bit 4 of the authenticator data flags: "Backup State" bit.
61+
bytes1 internal constant AUTH_DATA_FLAGS_BS = 0x10;
62+
63+
/**
64+
* @dev Performs standard verification of a WebAuthn Authentication Assertion.
65+
*/
66+
function verify(
67+
bytes memory challenge,
68+
WebAuthnAuth memory auth,
69+
bytes32 qx,
70+
bytes32 qy
71+
) internal view returns (bool) {
72+
return verify(challenge, auth, qx, qy, true);
73+
}
74+
75+
/**
76+
* @dev Performs verification of a WebAuthn Authentication Assertion. This variants allow the caller to select
77+
* whether of not to require the UV flag (step 17).
78+
*
79+
* Verifies:
80+
*
81+
* 1. Type is "webauthn.get" (see {_validateExpectedTypeHash})
82+
* 2. Challenge matches the expected value (see {_validateChallenge})
83+
* 3. Cryptographic signature is valid for the given public key
84+
* 4. confirming physical user presence during authentication
85+
* 5. (if `requireUV` is true) confirming stronger user authentication (biometrics/PIN)
86+
* 6. Backup Eligibility (`BE`) and Backup State (BS) bits relationship is valid
87+
*/
88+
function verify(
89+
bytes memory challenge,
90+
WebAuthnAuth memory auth,
91+
bytes32 qx,
92+
bytes32 qy,
93+
bool requireUV
94+
) internal view returns (bool) {
95+
// Verify authenticator data has sufficient length (37 bytes minimum):
96+
// - 32 bytes for rpIdHash
97+
// - 1 byte for flags
98+
// - 4 bytes for signature counter
99+
return
100+
auth.authenticatorData.length > 36 &&
101+
_validateExpectedTypeHash(auth.clientDataJSON, auth.typeIndex) && // 11
102+
_validateChallenge(auth.clientDataJSON, auth.challengeIndex, challenge) && // 12
103+
_validateUserPresentBitSet(auth.authenticatorData[32]) && // 16
104+
(!requireUV || _validateUserVerifiedBitSet(auth.authenticatorData[32])) && // 17
105+
_validateBackupEligibilityAndState(auth.authenticatorData[32]) && // Consistency check
106+
// P256.verify handles signature malleability internally
107+
P256.verify(
108+
sha256(
109+
abi.encodePacked(
110+
auth.authenticatorData,
111+
sha256(bytes(auth.clientDataJSON)) // 19
112+
)
113+
),
114+
auth.r,
115+
auth.s,
116+
qx,
117+
qy
118+
); // 20
119+
}
120+
121+
/**
122+
* @dev Validates that the https://www.w3.org/TR/webauthn-2/#type[Type] field in the client data JSON is set to
123+
* "webauthn.get".
124+
*
125+
* Step 11 in https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion[verifying an assertion].
126+
*/
127+
function _validateExpectedTypeHash(
128+
string memory clientDataJSON,
129+
uint256 typeIndex
130+
) private pure returns (bool success) {
131+
assembly ("memory-safe") {
132+
success := and(
133+
// clientDataJson.length >= typeIndex + 21
134+
gt(mload(clientDataJSON), add(typeIndex, 20)),
135+
eq(
136+
// get 32 bytes starting at index typexIndex in clientDataJSON, and keep the leftmost 21 bytes
137+
and(mload(add(add(clientDataJSON, 0x20), typeIndex)), shl(88, not(0))),
138+
// solhint-disable-next-line quotes
139+
'"type":"webauthn.get"'
140+
)
141+
)
142+
}
143+
}
144+
145+
/**
146+
* @dev Validates that the challenge in the client data JSON matches the `expectedChallenge`.
147+
*
148+
* Step 12 in https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion[verifying an assertion].
149+
*/
150+
function _validateChallenge(
151+
string memory clientDataJSON,
152+
uint256 challengeIndex,
153+
bytes memory challenge
154+
) private pure returns (bool) {
155+
// solhint-disable-next-line quotes
156+
string memory expectedChallenge = string.concat('"challenge":"', Base64.encodeURL(challenge), '"');
157+
string memory actualChallenge = string(
158+
Bytes.slice(bytes(clientDataJSON), challengeIndex, challengeIndex + bytes(expectedChallenge).length)
159+
);
160+
161+
return Strings.equal(actualChallenge, expectedChallenge);
162+
}
163+
164+
/**
165+
* @dev Validates that the https://www.w3.org/TR/webauthn-2/#up[User Present (UP)] bit is set.
166+
*
167+
* Step 16 in https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion[verifying an assertion].
168+
*
169+
* NOTE: Required by WebAuthn spec but may be skipped for platform authenticators
170+
* (Touch ID, Windows Hello) in controlled environments. Enforce for public-facing apps.
171+
*/
172+
function _validateUserPresentBitSet(bytes1 flags) private pure returns (bool) {
173+
return (flags & AUTH_DATA_FLAGS_UP) == AUTH_DATA_FLAGS_UP;
174+
}
175+
176+
/**
177+
* @dev Validates that the https://www.w3.org/TR/webauthn-2/#uv[User Verified (UV)] bit is set.
178+
*
179+
* Step 17 in https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion[verifying an assertion].
180+
*
181+
* The UV bit indicates whether the user was verified using a stronger identification method
182+
* (biometrics, PIN, password). While optional, requiring UV=1 is recommended for:
183+
*
184+
* * High-value transactions and sensitive operations
185+
* * Account recovery and critical settings changes
186+
* * Privileged operations
187+
*
188+
* NOTE: For routine operations or when using hardware authenticators without verification capabilities,
189+
* `UV=0` may be acceptable. The choice of whether to require UV represents a security vs. usability
190+
* tradeoff - for blockchain applications handling valuable assets, requiring UV is generally safer.
191+
*/
192+
function _validateUserVerifiedBitSet(bytes1 flags) private pure returns (bool) {
193+
return (flags & AUTH_DATA_FLAGS_UV) == AUTH_DATA_FLAGS_UV;
194+
}
195+
196+
/**
197+
* @dev Validates the relationship between Backup Eligibility (`BE`) and Backup State (`BS`) bits
198+
* according to the WebAuthn specification.
199+
*
200+
* The function enforces that if a credential is backed up (`BS=1`), it must also be eligible
201+
* for backup (`BE=1`). This prevents unauthorized credential backup and ensures compliance
202+
* with the WebAuthn spec.
203+
*
204+
* Returns true in these valid states:
205+
*
206+
* * `BE=1`, `BS=0`: Credential is eligible but not backed up
207+
* * `BE=1`, `BS=1`: Credential is eligible and backed up
208+
* * `BE=0`, `BS=0`: Credential is not eligible and not backed up
209+
*
210+
* Returns false only when `BE=0` and `BS=1`, which is an invalid state indicating
211+
* a credential that's backed up but not eligible for backup.
212+
*
213+
* NOTE: While the WebAuthn spec defines this relationship between `BE` and `BS` bits,
214+
* validating it is not explicitly required as part of the core verification procedure.
215+
* Some implementations may choose to skip this check for broader authenticator
216+
* compatibility or when the application's threat model doesn't consider credential
217+
* syncing a major risk.
218+
*/
219+
function _validateBackupEligibilityAndState(bytes1 flags) private pure returns (bool) {
220+
return (flags & AUTH_DATA_FLAGS_BE) == AUTH_DATA_FLAGS_BE || (flags & AUTH_DATA_FLAGS_BS) == 0;
221+
}
222+
223+
/**
224+
* @dev Verifies that calldata bytes (`input`) represents a valid `WebAuthnAuth` object. If encoding is valid,
225+
* returns true and the calldata view at the object. Otherwise, returns false and an invalid calldata object.
226+
*
227+
* NOTE: The returned `auth` object should not be accessed if `success` is false. Trying to access the data may
228+
* cause revert/panic.
229+
*/
230+
function tryDecodeAuth(bytes calldata input) internal pure returns (bool success, WebAuthnAuth calldata auth) {
231+
assembly ("memory-safe") {
232+
auth := input.offset
233+
}
234+
235+
// Minimum length to hold 6 objects (32 bytes each)
236+
if (input.length < 0xC0) return (false, auth);
237+
238+
// Get offset of non-value-type elements relative to the input buffer
239+
uint256 authenticatorDataOffset = uint256(bytes32(input[0x80:]));
240+
uint256 clientDataJSONOffset = uint256(bytes32(input[0xa0:]));
241+
242+
// The elements length (at the offset) should be 32 bytes long. We check that this is within the
243+
// buffer bounds. Since we know input.length is at least 32, we can subtract with no overflow risk.
244+
if (input.length - 0x20 < authenticatorDataOffset || input.length - 0x20 < clientDataJSONOffset)
245+
return (false, auth);
246+
247+
// Get the lengths. offset + 32 is bounded by input.length so it does not overflow.
248+
uint256 authenticatorDataLength = uint256(bytes32(input[authenticatorDataOffset:]));
249+
uint256 clientDataJSONLength = uint256(bytes32(input[clientDataJSONOffset:]));
250+
251+
// Check that the input buffer is long enough to store the non-value-type elements
252+
// Since we know input.length is at least xxxOffset + 32, we can subtract with no overflow risk.
253+
if (
254+
input.length - authenticatorDataOffset - 0x20 < authenticatorDataLength ||
255+
input.length - clientDataJSONOffset - 0x20 < clientDataJSONLength
256+
) return (false, auth);
257+
258+
return (true, auth);
259+
}
260+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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+
(bool decodeSuccess, WebAuthn.WebAuthnAuth calldata auth) = WebAuthn.tryDecodeAuth(signature);
44+
45+
return
46+
decodeSuccess
47+
? WebAuthn.verify(abi.encodePacked(hash), auth, qx, qy)
48+
: super._rawSignatureValidation(hash, signature);
49+
}
50+
}

0 commit comments

Comments
 (0)