ox 0.12.4 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/_cjs/core/P256.js +1 -1
  3. package/_cjs/core/P256.js.map +1 -1
  4. package/_cjs/core/WebAuthnP256.js +15 -256
  5. package/_cjs/core/WebAuthnP256.js.map +1 -1
  6. package/_cjs/core/WebCryptoP256.js +3 -1
  7. package/_cjs/core/WebCryptoP256.js.map +1 -1
  8. package/_cjs/core/internal/webauthn.js +5 -13
  9. package/_cjs/core/internal/webauthn.js.map +1 -1
  10. package/_cjs/index.docs.js +1 -0
  11. package/_cjs/index.docs.js.map +1 -1
  12. package/_cjs/tempo/KeyAuthorization.js +18 -3
  13. package/_cjs/tempo/KeyAuthorization.js.map +1 -1
  14. package/_cjs/tempo/SignatureEnvelope.js +26 -0
  15. package/_cjs/tempo/SignatureEnvelope.js.map +1 -1
  16. package/_cjs/tempo/TxEnvelopeTempo.js +5 -10
  17. package/_cjs/tempo/TxEnvelopeTempo.js.map +1 -1
  18. package/_cjs/version.js +1 -1
  19. package/_cjs/webauthn/Authentication.js +246 -0
  20. package/_cjs/webauthn/Authentication.js.map +1 -0
  21. package/_cjs/webauthn/Authenticator.js +55 -0
  22. package/_cjs/webauthn/Authenticator.js.map +1 -0
  23. package/_cjs/webauthn/Credential.js +53 -0
  24. package/_cjs/webauthn/Credential.js.map +1 -0
  25. package/_cjs/webauthn/Registration.js +349 -0
  26. package/_cjs/webauthn/Registration.js.map +1 -0
  27. package/_cjs/webauthn/Types.js +3 -0
  28. package/_cjs/webauthn/Types.js.map +1 -0
  29. package/_cjs/webauthn/index.js +9 -0
  30. package/_cjs/webauthn/index.js.map +1 -0
  31. package/_cjs/webauthn/internal/utils.js +53 -0
  32. package/_cjs/webauthn/internal/utils.js.map +1 -0
  33. package/_esm/core/P256.js +1 -1
  34. package/_esm/core/P256.js.map +1 -1
  35. package/_esm/core/WebAuthnP256.js +13 -261
  36. package/_esm/core/WebAuthnP256.js.map +1 -1
  37. package/_esm/core/WebCryptoP256.js +4 -1
  38. package/_esm/core/WebCryptoP256.js.map +1 -1
  39. package/_esm/core/internal/webauthn.js +5 -13
  40. package/_esm/core/internal/webauthn.js.map +1 -1
  41. package/_esm/erc8021/index.js +2 -2
  42. package/_esm/index.docs.js +1 -0
  43. package/_esm/index.docs.js.map +1 -1
  44. package/_esm/tempo/KeyAuthorization.js +66 -3
  45. package/_esm/tempo/KeyAuthorization.js.map +1 -1
  46. package/_esm/tempo/SignatureEnvelope.js +74 -0
  47. package/_esm/tempo/SignatureEnvelope.js.map +1 -1
  48. package/_esm/tempo/TransactionReceipt.js +1 -1
  49. package/_esm/tempo/TransactionRequest.js +1 -1
  50. package/_esm/tempo/TxEnvelopeTempo.js +5 -10
  51. package/_esm/tempo/TxEnvelopeTempo.js.map +1 -1
  52. package/_esm/version.js +1 -1
  53. package/_esm/webauthn/Authentication.js +453 -0
  54. package/_esm/webauthn/Authentication.js.map +1 -0
  55. package/_esm/webauthn/Authenticator.js +176 -0
  56. package/_esm/webauthn/Authenticator.js.map +1 -0
  57. package/_esm/webauthn/Credential.js +95 -0
  58. package/_esm/webauthn/Credential.js.map +1 -0
  59. package/_esm/webauthn/Registration.js +512 -0
  60. package/_esm/webauthn/Registration.js.map +1 -0
  61. package/_esm/webauthn/Types.js +2 -0
  62. package/_esm/webauthn/Types.js.map +1 -0
  63. package/_esm/webauthn/index.js +31 -0
  64. package/_esm/webauthn/index.js.map +1 -0
  65. package/_esm/webauthn/internal/utils.js +52 -0
  66. package/_esm/webauthn/internal/utils.js.map +1 -0
  67. package/_types/core/WebAuthnP256.d.ts +33 -208
  68. package/_types/core/WebAuthnP256.d.ts.map +1 -1
  69. package/_types/core/WebCryptoP256.d.ts +2 -0
  70. package/_types/core/WebCryptoP256.d.ts.map +1 -1
  71. package/_types/core/internal/webauthn.d.ts +2 -110
  72. package/_types/core/internal/webauthn.d.ts.map +1 -1
  73. package/_types/erc8021/index.d.ts +2 -2
  74. package/_types/index.docs.d.ts +1 -0
  75. package/_types/index.docs.d.ts.map +1 -1
  76. package/_types/tempo/KeyAuthorization.d.ts +57 -0
  77. package/_types/tempo/KeyAuthorization.d.ts.map +1 -1
  78. package/_types/tempo/SignatureEnvelope.d.ts +75 -0
  79. package/_types/tempo/SignatureEnvelope.d.ts.map +1 -1
  80. package/_types/tempo/Transaction.d.ts +2 -2
  81. package/_types/tempo/TransactionReceipt.d.ts +2 -2
  82. package/_types/tempo/TransactionRequest.d.ts +2 -2
  83. package/_types/tempo/TxEnvelopeTempo.d.ts.map +1 -1
  84. package/_types/version.d.ts +1 -1
  85. package/_types/webauthn/Authentication.d.ts +324 -0
  86. package/_types/webauthn/Authentication.d.ts.map +1 -0
  87. package/_types/webauthn/Authenticator.d.ts +182 -0
  88. package/_types/webauthn/Authenticator.d.ts.map +1 -0
  89. package/_types/webauthn/Credential.d.ts +77 -0
  90. package/_types/webauthn/Credential.d.ts.map +1 -0
  91. package/_types/webauthn/Registration.d.ts +308 -0
  92. package/_types/webauthn/Registration.d.ts.map +1 -0
  93. package/_types/webauthn/Types.d.ts +106 -0
  94. package/_types/webauthn/Types.d.ts.map +1 -0
  95. package/_types/webauthn/index.d.ts +33 -0
  96. package/_types/webauthn/index.d.ts.map +1 -0
  97. package/_types/webauthn/internal/utils.d.ts +17 -0
  98. package/_types/webauthn/internal/utils.d.ts.map +1 -0
  99. package/core/P256.ts +1 -1
  100. package/core/WebAuthnP256.ts +37 -582
  101. package/core/WebCryptoP256.ts +6 -1
  102. package/core/internal/webauthn.ts +6 -165
  103. package/erc8021/index.ts +2 -2
  104. package/index.docs.ts +1 -0
  105. package/package.json +31 -1
  106. package/tempo/KeyAuthorization.test.ts +139 -0
  107. package/tempo/KeyAuthorization.ts +82 -3
  108. package/tempo/SignatureEnvelope.test.ts +147 -0
  109. package/tempo/SignatureEnvelope.ts +113 -0
  110. package/tempo/Transaction.ts +2 -2
  111. package/tempo/TransactionReceipt.ts +2 -2
  112. package/tempo/TransactionRequest.ts +2 -2
  113. package/tempo/TxEnvelopeTempo.ts +5 -12
  114. package/tempo/e2e.test.ts +265 -0
  115. package/version.ts +1 -1
  116. package/webauthn/Authentication/package.json +6 -0
  117. package/webauthn/Authentication.ts +673 -0
  118. package/webauthn/Authenticator/package.json +6 -0
  119. package/webauthn/Authenticator.ts +259 -0
  120. package/webauthn/Credential/package.json +6 -0
  121. package/webauthn/Credential.ts +146 -0
  122. package/webauthn/Registration/package.json +6 -0
  123. package/webauthn/Registration.ts +805 -0
  124. package/webauthn/Types/package.json +6 -0
  125. package/webauthn/Types.ts +158 -0
  126. package/webauthn/index.ts +38 -0
  127. package/webauthn/internal/utils.ts +63 -0
  128. package/webauthn/package.json +6 -0
@@ -0,0 +1,805 @@
1
+ import * as Base64 from '../core/Base64.js'
2
+ import * as Bytes from '../core/Bytes.js'
3
+ import * as Cbor from '../core/Cbor.js'
4
+ import * as CoseKey from '../core/CoseKey.js'
5
+ import * as Errors from '../core/Errors.js'
6
+ import * as Hash from '../core/Hash.js'
7
+ import * as Hex from '../core/Hex.js'
8
+ import type { OneOf } from '../core/internal/types.js'
9
+ import * as internal from '../core/internal/webauthn.js'
10
+ import * as P256 from '../core/P256.js'
11
+ import * as PublicKey from '../core/PublicKey.js'
12
+ import * as Signature from '../core/Signature.js'
13
+ import type * as Credential_ from './Credential.js'
14
+ import {
15
+ base64UrlOptions,
16
+ bufferSourceToBytes,
17
+ bytesToArrayBuffer,
18
+ deserializeExtensions,
19
+ responseKeys,
20
+ serializeExtensions,
21
+ } from './internal/utils.js'
22
+ import type * as Types from './Types.js'
23
+
24
+ export const createChallenge = Uint8Array.from([
25
+ 105, 171, 180, 181, 160, 222, 75, 198, 42, 42, 32, 31, 141, 37, 186, 233,
26
+ ])
27
+
28
+ /** Response from a WebAuthn registration ceremony. */
29
+ export type Response<serialized extends boolean = false> = {
30
+ credential: Credential_.Credential<serialized>
31
+ counter: number
32
+ userVerified?: true | undefined
33
+ backedUp?: boolean | undefined
34
+ deviceType?: 'multiDevice' | 'singleDevice' | undefined
35
+ }
36
+
37
+ /**
38
+ * Creates a new WebAuthn P256 Credential, which can be stored and later used for signing.
39
+ *
40
+ * @example
41
+ * ```ts twoslash
42
+ * import { Registration } from 'ox/webauthn'
43
+ *
44
+ * const credential = await Registration.create({ name: 'Example' }) // [!code focus]
45
+ * // @log: {
46
+ * // @log: id: 'oZ48...',
47
+ * // @log: publicKey: { x: 51421...5123n, y: 12345...6789n },
48
+ * // @log: raw: PublicKeyCredential {},
49
+ * // @log: }
50
+ * ```
51
+ *
52
+ * @param options - Credential creation options.
53
+ * @returns A WebAuthn P256 credential.
54
+ */
55
+ export async function create(
56
+ options: create.Options,
57
+ ): Promise<Credential_.Credential> {
58
+ const {
59
+ createFn = window.navigator.credentials.create.bind(
60
+ window.navigator.credentials,
61
+ ),
62
+ ...rest
63
+ } = options
64
+ const creationOptions =
65
+ 'publicKey' in rest
66
+ ? (rest as Types.CredentialCreationOptions)
67
+ : getOptions(rest as never)
68
+ try {
69
+ const credential = (await createFn(
70
+ creationOptions as never,
71
+ )) as Types.PublicKeyCredential
72
+ if (!credential) throw new CreateFailedError()
73
+
74
+ const response =
75
+ credential.response as Types.AuthenticatorAttestationResponse
76
+ const publicKey = await internal.parseCredentialPublicKey(response)
77
+
78
+ return {
79
+ attestationObject: response.attestationObject,
80
+ clientDataJSON: response.clientDataJSON,
81
+ id: credential.id,
82
+ publicKey,
83
+ raw: credential,
84
+ }
85
+ } catch (error) {
86
+ throw new CreateFailedError({
87
+ cause: error as Error,
88
+ })
89
+ }
90
+ }
91
+
92
+ export declare namespace create {
93
+ type Options = OneOf<
94
+ | (getOptions.Options & {
95
+ /**
96
+ * Credential creation function. Useful for environments that do not support
97
+ * the WebAuthn API natively (i.e. React Native or testing environments).
98
+ *
99
+ * @default window.navigator.credentials.create
100
+ */
101
+ createFn?:
102
+ | ((
103
+ options?: Types.CredentialCreationOptions | undefined,
104
+ ) => Promise<Types.Credential | null>)
105
+ | undefined
106
+ })
107
+ | Types.CredentialCreationOptions
108
+ >
109
+
110
+ type ErrorType =
111
+ | getOptions.ErrorType
112
+ | internal.parseCredentialPublicKey.ErrorType
113
+ | Errors.GlobalErrorType
114
+ }
115
+
116
+ /**
117
+ * Returns the creation options for a P256 WebAuthn Credential to be used with
118
+ * the Web Authentication API.
119
+ *
120
+ * @example
121
+ * ```ts twoslash
122
+ * import { Registration } from 'ox/webauthn'
123
+ *
124
+ * const options = Registration.getOptions({ name: 'Example' })
125
+ *
126
+ * const credential = await window.navigator.credentials.create(options)
127
+ * ```
128
+ *
129
+ * @param options - Options.
130
+ * @returns The credential creation options.
131
+ */
132
+ export function getOptions(
133
+ options: getOptions.Options,
134
+ ): Types.CredentialCreationOptions {
135
+ const {
136
+ attestation = 'none',
137
+ authenticatorSelection = {
138
+ residentKey: 'preferred',
139
+ requireResidentKey: false,
140
+ userVerification: 'required',
141
+ },
142
+ challenge = createChallenge,
143
+ excludeCredentialIds,
144
+ extensions,
145
+ name: name_,
146
+ rp = {
147
+ id: window.location.hostname,
148
+ name: window.document.title,
149
+ },
150
+ user,
151
+ } = options
152
+ const name = (user?.name ?? name_)!
153
+ return {
154
+ publicKey: {
155
+ attestation,
156
+ authenticatorSelection,
157
+ challenge:
158
+ typeof challenge === 'string' ? Bytes.fromHex(challenge) : challenge,
159
+ ...(excludeCredentialIds
160
+ ? {
161
+ excludeCredentials: excludeCredentialIds?.map((id) => ({
162
+ id: Base64.toBytes(id),
163
+ type: 'public-key',
164
+ })),
165
+ }
166
+ : {}),
167
+ pubKeyCredParams: [
168
+ {
169
+ type: 'public-key',
170
+ alg: -7, // p256
171
+ },
172
+ ],
173
+ rp,
174
+ user: {
175
+ id: user?.id ?? Hash.keccak256(Bytes.fromString(name), { as: 'Bytes' }),
176
+ name,
177
+ displayName: user?.displayName ?? name,
178
+ },
179
+ ...(extensions && { extensions }),
180
+ },
181
+ }
182
+ }
183
+
184
+ export declare namespace getOptions {
185
+ type Options = {
186
+ /**
187
+ * A string specifying the relying party's preference for how the attestation statement
188
+ * (i.e., provision of verifiable evidence of the authenticity of the authenticator and its data)
189
+ * is conveyed during credential creation.
190
+ */
191
+ attestation?:
192
+ | Types.PublicKeyCredentialCreationOptions['attestation']
193
+ | undefined
194
+ /**
195
+ * An object whose properties are criteria used to filter out the potential authenticators
196
+ * for the credential creation operation.
197
+ */
198
+ authenticatorSelection?:
199
+ | Types.PublicKeyCredentialCreationOptions['authenticatorSelection']
200
+ | undefined
201
+ /**
202
+ * An `ArrayBuffer`, `TypedArray`, or `DataView` used as a cryptographic challenge.
203
+ */
204
+ challenge?:
205
+ | Hex.Hex
206
+ | Types.PublicKeyCredentialCreationOptions['challenge']
207
+ | undefined
208
+ /**
209
+ * List of credential IDs to exclude from the creation. This property can be used
210
+ * to prevent creation of a credential if it already exists.
211
+ */
212
+ excludeCredentialIds?: readonly string[] | undefined
213
+ /**
214
+ * List of Web Authentication API credentials to use during creation or authentication.
215
+ */
216
+ extensions?:
217
+ | Types.PublicKeyCredentialCreationOptions['extensions']
218
+ | undefined
219
+ /**
220
+ * An object describing the relying party that requested the credential creation
221
+ */
222
+ rp?:
223
+ | {
224
+ id: string
225
+ name: string
226
+ }
227
+ | undefined
228
+ /**
229
+ * A numerical hint, in milliseconds, which indicates the time the calling web app is willing to wait for the creation operation to complete.
230
+ */
231
+ timeout?: Types.PublicKeyCredentialCreationOptions['timeout'] | undefined
232
+ } & OneOf<
233
+ | {
234
+ /** Name for the credential (user.name). */
235
+ name: string
236
+ user?:
237
+ | {
238
+ displayName?: string
239
+ id?: Types.BufferSource
240
+ name: string
241
+ }
242
+ | undefined
243
+ }
244
+ | {
245
+ name?: string | undefined
246
+ /**
247
+ * An object describing the user account for which the credential is generated.
248
+ */
249
+ user: {
250
+ displayName?: string
251
+ id?: Types.BufferSource
252
+ name: string
253
+ }
254
+ }
255
+ >
256
+
257
+ type ErrorType =
258
+ | Base64.toBytes.ErrorType
259
+ | Hash.keccak256.ErrorType
260
+ | Bytes.fromString.ErrorType
261
+ | Errors.GlobalErrorType
262
+ }
263
+
264
+ /**
265
+ * Serializes a registration response into a JSON-serializable
266
+ * format, converting `ArrayBuffer` fields to base64url strings
267
+ * and the public key to a hex string.
268
+ *
269
+ * @example
270
+ * ```ts twoslash
271
+ * import { Registration } from 'ox/webauthn'
272
+ *
273
+ * const credential = await Registration.create({ name: 'Example' })
274
+ * const response = Registration.verify({
275
+ * credential,
276
+ * challenge: '0x...',
277
+ * origin: 'https://example.com',
278
+ * rpId: 'example.com',
279
+ * })
280
+ *
281
+ * const serialized = Registration.serializeResponse(response) // [!code focus]
282
+ *
283
+ * // `serialized` is JSON-serializable — send it to a server, store it, etc.
284
+ * const json = JSON.stringify(serialized)
285
+ * ```
286
+ *
287
+ * @param response - The registration response to serialize.
288
+ * @returns The serialized registration response.
289
+ */
290
+ export function serializeResponse(response: Response): Response<true> {
291
+ const { credential, ...rest } = response
292
+
293
+ const rawResponse = {} as Record<string, string>
294
+ for (const key of responseKeys) {
295
+ const value = (
296
+ credential.raw.response as unknown as Record<string, unknown>
297
+ )[key]
298
+ if (value instanceof ArrayBuffer)
299
+ rawResponse[key] = Base64.fromBytes(
300
+ new Uint8Array(value),
301
+ base64UrlOptions,
302
+ )
303
+ }
304
+
305
+ return {
306
+ ...rest,
307
+ credential: {
308
+ attestationObject: Base64.fromBytes(
309
+ new Uint8Array(credential.attestationObject),
310
+ base64UrlOptions,
311
+ ),
312
+ clientDataJSON: Base64.fromBytes(
313
+ new Uint8Array(credential.clientDataJSON),
314
+ base64UrlOptions,
315
+ ),
316
+ id: credential.id,
317
+ publicKey: PublicKey.toHex(credential.publicKey),
318
+ raw: {
319
+ id: credential.raw.id,
320
+ type: credential.raw.type,
321
+ authenticatorAttachment: credential.raw.authenticatorAttachment,
322
+ rawId: Base64.fromBytes(
323
+ bufferSourceToBytes(credential.raw.rawId),
324
+ base64UrlOptions,
325
+ ),
326
+ response: rawResponse as unknown as Types.AuthenticatorResponse<true>,
327
+ },
328
+ },
329
+ }
330
+ }
331
+
332
+ export declare namespace serializeResponse {
333
+ type ErrorType =
334
+ | Base64.fromBytes.ErrorType
335
+ | PublicKey.toHex.ErrorType
336
+ | Errors.GlobalErrorType
337
+ }
338
+
339
+ /**
340
+ * Serializes credential creation options into a JSON-serializable
341
+ * format, converting `BufferSource` fields to base64url strings.
342
+ *
343
+ * @example
344
+ * ```ts twoslash
345
+ * import { Registration } from 'ox/webauthn'
346
+ *
347
+ * const options = Registration.getOptions({ name: 'Example' })
348
+ *
349
+ * const serialized = Registration.serializeOptions(options) // [!code focus]
350
+ *
351
+ * // `serialized` is JSON-serializable — send it to a server, store it, etc.
352
+ * const json = JSON.stringify(serialized)
353
+ * ```
354
+ *
355
+ * @param options - The credential creation options to serialize.
356
+ * @returns The serialized credential creation options.
357
+ */
358
+ export function serializeOptions(
359
+ options: Types.CredentialCreationOptions,
360
+ ): Types.CredentialCreationOptions<true> {
361
+ const publicKey = options.publicKey
362
+ if (!publicKey) return {}
363
+
364
+ const { challenge, excludeCredentials, extensions, user, ...rest } = publicKey
365
+
366
+ return {
367
+ publicKey: {
368
+ ...rest,
369
+ challenge: Hex.fromBytes(bufferSourceToBytes(challenge)),
370
+ ...(excludeCredentials && {
371
+ excludeCredentials: excludeCredentials.map(({ id, ...rest }) => ({
372
+ ...rest,
373
+ id: Base64.fromBytes(bufferSourceToBytes(id), base64UrlOptions),
374
+ })),
375
+ }),
376
+ ...(extensions && {
377
+ extensions: serializeExtensions(extensions),
378
+ }),
379
+ user: {
380
+ ...user,
381
+ id: Base64.fromBytes(bufferSourceToBytes(user.id), base64UrlOptions),
382
+ },
383
+ },
384
+ }
385
+ }
386
+
387
+ export declare namespace serializeOptions {
388
+ type ErrorType = Base64.fromBytes.ErrorType | Errors.GlobalErrorType
389
+ }
390
+
391
+ /**
392
+ * Deserializes credential creation options that can be passed to
393
+ * `navigator.credentials.create()`.
394
+ *
395
+ * @example
396
+ * ```ts twoslash
397
+ * import { Registration } from 'ox/webauthn'
398
+ *
399
+ * const options = Registration.getOptions({ name: 'Example' })
400
+ * const serialized = Registration.serializeOptions(options)
401
+ *
402
+ * // ... send to server and back ...
403
+ *
404
+ * const deserialized = Registration.deserializeOptions(serialized) // [!code focus]
405
+ * const credential = await window.navigator.credentials.create(deserialized)
406
+ * ```
407
+ *
408
+ * @param options - The serialized credential creation options.
409
+ * @returns The deserialized credential creation options.
410
+ */
411
+ export function deserializeOptions(
412
+ options: Types.CredentialCreationOptions<true>,
413
+ ): Types.CredentialCreationOptions {
414
+ const publicKey = options.publicKey
415
+ if (!publicKey) return {}
416
+
417
+ const { challenge, excludeCredentials, extensions, user, ...rest } = publicKey
418
+
419
+ return {
420
+ publicKey: {
421
+ ...rest,
422
+ challenge: Bytes.fromHex(challenge),
423
+ ...(excludeCredentials && {
424
+ excludeCredentials: excludeCredentials.map(({ id, ...rest }) => ({
425
+ ...rest,
426
+ id: Base64.toBytes(id),
427
+ })),
428
+ }),
429
+ ...(extensions && {
430
+ extensions: deserializeExtensions(extensions),
431
+ }),
432
+ user: {
433
+ ...user,
434
+ id: Base64.toBytes(user.id),
435
+ },
436
+ },
437
+ }
438
+ }
439
+
440
+ export declare namespace deserializeOptions {
441
+ type ErrorType = Base64.toBytes.ErrorType | Errors.GlobalErrorType
442
+ }
443
+
444
+ /**
445
+ * Deserializes a serialized registration response.
446
+ *
447
+ * @example
448
+ * ```ts twoslash
449
+ * import { Registration } from 'ox/webauthn'
450
+ *
451
+ * const response = Registration.deserializeResponse({ // [!code focus]
452
+ * credential: { // [!code focus]
453
+ * attestationObject: 'o2NmbXRkbm9uZQ...', // [!code focus]
454
+ * clientDataJSON: 'eyJ0eXBlIjoid2Vi...', // [!code focus]
455
+ * id: 'm1-bMPuAqpWhCxHZQZTT6e-lSPntQbh3opIoGe7g4Qs', // [!code focus]
456
+ * publicKey: '0x04ab891400...', // [!code focus]
457
+ * raw: { id: '...', type: 'public-key', authenticatorAttachment: 'platform', rawId: '...', response: { clientDataJSON: 'eyJ0eXBlIjoid2Vi...' } }, // [!code focus]
458
+ * }, // [!code focus]
459
+ * counter: 0, // [!code focus]
460
+ * }) // [!code focus]
461
+ * ```
462
+ *
463
+ * @param response - The serialized registration response.
464
+ * @returns The deserialized registration response.
465
+ */
466
+ export function deserializeResponse(response: Response<true>): Response {
467
+ const { credential, ...rest } = response
468
+
469
+ const rawResponse: Record<string, ArrayBuffer> = {}
470
+ for (const [key, value] of Object.entries(credential.raw.response))
471
+ rawResponse[key] = bytesToArrayBuffer(Base64.toBytes(value))
472
+
473
+ return {
474
+ ...rest,
475
+ credential: {
476
+ attestationObject: bytesToArrayBuffer(
477
+ Base64.toBytes(credential.attestationObject),
478
+ ),
479
+ clientDataJSON: bytesToArrayBuffer(
480
+ Base64.toBytes(credential.clientDataJSON),
481
+ ),
482
+ id: credential.id,
483
+ publicKey: PublicKey.from(credential.publicKey),
484
+ raw: {
485
+ id: credential.raw.id,
486
+ type: credential.raw.type,
487
+ authenticatorAttachment: credential.raw.authenticatorAttachment,
488
+ rawId: bytesToArrayBuffer(Base64.toBytes(credential.raw.rawId)),
489
+ response: rawResponse as unknown as Types.AuthenticatorResponse,
490
+ getClientExtensionResults: () => ({}),
491
+ },
492
+ },
493
+ }
494
+ }
495
+
496
+ export declare namespace deserializeResponse {
497
+ type ErrorType =
498
+ | Base64.toBytes.ErrorType
499
+ | PublicKey.from.ErrorType
500
+ | Errors.GlobalErrorType
501
+ }
502
+
503
+ /**
504
+ * Verifies a WebAuthn registration (credential creation) response. Validates the
505
+ * `clientDataJSON`, `attestationObject`, authenticator flags, challenge, origin, and
506
+ * relying party ID, then extracts the credential ID and public key.
507
+ *
508
+ * @example
509
+ * ```ts twoslash
510
+ * import { Registration } from 'ox/webauthn'
511
+ *
512
+ * const credential = await Registration.create({ name: 'Example' })
513
+ *
514
+ * const result = Registration.verify({ // [!code focus]
515
+ * credential, // [!code focus]
516
+ * challenge: '0x69abb4b5a0de4bc62a2a201f8d25bae9', // [!code focus]
517
+ * origin: 'https://example.com', // [!code focus]
518
+ * rpId: 'example.com', // [!code focus]
519
+ * }) // [!code focus]
520
+ * // @log: {
521
+ * // @log: credential: {
522
+ * // @log: id: 'oZ48...',
523
+ * // @log: publicKey: { prefix: 4, x: 51421...5123n, y: 12345...6789n },
524
+ * // @log: },
525
+ * // @log: counter: 0,
526
+ * // @log: userVerified: true,
527
+ * // @log: }
528
+ * ```
529
+ *
530
+ * @param options - Verification options.
531
+ * @returns The verified registration result.
532
+ */
533
+ export function verify(options: verify.Options): verify.ReturnType {
534
+ const {
535
+ attestation = 'none',
536
+ credential,
537
+ origin,
538
+ rpId,
539
+ userVerification = 'required',
540
+ } = options
541
+
542
+ // 1. Decode and validate clientDataJSON
543
+ const clientDataJSONBytes = new Uint8Array(credential.clientDataJSON)
544
+ const clientDataJSON = Bytes.toString(clientDataJSONBytes)
545
+ const clientData = JSON.parse(clientDataJSON)
546
+
547
+ if (clientData.type !== 'webauthn.create')
548
+ throw new VerifyError(
549
+ `Expected clientData.type "webauthn.create", got "${clientData.type}"`,
550
+ )
551
+
552
+ // Validate challenge
553
+ const challengeResult = (() => {
554
+ if (typeof options.challenge === 'function')
555
+ return options.challenge(clientData.challenge)
556
+ const challengeBytes =
557
+ typeof options.challenge === 'string'
558
+ ? Bytes.fromHex(options.challenge)
559
+ : options.challenge
560
+ const challenge = Base64.fromBytes(challengeBytes, base64UrlOptions)
561
+ return clientData.challenge === challenge
562
+ })()
563
+ if (!challengeResult) throw new VerifyError('Challenge mismatch')
564
+
565
+ // Validate origin
566
+ const origins = Array.isArray(origin) ? origin : [origin]
567
+ if (!origins.includes(clientData.origin))
568
+ throw new VerifyError(
569
+ `Origin mismatch: expected ${JSON.stringify(origin)}, got "${clientData.origin}"`,
570
+ )
571
+
572
+ // 2. Decode attestationObject via CBOR
573
+ const attestationObjectBytes = new Uint8Array(credential.attestationObject)
574
+ const attestation_ = Cbor.decode<{
575
+ authData: Uint8Array
576
+ attStmt: Record<string, unknown>
577
+ fmt: string
578
+ }>(attestationObjectBytes)
579
+
580
+ // 3. Parse authenticatorData
581
+ const authData = attestation_.authData
582
+ const rpIdHash = authData.slice(0, 32)
583
+ const expectedRpIdHash = Hash.sha256(Hex.fromString(rpId), { as: 'Bytes' })
584
+
585
+ if (!Bytes.isEqual(rpIdHash, expectedRpIdHash))
586
+ throw new VerifyError('rpId hash mismatch')
587
+
588
+ const flags = authData[32]!
589
+ const up = (flags & 0x01) !== 0
590
+ const uv = (flags & 0x04) !== 0
591
+ const at = (flags & 0x40) !== 0
592
+ const be = (flags & 0x08) !== 0
593
+ const bs = (flags & 0x10) !== 0
594
+
595
+ if (!up) throw new VerifyError('User presence flag not set')
596
+ if (!at) throw new VerifyError('Attested credential data flag not set')
597
+ if (userVerification === 'required' && !uv)
598
+ throw new VerifyError('User verification flag not set')
599
+
600
+ // If the BE bit is not set, the BS bit must not be set.
601
+ if (!be && bs)
602
+ throw new VerifyError(
603
+ 'Backup state (BS) flag is set but backup eligibility (BE) flag is not',
604
+ )
605
+
606
+ // Minimum authData length: 37 (rpIdHash + flags + counter) + 16 (AAGUID) + 2 (credIdLen)
607
+ if (authData.length < 55)
608
+ throw new VerifyError('authData too short for attested credential data')
609
+
610
+ // Counter (4 bytes, big-endian, starting at offset 33)
611
+ const counter =
612
+ ((authData[33]! << 24) |
613
+ (authData[34]! << 16) |
614
+ (authData[35]! << 8) |
615
+ authData[36]!) >>>
616
+ 0
617
+
618
+ // Credential ID length (2 bytes at offset 53, big-endian)
619
+ const credIdLen = (authData[53]! << 8) | authData[54]!
620
+ if (55 + credIdLen > authData.length)
621
+ throw new VerifyError('credIdLen exceeds authData bounds')
622
+
623
+ // Credential ID (variable length starting at offset 55)
624
+ const credentialId = authData.slice(55, 55 + credIdLen)
625
+
626
+ // Verify credential ID consistency if caller-supplied id is present
627
+ if (credential.id !== undefined) {
628
+ const expectedId = Base64.fromBytes(credentialId, base64UrlOptions)
629
+ if (credential.id !== expectedId)
630
+ throw new VerifyError(
631
+ `Credential ID mismatch: supplied "${credential.id}" does not match authData "${expectedId}"`,
632
+ )
633
+ }
634
+
635
+ // 4. Parse and validate COSE public key
636
+ const ed = (flags & 0x80) !== 0
637
+ const coseKeyBytes = authData.slice(55 + credIdLen)
638
+ const coseKeyHex = Hex.fromBytes(coseKeyBytes)
639
+ const coseKeyData = Cbor.decode<Record<string, unknown>>(coseKeyHex)
640
+ // Validate key type is EC2 (2), algorithm is ES256 (-7), and curve is P-256 (1)
641
+ if (
642
+ coseKeyData['1'] !== 2 ||
643
+ coseKeyData['3'] !== -7 ||
644
+ coseKeyData['-1'] !== 1
645
+ )
646
+ throw new VerifyError(
647
+ 'COSE key must be EC2 (kty=2) with ES256 algorithm (alg=-7) on P-256 curve (crv=1)',
648
+ )
649
+ const publicKey = CoseKey.toPublicKey(coseKeyHex)
650
+
651
+ // Verify no unexpected trailing bytes after the COSE key.
652
+ // Re-encode the extracted public key as a COSE key to determine its expected length.
653
+ const expectedCoseKeyLen = Bytes.fromHex(
654
+ CoseKey.fromPublicKey(publicKey),
655
+ ).length
656
+ const trailingBytes = coseKeyBytes.length - expectedCoseKeyLen
657
+ if (trailingBytes > 0 && !ed)
658
+ throw new VerifyError(
659
+ `authData contains ${trailingBytes} unexpected trailing byte(s) after COSE key`,
660
+ )
661
+
662
+ // 5. Verify attestation statement (cryptographic binding of authData + clientDataJSON)
663
+ const clientDataHash = Hash.sha256(Bytes.fromString(clientDataJSON), {
664
+ as: 'Bytes',
665
+ })
666
+ const verificationData = Bytes.concat(authData, clientDataHash)
667
+
668
+ const { fmt, attStmt } = attestation_
669
+ if (fmt === 'none') {
670
+ // "none" format has no attestation signature; only accept if caller opts in
671
+ if (attestation === 'required')
672
+ throw new VerifyError(
673
+ 'Attestation format is "none" but attestation verification is required. ' +
674
+ 'Set `attestation: "none"` to accept unattested credentials.',
675
+ )
676
+ } else if (fmt === 'packed') {
677
+ // Packed attestation: verify signature over authData || clientDataHash
678
+ const sig = attStmt.sig
679
+ const alg = attStmt.alg
680
+ if (!(sig instanceof Uint8Array) || typeof alg !== 'number')
681
+ throw new VerifyError(
682
+ 'Invalid packed attestation statement: missing sig or alg',
683
+ )
684
+ if (alg !== -7)
685
+ throw new VerifyError(
686
+ `Unsupported attestation algorithm: ${alg} (expected -7 / ES256)`,
687
+ )
688
+ if (attStmt.x5c) {
689
+ // Full attestation with certificate chain is not supported
690
+ throw new VerifyError(
691
+ 'Packed attestation with x5c certificate chain is not supported. ' +
692
+ 'Use self attestation (no x5c) or set `attestation: "none"`.',
693
+ )
694
+ }
695
+ // Self attestation: verify using the credential public key
696
+ const attSignature = Signature.fromDerBytes(sig)
697
+ const verified = P256.verify({
698
+ hash: true,
699
+ payload: verificationData,
700
+ publicKey,
701
+ signature: attSignature,
702
+ })
703
+ if (!verified)
704
+ throw new VerifyError('Attestation signature verification failed')
705
+ } else {
706
+ throw new VerifyError(`Unsupported attestation format: "${fmt}"`)
707
+ }
708
+
709
+ // 6. Build credential ID string
710
+ const id = credential.id ?? Base64.fromBytes(credentialId, base64UrlOptions)
711
+
712
+ // 7. Build raw credential
713
+ const raw = credential.raw ?? {
714
+ authenticatorAttachment: null,
715
+ getClientExtensionResults: () => ({}),
716
+ id,
717
+ rawId: bytesToArrayBuffer(credentialId),
718
+ response: {
719
+ attestationObject: credential.attestationObject,
720
+ clientDataJSON: credential.clientDataJSON,
721
+ } as never,
722
+ type: 'public-key',
723
+ }
724
+
725
+ return {
726
+ credential: {
727
+ attestationObject: credential.attestationObject,
728
+ clientDataJSON: credential.clientDataJSON,
729
+ id,
730
+ publicKey,
731
+ raw,
732
+ },
733
+ counter,
734
+ ...(uv ? { userVerified: true as const } : {}),
735
+ ...(be ? { backedUp: bs } : {}),
736
+ ...(be
737
+ ? {
738
+ deviceType: bs ? ('multiDevice' as const) : ('singleDevice' as const),
739
+ }
740
+ : {}),
741
+ }
742
+ }
743
+
744
+ export declare namespace verify {
745
+ type Options = {
746
+ /**
747
+ * Attestation verification mode.
748
+ * - `'required'` (default): attestation signature must be present and valid (`packed` self-attestation).
749
+ * - `'none'`: accept `fmt: "none"` attestation (no cryptographic binding of authData to clientDataJSON).
750
+ *
751
+ * @default 'required'
752
+ */
753
+ attestation?: 'required' | 'none' | undefined
754
+ /** The credential response from `Registration.create()`. */
755
+ credential: {
756
+ attestationObject: Credential_.Credential['attestationObject']
757
+ clientDataJSON: Credential_.Credential['clientDataJSON']
758
+ id?: Credential_.Credential['id'] | undefined
759
+ raw?: Credential_.Credential['raw'] | undefined
760
+ }
761
+ /**
762
+ * Challenge to verify. Either the raw hex/bytes originally generated, or a
763
+ * function that receives the base64url challenge string and returns whether
764
+ * it is valid (for async/DB lookups).
765
+ */
766
+ challenge: Hex.Hex | Uint8Array | ((challenge: string) => boolean)
767
+ /** Expected origin(s) (e.g. `"https://example.com"`). */
768
+ origin: string | string[]
769
+ /** Relying party ID (e.g. `"example.com"`). */
770
+ rpId: string
771
+ /** The user verification requirement. @default 'required' */
772
+ userVerification?: Types.UserVerificationRequirement | undefined
773
+ }
774
+
775
+ type ReturnType = Response
776
+
777
+ type ErrorType =
778
+ | Base64.toBytes.ErrorType
779
+ | Base64.fromBytes.ErrorType
780
+ | Bytes.fromHex.ErrorType
781
+ | Bytes.isEqual.ErrorType
782
+ | Cbor.decode.ErrorType
783
+ | CoseKey.toPublicKey.ErrorType
784
+ | Hash.sha256.ErrorType
785
+ | P256.verify.ErrorType
786
+ | Signature.fromDerBytes.ErrorType
787
+ | VerifyError
788
+ | Errors.GlobalErrorType
789
+ }
790
+
791
+ /** Thrown when WebAuthn registration verification fails. */
792
+ export class VerifyError extends Errors.BaseError {
793
+ override readonly name = 'Registration.VerifyError'
794
+ }
795
+
796
+ /** Thrown when a WebAuthn P256 credential creation fails. */
797
+ export class CreateFailedError extends Errors.BaseError<Error> {
798
+ override readonly name = 'Registration.CreateFailedError'
799
+
800
+ constructor({ cause }: { cause?: Error | undefined } = {}) {
801
+ super('Failed to create credential.', {
802
+ cause,
803
+ })
804
+ }
805
+ }