@@ -52,43 +52,58 @@ library WebAuthn {
52
52
}
53
53
54
54
/// @dev Bit 0 of the authenticator data flags: "User Present" bit.
55
- bytes1 private constant AUTH_DATA_FLAGS_UP = 0x01 ;
55
+ bytes1 internal constant AUTH_DATA_FLAGS_UP = 0x01 ;
56
56
/// @dev Bit 2 of the authenticator data flags: "User Verified" bit.
57
- bytes1 private constant AUTH_DATA_FLAGS_UV = 0x04 ;
57
+ bytes1 internal constant AUTH_DATA_FLAGS_UV = 0x04 ;
58
58
/// @dev Bit 3 of the authenticator data flags: "Backup Eligibility" bit.
59
- bytes1 private constant AUTH_DATA_FLAGS_BE = 0x08 ;
59
+ bytes1 internal constant AUTH_DATA_FLAGS_BE = 0x08 ;
60
60
/// @dev Bit 4 of the authenticator data flags: "Backup State" bit.
61
- bytes1 private constant AUTH_DATA_FLAGS_BS = 0x10 ;
61
+ bytes1 internal constant AUTH_DATA_FLAGS_BS = 0x10 ;
62
62
63
63
/**
64
- * @dev Performs the absolute minimal verification of a WebAuthn Authentication Assertion.
65
- * This function includes only the essential checks required for basic WebAuthn security:
66
- *
67
- * 1. Type is "webauthn.get" (see {validateExpectedTypeHash})
68
- * 2. Challenge matches the expected value (see {validateChallenge})
69
- * 3. Cryptographic signature is valid for the given public key
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).
70
78
*
71
- * For most applications, use {verify} or {verifyStrict} instead.
79
+ * Verifies:
72
80
*
73
- * NOTE: This function intentionally omits User Presence (UP), User Verification (UV),
74
- * and Backup State/Eligibility checks. Use this only when broader compatibility with
75
- * authenticators is required or in constrained environments.
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
76
87
*/
77
- function verifyMinimal (
88
+ function verify (
78
89
bytes memory challenge ,
79
90
WebAuthnAuth memory auth ,
80
91
bytes32 qx ,
81
- bytes32 qy
92
+ bytes32 qy ,
93
+ bool requireUV
82
94
) internal view returns (bool ) {
83
95
// Verify authenticator data has sufficient length (37 bytes minimum):
84
96
// - 32 bytes for rpIdHash
85
97
// - 1 byte for flags
86
98
// - 4 bytes for signature counter
87
99
return
88
100
auth.authenticatorData.length > 36 &&
89
- validateExpectedTypeHash (auth.clientDataJSON, auth.typeIndex) && // 11
90
- validateChallenge (auth.clientDataJSON, auth.challengeIndex, challenge) && // 12
91
- // Handles signature malleability internally
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
92
107
P256.verify (
93
108
sha256 (
94
109
abi.encodePacked (
@@ -104,69 +119,63 @@ library WebAuthn {
104
119
}
105
120
106
121
/**
107
- * @dev Performs standard verification of a WebAuthn Authentication Assertion.
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".
108
124
*
109
- * Same as {verifyMinimal}, but also verifies:
110
- *
111
- * [start=4]
112
- * 4. {validateUserPresentBitSet} - confirming physical user presence during authentication
113
- *
114
- * This compliance level satisfies the core WebAuthn verification requirements while
115
- * maintaining broad compatibility with authenticators. For higher security requirements,
116
- * consider using {verifyStrict}.
125
+ * Step 11 in https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion[verifying an assertion].
117
126
*/
118
- function verify (
119
- bytes memory challenge ,
120
- WebAuthnAuth memory auth ,
121
- bytes32 qx ,
122
- bytes32 qy
123
- ) internal view returns (bool ) {
124
- // 16 && rest
125
- return validateUserPresentBitSet (auth.authenticatorData[32 ]) && verifyMinimal (challenge, auth, qx, qy);
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
+ }
126
143
}
127
144
128
145
/**
129
- * @dev Performs strict verification of a WebAuthn Authentication Assertion.
130
- *
131
- * Same as {verify}, but also also verifies:
132
- *
133
- * [start=5]
134
- * 5. {validateUserVerifiedBitSet} - confirming stronger user authentication (biometrics/PIN)
135
- * 6. {validateBackupEligibilityAndState}- Backup Eligibility (`BE`) and Backup State (BS) bits
136
- * relationship is valid
146
+ * @dev Validates that the challenge in the client data JSON matches the `expectedChallenge`.
137
147
*
138
- * This strict verification is recommended for:
139
- *
140
- * * High-value transactions
141
- * * Privileged operations
142
- * * Account recovery or critical settings changes
143
- * * Applications where security takes precedence over broad authenticator compatibility
148
+ * Step 12 in https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion[verifying an assertion].
144
149
*/
145
- function verifyStrict (
146
- bytes memory challenge ,
147
- WebAuthnAuth memory auth ,
148
- bytes32 qx ,
149
- bytes32 qy
150
- ) internal view returns (bool ) {
151
- return
152
- validateUserVerifiedBitSet (auth.authenticatorData[32 ]) && // 17
153
- validateBackupEligibilityAndState (auth.authenticatorData[32 ]) && // Consistency check
154
- verify (challenge, auth, qx, qy);
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);
155
162
}
156
163
157
164
/**
158
165
* @dev Validates that the https://www.w3.org/TR/webauthn-2/#up[User Present (UP)] bit is set.
166
+ *
159
167
* Step 16 in https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion[verifying an assertion].
160
168
*
161
169
* NOTE: Required by WebAuthn spec but may be skipped for platform authenticators
162
170
* (Touch ID, Windows Hello) in controlled environments. Enforce for public-facing apps.
163
171
*/
164
- function validateUserPresentBitSet (bytes1 flags ) internal pure returns (bool ) {
172
+ function _validateUserPresentBitSet (bytes1 flags ) private pure returns (bool ) {
165
173
return (flags & AUTH_DATA_FLAGS_UP) == AUTH_DATA_FLAGS_UP;
166
174
}
167
175
168
176
/**
169
177
* @dev Validates that the https://www.w3.org/TR/webauthn-2/#uv[User Verified (UV)] bit is set.
178
+ *
170
179
* Step 17 in https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion[verifying an assertion].
171
180
*
172
181
* The UV bit indicates whether the user was verified using a stronger identification method
@@ -180,7 +189,7 @@ library WebAuthn {
180
189
* `UV=0` may be acceptable. The choice of whether to require UV represents a security vs. usability
181
190
* tradeoff - for blockchain applications handling valuable assets, requiring UV is generally safer.
182
191
*/
183
- function validateUserVerifiedBitSet (bytes1 flags ) internal pure returns (bool ) {
192
+ function _validateUserVerifiedBitSet (bytes1 flags ) private pure returns (bool ) {
184
193
return (flags & AUTH_DATA_FLAGS_UV) == AUTH_DATA_FLAGS_UV;
185
194
}
186
195
@@ -207,35 +216,8 @@ library WebAuthn {
207
216
* compatibility or when the application's threat model doesn't consider credential
208
217
* syncing a major risk.
209
218
*/
210
- function validateBackupEligibilityAndState (bytes1 flags ) internal pure returns (bool ) {
211
- return (flags & AUTH_DATA_FLAGS_BE) != 0 || (flags & AUTH_DATA_FLAGS_BS) == 0 ;
212
- }
213
-
214
- /**
215
- * @dev Validates that the https://www.w3.org/TR/webauthn-2/#type[Type] field in the client data JSON
216
- * is set to "webauthn.get".
217
- */
218
- function validateExpectedTypeHash (string memory clientDataJSON , uint256 typeIndex ) internal pure returns (bool ) {
219
- // 21 = length of '"type":"webauthn.get"'
220
- bytes memory typeValueBytes = Bytes.slice (bytes (clientDataJSON), typeIndex, typeIndex + 21 );
221
-
222
- // solhint-disable-next-line quotes
223
- return bytes21 (typeValueBytes) == bytes21 ('"type":"webauthn.get" ' );
224
- }
225
-
226
- /// @dev Validates that the challenge in the client data JSON matches the `expectedChallenge`.
227
- function validateChallenge (
228
- string memory clientDataJSON ,
229
- uint256 challengeIndex ,
230
- bytes memory challenge
231
- ) internal pure returns (bool ) {
232
- // solhint-disable-next-line quotes
233
- string memory expectedChallenge = string .concat ('"challenge":" ' , Base64.encodeURL (challenge), '" ' );
234
- string memory actualChallenge = string (
235
- Bytes.slice (bytes (clientDataJSON), challengeIndex, challengeIndex + bytes (expectedChallenge).length )
236
- );
237
-
238
- return Strings.equal (actualChallenge, expectedChallenge);
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 ;
239
221
}
240
222
241
223
/**
0 commit comments