-
Notifications
You must be signed in to change notification settings - Fork 36
Expand file tree
/
Copy pathWebAuthn.sol
More file actions
278 lines (257 loc) · 12.9 KB
/
WebAuthn.sol
File metadata and controls
278 lines (257 loc) · 12.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
// 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 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
return
auth.authenticatorData.length > 36 &&
validateExpectedTypeHash(auth.clientDataJSON, auth.typeIndex) && // 11
validateChallenge(auth.clientDataJSON, auth.challengeIndex, challenge) && // 12
// Handles signature malleability internally
P256.verify(
sha256(
abi.encodePacked(
auth.authenticatorData,
sha256(bytes(auth.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;
}
/**
* @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(string memory clientDataJSON, uint256 typeIndex) internal pure returns (bool) {
// 21 = length of '"type":"webauthn.get"'
bytes memory typeValueBytes = Bytes.slice(bytes(clientDataJSON), typeIndex, typeIndex + 21);
// solhint-disable-next-line quotes
return bytes21(typeValueBytes) == bytes21('"type":"webauthn.get"');
}
/// @dev Validates that the challenge in the client data JSON matches the `expectedChallenge`.
function validateChallenge(
string memory clientDataJSON,
uint256 challengeIndex,
bytes memory challenge
) internal pure returns (bool) {
// solhint-disable-next-line quotes
string memory expectedChallenge = string.concat('"challenge":"', Base64.encodeURL(challenge), '"');
string memory actualChallenge = string(
Bytes.slice(bytes(clientDataJSON), challengeIndex, challengeIndex + bytes(expectedChallenge).length)
);
return Strings.equal(actualChallenge, expectedChallenge);
}
/**
* @dev Verifies that calldata bytes (`input`) represents a valid `WebAuthnAuth` object. If encoding is valid,
* returns true and the calldata view at the object. Otherwise, returns false and an invalid calldata object.
*
* NOTE: The returned `auth` object should not be accessed if `success` is false. Trying to access the data may
* cause revert/panic.
*/
function tryDecodeAuth(bytes calldata input) internal pure returns (bool success, WebAuthnAuth calldata auth) {
assembly ("memory-safe") {
auth := input.offset
}
// Minimum length to hold 6 objects (32 bytes each)
if (input.length < 0xC0) return (false, auth);
// Get offset of non-value-type elements relative to the input buffer
uint256 authenticatorDataOffset = uint256(bytes32(input[0x80:]));
uint256 clientDataJSONOffset = uint256(bytes32(input[0xa0:]));
// The elements length (at the offset) should be 32 bytes long. We check that this is within the
// buffer bounds. Since we know input.length is at least 32, we can subtract with no overflow risk.
if (input.length - 0x20 < authenticatorDataOffset || input.length - 0x20 < clientDataJSONOffset)
return (false, auth);
// Get the lengths. offset + 32 is bounded by input.length so it does not overflow.
uint256 authenticatorDataLength = uint256(bytes32(input[authenticatorDataOffset:]));
uint256 clientDataJSONLength = uint256(bytes32(input[clientDataJSONOffset:]));
// Check that the input buffer is long enough to store the non-value-type elements
// Since we know input.length is at least xxxOffset + 32, we can subtract with no overflow risk.
if (
input.length - authenticatorDataOffset - 0x20 < authenticatorDataLength ||
input.length - clientDataJSONOffset - 0x20 < clientDataJSONLength
) return (false, auth);
return (true, auth);
}
}