22pragma solidity >= 0.8.19 < 0.9.0 ;
33
44import { ECDSA256r1 } from "../lib/secp256r1-verify/src/ECDSA256r1.sol " ;
5- import { WebAuthnBase } from "./WebAuthnBase .sol " ;
5+ import { Base64 } from "../lib/solady/src/utils/Base64 .sol " ;
66
77/// @title WebAuthn256r1
88/// @notice A library to verify ECDSA signature though WebAuthn on the secp256r1 curve
99/// @custom:experimental This is an experimental library.
10- contract WebAuthn256r1 is WebAuthnBase {
10+ library WebAuthn256r1 {
11+ error InvalidAuthenticatorData ();
12+ error InvalidClientData ();
13+ error InvalidChallenge ();
14+
15+ /// @notice Validate the webauthn data and generate the signature message needed to recover
16+ /// @param authenticatorDataFlagMask This is a bit mask that will be used to validate the flag in the
17+ /// authenticator data. The flag is located at byte 32 of the authenticator
18+ /// data and is used to indicate, among other things, wheter the user's
19+ /// presence/verification ceremonies have been performed.
20+ /// This argument is not expected to be exposed to the end user, it is the
21+ /// responsibility of the caller to enforce the value of the flag for their flows.
22+ ///
23+ /// Here are some flags you may want to use depending on your needs.
24+ /// - 0x01: User presence (UP) is required. If the UP flag is not set, revert
25+ /// - 0x04: User verification (UV) is required. If the UV flag is not set, revert
26+ /// - 0x05: UV and UP are both accepted. If none of them is set, revert
27+ ///
28+ // Read more about UP here: https://www.w3.org/TR/webauthn-2/#test-of-user-presence
29+ // Read more about UV here: https://www.w3.org/TR/webauthn-2/#user-verification
30+ /// @param authenticatorData The authenticator data structure encodes contextual bindings made by the authenticator.
31+ /// Described here: https://www.w3.org/TR/webauthn-2/#authenticator-data
32+ /// @param clientData This is the client data that was signed. The client data represents the
33+ /// contextual bindings of both the WebAuthn Relying Party and the client.
34+ /// Described here: https://www.w3.org/TR/webauthn-2/#client-data
35+ /// @param clientChallenge This is the challenge that was sent to the client to sign. It is
36+ /// part of the client data. In a classic non-EVM flow, this challenge
37+ /// is generated by the server and sent to the client to avoid replay
38+ /// attack. In our context, as we already have the nonce for this purpose
39+ /// we use this field to pass the arbitrary execution order.
40+ /// This value is expected to not be encoded in Base64, the encoding is done
41+ /// during the verification.
42+ /// @param clientChallengeOffset The offset of the client challenge in the client data
43+ /// @return message The signature message needed to recover
44+ /// @dev 1. The signature counter is not checked in this implementation because
45+ /// we already have the nonce on-chain to prevent the anti-replay attack.
46+ /// The counter is 4-bytes long and it is located at bytes 33 of the authenticator data.
47+ /// 2. The RP.ID is not checked in this implementation as it is impossible to generate
48+ /// the same keys for different RP.IDs with a well formed authenticator. The hash of the id
49+ /// is 32-bytes long and it is located at bytes 0 of the authenticator data.
50+ /// 3. The length of the authenticator data is not fixed. It is at least 37 bytes
51+ /// (rpIdHash (32) + flags (1) + counter (4)) but it can be longer if there is an
52+ /// attested credential data and/or some extensions data. As we do not consider
53+ /// the counter in this implementation, we only require the authenticator data to be
54+ /// at least 32 bytes long in order to save some calldata gas.
55+ /// 4. You may probably ask why we encode the challenge in base64 on-chain instead of
56+ /// of sending it already encoded to save some gas. This library is opinionated and
57+ /// it assumes that it is used in the context of Account Abstraction. In this context,
58+ /// valuable informations required to proceed the transaction will be stored in the
59+ /// challenge meaning we need the challenge in clear to use it later in the flow.
60+ /// That's why we decided to add an extra encoding step during the validation.
61+ /// 5. It is assumed this is not the responsibility of this contract to check the value
62+ /// of the `alg` parameter. It is expected this contract will be extended by another
63+ /// contract that will redirect the message produced by this contract to the right
64+ /// recovery function.
65+ /// 6. Both extension data and attested credential data are out of scope of this implementation.
66+ /// 7. It is not the responsibility of this contract to validate the attestation statement formats
67+ ///
68+ /// This contract is based on the level 2 of the WebAuthn specification.
69+ /// and until proven otherwise compliant with the level 3 of the specification.
70+ function generateMessage (
71+ bytes1 authenticatorDataFlagMask ,
72+ bytes calldata authenticatorData ,
73+ bytes calldata clientData ,
74+ bytes calldata clientChallenge ,
75+ uint256 clientChallengeOffset
76+ )
77+ internal
78+ pure
79+ returns (bytes32 message )
80+ {
81+ unchecked {
82+ // Let the caller check the value of the flag in the authenticator data
83+ // @dev: we don't need to manually check the length of the authenticator data
84+ // here as the EVM will automatically revert if the length is lower than 32
85+ if ((authenticatorData[32 ] & authenticatorDataFlagMask) == 0 ) {
86+ revert InvalidAuthenticatorData ();
87+ }
88+
89+ // Ensure the client challenge is not null
90+ if (clientChallenge.length == 0 ) revert InvalidChallenge ();
91+
92+ // Encode the client challenge in base64 and explicitly convert it to bytes
93+ bytes memory challengeEncoded = bytes (Base64.encode (clientChallenge, true , true ));
94+
95+ // Extract the challenge from the client data and hash it
96+ // @dev: we don't need to check the overflow here as the EVM will automatically revert if
97+ // `clientChallengeOffset + challengeEncoded.length` overflow. This is because we will
98+ // try to access a chunk of memory by passing an end index lower than the start index
99+ bytes32 challengeHashed =
100+ keccak256 (clientData[clientChallengeOffset:(clientChallengeOffset + challengeEncoded.length )]);
101+
102+ // Hash the encoded challenge and check both challenges are equal
103+ if (keccak256 (challengeEncoded) != challengeHashed) {
104+ revert InvalidClientData ();
105+ }
106+
107+ // Craft the signature message by hashing the client data, then concatenating
108+ // it to the authenticator data without padding, before hashing the result
109+ message = sha256 (abi.encodePacked (authenticatorData, sha256 (clientData)));
110+ }
111+ }
112+
11113 /// @notice Verify ECDSA signature though WebAuthn on the secp256r1 curve
12114 function verify (
13115 bytes1 authenticatorDataFlagMask ,
@@ -20,7 +122,7 @@ contract WebAuthn256r1 is WebAuthnBase {
20122 uint256 qx ,
21123 uint256 qy
22124 )
23- external
125+ internal
24126 returns (bool )
25127 {
26128 unchecked {
0 commit comments