diff options
author | Matthew Miller <matthew@millerti.me> | 2020-06-02 15:50:11 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-06-02 15:50:11 -0700 |
commit | ed960d81a9667d5cca2d444839f5ce63e2f38911 (patch) | |
tree | 2d9f2f8e7ce60a83e5409d073f74422bcc2df60e | |
parent | 743de54fa9b0cbef261cdbedf1c567c2202737cd (diff) | |
parent | bb5e3e99f7e50b9cec607b4fda34dcbd1e04aae9 (diff) |
Merge pull request #21 from MasterKale/feature/improve-browser
Refactor Megamix 1
30 files changed, 541 insertions, 474 deletions
diff --git a/packages/browser/package-lock.json b/packages/browser/package-lock.json index 5145834..f9896f4 100644 --- a/packages/browser/package-lock.json +++ b/packages/browser/package-lock.json @@ -4,10 +4,8 @@ "lockfileVersion": 1, "requires": true, "dependencies": { - "@types/base64-js": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@types/base64-js/-/base64-js-1.2.5.tgz", - "integrity": "sha1-WCskdhaabLpGCiFNR2x0REHYc9U=", + "@simplewebauthn/typescript-types": { + "version": "file:../typescript-types", "dev": true }, "@webassemblyjs/ast": { @@ -185,10 +183,6 @@ "@xtuc/long": "4.2.2" } }, - "@simplewebauthn/typescript-types": { - "version": "file:../typescript-types", - "dev": true - }, "@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -416,7 +410,8 @@ "base64-js": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", - "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", + "dev": true }, "big.js": { "version": "5.2.2", diff --git a/packages/browser/package.json b/packages/browser/package.json index c6c9990..d3d348d 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -22,12 +22,9 @@ "typescript", "umd" ], - "dependencies": { - "base64-js": "^1.3.1" - }, + "dependencies": {}, "devDependencies": { "@simplewebauthn/typescript-types": "file:../typescript-types", - "@types/base64-js": "^1.2.5", "ts-loader": "^7.0.4", "webpack": "^4.43.0", "webpack-auto-inject-version": "^1.2.2", diff --git a/packages/browser/src/helpers/base64URLStringToBuffer.ts b/packages/browser/src/helpers/base64URLStringToBuffer.ts new file mode 100644 index 0000000..baa258f --- /dev/null +++ b/packages/browser/src/helpers/base64URLStringToBuffer.ts @@ -0,0 +1,33 @@ +/** + * Convert from a Base64URL-encoded string to an Array Buffer. Best used when converting a + * credential ID from a JSON string to an ArrayBuffer, like in allowCredentials or + * excludeCredentials + * + * Helper method to compliment `bufferToBase64URLString` + */ +export default function base64URLStringToBuffer(base64URLString: string): ArrayBuffer { + // Convert from Base64URL to Base64 + const base64 = base64URLString.replace(/-/g, '+').replace(/_/g, '/'); + /** + * Pad with '=' until it's a multiple of four + * (4 - (85 % 4 = 1) = 3) % 4 = 3 padding + * (4 - (86 % 4 = 2) = 2) % 4 = 2 padding + * (4 - (87 % 4 = 3) = 1) % 4 = 1 padding + * (4 - (88 % 4 = 0) = 4) % 4 = 0 padding + */ + const padLength = (4 - (base64.length % 4)) % 4; + const padded = base64.padEnd(base64.length + padLength, '='); + + // Convert to a binary string + const binary = atob(padded); + + // Convert binary string to buffer + const buffer = new ArrayBuffer(binary.length); + const bytes = new Uint8Array(buffer); + + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + + return buffer; +} diff --git a/packages/browser/src/helpers/bufferToBase64URLString.ts b/packages/browser/src/helpers/bufferToBase64URLString.ts new file mode 100644 index 0000000..954f4b0 --- /dev/null +++ b/packages/browser/src/helpers/bufferToBase64URLString.ts @@ -0,0 +1,21 @@ +/** + * Convert the given array buffer into a Base64URL-encoded string. Ideal for converting various + * credential response ArrayBuffers to string for sending back to the server as JSON. + * + * Helper method to compliment `base64URLStringToBuffer` + */ +export default function bufferToBase64URLString(buffer: ArrayBuffer): string { + const bytes = new Uint8Array(buffer); + let str = ''; + + for (const charCode of bytes) { + str += String.fromCharCode(charCode); + } + + const base64String = btoa(str); + + return base64String + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); +} diff --git a/packages/browser/src/helpers/toBase64String.test.ts b/packages/browser/src/helpers/toBase64String.test.ts deleted file mode 100644 index bbcb11b..0000000 --- a/packages/browser/src/helpers/toBase64String.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import toBase64String from './toBase64String'; - -import toUint8Array from './toUint8Array'; - -test('should convert a Buffer to a string with a length that is a multiple of 4', () => { - const base64 = toBase64String(Buffer.from('123456', 'ascii')); - - expect(base64.length % 4).toEqual(0); -}); - -test('should convert a Uint8Array to a string with a length that is a multiple of 4', () => { - const base64 = toBase64String(toUint8Array('123456')); - - expect(base64.length % 4).toEqual(0); -}); diff --git a/packages/browser/src/helpers/toBase64String.ts b/packages/browser/src/helpers/toBase64String.ts deleted file mode 100644 index 3178695..0000000 --- a/packages/browser/src/helpers/toBase64String.ts +++ /dev/null @@ -1,6 +0,0 @@ -import base64js from 'base64-js'; - -export default function toBase64String(buffer: ArrayBuffer): string { - // TODO: Make sure converting buffer to Uint8Array() is correct - return base64js.fromByteArray(new Uint8Array(buffer)).replace(/\+/g, '-').replace(/\//g, '_'); -} diff --git a/packages/browser/src/helpers/toPublicKeyCredentialDescriptor.ts b/packages/browser/src/helpers/toPublicKeyCredentialDescriptor.ts index c37fed0..8fad78b 100644 --- a/packages/browser/src/helpers/toPublicKeyCredentialDescriptor.ts +++ b/packages/browser/src/helpers/toPublicKeyCredentialDescriptor.ts @@ -1,16 +1,14 @@ -import base64js from 'base64-js'; import type { PublicKeyCredentialDescriptorJSON } from '@simplewebauthn/typescript-types'; +import base64URLStringToBuffer from './base64URLStringToBuffer'; + export default function toPublicKeyCredentialDescriptor( descriptor: PublicKeyCredentialDescriptorJSON, ): PublicKeyCredentialDescriptor { - // Make sure the Base64'd credential ID length is a multiple of 4 or else toByteArray will throw const { id } = descriptor; - const padLength = 4 - (id.length % 4); - const paddedId = id.padEnd(id.length + padLength, '='); return { ...descriptor, - id: base64js.toByteArray(paddedId), + id: base64URLStringToBuffer(id), }; } diff --git a/packages/browser/src/methods/startAssertion.test.ts b/packages/browser/src/methods/startAssertion.test.ts index db401dc..669e8eb 100644 --- a/packages/browser/src/methods/startAssertion.test.ts +++ b/packages/browser/src/methods/startAssertion.test.ts @@ -1,13 +1,9 @@ -import base64js from 'base64-js'; - import { AssertionCredential, PublicKeyCredentialRequestOptionsJSON, } from '@simplewebauthn/typescript-types'; -import toUint8Array from '../helpers/toUint8Array'; import supportsWebauthn from '../helpers/supportsWebauthn'; -import toBase64String from '../helpers/toBase64String'; import startAssertion from './startAssertion'; @@ -16,16 +12,16 @@ jest.mock('../helpers/supportsWebauthn'); const mockNavigatorGet = window.navigator.credentials.get as jest.Mock; const mockSupportsWebauthn = supportsWebauthn as jest.Mock; -const mockAuthenticatorData = toBase64String(toUint8Array('mockAuthenticatorData')); -const mockClientDataJSON = toBase64String(toUint8Array('mockClientDataJSON')); -const mockSignature = toBase64String(toUint8Array('mockSignature')); -const mockUserHandle = toBase64String(toUint8Array('mockUserHandle')); +const mockAuthenticatorData = 'mockAuthenticatorData'; +const mockClientDataJSON = 'mockClientDataJSON'; +const mockSignature = 'mockSignature'; +const mockUserHandle = 'mockUserHandle'; const goodOpts1: PublicKeyCredentialRequestOptionsJSON = { challenge: 'fizz', allowCredentials: [ { - id: 'abcdefgfdnsdfunguisdfgs', + id: 'C0VGlvYFratUdAV1iCw-ULpUW8E-exHPXQChBfyVeJZCMfjMFcwDmOFgoMUz39LoMtCJUBW8WPlLkGT6q8qTCg', type: 'public-key', transports: ['nfc'], }, @@ -45,7 +41,10 @@ test('should convert options before passing to navigator.credentials.get(...)', mockNavigatorGet.mockImplementation( (): Promise<any> => { return new Promise(resolve => { - resolve({ response: {} }); + resolve({ + response: {}, + getClientExtensionResults: () => ({}), + }); }); }, ); @@ -53,31 +52,30 @@ test('should convert options before passing to navigator.credentials.get(...)', await startAssertion(goodOpts1); const argsPublicKey = mockNavigatorGet.mock.calls[0][0].publicKey; - const credId = base64js.fromByteArray(argsPublicKey.allowCredentials[0].id); + const credId = argsPublicKey.allowCredentials[0].id; - expect(argsPublicKey.challenge).toEqual(toUint8Array(goodOpts1.challenge)); - // Make sure the credential ID is a proper base64 with a length that's a multiple of 4 - expect(credId.length % 4).toEqual(0); + expect(JSON.stringify(argsPublicKey.challenge)).toEqual('{"0":102,"1":105,"2":122,"3":122}'); + // Make sure the credential ID is an ArrayBuffer with a length of 64 + expect(credId instanceof ArrayBuffer).toEqual(true); + expect(credId.byteLength).toEqual(64); done(); }); -test('should return base64-encoded response values', async done => { +test('should return base64url-encoded response values', async done => { mockSupportsWebauthn.mockReturnValue(true); - const credentialID = 'foobar'; - mockNavigatorGet.mockImplementation( (): Promise<AssertionCredential> => { return new Promise(resolve => { resolve({ id: 'foobar', - rawId: toUint8Array('foobar'), + rawId: Buffer.from('foobar', 'ascii'), response: { - authenticatorData: base64js.toByteArray(mockAuthenticatorData), - clientDataJSON: base64js.toByteArray(mockClientDataJSON), - signature: base64js.toByteArray(mockSignature), - userHandle: base64js.toByteArray(mockUserHandle), + authenticatorData: Buffer.from(mockAuthenticatorData, 'ascii'), + clientDataJSON: Buffer.from(mockClientDataJSON, 'ascii'), + signature: Buffer.from(mockSignature, 'ascii'), + userHandle: Buffer.from(mockUserHandle, 'ascii'), }, getClientExtensionResults: () => ({}), type: 'webauthn.get', @@ -88,13 +86,11 @@ test('should return base64-encoded response values', async done => { const response = await startAssertion(goodOpts1); - expect(response).toEqual({ - base64CredentialID: credentialID, - base64AuthenticatorData: mockAuthenticatorData, - base64ClientDataJSON: mockClientDataJSON, - base64Signature: mockSignature, - base64UserHandle: mockUserHandle, - }); + expect(response.rawId).toEqual('Zm9vYmFy'); + expect(response.response.authenticatorData).toEqual('bW9ja0F1dGhlbnRpY2F0b3JEYXRh'); + expect(response.response.clientDataJSON).toEqual('bW9ja0NsaWVudERhdGFKU09O'); + expect(response.response.signature).toEqual('bW9ja1NpZ25hdHVyZQ'); + expect(response.response.userHandle).toEqual('bW9ja1VzZXJIYW5kbGU'); done(); }); diff --git a/packages/browser/src/methods/startAssertion.ts b/packages/browser/src/methods/startAssertion.ts index 093cf30..b65325b 100644 --- a/packages/browser/src/methods/startAssertion.ts +++ b/packages/browser/src/methods/startAssertion.ts @@ -1,11 +1,11 @@ import { PublicKeyCredentialRequestOptionsJSON, - AuthenticatorAssertionResponseJSON, AssertionCredential, + AssertionCredentialJSON, } from '@simplewebauthn/typescript-types'; import toUint8Array from '../helpers/toUint8Array'; -import toBase64String from '../helpers/toBase64String'; +import bufferToBase64URLString from '../helpers/bufferToBase64URLString'; import supportsWebauthn from '../helpers/supportsWebauthn'; import toPublicKeyCredentialDescriptor from '../helpers/toPublicKeyCredentialDescriptor'; @@ -16,7 +16,7 @@ import toPublicKeyCredentialDescriptor from '../helpers/toPublicKeyCredentialDes */ export default async function startAssertion( requestOptionsJSON: PublicKeyCredentialRequestOptionsJSON, -): Promise<AuthenticatorAssertionResponseJSON> { +): Promise<AssertionCredentialJSON> { if (!supportsWebauthn()) { throw new Error('WebAuthn is not supported in this browser'); } @@ -31,25 +31,29 @@ export default async function startAssertion( }; // Wait for the user to complete assertion - const credential = await navigator.credentials.get({ publicKey }); + const credential = await navigator.credentials.get({ publicKey }) as AssertionCredential; if (!credential) { throw new Error('Assertion was not completed'); } - const { response } = credential as AssertionCredential; + const { id, rawId, response, type } = credential; - let base64UserHandle = undefined; + let userHandle = undefined; if (response.userHandle) { - base64UserHandle = toBase64String(response.userHandle); + userHandle = bufferToBase64URLString(response.userHandle); } // Convert values to base64 to make it easier to send back to the server return { - base64CredentialID: credential.id, - base64AuthenticatorData: toBase64String(response.authenticatorData), - base64ClientDataJSON: toBase64String(response.clientDataJSON), - base64Signature: toBase64String(response.signature), - base64UserHandle, + id, + rawId: bufferToBase64URLString(rawId), + response: { + authenticatorData: bufferToBase64URLString(response.authenticatorData), + clientDataJSON: bufferToBase64URLString(response.clientDataJSON), + signature: bufferToBase64URLString(response.signature), + userHandle, + }, + type, }; } diff --git a/packages/browser/src/methods/startAttestation.test.ts b/packages/browser/src/methods/startAttestation.test.ts index ae79235..bf6ab9b 100644 --- a/packages/browser/src/methods/startAttestation.test.ts +++ b/packages/browser/src/methods/startAttestation.test.ts @@ -1,5 +1,3 @@ -import base64js from 'base64-js'; - import { AttestationCredential, PublicKeyCredentialCreationOptionsJSON, @@ -38,7 +36,7 @@ const goodOpts1: PublicKeyCredentialCreationOptionsJSON = { }, timeout: 1, excludeCredentials: [{ - id: 'authIdentifier', + id: 'C0VGlvYFratUdAV1iCw-ULpUW8E-exHPXQChBfyVeJZCMfjMFcwDmOFgoMUz39LoMtCJUBW8WPlLkGT6q8qTCg', type: 'public-key', transports: ['internal'], }], @@ -64,19 +62,22 @@ test('should convert options before passing to navigator.credentials.create(...) await startAttestation(goodOpts1); const argsPublicKey = mockNavigatorCreate.mock.calls[0][0].publicKey; + const credId = argsPublicKey.excludeCredentials[0].id; - expect(argsPublicKey.challenge).toEqual(toUint8Array(goodOpts1.challenge)); - expect(argsPublicKey.user.id).toEqual(toUint8Array(goodOpts1.user.id)); - expect(argsPublicKey.excludeCredentials).toEqual([{ - id: base64js.toByteArray('authIdentifier=='), - type: 'public-key', - transports: ['internal'], - }]) + // Make sure challenge and user.id are converted to Buffers + expect(JSON.stringify(argsPublicKey.challenge)).toEqual('{"0":102,"1":105,"2":122,"3":122}'); + expect(JSON.stringify(argsPublicKey.user.id)).toEqual('{"0":53,"1":54,"2":55,"3":56}'); + + // Confirm construction of excludeCredentials array + expect(credId instanceof ArrayBuffer).toEqual(true); + expect(credId.byteLength).toEqual(64); + expect(argsPublicKey.excludeCredentials[0].type).toEqual('public-key'); + expect(argsPublicKey.excludeCredentials[0].transports).toEqual(['internal']); done(); }); -test('should return base64-encoded response values', async done => { +test('should return base64url-encoded response values', async done => { mockSupportsWebauthn.mockReturnValue(true); mockNavigatorCreate.mockImplementation( @@ -86,8 +87,8 @@ test('should return base64-encoded response values', async done => { id: 'foobar', rawId: toUint8Array('foobar'), response: { - attestationObject: base64js.toByteArray(mockAttestationObject), - clientDataJSON: base64js.toByteArray(mockClientDataJSON), + attestationObject: Buffer.from(mockAttestationObject, 'ascii'), + clientDataJSON: Buffer.from(mockClientDataJSON, 'ascii'), }, getClientExtensionResults: () => ({}), type: 'webauthn.create', @@ -98,10 +99,9 @@ test('should return base64-encoded response values', async done => { const response = await startAttestation(goodOpts1); - expect(response).toEqual({ - base64AttestationObject: mockAttestationObject, - base64ClientDataJSON: mockClientDataJSON, - }); + expect(response.rawId).toEqual('Zm9vYmFy'); + expect(response.response.attestationObject).toEqual('bW9ja0F0dGU'); + expect(response.response.clientDataJSON).toEqual('bW9ja0NsaWU'); done(); }); diff --git a/packages/browser/src/methods/startAttestation.ts b/packages/browser/src/methods/startAttestation.ts index ea05fa8..d5e540f 100644 --- a/packages/browser/src/methods/startAttestation.ts +++ b/packages/browser/src/methods/startAttestation.ts @@ -1,11 +1,11 @@ import { PublicKeyCredentialCreationOptionsJSON, - AuthenticatorAttestationResponseJSON, AttestationCredential, + AttestationCredentialJSON, } from '@simplewebauthn/typescript-types'; import toUint8Array from '../helpers/toUint8Array'; -import toBase64String from '../helpers/toBase64String'; +import bufferToBase64URLString from '../helpers/bufferToBase64URLString'; import supportsWebauthn from '../helpers/supportsWebauthn'; import toPublicKeyCredentialDescriptor from '../helpers/toPublicKeyCredentialDescriptor'; @@ -16,7 +16,7 @@ import toPublicKeyCredentialDescriptor from '../helpers/toPublicKeyCredentialDes */ export default async function startAttestation( creationOptionsJSON: PublicKeyCredentialCreationOptionsJSON, -): Promise<AuthenticatorAttestationResponseJSON> { +): Promise<AttestationCredentialJSON> { if (!supportsWebauthn()) { throw new Error('WebAuthn is not supported in this browser'); } @@ -35,17 +35,22 @@ export default async function startAttestation( }; // Wait for the user to complete attestation - const credential = await navigator.credentials.create({ publicKey }); + const credential = await navigator.credentials.create({ publicKey }) as AttestationCredential; if (!credential) { throw new Error('Attestation was not completed'); } - const { response } = credential as AttestationCredential; + const { id, rawId, response, type } = credential; // Convert values to base64 to make it easier to send back to the server return { - base64AttestationObject: toBase64String(response.attestationObject), - base64ClientDataJSON: toBase64String(response.clientDataJSON), + id, + rawId: bufferToBase64URLString(rawId), + response: { + attestationObject: bufferToBase64URLString(response.attestationObject), + clientDataJSON: bufferToBase64URLString(response.clientDataJSON), + }, + type, }; } diff --git a/packages/server/src/assertion/generateAssertionOptions.test.ts b/packages/server/src/assertion/generateAssertionOptions.test.ts index bd2d48d..9cae665 100644 --- a/packages/server/src/assertion/generateAssertionOptions.test.ts +++ b/packages/server/src/assertion/generateAssertionOptions.test.ts @@ -61,7 +61,7 @@ test('should set extensions if specified', () => { const goodOpts1 = { challenge: 'totallyrandomvalue', - allowedBase64CredentialIDs: [ + allowedCredentialIDs: [ Buffer.from('1234', 'ascii').toString('base64'), Buffer.from('5678', 'ascii').toString('base64'), ], diff --git a/packages/server/src/assertion/generateAssertionOptions.ts b/packages/server/src/assertion/generateAssertionOptions.ts index 9444a54..6645e2e 100644 --- a/packages/server/src/assertion/generateAssertionOptions.ts +++ b/packages/server/src/assertion/generateAssertionOptions.ts @@ -1,10 +1,11 @@ import type { PublicKeyCredentialRequestOptionsJSON, + Base64URLString, } from '@simplewebauthn/typescript-types'; type Options = { challenge: string, - allowedBase64CredentialIDs: string[], + allowedCredentialIDs: Base64URLString[], suggestedTransports?: AuthenticatorTransport[], timeout?: number, userVerification?: UserVerificationRequirement, @@ -15,7 +16,7 @@ type Options = { * Prepare a value to pass into navigator.credentials.get(...) for authenticator "login" * * @param challenge Random string the authenticator needs to sign and pass back - * @param allowedBase64CredentialIDs Array of base64-encoded authenticator IDs registered by the + * @param allowedCredentialIDs Array of base64url-encoded authenticator IDs registered by the * user for assertion * @param timeout How long (in ms) the user can take to complete assertion * @param suggestedTransports Suggested types of authenticators for assertion @@ -28,7 +29,7 @@ export default function generateAssertionOptions( ): PublicKeyCredentialRequestOptionsJSON { const { challenge, - allowedBase64CredentialIDs, + allowedCredentialIDs, suggestedTransports = ['usb', 'ble', 'nfc', 'internal'], timeout = 60000, userVerification, @@ -37,7 +38,7 @@ export default function generateAssertionOptions( return { challenge, - allowCredentials: allowedBase64CredentialIDs.map(id => ({ + allowCredentials: allowedCredentialIDs.map(id => ({ id, type: 'public-key', transports: suggestedTransports, diff --git a/packages/server/src/assertion/verifyAssertionResponse.test.ts b/packages/server/src/assertion/verifyAssertionResponse.test.ts index 5895b66..9da06ce 100644 --- a/packages/server/src/assertion/verifyAssertionResponse.test.ts +++ b/packages/server/src/assertion/verifyAssertionResponse.test.ts @@ -48,7 +48,7 @@ test('should return authenticator info after verification', () => { expect(verification.authenticatorInfo.counter).toEqual(144); expect(verification.authenticatorInfo.base64CredentialID).toEqual( - authenticator.base64CredentialID, + authenticator.credentialID, ); }); @@ -111,24 +111,28 @@ test('should throw error if previous counter value is not less than in response' }); const assertionResponse = { - base64CredentialID: - 'KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Px' + 'g6jo_o0hYiew', - base64AuthenticatorData: 'PdxHEOnAiLIp26idVjIguzn3Ipr_RlsKZWsa-5qK-KABAAAAkA==', - base64ClientDataJSON: - 'eyJjaGFsbGVuZ2UiOiJkRzkwWVd4c2VWVnVhWEYxWlZaaGJIVmxSWFpsY25sVWFXMWwiLCJj' + - 'bGllbnRFeHRlbnNpb25zIjp7fSwiaGFzaEFsZ29yaXRobSI6IlNIQS0yNTYiLCJvcmlnaW4iOiJodHRwczovL2Rldi5k' + - 'b250bmVlZGEucHciLCJ0eXBlIjoid2ViYXV0aG4uZ2V0In0=', - base64Signature: - 'MEUCIQDYXBOpCWSWq2Ll4558GJKD2RoWg958lvJSB_GdeokxogIgWuEVQ7ee6AswQY0OsuQ6y8Ks6' + - 'jhd45bDx92wjXKs900=', + id: 'KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Pxg6jo_o0hYiew', + rawId: '', + response: { + authenticatorData: 'PdxHEOnAiLIp26idVjIguzn3Ipr_RlsKZWsa-5qK-KABAAAAkA==', + clientDataJSON: + 'eyJjaGFsbGVuZ2UiOiJkRzkwWVd4c2VWVnVhWEYxWlZaaGJIVmxSWFpsY25sVWFXMWwiLCJj' + + 'bGllbnRFeHRlbnNpb25zIjp7fSwiaGFzaEFsZ29yaXRobSI6IlNIQS0yNTYiLCJvcmlnaW4iOiJodHRwczovL2Rldi5k' + + 'b250bmVlZGEucHciLCJ0eXBlIjoid2ViYXV0aG4uZ2V0In0=', + signature: + 'MEUCIQDYXBOpCWSWq2Ll4558GJKD2RoWg958lvJSB_GdeokxogIgWuEVQ7ee6AswQY0OsuQ6y8Ks6' + + 'jhd45bDx92wjXKs900=', + }, + getClientExtensionResults: () => ({}), + type: 'webauthn.get', }; const assertionChallenge = 'totallyUniqueValueEveryTime'; const assertionOrigin = 'https://dev.dontneeda.pw'; const authenticator = { - base64PublicKey: + publicKey: 'BIheFp-u6GvFT2LNGovf3ZrT0iFVBsA_76rRysxRG9A18WGeA6hPmnab0HAViUYVRkwTNcN77QBf_' + 'RR0dv3lIvQ', - base64CredentialID: + credentialID: 'KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Px' + 'g6jo_o0hYiew', counter: 0, }; diff --git a/packages/server/src/assertion/verifyAssertionResponse.ts b/packages/server/src/assertion/verifyAssertionResponse.ts index 24ee17e..7d13271 100644 --- a/packages/server/src/assertion/verifyAssertionResponse.ts +++ b/packages/server/src/assertion/verifyAssertionResponse.ts @@ -1,8 +1,7 @@ import base64url from 'base64url'; import { - AuthenticatorAssertionResponseJSON, + AssertionCredentialJSON, AuthenticatorDevice, - VerifiedAssertion, } from '@simplewebauthn/typescript-types'; import decodeClientDataJSON from '../helpers/decodeClientDataJSON'; @@ -14,19 +13,19 @@ import parseAuthenticatorData from '../helpers/parseAuthenticatorData'; /** * Verify that the user has legitimately completed the login process * - * @param response Authenticator assertion response with base64-encoded values + * @param response Authenticator assertion response with base64url-encoded values * @param expectedChallenge The random value provided to generateAssertionOptions for the * authenticator to sign * @param expectedOrigin Expected URL of website assertion should have occurred on */ export default function verifyAssertionResponse( - response: AuthenticatorAssertionResponseJSON, + credential: AssertionCredentialJSON, expectedChallenge: string, expectedOrigin: string, authenticator: AuthenticatorDevice, ): VerifiedAssertion { - const { base64AuthenticatorData, base64ClientDataJSON, base64Signature } = response; - const clientDataJSON = decodeClientDataJSON(base64ClientDataJSON); + const { response } = credential; + const clientDataJSON = decodeClientDataJSON(response.clientDataJSON); const { type, origin, challenge } = clientDataJSON; @@ -50,7 +49,7 @@ export default function verifyAssertionResponse( throw new Error(`Unexpected assertion type: ${type}`); } - const authDataBuffer = base64url.toBuffer(base64AuthenticatorData); + const authDataBuffer = base64url.toBuffer(response.authenticatorData); const authDataStruct = parseAuthenticatorData(authDataBuffer); const { flags, counter } = authDataStruct; @@ -70,19 +69,37 @@ export default function verifyAssertionResponse( const { rpIdHash, flagsBuf, counterBuf } = authDataStruct; - const clientDataHash = toHash(base64url.toBuffer(base64ClientDataJSON)); + const clientDataHash = toHash(base64url.toBuffer(response.clientDataJSON)); const signatureBase = Buffer.concat([rpIdHash, flagsBuf, counterBuf, clientDataHash]); - const publicKey = convertASN1toPEM(base64url.toBuffer(authenticator.base64PublicKey)); - const signature = base64url.toBuffer(base64Signature); + const publicKey = convertASN1toPEM(base64url.toBuffer(authenticator.publicKey)); + const signature = base64url.toBuffer(response.signature); const toReturn = { verified: verifySignature(signature, signatureBase, publicKey), authenticatorInfo: { counter, - base64CredentialID: response.base64CredentialID, + base64CredentialID: credential.id, }, }; return toReturn; } + +/** + * Result of assertion verification + * + * @param verified If the assertion response could be verified + * @param authenticatorInfo.base64CredentialID The ID of the authenticator used during assertion. + * Should be used to identify which DB authenticator entry needs its `counter` updated to the value + * below + * @param authenticatorInfo.counter The number of times the authenticator identified above reported + * it has been used. **Should be kept in a DB for later reference to help prevent replay attacks!** + */ +export type VerifiedAssertion = { + verified: boolean; + authenticatorInfo: { + counter: number; + base64CredentialID: string; + }; +}; diff --git a/packages/server/src/attestation/generateAttestationOptions.test.ts b/packages/server/src/attestation/generateAttestationOptions.test.ts index 2f5c439..2b83fa9 100644 --- a/packages/server/src/attestation/generateAttestationOptions.test.ts +++ b/packages/server/src/attestation/generateAttestationOptions.test.ts @@ -49,7 +49,7 @@ test('should map excluded credential IDs if specified', () => { challenge: 'totallyrandomvalue', userID: '1234', userName: 'usernameHere', - excludedBase64CredentialIDs: ['someIDhere'], + excludedCredentialIDs: ['someIDhere'], }); expect(options.excludeCredentials).toEqual([{ diff --git a/packages/server/src/attestation/generateAttestationOptions.ts b/packages/server/src/attestation/generateAttestationOptions.ts index 89ac86a..d142740 100644 --- a/packages/server/src/attestation/generateAttestationOptions.ts +++ b/packages/server/src/attestation/generateAttestationOptions.ts @@ -1,5 +1,6 @@ -import { +import type { PublicKeyCredentialCreationOptionsJSON, + Base64URLString, } from '@simplewebauthn/typescript-types'; type Options = { @@ -11,7 +12,7 @@ type Options = { userDisplayName?: string, timeout?: number, attestationType?: AttestationConveyancePreference, - excludedBase64CredentialIDs?: string[], + excludedCredentialIDs?: Base64URLString[], suggestedTransports?: AuthenticatorTransport[], authenticatorSelection?: AuthenticatorSelectionCriteria, extensions?: AuthenticationExtensionsClientInputs, @@ -30,7 +31,7 @@ type Options = { * @param userDisplayName User's actual name * @param timeout How long (in ms) the user can take to complete attestation * @param attestationType Specific attestation statement - * @param excludedBase64CredentialIDs Array of base64-encoded authenticator IDs registered by the + * @param excludedCredentialIDs Array of base64url-encoded authenticator IDs registered by the * user so the user can't register the same credential multiple times * @param suggestedTransports Suggested types of authenticators for attestation * @param authenticatorSelection Advanced criteria for restricting the types of authenticators that @@ -49,7 +50,7 @@ export default function generateAttestationOptions( userDisplayName = userName, timeout = 60000, attestationType = 'none', - excludedBase64CredentialIDs = [], + excludedCredentialIDs = [], suggestedTransports = ['usb', 'ble', 'nfc', 'internal'], authenticatorSelection, extensions, @@ -74,7 +75,7 @@ export default function generateAttestationOptions( ], timeout, attestation: attestationType, - excludeCredentials: excludedBase64CredentialIDs.map((id) => ({ + excludeCredentials: excludedCredentialIDs.map((id) => ({ id, type: 'public-key', transports: suggestedTransports, diff --git a/packages/server/src/attestation/verifications/verifyAndroidSafetyNet.ts b/packages/server/src/attestation/verifications/verifyAndroidSafetyNet.ts index 0f604d0..a5dc89a 100644 --- a/packages/server/src/attestation/verifications/verifyAndroidSafetyNet.ts +++ b/packages/server/src/attestation/verifications/verifyAndroidSafetyNet.ts @@ -1,11 +1,7 @@ import base64url from 'base64url'; -import { - AttestationObject, - VerifiedAttestation, - SafetyNetJWTHeader, - SafetyNetJWTPayload, - SafetyNetJWTSignature, -} from '@simplewebauthn/typescript-types'; + +import type { AttestationObject } from '../../helpers/decodeAttestationObject'; +import type { VerifiedAttestation } from '../verifyAttestationResponse'; import toHash from '../../helpers/toHash'; import verifySignature from '../../helpers/verifySignature'; @@ -151,3 +147,20 @@ const GlobalSignRootCAR2 = '7mpM0sYmsL4h4hO291xNBrBVNpGP-DTKqttVCL1OmLNIG-6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavSot-3i9DAgBkcRcA' + 'tjOj4LaR0VknFBbVPFd5uRHg5h6h-u_N5GJG79G-dwfCMNYxdAfvDbbnvRG15RjF-Cv6pgsH_76tuIMRQyV-dTZsXjAzlA' + 'cmgQWpzU_qlULRuJQ_7TBj0_VLZjmmx6BEP3ojY-x1J96relc8geMJgEtslQIxq_H5COEBkEveegeGTLg'; + +type SafetyNetJWTHeader = { + alg: 'string'; + x5c: string[]; +}; + +type SafetyNetJWTPayload = { + nonce: string; + timestampMs: number; + apkPackageName: string; + apkDigestSha256: string; + ctsProfileMatch: boolean; + apkCertificateDigestSha256: string[]; + basicIntegrity: boolean; +}; + +type SafetyNetJWTSignature = string; diff --git a/packages/server/src/attestation/verifications/verifyFIDOU2F.ts b/packages/server/src/attestation/verifications/verifyFIDOU2F.ts index e518dc8..5842a3c 100644 --- a/packages/server/src/attestation/verifications/verifyFIDOU2F.ts +++ b/packages/server/src/attestation/verifications/verifyFIDOU2F.ts @@ -1,5 +1,7 @@ import base64url from 'base64url'; -import { AttestationObject, VerifiedAttestation } from '@simplewebauthn/typescript-types'; + +import type { AttestationObject } from '../../helpers/decodeAttestationObject'; +import type { VerifiedAttestation } from '../verifyAttestationResponse'; import toHash from '../../helpers/toHash'; import convertCOSEtoPKCS from '../../helpers/convertCOSEtoPKCS'; diff --git a/packages/server/src/attestation/verifications/verifyNone.ts b/packages/server/src/attestation/verifications/verifyNone.ts index 423f4fd..66fd7da 100644 --- a/packages/server/src/attestation/verifications/verifyNone.ts +++ b/packages/server/src/attestation/verifications/verifyNone.ts @@ -1,5 +1,7 @@ import base64url from 'base64url'; -import { AttestationObject, VerifiedAttestation } from '@simplewebauthn/typescript-types'; + +import type { AttestationObject } from '../../helpers/decodeAttestationObject'; +import type { VerifiedAttestation } from '../verifyAttestationResponse'; import convertCOSEtoPKCS from '../../helpers/convertCOSEtoPKCS'; import parseAuthenticatorData from '../../helpers/parseAuthenticatorData'; diff --git a/packages/server/src/attestation/verifications/verifyPacked.ts b/packages/server/src/attestation/verifications/verifyPacked.ts index f7b9932..16acdfd 100644 --- a/packages/server/src/attestation/verifications/verifyPacked.ts +++ b/packages/server/src/attestation/verifications/verifyPacked.ts @@ -2,14 +2,14 @@ import base64url from 'base64url'; import cbor from 'cbor'; import elliptic from 'elliptic'; import NodeRSA, { SigningSchemeHash } from 'node-rsa'; -import { - AttestationObject, - VerifiedAttestation, - COSEKEYS, - COSEPublicKey as COSEPublicKeyType, -} from '@simplewebauthn/typescript-types'; -import convertCOSEtoPKCS from '../../helpers/convertCOSEtoPKCS'; +import type { AttestationObject } from '../../helpers/decodeAttestationObject'; +import type { VerifiedAttestation } from '../verifyAttestationResponse'; + +import convertCOSEtoPKCS, { + COSEKEYS, + COSEPublicKey as COSEPublicKeyType +} from '../../helpers/convertCOSEtoPKCS'; import toHash from '../../helpers/toHash'; import convertASN1toPEM from '../../helpers/convertASN1toPEM'; import getCertificateInfo from '../../helpers/getCertificateInfo'; diff --git a/packages/server/src/attestation/verifyAttestationResponse.test.ts b/packages/server/src/attestation/verifyAttestationResponse.test.ts index 375264a..1e4cc0d 100644 --- a/packages/server/src/attestation/verifyAttestationResponse.test.ts +++ b/packages/server/src/attestation/verifyAttestationResponse.test.ts @@ -161,172 +161,203 @@ test('should throw if an unexpected attestation format is specified', () => { }); const attestationFIDOU2F = { - base64AttestationObject: - 'o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEgwRgIhAK40WxA0t7py7AjEXvwGw' + - 'TlmqlvrOks5g9lf+9zXzRiVAiEA3bv60xyXveKDOusYzniD7CDSostCet9PYK7FLdnTdZNjeDVjgVkCwTCCAr0wg' + - 'gGloAMCAQICBCrnYmMwDQYJKoZIhvcNAQELBQAwLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhb' + - 'CA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMG4xCzAJBgNVBAYTAlNFMRIwEAYDV' + - 'QQKDAlZdWJpY28gQUIxIjAgBgNVBAsMGUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xJzAlBgNVBAMMHll1Ymljb' + - 'yBVMkYgRUUgU2VyaWFsIDcxOTgwNzA3NTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCoDhl5gQ9meEf8QqiVUV' + - '4S/Ca+Oax47MhcpIW9VEhqM2RDTmd3HaL3+SnvH49q8YubSRp/1Z1uP+okMynSGnj+jbDBqMCIGCSsGAQQBgsQKA' + - 'gQVMS4zLjYuMS40LjEuNDE0ODIuMS4xMBMGCysGAQQBguUcAgEBBAQDAgQwMCEGCysGAQQBguUcAQEEBBIEEG1Eu' + - 'pv27C5JuTAMj+kgy3MwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAclfQPNzD4RVphJDW+A75W1MHI' + - '3PZ5kcyYysR3Nx3iuxr1ZJtB+F7nFQweI3jL05HtFh2/4xVIgKb6Th4eVcjMecncBaCinEbOcdP1sEli9Hk2eVm1' + - 'XB5A0faUjXAPw/+QLFCjgXG6ReZ5HVUcWkB7riLsFeJNYitiKrTDXFPLy+sNtVNutcQnFsCerDKuM81TvEAigkIb' + - 'KCGlq8M/NvBg5j83wIxbCYiyV7mIr3RwApHieShzLdJo1S6XydgQjC+/64G5r8C+8AVvNFR3zXXCpio5C3KRIj88' + - 'HEEIYjf6h1fdLfqeIsq+cUUqbq5T+c4nNoZUZCysTB9v5EY4akp+GhhdXRoRGF0YVjEAbElFazplpnc037DORGDZ' + - 'NjDq86cN9vm6+APoAM20wtBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQGFYevaR71ptU5YtXOSnVzPQTsGgK+gLiBKnq' + - 'PWBmZXNRvjISqlLxiwApzlrfkTc3lEMYMatjeACCnsijOkNEGOlAQIDJiABIVggdWLG6UvGyHFw/k/bv6/k6z/LL' + - 'gSO5KXzXw2EcUxkEX8iWCBeaVLz/cbyoKvRIg/q+q7tan0VN+i3WR0BOBCcuNP7yw==', - base64ClientDataJSON: - 'eyJjaGFsbGVuZ2UiOiJVMmQ0TjNZME0wOU1jbGRQYjFSNVpFeG5UbG95IiwiY2xpZW50' + - 'RXh0ZW5zaW9ucyI6e30sImhhc2hBbGdvcml0aG0iOiJTSEEtMjU2Iiwib3JpZ2luIjoiaHR0cHM6Ly9jbG92ZXIu' + - 'bWlsbGVydGltZS5kZXY6MzAwMCIsInR5cGUiOiJ3ZWJhdXRobi5jcmVhdGUifQ==', + id: 'YVh69pHvWm1Tli1c5KdXM9BOwaAr6AuIEqeo9YGZlc1G-MhKqUvGLACnOWt-RNzeUQxgxq2N4AIKeyKM6Q0QYw', + rawId: 'YVh69pHvWm1Tli1c5KdXM9BOwaAr6AuIEqeo9YGZlc1G+MhKqUvGLACnOWt+RNzeUQxgxq2N4AIKeyKM6Q0QYw==', + response: { + attestationObject: + 'o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEgwRgIhAK40WxA0t7py7AjEXvwGw' + + 'TlmqlvrOks5g9lf+9zXzRiVAiEA3bv60xyXveKDOusYzniD7CDSostCet9PYK7FLdnTdZNjeDVjgVkCwTCCAr0wg' + + 'gGloAMCAQICBCrnYmMwDQYJKoZIhvcNAQELBQAwLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhb' + + 'CA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMG4xCzAJBgNVBAYTAlNFMRIwEAYDV' + + 'QQKDAlZdWJpY28gQUIxIjAgBgNVBAsMGUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xJzAlBgNVBAMMHll1Ymljb' + + 'yBVMkYgRUUgU2VyaWFsIDcxOTgwNzA3NTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCoDhl5gQ9meEf8QqiVUV' + + '4S/Ca+Oax47MhcpIW9VEhqM2RDTmd3HaL3+SnvH49q8YubSRp/1Z1uP+okMynSGnj+jbDBqMCIGCSsGAQQBgsQKA' + + 'gQVMS4zLjYuMS40LjEuNDE0ODIuMS4xMBMGCysGAQQBguUcAgEBBAQDAgQwMCEGCysGAQQBguUcAQEEBBIEEG1Eu' + + 'pv27C5JuTAMj+kgy3MwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAclfQPNzD4RVphJDW+A75W1MHI' + + '3PZ5kcyYysR3Nx3iuxr1ZJtB+F7nFQweI3jL05HtFh2/4xVIgKb6Th4eVcjMecncBaCinEbOcdP1sEli9Hk2eVm1' + + 'XB5A0faUjXAPw/+QLFCjgXG6ReZ5HVUcWkB7riLsFeJNYitiKrTDXFPLy+sNtVNutcQnFsCerDKuM81TvEAigkIb' + + 'KCGlq8M/NvBg5j83wIxbCYiyV7mIr3RwApHieShzLdJo1S6XydgQjC+/64G5r8C+8AVvNFR3zXXCpio5C3KRIj88' + + 'HEEIYjf6h1fdLfqeIsq+cUUqbq5T+c4nNoZUZCysTB9v5EY4akp+GhhdXRoRGF0YVjEAbElFazplpnc037DORGDZ' + + 'NjDq86cN9vm6+APoAM20wtBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQGFYevaR71ptU5YtXOSnVzPQTsGgK+gLiBKnq' + + 'PWBmZXNRvjISqlLxiwApzlrfkTc3lEMYMatjeACCnsijOkNEGOlAQIDJiABIVggdWLG6UvGyHFw/k/bv6/k6z/LL' + + 'gSO5KXzXw2EcUxkEX8iWCBeaVLz/cbyoKvRIg/q+q7tan0VN+i3WR0BOBCcuNP7yw==', + clientDataJSON: + 'eyJjaGFsbGVuZ2UiOiJVMmQ0TjNZME0wOU1jbGRQYjFSNVpFeG5UbG95IiwiY2xpZW50' + + 'RXh0ZW5zaW9ucyI6e30sImhhc2hBbGdvcml0aG0iOiJTSEEtMjU2Iiwib3JpZ2luIjoiaHR0cHM6Ly9jbG92ZXIu' + + 'bWlsbGVydGltZS5kZXY6MzAwMCIsInR5cGUiOiJ3ZWJhdXRobi5jcmVhdGUifQ==', + }, + getClientExtensionResults: () => ({}), + type: 'webauthn.create', }; const attestationFIDOU2FChallenge = 'Sgx7v43OLrWOoTydLgNZ2'; const attestationPacked = { - base64AttestationObject: - 'o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIhANvrPZMUFrl_rvlgR' + - 'qz6lCPlF6B4y885FYUCCrhrzAYXAiAb4dQKXbP3IimsTTadkwXQlrRVdxzlbmPXt847-Oh6r2hhdXRoRGF0YVjhP' + - 'dxHEOnAiLIp26idVjIguzn3Ipr_RlsKZWsa-5qK-KBFXsOO-a3OAAI1vMYKZIsLJfHwVQMAXQGE4WNXLCDWOCa2x' + - '8hpqk5dZy_xdc4wBd4UgCJ4M_JAHI7oJgDDVb8WUcKqRB_mzRxwCL9vdTl-ZKPXg3_-Zrt1Adgb7EnK9ivqaTOKM' + - 'DqRrKsIObWYJaqpsSJtUKUBAgMmIAEhWCBKMVVaivqCBpqqAxMjuCo5jMeUdh3jDOC0EF4fLBNNTyJYILc7rqDDe' + - 'X1pwCLrl3ZX7IThrtZNwKQVLQyfHiorqP-n', - base64ClientDataJSON: - 'eyJjaGFsbGVuZ2UiOiJjelpRU1dKQ2JsQlFibkpIVGxOQ2VFNWtkRVJ5VkRkVmNsWlpT' + - 'a3M1U0UwIiwib3JpZ2luIjoiaHR0cHM6Ly9kZXYuZG9udG5lZWRhLnB3IiwidHlwZSI6IndlYmF1dGhuLmNyZWF0' + - 'ZSJ9', + id: '', + rawId: '', + response: { + attestationObject: + 'o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEcwRQIhANvrPZMUFrl_rvlgR' + + 'qz6lCPlF6B4y885FYUCCrhrzAYXAiAb4dQKXbP3IimsTTadkwXQlrRVdxzlbmPXt847-Oh6r2hhdXRoRGF0YVjhP' + + 'dxHEOnAiLIp26idVjIguzn3Ipr_RlsKZWsa-5qK-KBFXsOO-a3OAAI1vMYKZIsLJfHwVQMAXQGE4WNXLCDWOCa2x' + + '8hpqk5dZy_xdc4wBd4UgCJ4M_JAHI7oJgDDVb8WUcKqRB_mzRxwCL9vdTl-ZKPXg3_-Zrt1Adgb7EnK9ivqaTOKM' + + 'DqRrKsIObWYJaqpsSJtUKUBAgMmIAEhWCBKMVVaivqCBpqqAxMjuCo5jMeUdh3jDOC0EF4fLBNNTyJYILc7rqDDe' + + 'X1pwCLrl3ZX7IThrtZNwKQVLQyfHiorqP-n', + clientDataJSON: + 'eyJjaGFsbGVuZ2UiOiJjelpRU1dKQ2JsQlFibkpIVGxOQ2VFNWtkRVJ5VkRkVmNsWlpT' + + 'a3M1U0UwIiwib3JpZ2luIjoiaHR0cHM6Ly9kZXYuZG9udG5lZWRhLnB3IiwidHlwZSI6IndlYmF1dGhuLmNyZWF0' + + 'ZSJ9', + }, + getClientExtensionResults: () => ({}), + type: 'webauthn.create', }; const attestationPackedChallenge = 's6PIbBnPPnrGNSBxNdtDrT7UrVYJK9HM'; const attestationPackedX5C = { - base64AttestationObject: - 'o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEcwRQIhAIMt_hGMtdgpIVIwMOeKK' + - 'w0IkUUFkXSY8arKh3Q0c5QQAiB9Sv9JavAEmppeH_XkZjB7TFM3jfxsgl97iIkvuJOUImN4NWOBWQLBMIICvTCCAaWgA' + - 'wIBAgIEKudiYzANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwM' + - 'DYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowbjELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1Ymljb' + - 'yBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEnMCUGA1UEAwweWXViaWNvIFUyRiBFRSBTZXJpY' + - 'WwgNzE5ODA3MDc1MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKgOGXmBD2Z4R_xCqJVRXhL8Jr45rHjsyFykhb1USG' + - 'ozZENOZ3cdovf5Ke8fj2rxi5tJGn_VnW4_6iQzKdIaeP6NsMGowIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4M' + - 'i4xLjEwEwYLKwYBBAGC5RwCAQEEBAMCBDAwIQYLKwYBBAGC5RwBAQQEEgQQbUS6m_bsLkm5MAyP6SDLczAMBgNVHRMBA' + - 'f8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQByV9A83MPhFWmEkNb4DvlbUwcjc9nmRzJjKxHc3HeK7GvVkm0H4XucVDB4j' + - 'eMvTke0WHb_jFUiApvpOHh5VyMx5ydwFoKKcRs5x0_WwSWL0eTZ5WbVcHkDR9pSNcA_D_5AsUKOBcbpF5nkdVRxaQHuu' + - 'IuwV4k1iK2IqtMNcU8vL6w21U261xCcWwJ6sMq4zzVO8QCKCQhsoIaWrwz828GDmPzfAjFsJiLJXuYivdHACkeJ5KHMt' + - '0mjVLpfJ2BCML7_rgbmvwL7wBW80VHfNdcKmKjkLcpEiPzwcQQhiN_qHV90t-p4iyr5xRSpurlP5zic2hlRkLKxMH2_k' + - 'RjhqSn4aGF1dGhEYXRhWMQ93EcQ6cCIsinbqJ1WMiC7Ofcimv9GWwplaxr7mor4oEEAAAAcbUS6m_bsLkm5MAyP6SDLc' + - 'wBA4rrvMciHCkdLQ2HghazIp1sMc8TmV8W8RgoX-x8tqV_1AmlqWACqUK8mBGLandr-htduQKPzgb2yWxOFV56TlqUBA' + - 'gMmIAEhWCBsJbGAjckW-AA_XMk8OnB-VUvrs35ZpjtVJXRhnvXiGiJYIL2ncyg_KesCi44GH8UcZXYwjBkVdGMjNd6LF' + - 'myiD6xf', - base64ClientDataJSON: - 'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiZEc5MFlXeHNlVlZ1YVhG' + - 'MVpWWmhiSFZsUlhabGNubFVhVzFsIiwib3JpZ2luIjoiaHR0cHM6Ly9kZXYuZG9udG5lZWRhLnB3In0=', + // TODO: Grab these from another iPhone attestation + id: '', + rawId: '', + response: { + attestationObject: + 'o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEcwRQIhAIMt_hGMtdgpIVIwMOeKK' + + 'w0IkUUFkXSY8arKh3Q0c5QQAiB9Sv9JavAEmppeH_XkZjB7TFM3jfxsgl97iIkvuJOUImN4NWOBWQLBMIICvTCCAaWgA' + + 'wIBAgIEKudiYzANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwM' + + 'DYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowbjELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1Ymljb' + + 'yBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEnMCUGA1UEAwweWXViaWNvIFUyRiBFRSBTZXJpY' + + 'WwgNzE5ODA3MDc1MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKgOGXmBD2Z4R_xCqJVRXhL8Jr45rHjsyFykhb1USG' + + 'ozZENOZ3cdovf5Ke8fj2rxi5tJGn_VnW4_6iQzKdIaeP6NsMGowIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4M' + + 'i4xLjEwEwYLKwYBBAGC5RwCAQEEBAMCBDAwIQYLKwYBBAGC5RwBAQQEEgQQbUS6m_bsLkm5MAyP6SDLczAMBgNVHRMBA' + + 'f8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQByV9A83MPhFWmEkNb4DvlbUwcjc9nmRzJjKxHc3HeK7GvVkm0H4XucVDB4j' + + 'eMvTke0WHb_jFUiApvpOHh5VyMx5ydwFoKKcRs5x0_WwSWL0eTZ5WbVcHkDR9pSNcA_D_5AsUKOBcbpF5nkdVRxaQHuu' + + 'IuwV4k1iK2IqtMNcU8vL6w21U261xCcWwJ6sMq4zzVO8QCKCQhsoIaWrwz828GDmPzfAjFsJiLJXuYivdHACkeJ5KHMt' + + '0mjVLpfJ2BCML7_rgbmvwL7wBW80VHfNdcKmKjkLcpEiPzwcQQhiN_qHV90t-p4iyr5xRSpurlP5zic2hlRkLKxMH2_k' + + 'RjhqSn4aGF1dGhEYXRhWMQ93EcQ6cCIsinbqJ1WMiC7Ofcimv9GWwplaxr7mor4oEEAAAAcbUS6m_bsLkm5MAyP6SDLc' + + 'wBA4rrvMciHCkdLQ2HghazIp1sMc8TmV8W8RgoX-x8tqV_1AmlqWACqUK8mBGLandr-htduQKPzgb2yWxOFV56TlqUBA' + + 'gMmIAEhWCBsJbGAjckW-AA_XMk8OnB-VUvrs35ZpjtVJXRhnvXiGiJYIL2ncyg_KesCi44GH8UcZXYwjBkVdGMjNd6LF' + + 'myiD6xf', + clientDataJSON: + 'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiZEc5MFlXeHNlVlZ1YVhG' + + 'MVpWWmhiSFZsUlhabGNubFVhVzFsIiwib3JpZ2luIjoiaHR0cHM6Ly9kZXYuZG9udG5lZWRhLnB3In0=', + }, + getClientExtensionResults: () => ({}), + type: 'webauthn.create', }; const attestationPackedX5CChallenge = 'totallyUniqueValueEveryTime'; const attestationNone = { - base64AttestationObject: - 'o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjFPdxHEOnAiLIp26idVjIguzn3I' + - 'pr_RlsKZWsa-5qK-KBFAAAAAAAAAAAAAAAAAAAAAAAAAAAAQQHSlyRHIdWleVqO24-6ix7JFWODqDWo_arvEz3Se' + - '5EgIFHkcVjZ4F5XDSBreIHsWRilRnKmaaqlqK3V2_4XtYs2pQECAyYgASFYID5PQTZQQg6haZFQWFzqfAOyQ_ENs' + - 'MH8xxQ4GRiNPsqrIlggU8IVUOV8qpgk_Jh-OTaLuZL52KdX1fTht07X4DiQPow', - base64ClientDataJSON: - 'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiYUVWalkxQlhkWHBw' + - 'VURBd1NEQndOV2Q0YURKZmRUVmZVRU0wVG1WWloyUSIsIm9yaWdpbiI6Imh0dHBzOlwvXC9kZXYuZG9udG5lZWRh' + - 'LnB3IiwiYW5kcm9pZFBhY2thZ2VOYW1lIjoib3JnLm1vemlsbGEuZmlyZWZveCJ9', + id: 'AdKXJEch1aV5Wo7bj7qLHskVY4OoNaj9qu8TPdJ7kSAgUeRxWNngXlcNIGt4gexZGKVGcqZpqqWordXb_he1izY', + rawId: 'AdKXJEch1aV5Wo7bj7qLHskVY4OoNaj9qu8TPdJ7kSAgUeRxWNngXlcNIGt4gexZGKVGcqZpqqWordXb_he1izY', + response: { + attestationObject: + 'o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjFPdxHEOnAiLIp26idVjIguzn3I' + + 'pr_RlsKZWsa-5qK-KBFAAAAAAAAAAAAAAAAAAAAAAAAAAAAQQHSlyRHIdWleVqO24-6ix7JFWODqDWo_arvEz3Se' + + '5EgIFHkcVjZ4F5XDSBreIHsWRilRnKmaaqlqK3V2_4XtYs2pQECAyYgASFYID5PQTZQQg6haZFQWFzqfAOyQ_ENs' + + 'MH8xxQ4GRiNPsqrIlggU8IVUOV8qpgk_Jh-OTaLuZL52KdX1fTht07X4DiQPow', + clientDataJSON: + 'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiYUVWalkxQlhkWHBw' + + 'VURBd1NEQndOV2Q0YURKZmRUVmZVRU0wVG1WWloyUSIsIm9yaWdpbiI6Imh0dHBzOlwvXC9kZXYuZG9udG5lZWRh' + + 'LnB3IiwiYW5kcm9pZFBhY2thZ2VOYW1lIjoib3JnLm1vemlsbGEuZmlyZWZveCJ9', + }, + getClientExtensionResults: () => ({}), + type: 'webauthn.create', }; const attestationNoneChallenge = 'hEccPWuziP00H0p5gxh2_u5_PC4NeYgd'; const attestationAndroidSafetyNet = { - base64AttestationObject: - 'o2NmbXRxYW5kcm9pZC1zYWZldHluZXRnYXR0U3RtdKJjdmVyaDE3MTIyMDM3aHJlc' + - '3BvbnNlWRS9ZXlKaGJHY2lPaUpTVXpJMU5pSXNJbmcxWXlJNld5Sk5TVWxHYTJwRFEwSkljV2RCZDBsQ1FXZEpVV' + - 'kpZY205T01GcFBaRkpyUWtGQlFVRkJRVkIxYm5wQlRrSm5hM0ZvYTJsSE9YY3dRa0ZSYzBaQlJFSkRUVkZ6ZDBOU' + - 'ldVUldVVkZIUlhkS1ZsVjZSV1ZOUW5kSFFURlZSVU5vVFZaU01qbDJXako0YkVsR1VubGtXRTR3U1VaT2JHTnVXb' + - 'kJaTWxaNlRWSk5kMFZSV1VSV1VWRkVSWGR3U0ZaR1RXZFJNRVZuVFZVNGVFMUNORmhFVkVVMFRWUkJlRTFFUVROT' + - 'lZHc3dUbFp2V0VSVVJUVk5WRUYzVDFSQk0wMVVhekJPVm05M1lrUkZURTFCYTBkQk1WVkZRbWhOUTFaV1RYaEZla' + - '0ZTUW1kT1ZrSkJaMVJEYTA1b1lrZHNiV0l6U25WaFYwVjRSbXBCVlVKblRsWkNRV05VUkZVeGRtUlhOVEJaVjJ4M' + - 'VNVWmFjRnBZWTNoRmVrRlNRbWRPVmtKQmIxUkRhMlIyWWpKa2MxcFRRazFVUlUxNFIzcEJXa0puVGxaQ1FVMVVSV' + - 'zFHTUdSSFZucGtRelZvWW0xU2VXSXliR3RNYlU1MllsUkRRMEZUU1hkRVVWbEtTMjlhU1doMlkwNUJVVVZDUWxGQ' + - 'lJHZG5SVkJCUkVORFFWRnZRMmRuUlVKQlRtcFlhM293WlVzeFUwVTBiU3N2UnpWM1QyOHJXRWRUUlVOeWNXUnVPR' + - 'Gh6UTNCU04yWnpNVFJtU3pCU2FETmFRMWxhVEVaSWNVSnJOa0Z0V2xaM01rczVSa2N3VHpseVVsQmxVVVJKVmxKN' + - 'VJUTXdVWFZ1VXpsMVowaEROR1ZuT1c5MmRrOXRLMUZrV2pKd09UTllhSHAxYmxGRmFGVlhXRU40UVVSSlJVZEtTe' + - 'k5UTW1GQlpucGxPVGxRVEZNeU9XaE1ZMUYxV1ZoSVJHRkROMDlhY1U1dWIzTnBUMGRwWm5NNGRqRnFhVFpJTDNob' + - '2JIUkRXbVV5YkVvck4wZDFkSHBsZUV0d2VIWndSUzkwV2xObVlsazVNRFZ4VTJ4Q2FEbG1jR293TVRWamFtNVJSb' + - 'XRWYzBGVmQyMUxWa0ZWZFdWVmVqUjBTMk5HU3pSd1pYWk9UR0Y0UlVGc0swOXJhV3hOZEVsWlJHRmpSRFZ1Wld3M' + - 'GVFcHBlWE0wTVROb1lXZHhWekJYYUdnMVJsQXpPV2hIYXpsRkwwSjNVVlJxWVhwVGVFZGtkbGd3YlRaNFJsbG9hQ' + - 'zh5VmsxNVdtcFVORXQ2VUVwRlEwRjNSVUZCWVU5RFFXeG5kMmRuU2xWTlFUUkhRVEZWWkVSM1JVSXZkMUZGUVhkS' + - 'lJtOUVRVlJDWjA1V1NGTlZSVVJFUVV0Q1oyZHlRbWRGUmtKUlkwUkJWRUZOUW1kT1ZraFNUVUpCWmpoRlFXcEJRV' + - 'TFDTUVkQk1WVmtSR2RSVjBKQ1VYRkNVWGRIVjI5S1FtRXhiMVJMY1hWd2J6UlhObmhVTm1veVJFRm1RbWRPVmtoV' + - 'FRVVkhSRUZYWjBKVFdUQm1hSFZGVDNaUWJTdDRaMjU0YVZGSE5rUnlabEZ1T1V0NlFtdENaMmR5UW1kRlJrSlJZM' + - 'EpCVVZKWlRVWlpkMHAzV1VsTGQxbENRbEZWU0UxQlIwZEhNbWd3WkVoQk5reDVPWFpaTTA1M1RHNUNjbUZUTlc1a' + - 'U1qbHVUREprTUdONlJuWk5WRUZ5UW1kbmNrSm5SVVpDVVdOM1FXOVpabUZJVWpCalJHOTJURE5DY21GVE5XNWlNa' + - 'mx1VERKa2VtTnFTWFpTTVZKVVRWVTRlRXh0VG5sa1JFRmtRbWRPVmtoU1JVVkdha0ZWWjJoS2FHUklVbXhqTTFGM' + - 'VdWYzFhMk50T1hCYVF6VnFZakl3ZDBsUldVUldVakJuUWtKdmQwZEVRVWxDWjFwdVoxRjNRa0ZuU1hkRVFWbExTM' + - '2RaUWtKQlNGZGxVVWxHUVhwQmRrSm5UbFpJVWpoRlMwUkJiVTFEVTJkSmNVRm5hR2cxYjJSSVVuZFBhVGgyV1ROS' + - '2MweHVRbkpoVXpWdVlqSTVia3d3WkZWVmVrWlFUVk0xYW1OdGQzZG5aMFZGUW1kdmNrSm5SVVZCWkZvMVFXZFJRM' + - 'EpKU0RGQ1NVaDVRVkJCUVdSM1EydDFVVzFSZEVKb1dVWkpaVGRGTmt4TldqTkJTMUJFVjFsQ1VHdGlNemRxYW1RN' + - 'E1FOTVRVE5qUlVGQlFVRlhXbVJFTTFCTVFVRkJSVUYzUWtsTlJWbERTVkZEVTFwRFYyVk1Tblp6YVZaWE5rTm5LM' + - 'mRxTHpsM1dWUktVbnAxTkVocGNXVTBaVmswWXk5dGVYcHFaMGxvUVV4VFlta3ZWR2g2WTNweGRHbHFNMlJyTTNaa' + - 'VRHTkpWek5NYkRKQ01HODNOVWRSWkdoTmFXZGlRbWRCU0ZWQlZtaFJSMjFwTDFoM2RYcFVPV1ZIT1ZKTVNTdDRNR' + - 'm95ZFdKNVdrVldla0UzTlZOWlZtUmhTakJPTUVGQlFVWnRXRkU1ZWpWQlFVRkNRVTFCVW1wQ1JVRnBRbU5EZDBFN' + - 'WFqZE9WRWRZVURJM09IbzBhSEl2ZFVOSWFVRkdUSGx2UTNFeVN6QXJlVXhTZDBwVlltZEpaMlk0WjBocWRuQjNNb' + - 'TFDTVVWVGFuRXlUMll6UVRCQlJVRjNRMnR1UTJGRlMwWlZlVm8zWmk5UmRFbDNSRkZaU2t0dldrbG9kbU5PUVZGR' + - 'lRFSlJRVVJuWjBWQ1FVazVibFJtVWt0SlYyZDBiRmRzTTNkQ1REVTFSVlJXTm10aGVuTndhRmN4ZVVGak5VUjFiV' + - 'FpZVHpReGExcDZkMG8yTVhkS2JXUlNVbFF2VlhORFNYa3hTMFYwTW1Nd1JXcG5iRzVLUTBZeVpXRjNZMFZYYkV4U' + - 'ldUSllVRXg1Um1wclYxRk9ZbE5vUWpGcE5GY3lUbEpIZWxCb2RETnRNV0kwT1doaWMzUjFXRTAyZEZnMVEzbEZTR' + - 'zVVYURoQ2IyMDBMMWRzUm1sb2VtaG5iamd4Ukd4a2IyZDZMMHN5VlhkTk5sTTJRMEl2VTBWNGEybFdabllyZW1KS' + - '01ISnFkbWM1TkVGc1pHcFZabFYzYTBrNVZrNU5ha1ZRTldVNGVXUkNNMjlNYkRabmJIQkRaVVkxWkdkbVUxZzBWV' + - 'Gw0TXpWdmFpOUpTV1F6VlVVdlpGQndZaTl4WjBkMmMydG1aR1Y2ZEcxVmRHVXZTMU50Y21sM1kyZFZWMWRsV0daV' + - 'Vlra3plbk5wYTNkYVltdHdiVkpaUzIxcVVHMW9kalJ5YkdsNlIwTkhkRGhRYmpod2NUaE5Na3RFWmk5UU0ydFdiM' + - '1F6WlRFNFVUMGlMQ0pOU1VsRlUycERRMEY2UzJkQmQwbENRV2RKVGtGbFR6QnRjVWRPYVhGdFFrcFhiRkYxUkVGT' + - '1FtZHJjV2hyYVVjNWR6QkNRVkZ6UmtGRVFrMU5VMEYzU0dkWlJGWlJVVXhGZUdSSVlrYzVhVmxYZUZSaFYyUjFTV' + - 'VpLZG1JelVXZFJNRVZuVEZOQ1UwMXFSVlJOUWtWSFFURlZSVU5vVFV0U01uaDJXVzFHYzFVeWJHNWlha1ZVVFVKR' + - 'lIwRXhWVVZCZUUxTFVqSjRkbGx0Um5OVk1teHVZbXBCWlVaM01IaE9la0V5VFZSVmQwMUVRWGRPUkVwaFJuY3dlV' + - 'TFVUlhsTlZGVjNUVVJCZDA1RVNtRk5SVWw0UTNwQlNrSm5UbFpDUVZsVVFXeFdWRTFTTkhkSVFWbEVWbEZSUzBWN' + - 'FZraGlNamx1WWtkVloxWklTakZqTTFGblZUSldlV1J0YkdwYVdFMTRSWHBCVWtKblRsWkNRVTFVUTJ0a1ZWVjVRa' + - '1JSVTBGNFZIcEZkMmRuUldsTlFUQkhRMU54UjFOSllqTkVVVVZDUVZGVlFVRTBTVUpFZDBGM1oyZEZTMEZ2U1VKQ' + - 'lVVUlJSMDA1UmpGSmRrNHdOWHByVVU4NUszUk9NWEJKVW5aS2VucDVUMVJJVnpWRWVrVmFhRVF5WlZCRGJuWlZRV' + - 'EJSYXpJNFJtZEpRMlpMY1VNNVJXdHpRelJVTW1aWFFsbHJMMnBEWmtNelVqTldXazFrVXk5a1RqUmFTME5GVUZwU' + - '2NrRjZSSE5wUzFWRWVsSnliVUpDU2pWM2RXUm5lbTVrU1UxWlkweGxMMUpIUjBac05YbFBSRWxMWjJwRmRpOVRTa' + - '2d2VlV3clpFVmhiSFJPTVRGQ2JYTkxLMlZSYlUxR0t5dEJZM2hIVG1oeU5UbHhUUzg1YVd3M01Va3laRTQ0Umtkb' + - 'VkyUmtkM1ZoWldvMFlsaG9jREJNWTFGQ1ltcDRUV05KTjBwUU1HRk5NMVEwU1N0RWMyRjRiVXRHYzJKcWVtRlVUa' + - '001ZFhwd1JteG5UMGxuTjNKU01qVjRiM2x1VlhoMk9IWk9iV3R4TjNwa1VFZElXR3Q0VjFrM2IwYzVhaXRLYTFKN' + - 'VFrRkNhemRZY2twbWIzVmpRbHBGY1VaS1NsTlFhemRZUVRCTVMxY3dXVE42Tlc5Nk1rUXdZekYwU2t0M1NFRm5UV' + - 'UpCUVVkcVoyZEZlazFKU1VKTWVrRlBRbWRPVmtoUk9FSkJaamhGUWtGTlEwRlpXWGRJVVZsRVZsSXdiRUpDV1hkR' + - '1FWbEpTM2RaUWtKUlZVaEJkMFZIUTBOelIwRlJWVVpDZDAxRFRVSkpSMEV4VldSRmQwVkNMM2RSU1UxQldVSkJaa' + - 'mhEUVZGQmQwaFJXVVJXVWpCUFFrSlpSVVpLYWxJclJ6UlJOamdyWWpkSFEyWkhTa0ZpYjA5ME9VTm1NSEpOUWpoS' + - 'FFURlZaRWwzVVZsTlFtRkJSa3AyYVVJeFpHNUlRamRCWVdkaVpWZGlVMkZNWkM5alIxbFpkVTFFVlVkRFEzTkhRV' + - 'kZWUmtKM1JVSkNRMnQzU25wQmJFSm5aM0pDWjBWR1FsRmpkMEZaV1ZwaFNGSXdZMFJ2ZGt3eU9XcGpNMEYxWTBkM' + - 'GNFeHRaSFppTW1OMldqTk9lVTFxUVhsQ1owNVdTRkk0UlV0NlFYQk5RMlZuU21GQmFtaHBSbTlrU0ZKM1QyazRkb' + - 'Gt6U25OTWJrSnlZVk0xYm1JeU9XNU1NbVI2WTJwSmRsb3pUbmxOYVRWcVkyMTNkMUIzV1VSV1VqQm5Ra1JuZDA1c' + - 'VFUQkNaMXB1WjFGM1FrRm5TWGRMYWtGdlFtZG5ja0puUlVaQ1VXTkRRVkpaWTJGSVVqQmpTRTAyVEhrNWQyRXlhM' + - '1ZhTWpsMlduazVlVnBZUW5aak1td3dZak5LTlV4NlFVNUNaMnR4YUd0cFJ6bDNNRUpCVVhOR1FVRlBRMEZSUlVGS' + - 'GIwRXJUbTV1TnpoNU5uQlNhbVE1V0d4UlYwNWhOMGhVWjJsYUwzSXpVazVIYTIxVmJWbElVRkZ4TmxOamRHazVVR' + - 'VZoYW5aM1VsUXlhVmRVU0ZGeU1ESm1aWE54VDNGQ1dUSkZWRlYzWjFwUksyeHNkRzlPUm5ab2MwODVkSFpDUTA5S' + - 'llYcHdjM2RYUXpsaFNqbDRhblUwZEZkRVVVZzRUbFpWTmxsYVdpOVlkR1ZFVTBkVk9WbDZTbkZRYWxrNGNUTk5SS' + - 'Gh5ZW0xeFpYQkNRMlkxYnpodGR5OTNTalJoTWtjMmVIcFZjalpHWWpaVU9FMWpSRTh5TWxCTVVrdzJkVE5OTkZSN' + - 'mN6TkJNazB4YWpaaWVXdEtXV2s0ZDFkSlVtUkJka3RNVjFwMUwyRjRRbFppZWxsdGNXMTNhMjAxZWt4VFJGYzFia' + - '2xCU21KRlRFTlJRMXAzVFVnMU5uUXlSSFp4YjJaNGN6WkNRbU5EUmtsYVZWTndlSFUyZURaMFpEQldOMU4yU2tOR' + - 'GIzTnBjbE50U1dGMGFpODVaRk5UVmtSUmFXSmxkRGh4THpkVlN6UjJORnBWVGpnd1lYUnVXbm94ZVdjOVBTSmRmU' + - 'S5leUp1YjI1alpTSTZJbkZyYjB4dE9XSnJUeXNyYzJoMFZITnZheXRqUW1GRmJFcEJXa1pXTUcxRlFqQTVVbWcxV' + - 'TNKWVpGVTlJaXdpZEdsdFpYTjBZVzF3VFhNaU9qRTFOalUwTWpReU5qSTNOek1zSW1Gd2ExQmhZMnRoWjJWT1lXM' + - 'WxJam9pWTI5dExtZHZiMmRzWlM1aGJtUnliMmxrTG1kdGN5SXNJbUZ3YTBScFoyVnpkRk5vWVRJMU5pSTZJaXR0Y' + - '0ZKQ016RjRRemRTYUdsaWN5OWxWbUVyTDNWQ05XNTFaMVVyV0UxRFFXa3plSFZKZGpaMGIwMDlJaXdpWTNSelVIS' + - 'nZabWxzWlUxaGRHTm9JanAwY25WbExDSmhjR3REWlhKMGFXWnBZMkYwWlVScFoyVnpkRk5vWVRJMU5pSTZXeUk0V' + - 'URGelZ6QkZVRXBqYzJ4M04xVjZVbk5wV0V3Mk5IY3JUelV3UldRclVrSkpRM1JoZVRGbk1qUk5QU0pkTENKaVlYT' + - 'nBZMGx1ZEdWbmNtbDBlU0k2ZEhKMVpYMC5yUW5Ib2FZVGgxTEU2VVZwaU1lZWFidDdUeWJ3dzdXZk42RzJ5R01tZ' + - 'kVjbTFabjRWalZkenpoY1BqTS1WR052aWl1RGxyZ2VuWEViZ082V05YNlYzc0hHVjN1VGxGMlBuOUZsY3YxWmItS' + - '2NGVHZUd29iYnY3LUp5VUZzTlhTSnhHZFRTOWxwNU5EdDFnWGJ6OVpORWhzVXI3ajBqbWNyaU9rR29PRzM4MXRSa' + - '0Vqdk5aa0hpMkF1UDF2MWM4RXg3cEpZc09ISzJxaDlmSHFuSlAzcGowUFc3WThpcDBSTVZaNF9xZzFqc0dMMnZ0O' + - 'G12cEJFMjg5dE1fcnROdm94TWU2aEx0Q1ZkdE9ZRjIzMWMtWVFJd2FEbnZWdDcwYW5XLUZYdUx3R1J5dWhfRlpNM' + - '3FCSlhhcXdCNjNITk5uMmh5MFRDdHQ4RDdIMmI4MGltWkZRX1FoYXV0aERhdGFYxT3cRxDpwIiyKduonVYyILs59' + - 'yKa_0ZbCmVrGvuaivigRQAAAAC5P9lh8uZGL7EiggAiR954AEEBDL2BKZVhBca7N3j3asDaoSrA3tJgT_E4KN25T' + - 'hBVqBHCdffSZt9bvku7hPBcd76BzU7Y-ckXslUkD13Imbzde6UBAgMmIAEhWCCT4hId3ByJ_agRyznv1xIazx2nl' + - 'VEGyvN7intoZr7C2CJYIKo3XB-cca9aUOLC-xhp3GfhyfTS0hjws5zL_bT_N1AL', - base64ClientDataJSON: - 'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiWDNaV1VHOUZOREpF' + - 'YUMxM2F6Tmlka2h0WVd0MGFWWjJSVmxETFV4M1FsZyIsIm9yaWdpbiI6Imh0dHBzOlwvXC9kZXYuZG9udG5lZWRh' + - 'LnB3IiwiYW5kcm9pZFBhY2thZ2VOYW1lIjoiY29tLmFuZHJvaWQuY2hyb21lIn0', + id: 'AQy9gSmVYQXGuzd492rA2qEqwN7SYE_xOCjduU4QVagRwnX30mbfW75Lu4TwXHe-gc1O2PnJF7JVJA9dyJm83Xs', + rawId: 'AQy9gSmVYQXGuzd492rA2qEqwN7SYE_xOCjduU4QVagRwnX30mbfW75Lu4TwXHe-gc1O2PnJF7JVJA9dyJm83Xs', + response: { + attestationObject: + 'o2NmbXRxYW5kcm9pZC1zYWZldHluZXRnYXR0U3RtdKJjdmVyaDE3MTIyMDM3aHJlc' + + '3BvbnNlWRS9ZXlKaGJHY2lPaUpTVXpJMU5pSXNJbmcxWXlJNld5Sk5TVWxHYTJwRFEwSkljV2RCZDBsQ1FXZEpVV' + + 'kpZY205T01GcFBaRkpyUWtGQlFVRkJRVkIxYm5wQlRrSm5hM0ZvYTJsSE9YY3dRa0ZSYzBaQlJFSkRUVkZ6ZDBOU' + + 'ldVUldVVkZIUlhkS1ZsVjZSV1ZOUW5kSFFURlZSVU5vVFZaU01qbDJXako0YkVsR1VubGtXRTR3U1VaT2JHTnVXb' + + 'kJaTWxaNlRWSk5kMFZSV1VSV1VWRkVSWGR3U0ZaR1RXZFJNRVZuVFZVNGVFMUNORmhFVkVVMFRWUkJlRTFFUVROT' + + 'lZHc3dUbFp2V0VSVVJUVk5WRUYzVDFSQk0wMVVhekJPVm05M1lrUkZURTFCYTBkQk1WVkZRbWhOUTFaV1RYaEZla' + + '0ZTUW1kT1ZrSkJaMVJEYTA1b1lrZHNiV0l6U25WaFYwVjRSbXBCVlVKblRsWkNRV05VUkZVeGRtUlhOVEJaVjJ4M' + + 'VNVWmFjRnBZWTNoRmVrRlNRbWRPVmtKQmIxUkRhMlIyWWpKa2MxcFRRazFVUlUxNFIzcEJXa0puVGxaQ1FVMVVSV' + + 'zFHTUdSSFZucGtRelZvWW0xU2VXSXliR3RNYlU1MllsUkRRMEZUU1hkRVVWbEtTMjlhU1doMlkwNUJVVVZDUWxGQ' + + 'lJHZG5SVkJCUkVORFFWRnZRMmRuUlVKQlRtcFlhM293WlVzeFUwVTBiU3N2UnpWM1QyOHJXRWRUUlVOeWNXUnVPR' + + 'Gh6UTNCU04yWnpNVFJtU3pCU2FETmFRMWxhVEVaSWNVSnJOa0Z0V2xaM01rczVSa2N3VHpseVVsQmxVVVJKVmxKN' + + 'VJUTXdVWFZ1VXpsMVowaEROR1ZuT1c5MmRrOXRLMUZrV2pKd09UTllhSHAxYmxGRmFGVlhXRU40UVVSSlJVZEtTe' + + 'k5UTW1GQlpucGxPVGxRVEZNeU9XaE1ZMUYxV1ZoSVJHRkROMDlhY1U1dWIzTnBUMGRwWm5NNGRqRnFhVFpJTDNob' + + '2JIUkRXbVV5YkVvck4wZDFkSHBsZUV0d2VIWndSUzkwV2xObVlsazVNRFZ4VTJ4Q2FEbG1jR293TVRWamFtNVJSb' + + 'XRWYzBGVmQyMUxWa0ZWZFdWVmVqUjBTMk5HU3pSd1pYWk9UR0Y0UlVGc0swOXJhV3hOZEVsWlJHRmpSRFZ1Wld3M' + + 'GVFcHBlWE0wTVROb1lXZHhWekJYYUdnMVJsQXpPV2hIYXpsRkwwSjNVVlJxWVhwVGVFZGtkbGd3YlRaNFJsbG9hQ' + + 'zh5VmsxNVdtcFVORXQ2VUVwRlEwRjNSVUZCWVU5RFFXeG5kMmRuU2xWTlFUUkhRVEZWWkVSM1JVSXZkMUZGUVhkS' + + 'lJtOUVRVlJDWjA1V1NGTlZSVVJFUVV0Q1oyZHlRbWRGUmtKUlkwUkJWRUZOUW1kT1ZraFNUVUpCWmpoRlFXcEJRV' + + 'TFDTUVkQk1WVmtSR2RSVjBKQ1VYRkNVWGRIVjI5S1FtRXhiMVJMY1hWd2J6UlhObmhVTm1veVJFRm1RbWRPVmtoV' + + 'FRVVkhSRUZYWjBKVFdUQm1hSFZGVDNaUWJTdDRaMjU0YVZGSE5rUnlabEZ1T1V0NlFtdENaMmR5UW1kRlJrSlJZM' + + 'EpCVVZKWlRVWlpkMHAzV1VsTGQxbENRbEZWU0UxQlIwZEhNbWd3WkVoQk5reDVPWFpaTTA1M1RHNUNjbUZUTlc1a' + + 'U1qbHVUREprTUdONlJuWk5WRUZ5UW1kbmNrSm5SVVpDVVdOM1FXOVpabUZJVWpCalJHOTJURE5DY21GVE5XNWlNa' + + 'mx1VERKa2VtTnFTWFpTTVZKVVRWVTRlRXh0VG5sa1JFRmtRbWRPVmtoU1JVVkdha0ZWWjJoS2FHUklVbXhqTTFGM' + + 'VdWYzFhMk50T1hCYVF6VnFZakl3ZDBsUldVUldVakJuUWtKdmQwZEVRVWxDWjFwdVoxRjNRa0ZuU1hkRVFWbExTM' + + '2RaUWtKQlNGZGxVVWxHUVhwQmRrSm5UbFpJVWpoRlMwUkJiVTFEVTJkSmNVRm5hR2cxYjJSSVVuZFBhVGgyV1ROS' + + '2MweHVRbkpoVXpWdVlqSTVia3d3WkZWVmVrWlFUVk0xYW1OdGQzZG5aMFZGUW1kdmNrSm5SVVZCWkZvMVFXZFJRM' + + 'EpKU0RGQ1NVaDVRVkJCUVdSM1EydDFVVzFSZEVKb1dVWkpaVGRGTmt4TldqTkJTMUJFVjFsQ1VHdGlNemRxYW1RN' + + 'E1FOTVRVE5qUlVGQlFVRlhXbVJFTTFCTVFVRkJSVUYzUWtsTlJWbERTVkZEVTFwRFYyVk1Tblp6YVZaWE5rTm5LM' + + 'mRxTHpsM1dWUktVbnAxTkVocGNXVTBaVmswWXk5dGVYcHFaMGxvUVV4VFlta3ZWR2g2WTNweGRHbHFNMlJyTTNaa' + + 'VRHTkpWek5NYkRKQ01HODNOVWRSWkdoTmFXZGlRbWRCU0ZWQlZtaFJSMjFwTDFoM2RYcFVPV1ZIT1ZKTVNTdDRNR' + + 'm95ZFdKNVdrVldla0UzTlZOWlZtUmhTakJPTUVGQlFVWnRXRkU1ZWpWQlFVRkNRVTFCVW1wQ1JVRnBRbU5EZDBFN' + + 'WFqZE9WRWRZVURJM09IbzBhSEl2ZFVOSWFVRkdUSGx2UTNFeVN6QXJlVXhTZDBwVlltZEpaMlk0WjBocWRuQjNNb' + + 'TFDTVVWVGFuRXlUMll6UVRCQlJVRjNRMnR1UTJGRlMwWlZlVm8zWmk5UmRFbDNSRkZaU2t0dldrbG9kbU5PUVZGR' + + 'lRFSlJRVVJuWjBWQ1FVazVibFJtVWt0SlYyZDBiRmRzTTNkQ1REVTFSVlJXTm10aGVuTndhRmN4ZVVGak5VUjFiV' + + 'FpZVHpReGExcDZkMG8yTVhkS2JXUlNVbFF2VlhORFNYa3hTMFYwTW1Nd1JXcG5iRzVLUTBZeVpXRjNZMFZYYkV4U' + + 'ldUSllVRXg1Um1wclYxRk9ZbE5vUWpGcE5GY3lUbEpIZWxCb2RETnRNV0kwT1doaWMzUjFXRTAyZEZnMVEzbEZTR' + + 'zVVYURoQ2IyMDBMMWRzUm1sb2VtaG5iamd4Ukd4a2IyZDZMMHN5VlhkTk5sTTJRMEl2VTBWNGEybFdabllyZW1KS' + + '01ISnFkbWM1TkVGc1pHcFZabFYzYTBrNVZrNU5ha1ZRTldVNGVXUkNNMjlNYkRabmJIQkRaVVkxWkdkbVUxZzBWV' + + 'Gw0TXpWdmFpOUpTV1F6VlVVdlpGQndZaTl4WjBkMmMydG1aR1Y2ZEcxVmRHVXZTMU50Y21sM1kyZFZWMWRsV0daV' + + 'Vlra3plbk5wYTNkYVltdHdiVkpaUzIxcVVHMW9kalJ5YkdsNlIwTkhkRGhRYmpod2NUaE5Na3RFWmk5UU0ydFdiM' + + '1F6WlRFNFVUMGlMQ0pOU1VsRlUycERRMEY2UzJkQmQwbENRV2RKVGtGbFR6QnRjVWRPYVhGdFFrcFhiRkYxUkVGT' + + '1FtZHJjV2hyYVVjNWR6QkNRVkZ6UmtGRVFrMU5VMEYzU0dkWlJGWlJVVXhGZUdSSVlrYzVhVmxYZUZSaFYyUjFTV' + + 'VpLZG1JelVXZFJNRVZuVEZOQ1UwMXFSVlJOUWtWSFFURlZSVU5vVFV0U01uaDJXVzFHYzFVeWJHNWlha1ZVVFVKR' + + 'lIwRXhWVVZCZUUxTFVqSjRkbGx0Um5OVk1teHVZbXBCWlVaM01IaE9la0V5VFZSVmQwMUVRWGRPUkVwaFJuY3dlV' + + 'TFVUlhsTlZGVjNUVVJCZDA1RVNtRk5SVWw0UTNwQlNrSm5UbFpDUVZsVVFXeFdWRTFTTkhkSVFWbEVWbEZSUzBWN' + + 'FZraGlNamx1WWtkVloxWklTakZqTTFGblZUSldlV1J0YkdwYVdFMTRSWHBCVWtKblRsWkNRVTFVUTJ0a1ZWVjVRa' + + '1JSVTBGNFZIcEZkMmRuUldsTlFUQkhRMU54UjFOSllqTkVVVVZDUVZGVlFVRTBTVUpFZDBGM1oyZEZTMEZ2U1VKQ' + + 'lVVUlJSMDA1UmpGSmRrNHdOWHByVVU4NUszUk9NWEJKVW5aS2VucDVUMVJJVnpWRWVrVmFhRVF5WlZCRGJuWlZRV' + + 'EJSYXpJNFJtZEpRMlpMY1VNNVJXdHpRelJVTW1aWFFsbHJMMnBEWmtNelVqTldXazFrVXk5a1RqUmFTME5GVUZwU' + + '2NrRjZSSE5wUzFWRWVsSnliVUpDU2pWM2RXUm5lbTVrU1UxWlkweGxMMUpIUjBac05YbFBSRWxMWjJwRmRpOVRTa' + + '2d2VlV3clpFVmhiSFJPTVRGQ2JYTkxLMlZSYlUxR0t5dEJZM2hIVG1oeU5UbHhUUzg1YVd3M01Va3laRTQ0Umtkb' + + 'VkyUmtkM1ZoWldvMFlsaG9jREJNWTFGQ1ltcDRUV05KTjBwUU1HRk5NMVEwU1N0RWMyRjRiVXRHYzJKcWVtRlVUa' + + '001ZFhwd1JteG5UMGxuTjNKU01qVjRiM2x1VlhoMk9IWk9iV3R4TjNwa1VFZElXR3Q0VjFrM2IwYzVhaXRLYTFKN' + + 'VFrRkNhemRZY2twbWIzVmpRbHBGY1VaS1NsTlFhemRZUVRCTVMxY3dXVE42Tlc5Nk1rUXdZekYwU2t0M1NFRm5UV' + + 'UpCUVVkcVoyZEZlazFKU1VKTWVrRlBRbWRPVmtoUk9FSkJaamhGUWtGTlEwRlpXWGRJVVZsRVZsSXdiRUpDV1hkR' + + '1FWbEpTM2RaUWtKUlZVaEJkMFZIUTBOelIwRlJWVVpDZDAxRFRVSkpSMEV4VldSRmQwVkNMM2RSU1UxQldVSkJaa' + + 'mhEUVZGQmQwaFJXVVJXVWpCUFFrSlpSVVpLYWxJclJ6UlJOamdyWWpkSFEyWkhTa0ZpYjA5ME9VTm1NSEpOUWpoS' + + 'FFURlZaRWwzVVZsTlFtRkJSa3AyYVVJeFpHNUlRamRCWVdkaVpWZGlVMkZNWkM5alIxbFpkVTFFVlVkRFEzTkhRV' + + 'kZWUmtKM1JVSkNRMnQzU25wQmJFSm5aM0pDWjBWR1FsRmpkMEZaV1ZwaFNGSXdZMFJ2ZGt3eU9XcGpNMEYxWTBkM' + + 'GNFeHRaSFppTW1OMldqTk9lVTFxUVhsQ1owNVdTRkk0UlV0NlFYQk5RMlZuU21GQmFtaHBSbTlrU0ZKM1QyazRkb' + + 'Gt6U25OTWJrSnlZVk0xYm1JeU9XNU1NbVI2WTJwSmRsb3pUbmxOYVRWcVkyMTNkMUIzV1VSV1VqQm5Ra1JuZDA1c' + + 'VFUQkNaMXB1WjFGM1FrRm5TWGRMYWtGdlFtZG5ja0puUlVaQ1VXTkRRVkpaWTJGSVVqQmpTRTAyVEhrNWQyRXlhM' + + '1ZhTWpsMlduazVlVnBZUW5aak1td3dZak5LTlV4NlFVNUNaMnR4YUd0cFJ6bDNNRUpCVVhOR1FVRlBRMEZSUlVGS' + + 'GIwRXJUbTV1TnpoNU5uQlNhbVE1V0d4UlYwNWhOMGhVWjJsYUwzSXpVazVIYTIxVmJWbElVRkZ4TmxOamRHazVVR' + + 'VZoYW5aM1VsUXlhVmRVU0ZGeU1ESm1aWE54VDNGQ1dUSkZWRlYzWjFwUksyeHNkRzlPUm5ab2MwODVkSFpDUTA5S' + + 'llYcHdjM2RYUXpsaFNqbDRhblUwZEZkRVVVZzRUbFpWTmxsYVdpOVlkR1ZFVTBkVk9WbDZTbkZRYWxrNGNUTk5SS' + + 'Gh5ZW0xeFpYQkNRMlkxYnpodGR5OTNTalJoTWtjMmVIcFZjalpHWWpaVU9FMWpSRTh5TWxCTVVrdzJkVE5OTkZSN' + + 'mN6TkJNazB4YWpaaWVXdEtXV2s0ZDFkSlVtUkJka3RNVjFwMUwyRjRRbFppZWxsdGNXMTNhMjAxZWt4VFJGYzFia' + + '2xCU21KRlRFTlJRMXAzVFVnMU5uUXlSSFp4YjJaNGN6WkNRbU5EUmtsYVZWTndlSFUyZURaMFpEQldOMU4yU2tOR' + + 'GIzTnBjbE50U1dGMGFpODVaRk5UVmtSUmFXSmxkRGh4THpkVlN6UjJORnBWVGpnd1lYUnVXbm94ZVdjOVBTSmRmU' + + 'S5leUp1YjI1alpTSTZJbkZyYjB4dE9XSnJUeXNyYzJoMFZITnZheXRqUW1GRmJFcEJXa1pXTUcxRlFqQTVVbWcxV' + + 'TNKWVpGVTlJaXdpZEdsdFpYTjBZVzF3VFhNaU9qRTFOalUwTWpReU5qSTNOek1zSW1Gd2ExQmhZMnRoWjJWT1lXM' + + 'WxJam9pWTI5dExtZHZiMmRzWlM1aGJtUnliMmxrTG1kdGN5SXNJbUZ3YTBScFoyVnpkRk5vWVRJMU5pSTZJaXR0Y' + + '0ZKQ016RjRRemRTYUdsaWN5OWxWbUVyTDNWQ05XNTFaMVVyV0UxRFFXa3plSFZKZGpaMGIwMDlJaXdpWTNSelVIS' + + 'nZabWxzWlUxaGRHTm9JanAwY25WbExDSmhjR3REWlhKMGFXWnBZMkYwWlVScFoyVnpkRk5vWVRJMU5pSTZXeUk0V' + + 'URGelZ6QkZVRXBqYzJ4M04xVjZVbk5wV0V3Mk5IY3JUelV3UldRclVrSkpRM1JoZVRGbk1qUk5QU0pkTENKaVlYT' + + 'nBZMGx1ZEdWbmNtbDBlU0k2ZEhKMVpYMC5yUW5Ib2FZVGgxTEU2VVZwaU1lZWFidDdUeWJ3dzdXZk42RzJ5R01tZ' + + 'kVjbTFabjRWalZkenpoY1BqTS1WR052aWl1RGxyZ2VuWEViZ082V05YNlYzc0hHVjN1VGxGMlBuOUZsY3YxWmItS' + + '2NGVHZUd29iYnY3LUp5VUZzTlhTSnhHZFRTOWxwNU5EdDFnWGJ6OVpORWhzVXI3ajBqbWNyaU9rR29PRzM4MXRSa' + + '0Vqdk5aa0hpMkF1UDF2MWM4RXg3cEpZc09ISzJxaDlmSHFuSlAzcGowUFc3WThpcDBSTVZaNF9xZzFqc0dMMnZ0O' + + 'G12cEJFMjg5dE1fcnROdm94TWU2aEx0Q1ZkdE9ZRjIzMWMtWVFJd2FEbnZWdDcwYW5XLUZYdUx3R1J5dWhfRlpNM' + + '3FCSlhhcXdCNjNITk5uMmh5MFRDdHQ4RDdIMmI4MGltWkZRX1FoYXV0aERhdGFYxT3cRxDpwIiyKduonVYyILs59' + + 'yKa_0ZbCmVrGvuaivigRQAAAAC5P9lh8uZGL7EiggAiR954AEEBDL2BKZVhBca7N3j3asDaoSrA3tJgT_E4KN25T' + + 'hBVqBHCdffSZt9bvku7hPBcd76BzU7Y-ckXslUkD13Imbzde6UBAgMmIAEhWCCT4hId3ByJ_agRyznv1xIazx2nl' + + 'VEGyvN7intoZr7C2CJYIKo3XB-cca9aUOLC-xhp3GfhyfTS0hjws5zL_bT_N1AL', + clientDataJSON: + 'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiWDNaV1VHOUZOREpF' + + 'YUMxM2F6Tmlka2h0WVd0MGFWWjJSVmxETFV4M1FsZyIsIm9yaWdpbiI6Imh0dHBzOlwvXC9kZXYuZG9udG5lZWRh' + + 'LnB3IiwiYW5kcm9pZFBhY2thZ2VOYW1lIjoiY29tLmFuZHJvaWQuY2hyb21lIn0', + }, + getClientExtensionResults: () => ({}), + type: 'webauthn.create', }; const attestationAndroidSafetyNetChallenge = '_vVPoE42Dh-wk3bvHmaktiVvEYC-LwBX'; diff --git a/packages/server/src/attestation/verifyAttestationResponse.ts b/packages/server/src/attestation/verifyAttestationResponse.ts index e336d2a..ed4ac5c 100644 --- a/packages/server/src/attestation/verifyAttestationResponse.ts +++ b/packages/server/src/attestation/verifyAttestationResponse.ts @@ -1,11 +1,10 @@ -import decodeAttestationObject from '../helpers/decodeAttestationObject'; -import decodeClientDataJSON from '../helpers/decodeClientDataJSON'; import { - ATTESTATION_FORMATS, - AuthenticatorAttestationResponseJSON, - VerifiedAttestation, + AttestationCredentialJSON, } from '@simplewebauthn/typescript-types'; +import decodeAttestationObject, { ATTESTATION_FORMATS } from '../helpers/decodeAttestationObject'; +import decodeClientDataJSON from '../helpers/decodeClientDataJSON'; + import verifyFIDOU2F from './verifications/verifyFIDOU2F'; import verifyPacked from './verifications/verifyPacked'; import verifyNone from './verifications/verifyNone'; @@ -14,19 +13,19 @@ import verifyAndroidSafetynet from './verifications/verifyAndroidSafetyNet'; /** * Verify that the user has legitimately completed the registration process * - * @param response Authenticator attestation response with base64-encoded values + * @param response Authenticator attestation response with base64url-encoded values * @param expectedChallenge The random value provided to generateAttestationOptions for the * authenticator to sign * @param expectedOrigin Expected URL of website attestation should have occurred on */ export default function verifyAttestationResponse( - response: AuthenticatorAttestationResponseJSON, + credential: AttestationCredentialJSON, expectedChallenge: string, expectedOrigin: string, ): VerifiedAttestation { - const { base64AttestationObject, base64ClientDataJSON } = response; - const attestationObject = decodeAttestationObject(base64AttestationObject); - const clientDataJSON = decodeClientDataJSON(base64ClientDataJSON); + const { response } = credential; + const attestationObject = decodeAttestationObject(response.attestationObject); + const clientDataJSON = decodeClientDataJSON(response.clientDataJSON); const { type, origin, challenge } = clientDataJSON; @@ -52,15 +51,15 @@ export default function verifyAttestationResponse( * Verification can only be performed when attestation = 'direct' */ if (fmt === ATTESTATION_FORMATS.FIDO_U2F) { - return verifyFIDOU2F(attestationObject, base64ClientDataJSON); + return verifyFIDOU2F(attestationObject, response.clientDataJSON); } if (fmt === ATTESTATION_FORMATS.PACKED) { - return verifyPacked(attestationObject, base64ClientDataJSON); + return verifyPacked(attestationObject, response.clientDataJSON); } if (fmt === ATTESTATION_FORMATS.ANDROID_SAFETYNET) { - return verifyAndroidSafetynet(attestationObject, base64ClientDataJSON); + return verifyAndroidSafetynet(attestationObject, response.clientDataJSON); } if (fmt === ATTESTATION_FORMATS.NONE) { @@ -69,3 +68,28 @@ export default function verifyAttestationResponse( throw new Error(`Unsupported Attestation Format: ${fmt}`); } + +/** + * Result of attestation verification + * + * @param verified If the assertion response could be verified + * @param userVerified Whether the user was uniquely identified during attestation + * @param authenticatorInfo.fmt Type of attestation + * @param authenticatorInfo.counter The number of times the authenticator reported it has been used. + * Should be kept in a DB for later reference to help prevent replay attacks + * @param authenticatorInfo.base64PublicKey Base64URL-encoded ArrayBuffer containing the + * authenticator's public key. **Should be kept in a DB for later reference!** + * @param authenticatorInfo.base64CredentialID Base64URL-encoded ArrayBuffer containing the + * authenticator's credential ID for the public key above. **Should be kept in a DB for later + * reference!** + */ +export type VerifiedAttestation = { + verified: boolean; + userVerified: boolean; + authenticatorInfo?: { + fmt: ATTESTATION_FORMATS; + counter: number; + base64PublicKey: string; + base64CredentialID: string; + }; +}; diff --git a/packages/server/src/helpers/convertCOSEtoPKCS.test.ts b/packages/server/src/helpers/convertCOSEtoPKCS.test.ts index d17d4bd..e914cc7 100644 --- a/packages/server/src/helpers/convertCOSEtoPKCS.test.ts +++ b/packages/server/src/helpers/convertCOSEtoPKCS.test.ts @@ -1,7 +1,6 @@ import cbor from 'cbor'; -import { COSEKEYS } from '@simplewebauthn/typescript-types'; -import convertCOSEtoPKCS from './convertCOSEtoPKCS'; +import convertCOSEtoPKCS, { COSEKEYS } from './convertCOSEtoPKCS'; test('should throw an error curve if, somehow, curve coordinate x is missing', () => { const mockCOSEKey = new Map<number, number | Buffer>(); diff --git a/packages/server/src/helpers/convertCOSEtoPKCS.ts b/packages/server/src/helpers/convertCOSEtoPKCS.ts index 5b03b1a..3039415 100644 --- a/packages/server/src/helpers/convertCOSEtoPKCS.ts +++ b/packages/server/src/helpers/convertCOSEtoPKCS.ts @@ -1,5 +1,4 @@ import cbor from 'cbor'; -import { COSEKEYS, COSEPublicKey } from '@simplewebauthn/typescript-types'; /** * Takes COSE-encoded public key and converts it to PKCS key @@ -40,3 +39,15 @@ export default function convertCOSEtoPKCS(cosePublicKey: Buffer): Buffer { return Buffer.concat([tag, x as Buffer, y as Buffer]); } + +export type COSEPublicKey = Map<COSEAlgorithmIdentifier, number | Buffer>; + +export enum COSEKEYS { + kty = 1, + alg = 3, + crv = -1, + x = -2, + y = -3, + n = -1, + e = -2, +} diff --git a/packages/server/src/helpers/decodeAttestationObject.ts b/packages/server/src/helpers/decodeAttestationObject.ts index 3e66e67..2eb9997 100644 --- a/packages/server/src/helpers/decodeAttestationObject.ts +++ b/packages/server/src/helpers/decodeAttestationObject.ts @@ -1,11 +1,10 @@ import base64url from 'base64url'; import cbor from 'cbor'; -import { AttestationObject } from '@simplewebauthn/typescript-types'; /** - * Convert an AttestationObject from base64 string to a proper object + * Convert an AttestationObject from base64url string to a proper object * - * @param base64AttestationObject Base64-encoded Attestation Object + * @param base64AttestationObject Base64URL-encoded Attestation Object */ export default function decodeAttestationObject( base64AttestationObject: string, @@ -14,3 +13,20 @@ export default function decodeAttestationObject( const toCBOR: AttestationObject = cbor.decodeAllSync(toBuffer)[0]; return toCBOR; } + +export enum ATTESTATION_FORMATS { + FIDO_U2F = 'fido-u2f', + PACKED = 'packed', + ANDROID_SAFETYNET = 'android-safetynet', + NONE = 'none', +} + +export type AttestationObject = { + fmt: ATTESTATION_FORMATS; + attStmt: { + sig?: Buffer; + x5c?: Buffer[]; + response?: Buffer; + }; + authData: Buffer; +}; diff --git a/packages/server/src/helpers/decodeClientDataJSON.ts b/packages/server/src/helpers/decodeClientDataJSON.ts index fb909cf..c0ebb2b 100644 --- a/packages/server/src/helpers/decodeClientDataJSON.ts +++ b/packages/server/src/helpers/decodeClientDataJSON.ts @@ -1,5 +1,3 @@ -import { ClientDataJSON } from '@simplewebauthn/typescript-types'; - import asciiToBinary from './asciiToBinary'; /** @@ -15,3 +13,9 @@ export default function decodeClientDataJSON(data: string): ClientDataJSON { return clientData; } + +type ClientDataJSON = { + type: string; + challenge: string; + origin: string; +}; diff --git a/packages/server/src/helpers/getCertificateInfo.ts b/packages/server/src/helpers/getCertificateInfo.ts index b6d8e26..3741fac 100644 --- a/packages/server/src/helpers/getCertificateInfo.ts +++ b/packages/server/src/helpers/getCertificateInfo.ts @@ -1,5 +1,10 @@ import jsrsasign from 'jsrsasign'; -import { CertificateInfo } from '@simplewebauthn/typescript-types'; + +export type CertificateInfo = { + subject: { [key: string]: string }; + version: number; + basicConstraintsCA: boolean; +}; type ExtInfo = { critical: boolean; diff --git a/packages/server/src/helpers/parseAuthenticatorData.ts b/packages/server/src/helpers/parseAuthenticatorData.ts index 62c1cb1..3177dd5 100644 --- a/packages/server/src/helpers/parseAuthenticatorData.ts +++ b/packages/server/src/helpers/parseAuthenticatorData.ts @@ -1,5 +1,3 @@ -import { ParsedAuthenticatorData } from '@simplewebauthn/typescript-types'; - /** * Make sense of the authData buffer contained in an Attestation */ @@ -57,3 +55,20 @@ export default function parseAuthenticatorData(authData: Buffer): ParsedAuthenti COSEPublicKey, }; } + +type ParsedAuthenticatorData = { + rpIdHash: Buffer; + flagsBuf: Buffer; + flags: { + up: boolean; + uv: boolean; + at: boolean; + ed: boolean; + flagsInt: number; + }; + counter: number; + counterBuf: Buffer; + aaguid?: Buffer; + credentialID?: Buffer; + COSEPublicKey?: Buffer; +}; diff --git a/packages/typescript-types/src/index.ts b/packages/typescript-types/src/index.ts index dcd88a9..da063a5 100644 --- a/packages/typescript-types/src/index.ts +++ b/packages/typescript-types/src/index.ts @@ -10,9 +10,8 @@ export interface PublicKeyCredentialCreationOptionsJSON extends Omit< PublicKeyCredentialCreationOptions, 'challenge' | 'user' | 'excludeCredentials' > { - // Will be converted to a Uint8Array in the browser user: PublicKeyCredentialUserEntityJSON; - challenge: string; + challenge: Base64URLString; excludeCredentials: PublicKeyCredentialDescriptorJSON[]; } @@ -23,23 +22,20 @@ PublicKeyCredentialCreationOptions, 'challenge' | 'user' | 'excludeCredentials' export interface PublicKeyCredentialRequestOptionsJSON extends Omit< PublicKeyCredentialRequestOptions, 'challenge' |'allowCredentials' > { - // Will be converted to a Uint8Array in the browser - challenge: string; + challenge: Base64URLString; allowCredentials: PublicKeyCredentialDescriptorJSON[]; } export interface PublicKeyCredentialDescriptorJSON extends Omit< PublicKeyCredentialDescriptor, 'id' > { - // Should be a Base64-encoded credential ID. Will be converted to a Uint8Array in the browser - id: string; + id: Base64URLString; } export interface PublicKeyCredentialUserEntityJSON extends Omit < PublicKeyCredentialUserEntity, 'id' > { - // Should be a Base64-encoded credential ID. Will be converted to a Uint8Array in the browser - id: string; + id: Base64URLString; } /** @@ -50,6 +46,16 @@ export interface AttestationCredential extends PublicKeyCredential { } /** + * A slightly-modified AttestationCredential to simplify working with ArrayBuffers that + * are base64url-encoded in the browser so that they can be sent as JSON to the server. + */ +export interface AttestationCredentialJSON + extends Omit<AttestationCredential, 'response' | 'rawId' | 'getClientExtensionResults'> { + rawId: Base64URLString; + response: AuthenticatorAttestationResponseJSON; +} + +/** * The value returned from navigator.credentials.get() */ export interface AssertionCredential extends PublicKeyCredential { @@ -57,155 +63,43 @@ export interface AssertionCredential extends PublicKeyCredential { } /** - * A slightly-modified AuthenticatorAttestationResponse to simplify working with ArrayBuffers that - * are base64-encoded in the browser so that they can be sent as JSON to the server. + * A slightly-modified AssertionCredential to simplify working with ArrayBuffers that + * are base64url-encoded in the browser so that they can be sent as JSON to the server. */ -export interface AuthenticatorAttestationResponseJSON +export interface AssertionCredentialJSON + extends Omit<AssertionCredential, 'response' | 'rawId' | 'getClientExtensionResults'> { + rawId: Base64URLString; + response: AuthenticatorAssertionResponseJSON; +} + +interface AuthenticatorAttestationResponseJSON extends Omit<AuthenticatorAttestationResponse, 'clientDataJSON' | 'attestationObject'> { - base64ClientDataJSON: string; - base64AttestationObject: string; + clientDataJSON: Base64URLString; + attestationObject: Base64URLString; } -/** - * A slightly-modified AuthenticatorAttestationResponse to simplify working with ArrayBuffers that - * are base64-encoded in the browser so that they can be sent as JSON to the server. - */ -export interface AuthenticatorAssertionResponseJSON +interface AuthenticatorAssertionResponseJSON extends Omit< AuthenticatorAssertionResponse, - 'clientDataJSON' | 'authenticatorData' | 'signature' | 'userHandle' + 'authenticatorData' | 'clientDataJSON' | 'signature' | 'userHandle' > { - base64CredentialID: string; - base64AuthenticatorData: string; - base64ClientDataJSON: string; - base64Signature: string; - base64UserHandle?: string; -} - -export enum ATTESTATION_FORMATS { - FIDO_U2F = 'fido-u2f', - PACKED = 'packed', - ANDROID_SAFETYNET = 'android-safetynet', - NONE = 'none', -} - -export type AttestationObject = { - fmt: ATTESTATION_FORMATS; - attStmt: { - sig?: Buffer; - x5c?: Buffer[]; - response?: Buffer; - }; - authData: Buffer; -}; - -export type ParsedAuthenticatorData = { - rpIdHash: Buffer; - flagsBuf: Buffer; - flags: { - up: boolean; - uv: boolean; - at: boolean; - ed: boolean; - flagsInt: number; - }; - counter: number; - counterBuf: Buffer; - aaguid?: Buffer; - credentialID?: Buffer; - COSEPublicKey?: Buffer; -}; - -export type ClientDataJSON = { - type: string; - challenge: string; - origin: string; -}; - -/** - * Result of attestation verification - * - * @param verified If the assertion response could be verified - * @param userVerified Whether the user was uniquely identified during attestation - * @param authenticatorInfo.fmt Type of attestation - * @param authenticatorInfo.counter The number of times the authenticator reported it has been used. - * Should be kept in a DB for later reference to help prevent replay attacks - * @param authenticatorInfo.base64PublicKey Base64-encoded ArrayBuffer containing the - * authenticator's public key. **Should be kept in a DB for later reference!** - * @param authenticatorInfo.base64CredentialID Base64-encoded ArrayBuffer containing the - * authenticator's credential ID for the public key above. **Should be kept in a DB for later - * reference!** - */ -export type VerifiedAttestation = { - verified: boolean; - userVerified: boolean; - authenticatorInfo?: { - fmt: ATTESTATION_FORMATS; - counter: number; - base64PublicKey: string; - base64CredentialID: string; - }; -}; - -/** - * Result of assertion verification - * - * @param verified If the assertion response could be verified - * @param authenticatorInfo.base64CredentialID The ID of the authenticator used during assertion. - * Should be used to identify which DB authenticator entry needs its `counter` updated to the value - * below - * @param authenticatorInfo.counter The number of times the authenticator identified above reported - * it has been used. **Should be kept in a DB for later reference to help prevent replay attacks!** - */ -export type VerifiedAssertion = { - verified: boolean; - authenticatorInfo: { - counter: number; - base64CredentialID: string; - }; -}; - -export type CertificateInfo = { - subject: { [key: string]: string }; - version: number; - basicConstraintsCA: boolean; -}; - -export enum COSEKEYS { - kty = 1, - alg = 3, - crv = -1, - x = -2, - y = -3, - n = -1, - e = -2, + authenticatorData: Base64URLString; + clientDataJSON: Base64URLString; + signature: Base64URLString; + userHandle?: Base64URLString; } -export type COSEPublicKey = Map<COSEAlgorithmIdentifier, number | Buffer>; - -export type SafetyNetJWTHeader = { - alg: 'string'; - x5c: string[]; -}; - -export type SafetyNetJWTPayload = { - nonce: string; - timestampMs: number; - apkPackageName: string; - apkDigestSha256: string; - ctsProfileMatch: boolean; - apkCertificateDigestSha256: string[]; - basicIntegrity: boolean; -}; - -export type SafetyNetJWTSignature = string; - /** * A WebAuthn-compatible device and the information needed to verify assertions by it */ export type AuthenticatorDevice = { - base64PublicKey: string; - base64CredentialID: string; + publicKey: Base64URLString; + credentialID: Base64URLString; // Number of times this device is expected to have been used counter: number; }; + +/** + * An attempt to communicate that this isn't just any string, but a base64url-encoded string + */ +export type Base64URLString = string; |