ox
Version:
Ethereum Standard Library
1,786 lines (1,705 loc) • 51.5 kB
text/typescript
import { P256, PublicKey, Signature, WebAuthnP256, WebCryptoP256 } from 'ox'
import { afterAll, beforeAll, describe, expect, test, vi } from 'vp/test'
beforeAll(() => {
vi.stubGlobal('window', {
location: {
hostname: 'https://example.com',
},
document: {
title: 'My Website',
},
})
})
afterAll(() => {
vi.restoreAllMocks()
})
describe('createCredential', () => {
test('default', async () => {
let options: CredentialCreationOptions | undefined
const credential = await WebAuthnP256.createCredential({
createFn(options_) {
options = options_
return Promise.resolve({
id: 'm1-bMPuAqpWhCxHZQZTT6e-lSPntQbh3opIoGe7g4Qs',
response: {
getPublicKey() {
return [
48, 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134,
72, 206, 61, 3, 1, 7, 3, 66, 0, 4, 171, 137, 20, 0, 20, 15, 196,
248, 233, 65, 206, 15, 249, 14, 65, 157, 233, 71, 10, 202, 202,
97, 59, 189, 113, 122, 71, 117, 67, 80, 49, 167, 216, 132, 49,
142, 145, 159, 211, 179, 229, 166, 49, 216, 102, 216, 163, 128,
180, 64, 99, 231, 15, 12, 56, 30, 225, 110, 6, 82, 247, 249,
117, 84,
]
},
},
} as any)
},
name: 'Foo',
})
expect(credential).toMatchInlineSnapshot(`
{
"attestationObject": undefined,
"clientDataJSON": undefined,
"id": "m1-bMPuAqpWhCxHZQZTT6e-lSPntQbh3opIoGe7g4Qs",
"publicKey": {
"prefix": 4,
"x": "0xab891400140fc4f8e941ce0ff90e419de9470acaca613bbd717a4775435031a7",
"y": "0xd884318e919fd3b3e5a631d866d8a380b44063e70f0c381ee16e0652f7f97554",
},
"raw": {
"id": "m1-bMPuAqpWhCxHZQZTT6e-lSPntQbh3opIoGe7g4Qs",
"response": {
"getPublicKey": [Function],
},
},
}
`)
expect(options).toMatchInlineSnapshot(`
{
"publicKey": {
"attestation": "none",
"authenticatorSelection": {
"requireResidentKey": false,
"residentKey": "preferred",
"userVerification": "required",
},
"challenge": Uint8Array [
105,
171,
180,
181,
160,
222,
75,
198,
42,
42,
32,
31,
141,
37,
186,
233,
],
"pubKeyCredParams": [
{
"alg": -7,
"type": "public-key",
},
],
"rp": {
"id": "https://example.com",
"name": "My Website",
},
"user": {
"displayName": "Foo",
"id": Uint8Array [
182,
8,
199,
66,
131,
243,
52,
225,
240,
71,
219,
191,
29,
170,
36,
7,
212,
29,
70,
137,
172,
166,
124,
66,
39,
150,
249,
54,
172,
206,
22,
183,
],
"name": "Foo",
},
},
}
`)
})
test('error: null credential', async () => {
await expect(() =>
WebAuthnP256.createCredential({
createFn() {
return Promise.resolve(null)
},
name: 'Foo',
}),
).rejects.toMatchInlineSnapshot(`
[Registration.CreateFailedError: Failed to create credential.
Details: Failed to create credential.]
`)
})
test('error: thrown', async () => {
await expect(() =>
WebAuthnP256.createCredential({
createFn() {
return Promise.reject(new Error('foo'))
},
name: 'Foo',
}),
).rejects.toMatchInlineSnapshot(`
[Registration.CreateFailedError: Failed to create credential.
Details: foo]
`)
})
test('behavior: firefox workaround', async () => {
const credential = await WebAuthnP256.createCredential({
createFn(_options) {
return Promise.resolve({
id: 'DxRcX5C6BRQ-q-CO7XEFwrnmKlk',
response: {
attestationObject: new Uint8Array([
163, 99, 102, 109, 116, 100, 110, 111, 110, 101, 103, 97, 116,
116, 83, 116, 109, 116, 160, 104, 97, 117, 116, 104, 68, 97, 116,
97, 88, 152, 73, 150, 13, 229, 136, 14, 140, 104, 116, 52, 23, 15,
100, 118, 96, 91, 143, 228, 174, 185, 162, 134, 50, 199, 153, 92,
243, 186, 131, 29, 151, 99, 93, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 15, 20, 92, 95, 144, 186, 5, 20,
62, 171, 224, 142, 237, 113, 5, 194, 185, 230, 42, 89, 165, 1, 2,
3, 38, 32, 1, 33, 88, 32, 98, 163, 23, 104, 212, 79, 94, 255, 34,
47, 141, 112, 196, 203, 97, 171, 210, 132, 11, 39, 214, 23, 167,
254, 141, 17, 183, 45, 213, 232, 111, 193, 34, 88, 32, 102, 17,
186, 227, 241, 226, 205, 56, 228, 5, 21, 55, 118, 167, 220, 182,
153, 91, 130, 84, 161, 65, 110, 173, 16, 42, 9, 108, 176, 216, 6,
24,
]),
getPublicKey() {
throw new Error('Permission denied to access object')
},
},
} as any)
},
name: 'Example',
})
const publicKey = credential.publicKey
expect(publicKey).toMatchInlineSnapshot(`
{
"prefix": 4,
"x": "0x62a31768d44f5eff222f8d70c4cb61abd2840b27d617a7fe8d11b72dd5e86fc1",
"y": "0x6611bae3f1e2cd38e405153776a7dcb6995b8254a1416ead102a096cb0d80618",
}
`)
expect(
WebAuthnP256.verify({
metadata: {
authenticatorData:
'0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97631d00000000',
clientDataJSON:
'{"type":"webauthn.get","challenge":"9jEFijuhEWrM4SOW-tChJbUEHEP44VcjcJ-Bqo1fTM8","origin":"http://localhost:5173"}',
challengeIndex: 23,
typeIndex: 1,
userVerificationRequired: true,
},
challenge:
'0xf631058a3ba1116acce12396fad0a125b5041c43f8e15723709f81aa8d5f4ccf',
publicKey,
signature: {
r: '0xfb93e78ee89f6110313194b0e6ea55c8a33d59ec89a1eb009a179dcbebcd33f1',
s: '0x0beb1ae5eaa0c2823343507fe836b35b755b6fea5973fbe58941eb7184e714ce',
},
}),
).toBeTruthy()
})
})
describe('getAuthenticatorData', () => {
test('default', () => {
const authenticatorData = WebAuthnP256.getAuthenticatorData({
rpId: 'example.com',
})
expect(authenticatorData).toMatchInlineSnapshot(
`"0xa379a6f6eeafb9a55e378c118034e2751e682fab9f2d30ab13d2125586ce19470500000000"`,
)
})
test('options: signCount', () => {
const authenticatorData = WebAuthnP256.getAuthenticatorData({
rpId: 'example.com',
signCount: 420,
})
expect(authenticatorData).toMatchInlineSnapshot(
`"0xa379a6f6eeafb9a55e378c118034e2751e682fab9f2d30ab13d2125586ce194705000001a4"`,
)
})
test('options: flag', () => {
const authenticatorData = WebAuthnP256.getAuthenticatorData({
rpId: 'example.com',
flag: 1,
signCount: 420,
})
expect(authenticatorData).toMatchInlineSnapshot(
`"0xa379a6f6eeafb9a55e378c118034e2751e682fab9f2d30ab13d2125586ce194701000001a4"`,
)
})
})
describe('getClientDataJSON', () => {
test('default', () => {
const clientDataJSON = WebAuthnP256.getClientDataJSON({
challenge: '0xdeadbeef',
origin: 'https://example.com',
})
expect(clientDataJSON).toMatchInlineSnapshot(
`"{"type":"webauthn.get","challenge":"3q2-7w","origin":"https://example.com","crossOrigin":false}"`,
)
})
test('options: crossOrigin', () => {
const clientDataJSON = WebAuthnP256.getClientDataJSON({
challenge: '0xdeadbeef',
origin: 'https://example.com',
crossOrigin: true,
})
expect(clientDataJSON).toMatchInlineSnapshot(
`"{"type":"webauthn.get","challenge":"3q2-7w","origin":"https://example.com","crossOrigin":true}"`,
)
})
})
describe('getCredentialCreationOptions', () => {
test('default', () => {
expect(
WebAuthnP256.getCredentialCreationOptions({
name: 'Foo',
}),
).toMatchInlineSnapshot(`
{
"publicKey": {
"attestation": "none",
"authenticatorSelection": {
"requireResidentKey": false,
"residentKey": "preferred",
"userVerification": "required",
},
"challenge": Uint8Array [
105,
171,
180,
181,
160,
222,
75,
198,
42,
42,
32,
31,
141,
37,
186,
233,
],
"pubKeyCredParams": [
{
"alg": -7,
"type": "public-key",
},
],
"rp": {
"id": "https://example.com",
"name": "My Website",
},
"user": {
"displayName": "Foo",
"id": Uint8Array [
182,
8,
199,
66,
131,
243,
52,
225,
240,
71,
219,
191,
29,
170,
36,
7,
212,
29,
70,
137,
172,
166,
124,
66,
39,
150,
249,
54,
172,
206,
22,
183,
],
"name": "Foo",
},
},
}
`)
})
test('args: excludeCredentialIds', () => {
expect(
WebAuthnP256.getCredentialCreationOptions({
excludeCredentialIds: ['pzpQZRhXUkboj-b_srH0X42XJS7Ai2ZXd6-9lnFULig'],
name: 'Foo',
}),
).toMatchInlineSnapshot(`
{
"publicKey": {
"attestation": "none",
"authenticatorSelection": {
"requireResidentKey": false,
"residentKey": "preferred",
"userVerification": "required",
},
"challenge": Uint8Array [
105,
171,
180,
181,
160,
222,
75,
198,
42,
42,
32,
31,
141,
37,
186,
233,
],
"excludeCredentials": [
{
"id": Uint8Array [
167,
58,
80,
101,
24,
87,
82,
70,
232,
143,
230,
255,
178,
177,
244,
95,
141,
151,
37,
46,
192,
139,
102,
87,
119,
175,
189,
150,
113,
84,
46,
40,
],
"type": "public-key",
},
],
"pubKeyCredParams": [
{
"alg": -7,
"type": "public-key",
},
],
"rp": {
"id": "https://example.com",
"name": "My Website",
},
"user": {
"displayName": "Foo",
"id": Uint8Array [
182,
8,
199,
66,
131,
243,
52,
225,
240,
71,
219,
191,
29,
170,
36,
7,
212,
29,
70,
137,
172,
166,
124,
66,
39,
150,
249,
54,
172,
206,
22,
183,
],
"name": "Foo",
},
},
}
`)
})
test('args: user', () => {
expect(
WebAuthnP256.getCredentialCreationOptions({
user: {
name: 'Foo',
},
}),
).toMatchInlineSnapshot(`
{
"publicKey": {
"attestation": "none",
"authenticatorSelection": {
"requireResidentKey": false,
"residentKey": "preferred",
"userVerification": "required",
},
"challenge": Uint8Array [
105,
171,
180,
181,
160,
222,
75,
198,
42,
42,
32,
31,
141,
37,
186,
233,
],
"pubKeyCredParams": [
{
"alg": -7,
"type": "public-key",
},
],
"rp": {
"id": "https://example.com",
"name": "My Website",
},
"user": {
"displayName": "Foo",
"id": Uint8Array [
182,
8,
199,
66,
131,
243,
52,
225,
240,
71,
219,
191,
29,
170,
36,
7,
212,
29,
70,
137,
172,
166,
124,
66,
39,
150,
249,
54,
172,
206,
22,
183,
],
"name": "Foo",
},
},
}
`)
})
})
describe('getCredentialRequestOptions', () => {
test('default', () => {
expect(
WebAuthnP256.getCredentialRequestOptions({
challenge:
'0xf631058a3ba1116acce12396fad0a125b5041c43f8e15723709f81aa8d5f4ccf',
rpId: 'foo',
}),
).toMatchInlineSnapshot(`
{
"publicKey": {
"challenge": Uint8Array [
246,
49,
5,
138,
59,
161,
17,
106,
204,
225,
35,
150,
250,
208,
161,
37,
181,
4,
28,
67,
248,
225,
87,
35,
112,
159,
129,
170,
141,
95,
76,
207,
],
"rpId": "foo",
"userVerification": "required",
},
}
`)
})
test('options: credentialId', () => {
expect(
WebAuthnP256.getCredentialRequestOptions({
credentialId: 'm1-bMPuAqpWhCxHZQZTT6e-lSPntQbh3opIoGe7g4Qs',
challenge:
'0xf631058a3ba1116acce12396fad0a125b5041c43f8e15723709f81aa8d5f4ccf',
rpId: 'foo',
}),
).toMatchInlineSnapshot(`
{
"publicKey": {
"allowCredentials": [
{
"id": Uint8Array [
155,
95,
155,
48,
251,
128,
170,
149,
161,
11,
17,
217,
65,
148,
211,
233,
239,
165,
72,
249,
237,
65,
184,
119,
162,
146,
40,
25,
238,
224,
225,
11,
],
"type": "public-key",
},
],
"challenge": Uint8Array [
246,
49,
5,
138,
59,
161,
17,
106,
204,
225,
35,
150,
250,
208,
161,
37,
181,
4,
28,
67,
248,
225,
87,
35,
112,
159,
129,
170,
141,
95,
76,
207,
],
"rpId": "foo",
"userVerification": "required",
},
}
`)
})
})
describe('getSignPayload', () => {
test('default', () => {
const payload = WebAuthnP256.getSignPayload({
challenge:
'0xf631058a3ba1116acce12396fad0a125b5041c43f8e15723709f81aa8d5f4ccf',
origin: 'http://localhost:5173',
rpId: 'localhost',
extraClientData: {
other_keys_can_be_added_here:
'do not compare clientDataJSON against a template. See https://goo.gl/yabPex',
},
})
expect(payload).toMatchInlineSnapshot(
`
{
"metadata": {
"authenticatorData": "0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000",
"challengeIndex": 23,
"clientDataJSON": "{"type":"webauthn.get","challenge":"9jEFijuhEWrM4SOW-tChJbUEHEP44VcjcJ-Bqo1fTM8","origin":"http://localhost:5173","crossOrigin":false,"other_keys_can_be_added_here":"do not compare clientDataJSON against a template. See https://goo.gl/yabPex"}",
"typeIndex": 1,
"userVerificationRequired": true,
},
"payload": "0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d9763050000000045086dcb06a5f234db625bcdc94e657f86b76b6fd3eb9c30543eabc1e577a4b0",
}
`,
)
})
test('behavior: P256.sign + WebAuthnP256.verify', async () => {
const { privateKey, publicKey } = P256.createKeyPair()
const challenge =
'0xf631058a3ba1116acce12396fad0a125b5041c43f8e15723709f81aa8d5f4ccf' as const
const { metadata, payload } = WebAuthnP256.getSignPayload({
challenge,
origin: 'http://localhost:5173',
rpId: 'localhost',
extraClientData: {
other_keys_can_be_added_here:
'do not compare clientDataJSON against a template. See https://goo.gl/yabPex',
},
})
const signature = P256.sign({
hash: true,
payload,
privateKey,
})
expect(
WebAuthnP256.verify({
challenge,
publicKey,
signature,
metadata,
}),
).toBeTruthy()
})
test('behavior: WebCryptoP256.sign + WebAuthnP256.verify', async () => {
const keyPair = await WebCryptoP256.createKeyPair()
const { privateKey, publicKey } = keyPair
const challenge =
'0xf631058a3ba1116acce12396fad0a125b5041c43f8e15723709f81aa8d5f4ccf' as const
const data = {
challenge,
origin: 'http://localhost:5173',
rpId: 'localhost',
extraClientData: {
other_keys_can_be_added_here:
'do not compare clientDataJSON against a template. See https://goo.gl/yabPex',
},
} as const
const { metadata, payload } = WebAuthnP256.getSignPayload(data)
const signature = await WebCryptoP256.sign({
payload,
privateKey,
})
expect(
WebAuthnP256.verify({
challenge,
publicKey,
signature,
metadata,
}),
).toBeTruthy()
})
test('behavior: WebAuthnP256.sign + P256.verify', () => {
// Note: Public Key and Signature generated from WebAuthn Authenticator from examples/webauthn-p256.
const publicKey = PublicKey.from({
prefix: 4,
x: '0x84c1c30aa9e8605f16eee6ae9ba545cfd0286da162f8eca74f4fdd7bb029fc65',
y: '0x6e658291ce0aadd8da3f81f9191d1b952fe80961d3daf59645324828a6485ec2',
})
const signature = Signature.from({
r: '0x725d7c3f2bfd9ecc3af8e267e6a6ab186b9708cd363eff5e86deae564833dedb',
s: '0x5d7c03366ad5e35d3a70fab70e7cad31977f6fb31c3b63b8576509daf50ce18c',
})
const { payload } = WebAuthnP256.getSignPayload({
challenge:
'0xf631058a3ba1116acce12396fad0a125b5041c43f8e15723709f81aa8d5f4ccf',
origin: 'http://localhost:5173',
rpId: 'localhost',
extraClientData: {
other_keys_can_be_added_here:
'do not compare clientDataJSON against a template. See https://goo.gl/yabPex',
},
})
expect(
P256.verify({
hash: true,
publicKey,
payload,
signature,
}),
).toBeTruthy()
})
test('options: crossOrigin', () => {
const payload = WebAuthnP256.getSignPayload({
challenge:
'0xf631058a3ba1116acce12396fad0a125b5041c43f8e15723709f81aa8d5f4ccf',
origin: 'http://localhost:5173',
rpId: 'foo',
crossOrigin: true,
})
expect(payload).toMatchInlineSnapshot(
`
{
"metadata": {
"authenticatorData": "0x2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae0500000000",
"challengeIndex": 23,
"clientDataJSON": "{"type":"webauthn.get","challenge":"9jEFijuhEWrM4SOW-tChJbUEHEP44VcjcJ-Bqo1fTM8","origin":"http://localhost:5173","crossOrigin":true}",
"typeIndex": 1,
"userVerificationRequired": true,
},
"payload": "0x2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae05000000000b0ba56d14f4fe7d5eae0a9ec8c1a0605b83b7173722c0cc7fbd6ec2567407e5",
}
`,
)
})
test('options: flag', () => {
const payload = WebAuthnP256.getSignPayload({
challenge:
'0xf631058a3ba1116acce12396fad0a125b5041c43f8e15723709f81aa8d5f4ccf',
origin: 'http://localhost:5173',
rpId: 'foo',
crossOrigin: true,
flag: 4,
})
expect(payload).toMatchInlineSnapshot(
`
{
"metadata": {
"authenticatorData": "0x2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae0400000000",
"challengeIndex": 23,
"clientDataJSON": "{"type":"webauthn.get","challenge":"9jEFijuhEWrM4SOW-tChJbUEHEP44VcjcJ-Bqo1fTM8","origin":"http://localhost:5173","crossOrigin":true}",
"typeIndex": 1,
"userVerificationRequired": true,
},
"payload": "0x2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae04000000000b0ba56d14f4fe7d5eae0a9ec8c1a0605b83b7173722c0cc7fbd6ec2567407e5",
}
`,
)
})
test('options: signCount', () => {
const payload = WebAuthnP256.getSignPayload({
challenge:
'0xf631058a3ba1116acce12396fad0a125b5041c43f8e15723709f81aa8d5f4ccf',
origin: 'http://localhost:5173',
rpId: 'foo',
crossOrigin: true,
signCount: 69,
})
expect(payload).toMatchInlineSnapshot(
`
{
"metadata": {
"authenticatorData": "0x2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae0500000045",
"challengeIndex": 23,
"clientDataJSON": "{"type":"webauthn.get","challenge":"9jEFijuhEWrM4SOW-tChJbUEHEP44VcjcJ-Bqo1fTM8","origin":"http://localhost:5173","crossOrigin":true}",
"typeIndex": 1,
"userVerificationRequired": true,
},
"payload": "0x2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae05000000450b0ba56d14f4fe7d5eae0a9ec8c1a0605b83b7173722c0cc7fbd6ec2567407e5",
}
`,
)
})
})
describe('sign', () => {
test('default', async () => {
let options: CredentialRequestOptions | undefined
const signature = await WebAuthnP256.sign({
getFn(options_) {
options = options_
return Promise.resolve({
id: 'm1-bMPuAqpWhCxHZQZTT6e-lSPntQbh3opIoGe7g4Qs',
response: {
authenticatorData: [
73, 150, 13, 229, 136, 14, 140, 104, 116, 52, 23, 15, 100, 118,
96, 91, 143, 228, 174, 185, 162, 134, 50, 199, 153, 92, 243, 186,
131, 29, 151, 99, 5, 0, 0, 0, 0,
],
clientDataJSON: [
123, 34, 116, 121, 112, 101, 34, 58, 34, 119, 101, 98, 97, 117,
116, 104, 110, 46, 103, 101, 116, 34, 44, 34, 99, 104, 97, 108,
108, 101, 110, 103, 101, 34, 58, 34, 57, 106, 69, 70, 105, 106,
117, 104, 69, 87, 114, 77, 52, 83, 79, 87, 45, 116, 67, 104, 74,
98, 85, 69, 72, 69, 80, 52, 52, 86, 99, 106, 99, 74, 45, 66, 113,
111, 49, 102, 84, 77, 56, 34, 44, 34, 111, 114, 105, 103, 105,
110, 34, 58, 34, 104, 116, 116, 112, 58, 47, 47, 108, 111, 99, 97,
108, 104, 111, 115, 116, 58, 53, 49, 55, 51, 34, 44, 34, 99, 114,
111, 115, 115, 79, 114, 105, 103, 105, 110, 34, 58, 102, 97, 108,
115, 101, 125,
],
signature: [
48, 70, 2, 33, 0, 146, 61, 150, 57, 188, 182, 119, 250, 23, 162,
103, 56, 232, 200, 162, 77, 88, 37, 145, 151, 40, 59, 42, 63, 46,
225, 53, 221, 74, 128, 13, 165, 2, 33, 0, 128, 39, 38, 71, 180,
153, 30, 232, 243, 94, 159, 66, 42, 246, 56, 195, 195, 139, 40,
163, 26, 34, 125, 244, 171, 166, 7, 178, 169, 246, 142, 198,
],
},
} as any)
},
challenge:
'0xf631058a3ba1116acce12396fad0a125b5041c43f8e15723709f81aa8d5f4ccf',
})
expect(signature).toMatchInlineSnapshot(`
{
"id": "m1-bMPuAqpWhCxHZQZTT6e-lSPntQbh3opIoGe7g4Qs",
"metadata": {
"authenticatorData": "0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000",
"challengeIndex": 23,
"clientDataJSON": "{"type":"webauthn.get","challenge":"9jEFijuhEWrM4SOW-tChJbUEHEP44VcjcJ-Bqo1fTM8","origin":"http://localhost:5173","crossOrigin":false}",
"typeIndex": 1,
"userVerificationRequired": true,
},
"raw": {
"id": "m1-bMPuAqpWhCxHZQZTT6e-lSPntQbh3opIoGe7g4Qs",
"response": {
"authenticatorData": [
73,
150,
13,
229,
136,
14,
140,
104,
116,
52,
23,
15,
100,
118,
96,
91,
143,
228,
174,
185,
162,
134,
50,
199,
153,
92,
243,
186,
131,
29,
151,
99,
5,
0,
0,
0,
0,
],
"clientDataJSON": [
123,
34,
116,
121,
112,
101,
34,
58,
34,
119,
101,
98,
97,
117,
116,
104,
110,
46,
103,
101,
116,
34,
44,
34,
99,
104,
97,
108,
108,
101,
110,
103,
101,
34,
58,
34,
57,
106,
69,
70,
105,
106,
117,
104,
69,
87,
114,
77,
52,
83,
79,
87,
45,
116,
67,
104,
74,
98,
85,
69,
72,
69,
80,
52,
52,
86,
99,
106,
99,
74,
45,
66,
113,
111,
49,
102,
84,
77,
56,
34,
44,
34,
111,
114,
105,
103,
105,
110,
34,
58,
34,
104,
116,
116,
112,
58,
47,
47,
108,
111,
99,
97,
108,
104,
111,
115,
116,
58,
53,
49,
55,
51,
34,
44,
34,
99,
114,
111,
115,
115,
79,
114,
105,
103,
105,
110,
34,
58,
102,
97,
108,
115,
101,
125,
],
"signature": [
48,
70,
2,
33,
0,
146,
61,
150,
57,
188,
182,
119,
250,
23,
162,
103,
56,
232,
200,
162,
77,
88,
37,
145,
151,
40,
59,
42,
63,
46,
225,
53,
221,
74,
128,
13,
165,
2,
33,
0,
128,
39,
38,
71,
180,
153,
30,
232,
243,
94,
159,
66,
42,
246,
56,
195,
195,
139,
40,
163,
26,
34,
125,
244,
171,
166,
7,
178,
169,
246,
142,
198,
],
},
},
"signature": {
"r": "0x923d9639bcb677fa17a26738e8c8a24d58259197283b2a3f2ee135dd4a800da5",
"s": "0x7fd8d9b74b66e1180ca160bdd509c73bf95bd20a8cf520904813c310526c968b",
},
}
`)
expect(options).toMatchInlineSnapshot(`
{
"publicKey": {
"challenge": Uint8Array [
246,
49,
5,
138,
59,
161,
17,
106,
204,
225,
35,
150,
250,
208,
161,
37,
181,
4,
28,
67,
248,
225,
87,
35,
112,
159,
129,
170,
141,
95,
76,
207,
],
"rpId": "https://example.com",
"userVerification": "required",
},
}
`)
})
test('error: null credential', async () => {
await expect(() =>
WebAuthnP256.sign({
getFn() {
return Promise.resolve(null)
},
challenge:
'0xf631058a3ba1116acce12396fad0a125b5041c43f8e15723709f81aa8d5f4ccf',
}),
).rejects.toMatchInlineSnapshot(`
[Authentication.SignFailedError: Failed to request credential.
Details: Failed to request credential.]
`)
})
test('error: thrown', async () => {
await expect(() =>
WebAuthnP256.sign({
getFn() {
return Promise.reject(new Error('foo'))
},
challenge:
'0xf631058a3ba1116acce12396fad0a125b5041c43f8e15723709f81aa8d5f4ccf',
}),
).rejects.toMatchInlineSnapshot(`
[Authentication.SignFailedError: Failed to request credential.
Details: foo]
`)
})
})
describe('verify', () => {
test('default', async () => {
const publicKey = PublicKey.from({
prefix: 4,
x: '0x21e1cbfd809fcc978340e1d3442bf9893391628620c065ff9b519b537afceba0',
y: '0xa5b31085b870a1de535866d0aec5df21bc6c98de71f356d96886e84510b8f374',
})
const signature = Signature.from({
r: '0x16d6f4bd3231c71c5e58927b9cf2ee701df03b52e3db71efc03d1139122f854f',
s: '0x67f32a4fcb17b07ab9b7755b61e999b99139074fc8e1aa6d33d25beccbb2fbd4',
})
const metadata = {
authenticatorData:
'0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000',
challengeIndex: 23,
clientDataJSON:
'{"type":"webauthn.get","challenge":"9jEFijuhEWrM4SOW-tChJbUEHEP44VcjcJ-Bqo1fTM8","origin":"http://localhost:5173","crossOrigin":false}',
typeIndex: 1,
userVerificationRequired: true,
} as const
expect(
WebAuthnP256.verify({
metadata,
challenge:
'0xf631058a3ba1116acce12396fad0a125b5041c43f8e15723709f81aa8d5f4ccf',
publicKey,
signature,
}),
).toBeTruthy()
})
test('default', async () => {
const publicKey = PublicKey.from({
prefix: 4,
x: '0x21e1cbfd809fcc978340e1d3442bf9893391628620c065ff9b519b537afceba0',
y: '0xa5b31085b870a1de535866d0aec5df21bc6c98de71f356d96886e84510b8f374',
})
const signature = Signature.from({
r: '0xcbe10bc71cbbc0552f58aa5b8742c954f10335943abb9cfd186ec4d066e44d22',
s: '0x3901d4c1c5e613cfbcd1ce32c9679d4daafda3c73e745017cad55ec2ffd0a895',
})
const metadata = {
authenticatorData:
'0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000',
challengeIndex: 23,
clientDataJSON:
'{"type":"webauthn.get","challenge":"9jEFijuhEWrM4SOW-tChJbUEHEP44VcjcJ-Bqo1fTM8","origin":"http://localhost:5173","crossOrigin":false,"other_keys_can_be_added_here":"do not compare clientDataJSON against a template. See https://goo.gl/yabPex"}',
typeIndex: 1,
userVerificationRequired: true,
} as const
expect(
WebAuthnP256.verify({
challenge:
'0xf631058a3ba1116acce12396fad0a125b5041c43f8e15723709f81aa8d5f4ccf',
publicKey,
signature,
metadata,
}),
).toBeTruthy()
})
test('behavior: invalid hash', async () => {
const publicKey = PublicKey.from({
prefix: 4,
x: '0x21e1cbfd809fcc978340e1d3442bf9893391628620c065ff9b519b537afceba0',
y: '0xa5b31085b870a1de535866d0aec5df21bc6c98de71f356d96886e84510b8f374',
})
const signature = Signature.from({
r: '0x16d6f4bd3231c71c5e58927b9cf2ee701df03b52e3db71efc03d1139122f854f',
s: '0x67f32a4fcb17b07ab9b7755b61e999b99139074fc8e1aa6d33d25beccbb2fbd4',
})
const metadata = {
authenticatorData:
'0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000',
challengeIndex: 23,
clientDataJSON:
'{"type":"webauthn.get","challenge":"9jEFijuhEWrM4SOW-tChJbUEHEP44VcjcJ-Bqo1fTM8","origin":"http://localhost:5173","crossOrigin":false}',
typeIndex: 1,
userVerificationRequired: true,
} as const
expect(
WebAuthnP256.verify({
challenge:
'0xa631058a3ba1116acce12396fad0a125b5041c43f8e15723709f81aa8d5f4ccf',
publicKey,
signature,
metadata,
}),
).toBeFalsy()
})
test('behavior: invalid signature', async () => {
const publicKey = PublicKey.from({
prefix: 4,
x: '0x21e1cbfd809fcc978340e1d3442bf9893391628620c065ff9b519b537afceba0',
y: '0xa5b31085b870a1de535866d0aec5df21bc6c98de71f356d96886e84510b8f374',
})
const signature = Signature.from({
r: '0x16d6f4bd3231c71c5e58927b9cf2ee701df03b52e3db71efc03d1139122f8550',
s: '0x67f32a4fcb17b07ab9b7755b61e999b99139074fc8e1aa6d33d25beccbb2fbd4',
})
const metadata = {
authenticatorData:
'0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000',
challengeIndex: 23,
clientDataJSON:
'{"type":"webauthn.get","challenge":"9jEFijuhEWrM4SOW-tChJbUEHEP44VcjcJ-Bqo1fTM8","origin":"http://localhost:5173","crossOrigin":false}',
typeIndex: 1,
userVerificationRequired: true,
} as const
expect(
WebAuthnP256.verify({
challenge:
'0xa631058a3ba1116acce12396fad0a125b5041c43f8e15723709f81aa8d5f4ccf',
publicKey,
signature,
metadata,
}),
).toBeFalsy()
})
test('behavior: authenticator data too short', async () => {
const publicKey = PublicKey.from({
prefix: 4,
x: '0x21e1cbfd809fcc978340e1d3442bf9893391628620c065ff9b519b537afceba0',
y: '0xa5b31085b870a1de535866d0aec5df21bc6c98de71f356d96886e84510b8f374',
})
const signature = Signature.from({
r: '0x16d6f4bd3231c71c5e58927b9cf2ee701df03b52e3db71efc03d1139122f8550',
s: '0x67f32a4fcb17b07ab9b7755b61e999b99139074fc8e1aa6d33d25beccbb2fbd4',
})
const metadata = {
authenticatorData:
'0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d976305000000',
challengeIndex: 23,
clientDataJSON:
'{"type":"webauthn.get","challenge":"9jEFijuhEWrM4SOW-tChJbUEHEP44VcjcJ-Bqo1fTM8","origin":"http://localhost:5173","crossOrigin":false}',
typeIndex: 1,
userVerificationRequired: true,
} as const
expect(
WebAuthnP256.verify({
challenge:
'0xa631058a3ba1116acce12396fad0a125b5041c43f8e15723709f81aa8d5f4ccf',
publicKey,
signature,
metadata,
}),
).toBeFalsy()
})
test('behavior: invalid flag', async () => {
const publicKey = PublicKey.from({
prefix: 4,
x: '0x21e1cbfd809fcc978340e1d3442bf9893391628620c065ff9b519b537afceba0',
y: '0xa5b31085b870a1de535866d0aec5df21bc6c98de71f356d96886e84510b8f374',
})
const signature = Signature.from({
r: '0xcbe10bc71cbbc0552f58aa5b8742c954f10335943abb9cfd186ec4d066e44d22',
s: '0x3901d4c1c5e613cfbcd1ce32c9679d4daafda3c73e745017cad55ec2ffd0a895',
})
const metadata = {
authenticatorData:
'0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630000010000',
challengeIndex: 23,
clientDataJSON:
'{"type":"webauthn.get","challenge":"9jEFijuhEWrM4SOW-tChJbUEHEP44VcjcJ-Bqo1fTM8","origin":"http://localhost:5173","crossOrigin":false,"other_keys_can_be_added_here":"do not compare clientDataJSON against a template. See https://goo.gl/yabPex"}',
typeIndex: 1,
userVerificationRequired: true,
} as const
expect(
WebAuthnP256.verify({
metadata,
challenge:
'0xf631058a3ba1116acce12396fad0a125b5041c43f8e15723709f81aa8d5f4ccf',
publicKey,
signature,
}),
).toBeFalsy()
})
test('behavior: invalid flag', async () => {
const publicKey = PublicKey.from({
prefix: 4,
x: '0x21e1cbfd809fcc978340e1d3442bf9893391628620c065ff9b519b537afceba0',
y: '0xa5b31085b870a1de535866d0aec5df21bc6c98de71f356d96886e84510b8f374',
})
const signature = Signature.from({
r: '0x16d6f4bd3231c71c5e58927b9cf2ee701df03b52e3db71efc03d1139122f854f',
s: '0x67f32a4fcb17b07ab9b7755b61e999b99139074fc8e1aa6d33d25beccbb2fbd4',
})
const metadata = {
authenticatorData:
'0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630900000000',
challengeIndex: 23,
clientDataJSON:
'{"type":"webauthn.get","challenge":"9jEFijuhEWrM4SOW-tChJbUEHEP44VcjcJ-Bqo1fTM8","origin":"http://localhost:5173","crossOrigin":false}',
typeIndex: 1,
userVerificationRequired: true,
} as const
expect(
WebAuthnP256.verify({
challenge:
'0xf631058a3ba1116acce12396fad0a125b5041c43f8e15723709f81aa8d5f4ccf',
publicKey,
signature,
metadata,
}),
).toBeFalsy()
})
test('behavior: invalid flag', async () => {
const publicKey = PublicKey.from({
prefix: 4,
x: '0x21e1cbfd809fcc978340e1d3442bf9893391628620c065ff9b519b537afceba0',
y: '0xa5b31085b870a1de535866d0aec5df21bc6c98de71f356d96886e84510b8f374',
})
const signature = Signature.from({
r: '0x16d6f4bd3231c71c5e58927b9cf2ee701df03b52e3db71efc03d1139122f854f',
s: '0x67f32a4fcb17b07ab9b7755b61e999b99139074fc8e1aa6d33d25beccbb2fbd4',
})
const metadata = {
authenticatorData:
'0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97631500000000',
challengeIndex: 23,
clientDataJSON:
'{"type":"webauthn.get","challenge":"9jEFijuhEWrM4SOW-tChJbUEHEP44VcjcJ-Bqo1fTM8","origin":"http://localhost:5173","crossOrigin":false}',
typeIndex: 1,
userVerificationRequired: true,
} as const
expect(
WebAuthnP256.verify({
challenge:
'0xf631058a3ba1116acce12396fad0a125b5041c43f8e15723709f81aa8d5f4ccf',
publicKey,
signature,
metadata,
}),
).toBeFalsy()
})
test('behavior: invalid type', async () => {
const publicKey = PublicKey.from({
prefix: 4,
x: '0x21e1cbfd809fcc978340e1d3442bf9893391628620c065ff9b519b537afceba0',
y: '0xa5b31085b870a1de535866d0aec5df21bc6c98de71f356d96886e84510b8f374',
})
const signature = Signature.from({
r: '0x16d6f4bd3231c71c5e58927b9cf2ee701df03b52e3db71efc03d1139122f854f',
s: '0x67f32a4fcb17b07ab9b7755b61e999b99139074fc8e1aa6d33d25beccbb2fbd4',
})
const metadata = {
authenticatorData:
'0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000',
challengeIndex: 23,
clientDataJSON:
'{"type":"webauthn.create","challenge":"9jEFijuhEWrM4SOW-tChJbUEHEP44VcjcJ-Bqo1fTM8","origin":"http://localhost:5173","crossOrigin":false}',
typeIndex: 1,
userVerificationRequired: true,
} as const
expect(
WebAuthnP256.verify({
challenge:
'0xf631058a3ba1116acce12396fad0a125b5041c43f8e15723709f81aa8d5f4ccf',
publicKey,
signature,
metadata,
}),
).toBeFalsy()
})
test('behavior: invalid challenge', async () => {
const publicKey = PublicKey.from({
prefix: 4,
x: '0x21e1cbfd809fcc978340e1d3442bf9893391628620c065ff9b519b537afceba0',
y: '0xa5b31085b870a1de535866d0aec5df21bc6c98de71f356d96886e84510b8f374',
})
const signature = Signature.from({
r: '0x16d6f4bd3231c71c5e58927b9cf2ee701df03b52e3db71efc03d1139122f854f',
s: '0x67f32a4fcb17b07ab9b7755b61e999b99139074fc8e1aa6d33d25beccbb2fbd4',
})
const metadata = {
authenticatorData:
'0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000',
challengeIndex: 23,
clientDataJSON:
'{"type":"webauthn.get","challenge":"9jEFijuhEWrM4SOW-tChJbUEHEP44VcjcJ-Bqo1fTM9","origin":"http://localhost:5173","crossOrigin":false}',
typeIndex: 2,
userVerificationRequired: true,
} as const
expect(
WebAuthnP256.verify({
challenge:
'0xf631058a3ba1116acce12396fad0a125b5041c43f8e15723709f81aa8d5f4ccf',
publicKey,
signature,
metadata,
}),
).toBeFalsy()
})
test('behavior: invalid challenge match', async () => {
const publicKey = PublicKey.from({
prefix: 4,
x: '0x21e1cbfd809fcc978340e1d3442bf9893391628620c065ff9b519b537afceba0',
y: '0xa5b31085b870a1de535866d0aec5df21bc6c98de71f356d96886e84510b8f374',
})
const signature = Signature.from({
r: '0x16d6f4bd3231c71c5e58927b9cf2ee701df03b52e3db71efc03d1139122f854f',
s: '0x67f32a4fcb17b07ab9b7755b61e999b99139074fc8e1aa6d33d25beccbb2fbd4',
})
const metadata = {
authenticatorData:
'0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000',
challengeIndex: 23,
clientDataJSON:
'{"type":"webauthn.get","challene":"9jEFijuhEWrM4SOW-tChJbUEHEP44VcjcJ-Bqo1fTM9","origin":"http://localhost:5173","crossOrigin":false}',
typeIndex: 2,
userVerificationRequired: true,
} as const
expect(
WebAuthnP256.verify({
challenge:
'0xf631058a3ba1116acce12396fad0a125b5041c43f8e15723709f81aa8d5f4ccf',
publicKey,
signature,
metadata,
}),
).toBeFalsy()
})
test('options: minimal metadata', async () => {
const publicKey = PublicKey.from({
prefix: 4,
x: '0x21e1cbfd809fcc978340e1d3442bf9893391628620c065ff9b519b537afceba0',
y: '0xa5b31085b870a1de535866d0aec5df21bc6c98de71f356d96886e84510b8f374',
})
const signature = Signature.from({
r: '0x16d6f4bd3231c71c5e58927b9cf2ee701df03b52e3db71efc03d1139122f854f',
s: '0x67f32a4fcb17b07ab9b7755b61e999b99139074fc8e1aa6d33d25beccbb2fbd4',
})
const metadata = {
authenticatorData:
'0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000',
clientDataJSON:
'{"type":"webauthn.get","challenge":"9jEFijuhEWrM4SOW-tChJbUEHEP44VcjcJ-Bqo1fTM8","origin":"http://localhost:5173","crossOrigin":false}',
} as const
expect(
WebAuthnP256.verify({
metadata,
challenge:
'0xf631058a3ba1116acce12396fad0a125b5041c43f8e15723709f81aa8d5f4ccf',
publicKey,
signature,
}),
).toBeTruthy()
})
test('options: challengeIndex omitted', async () => {
const publicKey = PublicKey.from({
prefix: 4,
x: '0x21e1cbfd809fcc978340e1d3442bf9893391628620c065ff9b519b537afceba0',
y: '0xa5b31085b870a1de535866d0aec5df21bc6c98de71f356d96886e84510b8f374',
})
const signature = Signature.from({
r: '0x16d6f4bd3231c71c5e58927b9cf2ee701df03b52e3db71efc03d1139122f854f',
s: '0x67f32a4fcb17b07ab9b7755b61e999b99139074fc8e1aa6d33d25beccbb2fbd4',
})
const metadata = {
authenticatorData:
'0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000',
clientDataJSON:
'{"type":"webauthn.get","challenge":"9jEFijuhEWrM4SOW-tChJbUEHEP44VcjcJ-Bqo1fTM8","origin":"http://localhost:5173","crossOrigin":false}',
typeIndex: 1,
userVerificationRequired: true,
} as const
expect(
WebAuthnP256.verify({
metadata,
challenge:
'0xf631058a3ba1116acce12396fad0a125b5041c43f8e15723709f81aa8d5f4ccf',
publicKey,
signature,
}),
).toBeTruthy()
})
test('options: typeIndex omitted', async () => {
const publicKey = PublicKey.from({
prefix: 4,
x: '0x21e1cbfd809fcc978340e1d3442bf9893391628620c065ff9b519b537afceba0',
y: '0xa5b31085b870a1de535866d0aec5df21bc6c98de71f356d96886e84510b8f374',
})
const signature = Signature.from({
r: '0x16d6f4bd3231c71c5e58927b9cf2ee701df03b52e3db71efc03d1139122f854f',
s: '0x67f32a4fcb17b07ab9b7755b61e999b99139074fc8e1aa6d33d25beccbb2fbd4',
})
const metadata = {
authenticatorData:
'0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000',
challengeIndex: 23,
clientDataJSON:
'{"type":"webauthn.get","challenge":"9jEFijuhEWrM4SOW-tChJbUEHEP44VcjcJ-Bqo1fTM8","origin":"http://localhost:5173","crossOrigin":false}',
userVerificationRequired: true,
} as const
expect(
WebAuthnP256.verify({
metadata,
challenge:
'0xf631058a3ba1116acce12396fad0a125b5041c43f8e15723709f81aa8d5f4ccf',
publicKey,
signature,
}),
).toBeTruthy()
})
test('options: challengeIndex and typeIndex omitted', async () => {
const publicKey = PublicKey.from({
prefix: 4,
x: '0x21e1cbfd809fcc978340e1d3442bf9893391628620c065ff9b519b537afceba0',
y: '0xa5b31085b870a1de535866d0aec5df21bc6c98de71f356d96886e84510b8f374',
})
const signature = Signature.from({
r: '0x16d6f4bd3231c71c5e58927b9cf2ee701df03b52e3db71efc03d1139122f854f',
s: '0x67f32a4fcb17b07ab9b7755b61e999b99139074fc8e1aa6d33d25beccbb2fbd4',
})
const metadata = {
authenticatorData:
'0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000',
clientDataJSON:
'{"type":"webauthn.get","challenge":"9jEFijuhEWrM4SOW-tChJbUEHEP44VcjcJ-Bqo1fTM8","origin":"http://localhost:5173","crossOrigin":false}',
userVerificationRequired: true,
} as const
expect(
WebAuthnP256.verify({
metadata,
challenge:
'0xf631058a3ba1116acce12396fad0a125b5041c43f8e15723709f81aa8d5f4ccf',
publicKey,
signature,
}),
).toBeTruthy()
})
test('behavior: invalid challen