ox 0.12.3 → 0.13.0

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 (112) hide show
  1. package/CHANGELOG.md +17 -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/TxEnvelopeTempo.js +19 -1
  13. package/_cjs/tempo/TxEnvelopeTempo.js.map +1 -1
  14. package/_cjs/version.js +1 -1
  15. package/_cjs/webauthn/Authentication.js +246 -0
  16. package/_cjs/webauthn/Authentication.js.map +1 -0
  17. package/_cjs/webauthn/Authenticator.js +55 -0
  18. package/_cjs/webauthn/Authenticator.js.map +1 -0
  19. package/_cjs/webauthn/Credential.js +53 -0
  20. package/_cjs/webauthn/Credential.js.map +1 -0
  21. package/_cjs/webauthn/Registration.js +349 -0
  22. package/_cjs/webauthn/Registration.js.map +1 -0
  23. package/_cjs/webauthn/Types.js +3 -0
  24. package/_cjs/webauthn/Types.js.map +1 -0
  25. package/_cjs/webauthn/index.js +9 -0
  26. package/_cjs/webauthn/index.js.map +1 -0
  27. package/_cjs/webauthn/internal/utils.js +53 -0
  28. package/_cjs/webauthn/internal/utils.js.map +1 -0
  29. package/_esm/core/P256.js +1 -1
  30. package/_esm/core/P256.js.map +1 -1
  31. package/_esm/core/WebAuthnP256.js +13 -261
  32. package/_esm/core/WebAuthnP256.js.map +1 -1
  33. package/_esm/core/WebCryptoP256.js +4 -1
  34. package/_esm/core/WebCryptoP256.js.map +1 -1
  35. package/_esm/core/internal/webauthn.js +5 -13
  36. package/_esm/core/internal/webauthn.js.map +1 -1
  37. package/_esm/erc8021/index.js +2 -2
  38. package/_esm/index.docs.js +1 -0
  39. package/_esm/index.docs.js.map +1 -1
  40. package/_esm/tempo/TransactionReceipt.js +1 -1
  41. package/_esm/tempo/TransactionRequest.js +1 -1
  42. package/_esm/tempo/TxEnvelopeTempo.js +20 -1
  43. package/_esm/tempo/TxEnvelopeTempo.js.map +1 -1
  44. package/_esm/version.js +1 -1
  45. package/_esm/webauthn/Authentication.js +453 -0
  46. package/_esm/webauthn/Authentication.js.map +1 -0
  47. package/_esm/webauthn/Authenticator.js +176 -0
  48. package/_esm/webauthn/Authenticator.js.map +1 -0
  49. package/_esm/webauthn/Credential.js +95 -0
  50. package/_esm/webauthn/Credential.js.map +1 -0
  51. package/_esm/webauthn/Registration.js +512 -0
  52. package/_esm/webauthn/Registration.js.map +1 -0
  53. package/_esm/webauthn/Types.js +2 -0
  54. package/_esm/webauthn/Types.js.map +1 -0
  55. package/_esm/webauthn/index.js +31 -0
  56. package/_esm/webauthn/index.js.map +1 -0
  57. package/_esm/webauthn/internal/utils.js +52 -0
  58. package/_esm/webauthn/internal/utils.js.map +1 -0
  59. package/_types/core/WebAuthnP256.d.ts +33 -208
  60. package/_types/core/WebAuthnP256.d.ts.map +1 -1
  61. package/_types/core/WebCryptoP256.d.ts +2 -0
  62. package/_types/core/WebCryptoP256.d.ts.map +1 -1
  63. package/_types/core/internal/webauthn.d.ts +2 -110
  64. package/_types/core/internal/webauthn.d.ts.map +1 -1
  65. package/_types/erc8021/index.d.ts +2 -2
  66. package/_types/index.docs.d.ts +1 -0
  67. package/_types/index.docs.d.ts.map +1 -1
  68. package/_types/tempo/Transaction.d.ts +2 -2
  69. package/_types/tempo/TransactionReceipt.d.ts +2 -2
  70. package/_types/tempo/TransactionRequest.d.ts +2 -2
  71. package/_types/tempo/TxEnvelopeTempo.d.ts.map +1 -1
  72. package/_types/version.d.ts +1 -1
  73. package/_types/webauthn/Authentication.d.ts +324 -0
  74. package/_types/webauthn/Authentication.d.ts.map +1 -0
  75. package/_types/webauthn/Authenticator.d.ts +182 -0
  76. package/_types/webauthn/Authenticator.d.ts.map +1 -0
  77. package/_types/webauthn/Credential.d.ts +77 -0
  78. package/_types/webauthn/Credential.d.ts.map +1 -0
  79. package/_types/webauthn/Registration.d.ts +308 -0
  80. package/_types/webauthn/Registration.d.ts.map +1 -0
  81. package/_types/webauthn/Types.d.ts +106 -0
  82. package/_types/webauthn/Types.d.ts.map +1 -0
  83. package/_types/webauthn/index.d.ts +33 -0
  84. package/_types/webauthn/index.d.ts.map +1 -0
  85. package/_types/webauthn/internal/utils.d.ts +17 -0
  86. package/_types/webauthn/internal/utils.d.ts.map +1 -0
  87. package/core/P256.ts +1 -1
  88. package/core/WebAuthnP256.ts +37 -582
  89. package/core/WebCryptoP256.ts +6 -1
  90. package/core/internal/webauthn.ts +6 -165
  91. package/erc8021/index.ts +2 -2
  92. package/index.docs.ts +1 -0
  93. package/package.json +31 -1
  94. package/tempo/Transaction.ts +2 -2
  95. package/tempo/TransactionReceipt.ts +2 -2
  96. package/tempo/TransactionRequest.ts +2 -2
  97. package/tempo/TxEnvelopeTempo.test.ts +6 -0
  98. package/tempo/TxEnvelopeTempo.ts +22 -2
  99. package/version.ts +1 -1
  100. package/webauthn/Authentication/package.json +6 -0
  101. package/webauthn/Authentication.ts +673 -0
  102. package/webauthn/Authenticator/package.json +6 -0
  103. package/webauthn/Authenticator.ts +259 -0
  104. package/webauthn/Credential/package.json +6 -0
  105. package/webauthn/Credential.ts +146 -0
  106. package/webauthn/Registration/package.json +6 -0
  107. package/webauthn/Registration.ts +805 -0
  108. package/webauthn/Types/package.json +6 -0
  109. package/webauthn/Types.ts +158 -0
  110. package/webauthn/index.ts +38 -0
  111. package/webauthn/internal/utils.ts +63 -0
  112. package/webauthn/package.json +6 -0
@@ -0,0 +1,673 @@
1
+ import * as Base64 from '../core/Base64.js'
2
+ import * as Bytes from '../core/Bytes.js'
3
+ import * as Errors from '../core/Errors.js'
4
+ import * as Hash from '../core/Hash.js'
5
+ import * as Hex from '../core/Hex.js'
6
+ import type { OneOf } from '../core/internal/types.js'
7
+ import * as internal from '../core/internal/webauthn.js'
8
+ import * as P256 from '../core/P256.js'
9
+ import type * as PublicKey from '../core/PublicKey.js'
10
+ import * as Signature from '../core/Signature.js'
11
+ import { getAuthenticatorData, getClientDataJSON } from './Authenticator.js'
12
+ import type * as Credential_ from './Credential.js'
13
+ import {
14
+ base64UrlOptions,
15
+ bufferSourceToBytes,
16
+ bytesToArrayBuffer,
17
+ deserializeExtensions,
18
+ responseKeys,
19
+ serializeExtensions,
20
+ } from './internal/utils.js'
21
+ import type * as Types from './Types.js'
22
+
23
+ /** Response from a WebAuthn authentication ceremony. */
24
+ export type Response<serialized extends boolean = false> = {
25
+ id: string
26
+ metadata: Credential_.SignMetadata
27
+ raw: Types.PublicKeyCredential<serialized>
28
+ signature: serialized extends true ? Hex.Hex : Signature.Signature<false>
29
+ }
30
+
31
+ /**
32
+ * Deserializes credential request options that can be passed to
33
+ * `navigator.credentials.get()`.
34
+ *
35
+ * @example
36
+ * ```ts twoslash
37
+ * import { Authentication } from 'ox/webauthn'
38
+ *
39
+ * const options = Authentication.getOptions({
40
+ * challenge: '0xdeadbeef',
41
+ * })
42
+ * const serialized = Authentication.serializeOptions(options)
43
+ *
44
+ * // ... send to server and back ...
45
+ *
46
+ * const deserialized = Authentication.deserializeOptions(serialized) // [!code focus]
47
+ * const credential = await window.navigator.credentials.get(deserialized)
48
+ * ```
49
+ *
50
+ * @param options - The serialized credential request options.
51
+ * @returns The deserialized credential request options.
52
+ */
53
+ export function deserializeOptions(
54
+ options: Types.CredentialRequestOptions<true>,
55
+ ): Types.CredentialRequestOptions {
56
+ const { publicKey, ...rest } = options
57
+ if (!publicKey) return { ...rest }
58
+
59
+ const { allowCredentials, challenge, extensions, ...publicKeyRest } =
60
+ publicKey
61
+
62
+ return {
63
+ ...rest,
64
+ publicKey: {
65
+ ...publicKeyRest,
66
+ challenge: Bytes.fromHex(challenge),
67
+ ...(allowCredentials && {
68
+ allowCredentials: allowCredentials.map(({ id, ...rest }) => ({
69
+ ...rest,
70
+ id: Base64.toBytes(id),
71
+ })),
72
+ }),
73
+ ...(extensions && {
74
+ extensions: deserializeExtensions(extensions),
75
+ }),
76
+ },
77
+ }
78
+ }
79
+
80
+ export declare namespace deserializeOptions {
81
+ type ErrorType = Base64.toBytes.ErrorType | Errors.GlobalErrorType
82
+ }
83
+
84
+ /**
85
+ * Deserializes a serialized authentication response.
86
+ *
87
+ * @example
88
+ * ```ts twoslash
89
+ * import { Authentication } from 'ox/webauthn'
90
+ *
91
+ * const response = Authentication.deserializeResponse({ // [!code focus]
92
+ * id: 'm1-bMPuAqpWhCxHZQZTT6e-lSPntQbh3opIoGe7g4Qs', // [!code focus]
93
+ * metadata: { // [!code focus]
94
+ * authenticatorData: '0x49960de5...', // [!code focus]
95
+ * clientDataJSON: '{"type":"webauthn.get",...}', // [!code focus]
96
+ * challengeIndex: 23, // [!code focus]
97
+ * typeIndex: 1, // [!code focus]
98
+ * userVerificationRequired: true, // [!code focus]
99
+ * }, // [!code focus]
100
+ * raw: { // [!code focus]
101
+ * id: 'm1-bMPuAqpWhCxHZQZTT6e-lSPntQbh3opIoGe7g4Qs', // [!code focus]
102
+ * type: 'public-key', // [!code focus]
103
+ * authenticatorAttachment: 'platform', // [!code focus]
104
+ * rawId: 'm1-bMPuAqpWhCxHZQZTT6e-lSPntQbh3opIoGe7g4Qs', // [!code focus]
105
+ * response: { clientDataJSON: 'eyJ0eXBlIjoid2ViYXV0aG4uZ2V0In0' }, // [!code focus]
106
+ * }, // [!code focus]
107
+ * signature: '0x...', // [!code focus]
108
+ * }) // [!code focus]
109
+ * ```
110
+ *
111
+ * @param response - The serialized authentication response.
112
+ * @returns The deserialized authentication response.
113
+ */
114
+ export function deserializeResponse(response: Response<true>): Response {
115
+ const { id, metadata, raw, signature } = response
116
+
117
+ const rawResponse: Record<string, ArrayBuffer> = {}
118
+ for (const [key, value] of Object.entries(raw.response))
119
+ rawResponse[key] = bytesToArrayBuffer(Base64.toBytes(value))
120
+
121
+ return {
122
+ id,
123
+ metadata,
124
+ raw: {
125
+ id: raw.id,
126
+ type: raw.type,
127
+ authenticatorAttachment: raw.authenticatorAttachment,
128
+ rawId: bytesToArrayBuffer(Base64.toBytes(raw.rawId)),
129
+ response: rawResponse as unknown as Types.AuthenticatorResponse,
130
+ getClientExtensionResults: () => ({}),
131
+ },
132
+ signature: Signature.from(signature),
133
+ }
134
+ }
135
+
136
+ export declare namespace deserializeResponse {
137
+ type ErrorType =
138
+ | Base64.toBytes.ErrorType
139
+ | Signature.from.ErrorType
140
+ | Errors.GlobalErrorType
141
+ }
142
+
143
+ /**
144
+ * Returns the request options to sign a challenge with the Web Authentication API.
145
+ *
146
+ * @example
147
+ * ```ts twoslash
148
+ * import { Authentication } from 'ox/webauthn'
149
+ *
150
+ * const options = Authentication.getOptions({
151
+ * challenge: '0xdeadbeef',
152
+ * })
153
+ *
154
+ * const credential = await window.navigator.credentials.get(options)
155
+ * ```
156
+ *
157
+ * @param options - Options.
158
+ * @returns The credential request options.
159
+ */
160
+ export function getOptions(
161
+ options: getOptions.Options,
162
+ ): Types.CredentialRequestOptions {
163
+ const {
164
+ credentialId,
165
+ challenge,
166
+ extensions,
167
+ rpId = window.location.hostname,
168
+ userVerification = 'required',
169
+ } = options
170
+ return {
171
+ publicKey: {
172
+ ...(credentialId
173
+ ? {
174
+ allowCredentials: Array.isArray(credentialId)
175
+ ? credentialId.map((id) => ({
176
+ id: Base64.toBytes(id),
177
+ type: 'public-key',
178
+ }))
179
+ : [
180
+ {
181
+ id: Base64.toBytes(credentialId),
182
+ type: 'public-key',
183
+ },
184
+ ],
185
+ }
186
+ : {}),
187
+ challenge: Bytes.fromHex(challenge),
188
+ ...(extensions && { extensions }),
189
+ rpId,
190
+ userVerification,
191
+ },
192
+ }
193
+ }
194
+
195
+ export declare namespace getOptions {
196
+ type Options = {
197
+ /** The credential ID to use. */
198
+ credentialId?: string | string[] | undefined
199
+ /** The challenge to sign. */
200
+ challenge: Hex.Hex
201
+ /** List of Web Authentication API credentials to use during creation or authentication. */
202
+ extensions?:
203
+ | Types.PublicKeyCredentialRequestOptions['extensions']
204
+ | undefined
205
+ /** The relying party identifier to use. */
206
+ rpId?: Types.PublicKeyCredentialRequestOptions['rpId'] | undefined
207
+ /** The user verification requirement. */
208
+ userVerification?:
209
+ | Types.PublicKeyCredentialRequestOptions['userVerification']
210
+ | undefined
211
+ }
212
+
213
+ type ErrorType =
214
+ | Bytes.fromHex.ErrorType
215
+ | Base64.toBytes.ErrorType
216
+ | Errors.GlobalErrorType
217
+ }
218
+
219
+ /**
220
+ * Constructs the final digest that was signed and computed by the authenticator. This payload includes
221
+ * the cryptographic `challenge`, as well as authenticator metadata (`authenticatorData` + `clientDataJSON`).
222
+ * This value can be also used with raw P256 verification (such as `P256.verify` or
223
+ * `WebCryptoP256.verify`).
224
+ *
225
+ * :::warning
226
+ *
227
+ * This function is mainly for testing purposes or for manually constructing
228
+ * signing payloads. In most cases you will not need this function and
229
+ * instead use `Authentication.sign`.
230
+ *
231
+ * :::
232
+ *
233
+ * @example
234
+ * ```ts twoslash
235
+ * import { Authentication } from 'ox/webauthn'
236
+ * import { WebCryptoP256 } from 'ox'
237
+ *
238
+ * const { metadata, payload } = Authentication.getSignPayload({ // [!code focus]
239
+ * challenge: '0xdeadbeef', // [!code focus]
240
+ * }) // [!code focus]
241
+ *
242
+ * const { publicKey, privateKey } = await WebCryptoP256.createKeyPair()
243
+ *
244
+ * const signature = await WebCryptoP256.sign({
245
+ * payload,
246
+ * privateKey,
247
+ * })
248
+ * ```
249
+ *
250
+ * @param options - Options to construct the signing payload.
251
+ * @returns The signing payload.
252
+ */
253
+ export function getSignPayload(
254
+ options: getSignPayload.Options,
255
+ ): getSignPayload.ReturnType {
256
+ const {
257
+ challenge,
258
+ crossOrigin,
259
+ extraClientData,
260
+ flag,
261
+ origin,
262
+ rpId,
263
+ signCount,
264
+ userVerification = 'required',
265
+ } = options
266
+
267
+ const authenticatorData = getAuthenticatorData({
268
+ flag,
269
+ rpId,
270
+ signCount,
271
+ })
272
+ const clientDataJSON = getClientDataJSON({
273
+ challenge,
274
+ crossOrigin,
275
+ extraClientData,
276
+ origin,
277
+ })
278
+ const clientDataJSONHash = Hash.sha256(Hex.fromString(clientDataJSON))
279
+
280
+ const challengeIndex = clientDataJSON.indexOf('"challenge"')
281
+ const typeIndex = clientDataJSON.indexOf('"type"')
282
+
283
+ const metadata = {
284
+ authenticatorData,
285
+ clientDataJSON,
286
+ challengeIndex,
287
+ typeIndex,
288
+ userVerificationRequired: userVerification === 'required',
289
+ }
290
+
291
+ const payload = Hex.concat(authenticatorData, clientDataJSONHash)
292
+
293
+ return { metadata, payload }
294
+ }
295
+
296
+ export declare namespace getSignPayload {
297
+ type Options = {
298
+ /** The challenge to sign. */
299
+ challenge: Hex.Hex
300
+ /** If set to `true`, it means that the calling context is an `<iframe>` that is not same origin with its ancestor frames. */
301
+ crossOrigin?: boolean | undefined
302
+ /** Additional client data to include in the client data JSON. */
303
+ extraClientData?: Record<string, unknown> | undefined
304
+ /** If set to `true`, the payload will be hashed before being returned. */
305
+ hash?: boolean | undefined
306
+ /** A bitfield that indicates various attributes that were asserted by the authenticator. [Read more](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API/Authenticator_data#flags) */
307
+ flag?: number | undefined
308
+ /** The fully qualified origin of the relying party which has been given by the client/browser to the authenticator. */
309
+ origin?: string | undefined
310
+ /** The [Relying Party ID](https://w3c.github.io/webauthn/#relying-party-identifier) that the credential is scoped to. */
311
+ rpId?: Types.PublicKeyCredentialRequestOptions['rpId'] | undefined
312
+ /** A signature counter, if supported by the authenticator (set to 0 otherwise). */
313
+ signCount?: number | undefined
314
+ /** The user verification requirement that the authenticator will enforce. */
315
+ userVerification?:
316
+ | Types.PublicKeyCredentialRequestOptions['userVerification']
317
+ | undefined
318
+ }
319
+
320
+ type ReturnType = {
321
+ metadata: Credential_.SignMetadata
322
+ payload: Hex.Hex
323
+ }
324
+
325
+ type ErrorType =
326
+ | Hash.sha256.ErrorType
327
+ | Hex.concat.ErrorType
328
+ | Hex.fromString.ErrorType
329
+ | Errors.GlobalErrorType
330
+ }
331
+
332
+ /**
333
+ * Serializes credential request options into a JSON-serializable
334
+ * format, converting `BufferSource` fields to base64url strings.
335
+ *
336
+ * @example
337
+ * ```ts twoslash
338
+ * import { Authentication } from 'ox/webauthn'
339
+ *
340
+ * const options = Authentication.getOptions({
341
+ * challenge: '0xdeadbeef',
342
+ * })
343
+ *
344
+ * const serialized = Authentication.serializeOptions(options) // [!code focus]
345
+ *
346
+ * // `serialized` is JSON-serializable — send it to a server, store it, etc.
347
+ * const json = JSON.stringify(serialized)
348
+ * ```
349
+ *
350
+ * @param options - The credential request options to serialize.
351
+ * @returns The serialized credential request options.
352
+ */
353
+ export function serializeOptions(
354
+ options: Types.CredentialRequestOptions,
355
+ ): Types.CredentialRequestOptions<true> {
356
+ const { publicKey, signal: _, ...rest } = options
357
+ if (!publicKey) return { ...rest }
358
+
359
+ const { allowCredentials, challenge, extensions, ...publicKeyRest } =
360
+ publicKey
361
+
362
+ return {
363
+ ...rest,
364
+ publicKey: {
365
+ ...publicKeyRest,
366
+ challenge: Hex.fromBytes(bufferSourceToBytes(challenge)),
367
+ ...(allowCredentials && {
368
+ allowCredentials: allowCredentials.map(({ id, ...rest }) => ({
369
+ ...rest,
370
+ id: Base64.fromBytes(bufferSourceToBytes(id), base64UrlOptions),
371
+ })),
372
+ }),
373
+ ...(extensions && {
374
+ extensions: serializeExtensions(extensions),
375
+ }),
376
+ },
377
+ }
378
+ }
379
+
380
+ export declare namespace serializeOptions {
381
+ type ErrorType = Base64.fromBytes.ErrorType | Errors.GlobalErrorType
382
+ }
383
+
384
+ /**
385
+ * Serializes an authentication response into a JSON-serializable
386
+ * format, converting `BufferSource` fields to base64url strings
387
+ * and the signature to a hex string.
388
+ *
389
+ * @example
390
+ * ```ts twoslash
391
+ * import { Authentication } from 'ox/webauthn'
392
+ *
393
+ * const response = await Authentication.sign({
394
+ * challenge: '0xdeadbeef',
395
+ * })
396
+ *
397
+ * const serialized = Authentication.serializeResponse(response) // [!code focus]
398
+ *
399
+ * // `serialized` is JSON-serializable — send it to a server, store it, etc.
400
+ * const json = JSON.stringify(serialized)
401
+ * ```
402
+ *
403
+ * @param response - The authentication response to serialize.
404
+ * @returns The serialized authentication response.
405
+ */
406
+ export function serializeResponse(response: Response): Response<true> {
407
+ const { id, metadata, raw, signature } = response
408
+
409
+ const rawResponse = {} as Record<string, string>
410
+ for (const key of responseKeys) {
411
+ const value = (raw.response as unknown as Record<string, unknown>)[key]
412
+ if (value instanceof ArrayBuffer)
413
+ rawResponse[key] = Base64.fromBytes(
414
+ new Uint8Array(value),
415
+ base64UrlOptions,
416
+ )
417
+ }
418
+
419
+ return {
420
+ id,
421
+ metadata,
422
+ raw: {
423
+ id: raw.id,
424
+ type: raw.type,
425
+ authenticatorAttachment: raw.authenticatorAttachment,
426
+ rawId: Base64.fromBytes(bufferSourceToBytes(raw.rawId), base64UrlOptions),
427
+ response: rawResponse as unknown as Types.AuthenticatorResponse<true>,
428
+ },
429
+ signature: Signature.toHex(signature),
430
+ }
431
+ }
432
+
433
+ export declare namespace serializeResponse {
434
+ type ErrorType =
435
+ | Base64.fromBytes.ErrorType
436
+ | Signature.toHex.ErrorType
437
+ | Errors.GlobalErrorType
438
+ }
439
+
440
+ /**
441
+ * Signs a challenge using a stored WebAuthn P256 Credential. If no Credential is provided,
442
+ * a prompt will be displayed for the user to select an existing Credential
443
+ * that was previously registered.
444
+ *
445
+ * @example
446
+ * ```ts twoslash
447
+ * import { Registration, Authentication } from 'ox/webauthn'
448
+ *
449
+ * const credential = await Registration.create({
450
+ * name: 'Example',
451
+ * })
452
+ *
453
+ * const { metadata, signature } = await Authentication.sign({ // [!code focus]
454
+ * credentialId: credential.id, // [!code focus]
455
+ * challenge: '0xdeadbeef', // [!code focus]
456
+ * }) // [!code focus]
457
+ * // @log: {
458
+ * // @log: metadata: {
459
+ * // @log: authenticatorData: '0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000',
460
+ * // @log: clientDataJSON: '{"type":"webauthn.get","challenge":"9jEFijuhEWrM4SOW-tChJbUEHEP44VcjcJ-Bqo1fTM8","origin":"http://localhost:5173","crossOrigin":false}',
461
+ * // @log: challengeIndex: 23,
462
+ * // @log: typeIndex: 1,
463
+ * // @log: userVerificationRequired: true,
464
+ * // @log: },
465
+ * // @log: signature: { r: 51231...4215n, s: 12345...6789n },
466
+ * // @log: }
467
+ * ```
468
+ *
469
+ * @param options - Options.
470
+ * @returns The signature.
471
+ */
472
+ export async function sign(options: sign.Options): Promise<sign.ReturnType> {
473
+ const {
474
+ getFn = window.navigator.credentials.get.bind(window.navigator.credentials),
475
+ ...rest
476
+ } = options
477
+ const requestOptions =
478
+ 'publicKey' in rest
479
+ ? (rest as Types.CredentialRequestOptions)
480
+ : getOptions(rest as never)
481
+ try {
482
+ const credential = (await getFn(
483
+ requestOptions as never,
484
+ )) as Types.PublicKeyCredential
485
+ if (!credential) throw new SignFailedError()
486
+ const response = credential.response as AuthenticatorAssertionResponse
487
+
488
+ const clientDataJSON = String.fromCharCode(
489
+ ...new Uint8Array(response.clientDataJSON),
490
+ )
491
+ const challengeIndex = clientDataJSON.indexOf('"challenge"')
492
+ const typeIndex = clientDataJSON.indexOf('"type"')
493
+
494
+ const signature = internal.parseAsn1Signature(
495
+ new Uint8Array(response.signature),
496
+ )
497
+
498
+ return {
499
+ id: credential.id,
500
+ metadata: {
501
+ authenticatorData: Hex.fromBytes(
502
+ new Uint8Array(response.authenticatorData),
503
+ ),
504
+ clientDataJSON,
505
+ challengeIndex,
506
+ typeIndex,
507
+ userVerificationRequired:
508
+ requestOptions.publicKey!.userVerification === 'required',
509
+ },
510
+ signature,
511
+ raw: credential,
512
+ }
513
+ } catch (error) {
514
+ throw new SignFailedError({
515
+ cause: error as Error,
516
+ })
517
+ }
518
+ }
519
+
520
+ export declare namespace sign {
521
+ type Options = OneOf<
522
+ | (getOptions.Options & {
523
+ /**
524
+ * Credential request function. Useful for environments that do not support
525
+ * the WebAuthn API natively (i.e. React Native or testing environments).
526
+ *
527
+ * @default window.navigator.credentials.get
528
+ */
529
+ getFn?:
530
+ | ((
531
+ options?: Types.CredentialRequestOptions | undefined,
532
+ ) => Promise<Types.Credential | null>)
533
+ | undefined
534
+ })
535
+ | Types.CredentialRequestOptions
536
+ >
537
+
538
+ type ReturnType = Response
539
+
540
+ type ErrorType =
541
+ | Hex.fromBytes.ErrorType
542
+ | getOptions.ErrorType
543
+ | Errors.GlobalErrorType
544
+ }
545
+
546
+ /** Thrown when a WebAuthn P256 credential request fails. */
547
+ export class SignFailedError extends Errors.BaseError<Error> {
548
+ override readonly name = 'Authentication.SignFailedError'
549
+
550
+ constructor({ cause }: { cause?: Error | undefined } = {}) {
551
+ super('Failed to request credential.', {
552
+ cause,
553
+ })
554
+ }
555
+ }
556
+
557
+ /**
558
+ * Verifies a signature using the Credential's public key and the challenge which was signed.
559
+ *
560
+ * @example
561
+ * ```ts twoslash
562
+ * import { Registration, Authentication } from 'ox/webauthn'
563
+ *
564
+ * const credential = await Registration.create({
565
+ * name: 'Example',
566
+ * })
567
+ *
568
+ * const { metadata, signature } = await Authentication.sign({
569
+ * credentialId: credential.id,
570
+ * challenge: '0xdeadbeef',
571
+ * })
572
+ *
573
+ * const result = Authentication.verify({ // [!code focus]
574
+ * metadata, // [!code focus]
575
+ * challenge: '0xdeadbeef', // [!code focus]
576
+ * publicKey: credential.publicKey, // [!code focus]
577
+ * signature, // [!code focus]
578
+ * }) // [!code focus]
579
+ * // @log: true
580
+ * ```
581
+ *
582
+ * @param options - Options.
583
+ * @returns Whether the signature is valid.
584
+ */
585
+ export function verify(options: verify.Options): boolean {
586
+ const { challenge, metadata, origin, publicKey, rpId, signature } = options
587
+ const { authenticatorData, clientDataJSON, userVerificationRequired } =
588
+ metadata
589
+
590
+ const authenticatorDataBytes = Bytes.fromHex(authenticatorData)
591
+
592
+ // Check length of `authenticatorData`.
593
+ if (authenticatorDataBytes.length < 37) return false
594
+
595
+ // If rpId is provided, validate the rpIdHash in authenticatorData.
596
+ if (rpId !== undefined) {
597
+ const rpIdHash = authenticatorDataBytes.slice(0, 32)
598
+ const expectedRpIdHash = Hash.sha256(Hex.fromString(rpId), { as: 'Bytes' })
599
+ if (!Bytes.isEqual(rpIdHash, expectedRpIdHash)) return false
600
+ }
601
+
602
+ const flag = authenticatorDataBytes[32]!
603
+
604
+ // Verify that the UP bit of the flags in authData is set.
605
+ if ((flag & 0x01) !== 0x01) return false
606
+
607
+ // If user verification was determined to be required, verify that
608
+ // the UV bit of the flags in authData is set. Otherwise, ignore the
609
+ // value of the UV flag.
610
+ if (userVerificationRequired && (flag & 0x04) !== 0x04) return false
611
+
612
+ // If the BE bit of the flags in authData is not set, verify that
613
+ // the BS bit is not set.
614
+ if ((flag & 0x08) !== 0x08 && (flag & 0x10) === 0x10) return false
615
+
616
+ // Parse clientDataJSON for validation.
617
+ const clientData = JSON.parse(clientDataJSON)
618
+
619
+ // Verify that response is for an authentication assertion.
620
+ if (clientData.type !== 'webauthn.get') return false
621
+
622
+ // Validate the challenge in the clientDataJSON.
623
+ if (
624
+ !clientData.challenge ||
625
+ Hex.fromBytes(Base64.toBytes(clientData.challenge)) !== challenge
626
+ )
627
+ return false
628
+
629
+ // If origin is provided, validate origin.
630
+ if (origin !== undefined) {
631
+ const origins = Array.isArray(origin) ? origin : [origin]
632
+ if (!origins.includes(clientData.origin)) return false
633
+ }
634
+
635
+ const clientDataJSONHash = Hash.sha256(Bytes.fromString(clientDataJSON), {
636
+ as: 'Bytes',
637
+ })
638
+ const payload = Bytes.concat(authenticatorDataBytes, clientDataJSONHash)
639
+
640
+ return P256.verify({
641
+ hash: true,
642
+ payload,
643
+ publicKey,
644
+ signature,
645
+ })
646
+ }
647
+
648
+ export declare namespace verify {
649
+ type Options = {
650
+ /** The challenge to verify. */
651
+ challenge: Hex.Hex
652
+ /** The public key to verify the signature with. */
653
+ publicKey: PublicKey.PublicKey
654
+ /** The signature to verify. */
655
+ signature: Signature.Signature<false>
656
+ /** The metadata to verify the signature with. */
657
+ metadata: Credential_.SignMetadata
658
+ /** Expected origin(s). If provided, the `clientDataJSON` origin will be validated. */
659
+ origin?: string | string[] | undefined
660
+ /** Expected relying party ID. If provided, the `rpIdHash` in `authenticatorData` will be validated. */
661
+ rpId?: string | undefined
662
+ }
663
+
664
+ type ErrorType =
665
+ | Base64.toBytes.ErrorType
666
+ | Bytes.concat.ErrorType
667
+ | Bytes.fromHex.ErrorType
668
+ | Bytes.isEqual.ErrorType
669
+ | Hash.sha256.ErrorType
670
+ | Hex.fromString.ErrorType
671
+ | P256.verify.ErrorType
672
+ | Errors.GlobalErrorType
673
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "type": "module",
3
+ "types": "../../_types/webauthn/Authenticator.d.ts",
4
+ "main": "../../_cjs/webauthn/Authenticator.js",
5
+ "module": "../../_esm/webauthn/Authenticator.js"
6
+ }