From 4fce0476452002ee6e65e37d939cfd54194b19bc Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Fri, 11 Nov 2022 15:37:44 -0800 Subject: Refactor parseAuthenticatorData --- .../server/src/helpers/parseAuthenticatorData.test.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) (limited to 'packages/server/src/helpers/parseAuthenticatorData.test.ts') diff --git a/packages/server/src/helpers/parseAuthenticatorData.test.ts b/packages/server/src/helpers/parseAuthenticatorData.test.ts index a706718..e08d95b 100644 --- a/packages/server/src/helpers/parseAuthenticatorData.test.ts +++ b/packages/server/src/helpers/parseAuthenticatorData.test.ts @@ -1,12 +1,9 @@ -import cbor from 'cbor'; - import { parseAuthenticatorData } from './parseAuthenticatorData'; +import * as base64url from './base64url'; // Grabbed this from a Conformance test, contains attestation data -const authDataWithAT = Buffer.from( - 'SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2NBAAAAJch83ZdWwUm4niTLNjZU81AAIHa7Ksm5br3hAh3UjxP9+4rqu8BEsD+7SZ2xWe1/yHv6pAEDAzkBACBZAQDcxA7Ehs9goWB2Hbl6e9v+aUub9rvy2M7Hkvf+iCzMGE63e3sCEW5Ru33KNy4um46s9jalcBHtZgtEnyeRoQvszis+ws5o4Da0vQfuzlpBmjWT1dV6LuP+vs9wrfObW4jlA5bKEIhv63+jAxOtdXGVzo75PxBlqxrmrr5IR9n8Fw7clwRsDkjgRHaNcQVbwq/qdNwU5H3hZKu9szTwBS5NGRq01EaDF2014YSTFjwtAmZ3PU1tcO/QD2U2zg6eB5grfWDeAJtRE8cbndDWc8aLL0aeC37Q36+TVsGe6AhBgHEw6eO3I3NW5r9v/26CqMPBDwmEundeq1iGyKfMloobIUMBAAE=', - 'base64', -); +const authDataWithAT = base64url.toBuffer('SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2NBAAAAJch83ZdWwUm4niTLNjZU81AAIHa7Ksm5br3hAh3UjxP9+4rqu8BEsD+7SZ2xWe1/yHv6pAEDAzkBACBZAQDcxA7Ehs9goWB2Hbl6e9v+aUub9rvy2M7Hkvf+iCzMGE63e3sCEW5Ru33KNy4um46s9jalcBHtZgtEnyeRoQvszis+ws5o4Da0vQfuzlpBmjWT1dV6LuP+vs9wrfObW4jlA5bKEIhv63+jAxOtdXGVzo75PxBlqxrmrr5IR9n8Fw7clwRsDkjgRHaNcQVbwq/qdNwU5H3hZKu9szTwBS5NGRq01EaDF2014YSTFjwtAmZ3PU1tcO/QD2U2zg6eB5grfWDeAJtRE8cbndDWc8aLL0aeC37Q36+TVsGe6AhBgHEw6eO3I3NW5r9v/26CqMPBDwmEundeq1iGyKfMloobIUMBAAE=', 'base64'); + // Grabbed this from a Conformance test, contains extension data const authDataWithED = Buffer.from( 'SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2OBAAAAjaFxZXhhbXBsZS5leHRlbnNpb254dlRoaXMgaXMgYW4gZXhhbXBsZSBleHRlbnNpb24hIElmIHlvdSByZWFkIHRoaXMgbWVzc2FnZSwgeW91IHByb2JhYmx5IHN1Y2Nlc3NmdWxseSBwYXNzaW5nIGNvbmZvcm1hbmNlIHRlc3RzLiBHb29kIGpvYiE=', @@ -29,13 +26,14 @@ test('should parse flags', () => { test('should parse attestation data', () => { const parsed = parseAuthenticatorData(authDataWithAT); - const { credentialID, credentialPublicKey, aaguid } = parsed; + const { credentialID, credentialPublicKey, aaguid, counter } = parsed; - expect(credentialID?.toString('base64')).toEqual('drsqybluveECHdSPE/37iuq7wESwP7tJnbFZ7X/Ie/o='); - expect(credentialPublicKey?.toString('base64')).toEqual( + expect(base64url.fromBuffer(credentialID!)).toEqual('drsqybluveECHdSPE_37iuq7wESwP7tJnbFZ7X_Ie_o'); + expect(base64url.fromBuffer(credentialPublicKey!, 'base64')).toEqual( 'pAEDAzkBACBZAQDcxA7Ehs9goWB2Hbl6e9v+aUub9rvy2M7Hkvf+iCzMGE63e3sCEW5Ru33KNy4um46s9jalcBHtZgtEnyeRoQvszis+ws5o4Da0vQfuzlpBmjWT1dV6LuP+vs9wrfObW4jlA5bKEIhv63+jAxOtdXGVzo75PxBlqxrmrr5IR9n8Fw7clwRsDkjgRHaNcQVbwq/qdNwU5H3hZKu9szTwBS5NGRq01EaDF2014YSTFjwtAmZ3PU1tcO/QD2U2zg6eB5grfWDeAJtRE8cbndDWc8aLL0aeC37Q36+TVsGe6AhBgHEw6eO3I3NW5r9v/26CqMPBDwmEundeq1iGyKfMloobIUMBAAE=', ); - expect(aaguid?.toString('base64')).toEqual('yHzdl1bBSbieJMs2NlTzUA=='); + expect(base64url.fromBuffer(aaguid!, 'base64')).toEqual('yHzdl1bBSbieJMs2NlTzUA=='); + expect(counter).toEqual(37); }); test('should parse extension data', () => { -- cgit v1.2.3 From 6e3db9655c6aa2aef590c66295df500271bfdd17 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Sat, 12 Nov 2022 19:51:29 -0800 Subject: Figure out a better pattern for exposing iso funcs --- .../generateAuthenticationOptions.test.ts | 4 +- .../generateAuthenticationOptions.ts | 9 +-- .../verifyAuthenticationResponse.test.ts | 43 +++++----- .../authentication/verifyAuthenticationResponse.ts | 11 ++- .../server/src/helpers/convertAAGUIDToString.ts | 4 +- .../server/src/helpers/convertCOSEtoPKCS.test.ts | 6 +- packages/server/src/helpers/convertCOSEtoPKCS.ts | 10 +-- .../server/src/helpers/convertCertBufferToPEM.ts | 10 +-- .../src/helpers/convertPublicKeyToPEM.test.ts | 15 ++-- .../server/src/helpers/convertPublicKeyToPEM.ts | 13 ++- .../server/src/helpers/decodeAttestationObject.ts | 4 +- .../helpers/decodeAuthenticatorExtensions.test.ts | 14 ++-- .../src/helpers/decodeAuthenticatorExtensions.ts | 4 +- .../server/src/helpers/decodeClientDataJSON.ts | 4 +- .../src/helpers/decodeCredentialPublicKey.ts | 4 +- packages/server/src/helpers/index.ts | 10 +-- packages/server/src/helpers/isCertRevoked.ts | 6 +- packages/server/src/helpers/iso/index.ts | 10 +++ packages/server/src/helpers/iso/isoBase64URL.ts | 62 ++++++++++++++ packages/server/src/helpers/iso/isoCBOR.ts | 46 +++++++++++ packages/server/src/helpers/iso/isoUint8Array.ts | 90 +++++++++++++++++++++ packages/server/src/helpers/isoBase64URL.ts | 65 --------------- packages/server/src/helpers/isoCBOR.ts | 49 ----------- packages/server/src/helpers/isoUint8Array.ts | 94 ---------------------- packages/server/src/helpers/matchExpectedRPID.ts | 6 +- .../src/helpers/parseAuthenticatorData.test.ts | 10 +-- .../server/src/helpers/parseAuthenticatorData.ts | 13 ++- packages/server/src/helpers/verifySignature.ts | 4 +- packages/server/src/metadata/parseJWT.ts | 6 +- .../metadata/verifyAttestationWithMetadata.test.ts | 8 +- .../registration/generateRegistrationOptions.ts | 9 +-- .../verifications/tpm/parseCertInfo.ts | 6 +- .../registration/verifications/tpm/parsePubArea.ts | 6 +- .../verifications/tpm/verifyAttestationTPM.test.ts | 14 ++-- .../verifications/tpm/verifyAttestationTPM.ts | 14 ++-- .../verifyAttestationAndroidKey.test.ts | 4 +- .../verifications/verifyAttestationAndroidKey.ts | 8 +- .../verifyAttestationAndroidSafetyNet.test.ts | 10 +-- .../verifyAttestationAndroidSafetyNet.ts | 19 +++-- .../verifications/verifyAttestationApple.ts | 8 +- .../verifications/verifyAttestationFIDOU2F.ts | 6 +- .../verifications/verifyAttestationPacked.ts | 4 +- .../verifyRegistrationResponse.test.ts | 61 +++++++------- .../src/registration/verifyRegistrationResponse.ts | 7 +- 44 files changed, 399 insertions(+), 411 deletions(-) create mode 100644 packages/server/src/helpers/iso/index.ts create mode 100644 packages/server/src/helpers/iso/isoBase64URL.ts create mode 100644 packages/server/src/helpers/iso/isoCBOR.ts create mode 100644 packages/server/src/helpers/iso/isoUint8Array.ts delete mode 100644 packages/server/src/helpers/isoBase64URL.ts delete mode 100644 packages/server/src/helpers/isoCBOR.ts delete mode 100644 packages/server/src/helpers/isoUint8Array.ts (limited to 'packages/server/src/helpers/parseAuthenticatorData.test.ts') diff --git a/packages/server/src/authentication/generateAuthenticationOptions.test.ts b/packages/server/src/authentication/generateAuthenticationOptions.test.ts index a1a0588..65dc66e 100644 --- a/packages/server/src/authentication/generateAuthenticationOptions.test.ts +++ b/packages/server/src/authentication/generateAuthenticationOptions.test.ts @@ -1,11 +1,11 @@ jest.mock('../helpers/generateChallenge'); -import * as base64url from '../helpers/base64url'; +import { isoBase64URL } from '../helpers/iso'; import { generateAuthenticationOptions } from './generateAuthenticationOptions'; const challengeString = 'dG90YWxseXJhbmRvbXZhbHVl'; -const challengeBuffer = base64url.toBuffer(challengeString) +const challengeBuffer = isoBase64URL.toBuffer(challengeString) test('should generate credential request options suitable for sending via JSON', () => { diff --git a/packages/server/src/authentication/generateAuthenticationOptions.ts b/packages/server/src/authentication/generateAuthenticationOptions.ts index a7c3a85..3847b39 100644 --- a/packages/server/src/authentication/generateAuthenticationOptions.ts +++ b/packages/server/src/authentication/generateAuthenticationOptions.ts @@ -4,9 +4,8 @@ import type { PublicKeyCredentialDescriptorFuture, UserVerificationRequirement, } from '@simplewebauthn/typescript-types'; -import * as isoBase64URL from '../helpers/isoBase64URL' -import * as isoUint8Array from '../helpers/isoUint8Array'; +import { isoBase64URL, isoUint8Array } from '../helpers/iso' import { generateChallenge } from '../helpers/generateChallenge'; export type GenerateAuthenticationOptionsOpts = { @@ -48,14 +47,14 @@ export function generateAuthenticationOptions( */ let _challenge = challenge; if (typeof _challenge === 'string') { - _challenge = uint8Array.fromUTF8String(_challenge); + _challenge = isoUint8Array.fromUTF8String(_challenge); } return { - challenge: base64url.fromBuffer(_challenge), + challenge: isoBase64URL.fromBuffer(_challenge), allowCredentials: allowCredentials?.map(cred => ({ ...cred, - id: base64url.fromBuffer(cred.id as Uint8Array), + id: isoBase64URL.fromBuffer(cred.id as Uint8Array), })), timeout, userVerification, diff --git a/packages/server/src/authentication/verifyAuthenticationResponse.test.ts b/packages/server/src/authentication/verifyAuthenticationResponse.test.ts index 58e24b8..6b89bd0 100644 --- a/packages/server/src/authentication/verifyAuthenticationResponse.test.ts +++ b/packages/server/src/authentication/verifyAuthenticationResponse.test.ts @@ -7,8 +7,7 @@ import { AuthenticatorDevice, AuthenticationCredentialJSON, } from '@simplewebauthn/typescript-types'; -import * as uint8Array from '../helpers/uint8array'; -import * as base64url from '../helpers/base64url'; +import { isoUint8Array, isoBase64URL } from '../helpers/iso'; let mockDecodeClientData: jest.SpyInstance; let mockParseAuthData: jest.SpyInstance; @@ -158,7 +157,7 @@ test('should not compare counters if both are 0', async () => { test('should throw an error if user verification is required but user was not verified', async () => { const actualData = esmParseAuthenticatorData.parseAuthenticatorData( - base64url.toBuffer(assertionResponse.response.authenticatorData), + isoBase64URL.toBuffer(assertionResponse.response.authenticatorData), ); mockParseAuthData.mockReturnValue({ @@ -184,7 +183,7 @@ test('should throw an error if user verification is required but user was not ve // TODO: Get a real TPM authentication response in here test.skip('should verify TPM assertion', async () => { const expectedChallenge = 'dG90YWxseVVuaXF1ZVZhbHVlRXZlcnlBc3NlcnRpb24'; - jest.spyOn(base64url, 'toString').mockReturnValueOnce(expectedChallenge); + jest.spyOn(isoBase64URL, 'toString').mockReturnValueOnce(expectedChallenge); const verification = await verifyAuthenticationResponse({ credential: { id: 'YJ8FMM-AmcUt73XPX341WXWd7ypBMylGjjhu0g3VzME', @@ -204,8 +203,8 @@ test.skip('should verify TPM assertion', async () => { expectedOrigin: assertionOrigin, expectedRPID: 'dev.dontneeda.pw', authenticator: { - credentialPublicKey: base64url.toBuffer('BAEAAQ'), - credentialID: base64url.toBuffer('YJ8FMM-AmcUt73XPX341WXWd7ypBMylGjjhu0g3VzME'), + credentialPublicKey: isoBase64URL.toBuffer('BAEAAQ'), + credentialID: isoBase64URL.toBuffer('YJ8FMM-AmcUt73XPX341WXWd7ypBMylGjjhu0g3VzME'), counter: 0, }, }); @@ -280,17 +279,17 @@ test('should pass verification if custom challenge verifier returns true', async }, expectedChallenge: (challenge: string) => { const parsedChallenge: { actualChallenge: string; arbitraryData: string } = JSON.parse( - base64url.toString(challenge), + isoBase64URL.toString(challenge), ); return parsedChallenge.actualChallenge === 'K3QxOjnVJLiGlnVEp5va5QJeMVWNf_7PYgutgbAtAUA'; }, expectedOrigin: 'http://localhost:8000', expectedRPID: 'localhost', authenticator: { - credentialID: base64url.toBuffer( + credentialID: isoBase64URL.toBuffer( 'AaIBxnYfL2pDWJmIii6CYgHBruhVvFGHheWamphVioG_TnEXxKA9MW4FWnJh21zsbmRpRJso9i2JmAtWOtXfVd4oXTgYVusXwhWWsA', ), - credentialPublicKey: base64url.toBuffer( + credentialPublicKey: isoBase64URL.toBuffer( 'pQECAyYgASFYILTrxTUQv3X4DRM6L_pk65FSMebenhCx3RMsTKoBm-AxIlggEf3qk5552QLNSh1T1oQs7_2C2qysDwN4r4fCp52Hsqs', ), counter: 0, @@ -333,10 +332,10 @@ test('should return authenticator extension output', async () => { expectedRPID: 'try-webauthn.appspot.com', expectedChallenge: 'iZsVCztrDW7D2U_GHCIlYKLwV2bCsBTRqVQUnJXn9Tk', authenticator: { - credentialID: base64url.toBuffer( + credentialID: isoBase64URL.toBuffer( 'AaIBxnYfL2pDWJmIii6CYgHBruhVvFGHheWamphVioG_TnEXxKA9MW4FWnJh21zsbmRpRJso9i2JmAtWOtXfVd4oXTgYVusXwhWWsA', ), - credentialPublicKey: base64url.toBuffer( + credentialPublicKey: isoBase64URL.toBuffer( 'pQECAyYgASFYILTrxTUQv3X4DRM6L_pk65FSMebenhCx3RMsTKoBm-AxIlggEf3qk5552QLNSh1T1oQs7_2C2qysDwN4r4fCp52Hsqs', ), counter: 0, @@ -345,15 +344,15 @@ test('should return authenticator extension output', async () => { expect(verification.authenticationInfo?.authenticatorExtensionResults).toMatchObject({ devicePubKey: { - dpk: uint8Array.fromHex( + dpk: isoUint8Array.fromHex( 'A5010203262001215820991AABED9DE4271A9EDEAD8806F9DC96D6DCCD0C476253A5510489EC8379BE5B225820A0973CFDEDBB79E27FEF4EE7481673FB3312504DDCA5434CFD23431D6AD29EDA', ), - sig: uint8Array.fromHex( + sig: isoUint8Array.fromHex( '3045022049526CD28AEF6B4E621A7D5936D2B504952FC0AE2313A4F0357AAFFFAEA964740221009D513ACAEFB0B32C765AAE6FEBA8C294685EFF63FF1CBF11ECF2107AF4FEB8F8', ), - nonce: uint8Array.fromHex(''), - scope: uint8Array.fromHex('00'), - aaguid: uint8Array.fromHex('B93FD961F2E6462FB12282002247DE78'), + nonce: isoUint8Array.fromHex(''), + scope: isoUint8Array.fromHex('00'), + aaguid: isoUint8Array.fromHex('B93FD961F2E6462FB12282002247DE78'), }, }); }); @@ -391,14 +390,14 @@ const assertionResponse: AuthenticationCredentialJSON = { clientExtensionResults: {}, type: 'public-key', }; -const assertionChallenge = base64url.fromString('totallyUniqueValueEveryTime'); +const assertionChallenge = isoBase64URL.fromString('totallyUniqueValueEveryTime'); const assertionOrigin = 'https://dev.dontneeda.pw'; const authenticator: AuthenticatorDevice = { - credentialPublicKey: base64url.toBuffer( + credentialPublicKey: isoBase64URL.toBuffer( 'pQECAyYgASFYIIheFp-u6GvFT2LNGovf3ZrT0iFVBsA_76rRysxRG9A1Ilgg8WGeA6hPmnab0HAViUYVRkwTNcN77QBf_RR0dv3lIvQ', ), - credentialID: base64url.toBuffer( + credentialID: isoBase64URL.toBuffer( 'KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Pxg6jo_o0hYiew', ), counter: 143, @@ -420,13 +419,13 @@ const assertionFirstTimeUsedResponse: AuthenticationCredentialJSON = { type: 'public-key', clientExtensionResults: {}, }; -const assertionFirstTimeUsedChallenge = base64url.fromString('totallyUniqueValueEveryAssertion'); +const assertionFirstTimeUsedChallenge = isoBase64URL.fromString('totallyUniqueValueEveryAssertion'); const assertionFirstTimeUsedOrigin = 'https://dev.dontneeda.pw'; const authenticatorFirstTimeUsed: AuthenticatorDevice = { - credentialPublicKey: base64url.toBuffer( + credentialPublicKey: isoBase64URL.toBuffer( 'pQECAyYgASFYIGmaxR4mBbukc2QhtW2ldhAAd555r-ljlGQN8MbcTnPPIlgg9CyUlE-0AB2fbzZbNgBvJuRa7r6o2jPphOmtyNPR_kY', ), - credentialID: base64url.toBuffer( + credentialID: isoBase64URL.toBuffer( 'wSisR0_4hlzw3Y1tj4uNwwifIhRa-ZxWJwWbnfror0pVK9qPdBPO5pW3gasPqn6wXHb0LNhXB_IrA1nFoSQJ9A', ), counter: 0, diff --git a/packages/server/src/authentication/verifyAuthenticationResponse.ts b/packages/server/src/authentication/verifyAuthenticationResponse.ts index dbec6b8..6d68e19 100644 --- a/packages/server/src/authentication/verifyAuthenticationResponse.ts +++ b/packages/server/src/authentication/verifyAuthenticationResponse.ts @@ -13,8 +13,7 @@ import { isBase64URLString } from '../helpers/isBase64URLString'; import { parseBackupFlags } from '../helpers/parseBackupFlags'; import { AuthenticationExtensionsAuthenticatorOutputs } from '../helpers/decodeAuthenticatorExtensions'; import { matchExpectedRPID } from '../helpers/matchExpectedRPID'; -import * as isoUint8Array from '../helpers/isoUint8Array'; -import * as isoBase64URL from '../helpers/isoBase64URL'; +import { isoUint8Array, isoBase64URL } from '../helpers/iso'; export type VerifyAuthenticationResponseOpts = { credential: AuthenticationCredentialJSON; @@ -144,7 +143,7 @@ export async function verifyAuthenticationResponse( } } - const authDataBuffer = base64url.toBuffer(response.authenticatorData); + const authDataBuffer = isoBase64URL.toBuffer(response.authenticatorData); const parsedAuthData = parseAuthenticatorData(authDataBuffer); const { rpIdHash, flags, counter, extensionsData } = parsedAuthData; @@ -187,10 +186,10 @@ export async function verifyAuthenticationResponse( } } - const clientDataHash = await toHash(base64url.toBuffer(response.clientDataJSON)); - const signatureBase = uint8Array.concat([authDataBuffer, clientDataHash]); + const clientDataHash = await toHash(isoBase64URL.toBuffer(response.clientDataJSON)); + const signatureBase = isoUint8Array.concat([authDataBuffer, clientDataHash]); - const signature = base64url.toBuffer(response.signature); + const signature = isoBase64URL.toBuffer(response.signature); if ((counter > 0 || authenticator.counter > 0) && counter <= authenticator.counter) { // Error out when the counter in the DB is greater than or equal to the counter in the diff --git a/packages/server/src/helpers/convertAAGUIDToString.ts b/packages/server/src/helpers/convertAAGUIDToString.ts index 8375099..db9622a 100644 --- a/packages/server/src/helpers/convertAAGUIDToString.ts +++ b/packages/server/src/helpers/convertAAGUIDToString.ts @@ -1,11 +1,11 @@ -import * as isoUint8Array from './isoUint8Array'; +import { isoUint8Array } from './iso'; /** * Convert the aaguid buffer in authData into a UUID string */ export function convertAAGUIDToString(aaguid: Uint8Array): string { // Raw Hex: adce000235bcc60a648b0b25f1f05503 - const hex = uint8Array.toHex(aaguid); + const hex = isoUint8Array.toHex(aaguid); const segments: string[] = [ hex.slice(0, 8), // 8 diff --git a/packages/server/src/helpers/convertCOSEtoPKCS.test.ts b/packages/server/src/helpers/convertCOSEtoPKCS.test.ts index d5d269e..db760d7 100644 --- a/packages/server/src/helpers/convertCOSEtoPKCS.test.ts +++ b/packages/server/src/helpers/convertCOSEtoPKCS.test.ts @@ -1,4 +1,4 @@ -import * as esmDecodeCbor from './cbor'; +import { isoCBOR } from './iso'; import { convertCOSEtoPKCS, COSEKEYS } from './convertCOSEtoPKCS'; @@ -7,7 +7,7 @@ test('should throw an error curve if, somehow, curve coordinate x is missing', ( mockCOSEKey.set(COSEKEYS.y, 1); - jest.spyOn(esmDecodeCbor, 'decodeFirst').mockReturnValue(mockCOSEKey); + jest.spyOn(isoCBOR, 'decodeFirst').mockReturnValue(mockCOSEKey); expect(() => { convertCOSEtoPKCS(Buffer.from('123', 'ascii')); @@ -19,7 +19,7 @@ test('should throw an error curve if, somehow, curve coordinate y is missing', ( mockCOSEKey.set(COSEKEYS.x, 1); - jest.spyOn(esmDecodeCbor, 'decodeFirst').mockReturnValue(mockCOSEKey); + jest.spyOn(isoCBOR, 'decodeFirst').mockReturnValue(mockCOSEKey); expect(() => { convertCOSEtoPKCS(Buffer.from('123', 'ascii')); diff --git a/packages/server/src/helpers/convertCOSEtoPKCS.ts b/packages/server/src/helpers/convertCOSEtoPKCS.ts index 0c1a8b5..f897cc8 100644 --- a/packages/server/src/helpers/convertCOSEtoPKCS.ts +++ b/packages/server/src/helpers/convertCOSEtoPKCS.ts @@ -1,12 +1,12 @@ import { COSEAlgorithmIdentifier } from '@simplewebauthn/typescript-types'; -import * as isoCBOR from './isoCBOR'; -import * as isoUint8Array from './isoUint8Array'; + +import { isoCBOR, isoUint8Array } from './iso'; /** * Takes COSE-encoded public key and converts it to PKCS key */ export function convertCOSEtoPKCS(cosePublicKey: Uint8Array): Uint8Array { - const struct = cbor.decodeFirst(cosePublicKey); + const struct = isoCBOR.decodeFirst(cosePublicKey); const tag = Uint8Array.from([0x04]); const x = struct.get(COSEKEYS.x); @@ -17,10 +17,10 @@ export function convertCOSEtoPKCS(cosePublicKey: Uint8Array): Uint8Array { } if (y) { - return uint8Array.concat([tag, x as Uint8Array, y as Uint8Array]); + return isoUint8Array.concat([tag, x as Uint8Array, y as Uint8Array]); } - return uint8Array.concat([tag, x as Uint8Array]); + return isoUint8Array.concat([tag, x as Uint8Array]); } export type COSEPublicKey = Map; diff --git a/packages/server/src/helpers/convertCertBufferToPEM.ts b/packages/server/src/helpers/convertCertBufferToPEM.ts index 4a0c674..0179eee 100644 --- a/packages/server/src/helpers/convertCertBufferToPEM.ts +++ b/packages/server/src/helpers/convertCertBufferToPEM.ts @@ -1,6 +1,6 @@ import type { Base64URLString } from '@simplewebauthn/typescript-types'; -import * as isoBase64URL from './isoBase64URL'; +import { isoBase64URL } from './iso'; /** * Convert buffer to an OpenSSL-compatible PEM text format. @@ -12,15 +12,15 @@ export function convertCertBufferToPEM(certBuffer: Uint8Array | Base64URLString) * Get certBuffer to a base64 representation */ if (typeof certBuffer === 'string') { - if (base64url.isBase64url(certBuffer)) { - b64cert = base64url.toBase64(certBuffer); - } else if (base64url.isBase64(certBuffer)) { + if (isoBase64URL.isBase64url(certBuffer)) { + b64cert = isoBase64URL.toBase64(certBuffer); + } else if (isoBase64URL.isBase64(certBuffer)) { b64cert = certBuffer } else { throw new Error('Certificate is not a valid base64 or base64url string'); } } else { - b64cert = base64url.fromBuffer(certBuffer, 'base64'); + b64cert = isoBase64URL.fromBuffer(certBuffer, 'base64'); } let PEMKey = ''; diff --git a/packages/server/src/helpers/convertPublicKeyToPEM.test.ts b/packages/server/src/helpers/convertPublicKeyToPEM.test.ts index f7cd401..efeaf3f 100644 --- a/packages/server/src/helpers/convertPublicKeyToPEM.test.ts +++ b/packages/server/src/helpers/convertPublicKeyToPEM.test.ts @@ -1,20 +1,19 @@ import { COSEKEYS } from './convertCOSEtoPKCS'; import { convertPublicKeyToPEM } from './convertPublicKeyToPEM'; -import * as cbor from './cbor'; -import * as uint8Array from './uint8Array'; +import { isoCBOR, isoUint8Array } from './iso'; test('should return pem - EC2', () => { const mockEC2Key = new Map(); - const x = uint8Array.fromHex('821f4c9978ed99c1c57aca1fa9667a8aec52740620a0f56f7c9aa9bf5f35f25a'); - const y = uint8Array.fromHex('dc10d91ec36f2946f955bc863ea70015fe051ae3e12765f2db5e68583c3fd637'); + const x = isoUint8Array.fromHex('821f4c9978ed99c1c57aca1fa9667a8aec52740620a0f56f7c9aa9bf5f35f25a'); + const y = isoUint8Array.fromHex('dc10d91ec36f2946f955bc863ea70015fe051ae3e12765f2db5e68583c3fd637'); mockEC2Key.set(COSEKEYS.kty, 2); mockEC2Key.set(COSEKEYS.alg, -7); mockEC2Key.set(COSEKEYS.crv, 1); mockEC2Key.set(COSEKEYS.x, x); mockEC2Key.set(COSEKEYS.y, y); - const pubKeyCBOR = cbor.encode(mockEC2Key); + const pubKeyCBOR = isoCBOR.encode(mockEC2Key); const actual = convertPublicKeyToPEM(pubKeyCBOR); expect(actual).toEqual(`-----BEGIN PUBLIC KEY----- @@ -37,7 +36,7 @@ test('should return pem - RSA', () => { mockRSAKey.set(COSEKEYS.n, n); mockRSAKey.set(COSEKEYS.e, e); - const pubKeyCBOR = cbor.encode(mockRSAKey); + const pubKeyCBOR = isoCBOR.encode(mockRSAKey); const actual = convertPublicKeyToPEM(pubKeyCBOR); expect(actual).toEqual(`-----BEGIN PUBLIC KEY----- @@ -58,7 +57,7 @@ test('should return pem when input is base64URLString', () => { mockCOSEKey.set(COSEKEYS.kty, 0); mockCOSEKey.set(COSEKEYS.alg, -7); - const pubKeyCBOR = cbor.encode(mockCOSEKey); + const pubKeyCBOR = isoCBOR.encode(mockCOSEKey); try { convertPublicKeyToPEM(pubKeyCBOR); @@ -73,7 +72,7 @@ test('should raise error when kty is OKP (1)', () => { mockOKPKey.set(COSEKEYS.kty, 1); mockOKPKey.set(COSEKEYS.alg, -7); - const pubKeyCBOR = cbor.encode(mockOKPKey); + const pubKeyCBOR = isoCBOR.encode(mockOKPKey); try { convertPublicKeyToPEM(pubKeyCBOR); diff --git a/packages/server/src/helpers/convertPublicKeyToPEM.ts b/packages/server/src/helpers/convertPublicKeyToPEM.ts index 74cc77f..a41952f 100644 --- a/packages/server/src/helpers/convertPublicKeyToPEM.ts +++ b/packages/server/src/helpers/convertPublicKeyToPEM.ts @@ -1,13 +1,12 @@ import jwkToPem from 'jwk-to-pem'; import { COSEKEYS, COSEKTY, COSECRV, COSEPublicKey } from './convertCOSEtoPKCS'; -import * as isoCBOR from './isoCBOR'; -import * as isoBase64URL from './isoBase64URL'; +import { isoBase64URL, isoCBOR } from './iso'; export function convertPublicKeyToPEM(publicKey: Uint8Array): string { let struct; try { - struct = cbor.decodeFirst(publicKey); + struct = isoCBOR.decodeFirst(publicKey); } catch (err) { const _err = err as Error; throw new Error(`Error decoding public key while converting to PEM: ${_err.message}`); @@ -40,8 +39,8 @@ export function convertPublicKeyToPEM(publicKey: Uint8Array): string { kty: 'EC', // Specify curve as "P-256" from "p256" crv: COSECRV[crv as number].replace('p', 'P-'), - x: base64url.fromBuffer(x as Uint8Array, 'base64'), - y: base64url.fromBuffer(y as Uint8Array, 'base64'), + x: isoBase64URL.fromBuffer(x as Uint8Array, 'base64'), + y: isoBase64URL.fromBuffer(y as Uint8Array, 'base64'), }); return ecPEM; @@ -59,8 +58,8 @@ export function convertPublicKeyToPEM(publicKey: Uint8Array): string { const rsaPEM = jwkToPem({ kty: 'RSA', - n: base64url.fromBuffer(n as Uint8Array, 'base64'), - e: base64url.fromBuffer(e as Uint8Array, 'base64'), + n: isoBase64URL.fromBuffer(n as Uint8Array, 'base64'), + e: isoBase64URL.fromBuffer(e as Uint8Array, 'base64'), }); return rsaPEM; diff --git a/packages/server/src/helpers/decodeAttestationObject.ts b/packages/server/src/helpers/decodeAttestationObject.ts index 5c3a6d8..fc98aa6 100644 --- a/packages/server/src/helpers/decodeAttestationObject.ts +++ b/packages/server/src/helpers/decodeAttestationObject.ts @@ -1,4 +1,4 @@ -import * as isoCBOR from './isoCBOR'; +import { isoCBOR } from './iso'; /** * Convert an AttestationObject buffer to a proper object @@ -6,7 +6,7 @@ import * as isoCBOR from './isoCBOR'; * @param base64AttestationObject Attestation Object buffer */ export function decodeAttestationObject(attestationObject: Uint8Array): AttestationObject { - return cbor.decodeFirst(attestationObject); + return isoCBOR.decodeFirst(attestationObject); } export type AttestationFormat = diff --git a/packages/server/src/helpers/decodeAuthenticatorExtensions.test.ts b/packages/server/src/helpers/decodeAuthenticatorExtensions.test.ts index 560a5c4..8afa3d7 100644 --- a/packages/server/src/helpers/decodeAuthenticatorExtensions.test.ts +++ b/packages/server/src/helpers/decodeAuthenticatorExtensions.test.ts @@ -1,9 +1,9 @@ import { decodeAuthenticatorExtensions } from './decodeAuthenticatorExtensions'; -import * as uint8Array from './uint8Array'; +import { isoUint8Array } from './iso'; test('should decode authenticator extensions', () => { const extensions = decodeAuthenticatorExtensions( - uint8Array.fromHex( + isoUint8Array.fromHex( 'A16C6465766963655075624B6579A56364706B584DA5010203262001215820991AABED9D' + 'E4271A9EDEAD8806F9DC96D6DCCD0C476253A5510489EC8379BE5B225820A0973CFDEDBB' + '79E27FEF4EE7481673FB3312504DDCA5434CFD23431D6AD29EDA63736967584730450221' + @@ -14,15 +14,15 @@ test('should decode authenticator extensions', () => { ); expect(extensions).toMatchObject({ devicePubKey: { - dpk: uint8Array.fromHex( + dpk: isoUint8Array.fromHex( 'A5010203262001215820991AABED9DE4271A9EDEAD8806F9DC96D6DCCD0C476253A5510489EC8379BE5B225820A0973CFDEDBB79E27FEF4EE7481673FB3312504DDCA5434CFD23431D6AD29EDA', ), - sig: uint8Array.fromHex( + sig: isoUint8Array.fromHex( '3045022100EFB38074BD15B8C82CF09F87FBC6FB3C7169EA4F1806B7E90937374302345B7A02202B7113040731A0E727D338D48542863CE65880AA79E5EA740AC8CCD94347988E', ), - nonce: uint8Array.fromHex(''), - scope: uint8Array.fromHex('00'), - aaguid: uint8Array.fromHex('00000000000000000000000000000000'), + nonce: isoUint8Array.fromHex(''), + scope: isoUint8Array.fromHex('00'), + aaguid: isoUint8Array.fromHex('00000000000000000000000000000000'), }, }); }); diff --git a/packages/server/src/helpers/decodeAuthenticatorExtensions.ts b/packages/server/src/helpers/decodeAuthenticatorExtensions.ts index af38f1a..7bd583c 100644 --- a/packages/server/src/helpers/decodeAuthenticatorExtensions.ts +++ b/packages/server/src/helpers/decodeAuthenticatorExtensions.ts @@ -1,4 +1,4 @@ -import * as isoCBOR from './isoCBOR'; +import { isoCBOR } from './iso'; /** * Convert authenticator extension data buffer to a proper object @@ -10,7 +10,7 @@ export function decodeAuthenticatorExtensions( ): AuthenticationExtensionsAuthenticatorOutputs | undefined { let toCBOR: Map; try { - toCBOR = cbor.decodeFirst(extensionData); + toCBOR = isoCBOR.decodeFirst(extensionData); } catch (err) { const _err = err as Error; throw new Error(`Error decoding authenticator extensions: ${_err.message}`); diff --git a/packages/server/src/helpers/decodeClientDataJSON.ts b/packages/server/src/helpers/decodeClientDataJSON.ts index 82ca17b..36a949e 100644 --- a/packages/server/src/helpers/decodeClientDataJSON.ts +++ b/packages/server/src/helpers/decodeClientDataJSON.ts @@ -1,10 +1,10 @@ -import * as isoBase64URL from "./isoBase64URL"; +import { isoBase64URL } from "./iso"; /** * Decode an authenticator's base64url-encoded clientDataJSON to JSON */ export function decodeClientDataJSON(data: string): ClientDataJSON { - const toString = base64url.toString(data); + const toString = isoBase64URL.toString(data); const clientData: ClientDataJSON = JSON.parse(toString); return clientData; diff --git a/packages/server/src/helpers/decodeCredentialPublicKey.ts b/packages/server/src/helpers/decodeCredentialPublicKey.ts index 5891b03..a2f8e55 100644 --- a/packages/server/src/helpers/decodeCredentialPublicKey.ts +++ b/packages/server/src/helpers/decodeCredentialPublicKey.ts @@ -1,6 +1,6 @@ import { COSEPublicKey } from './convertCOSEtoPKCS'; -import * as isoCBOR from './isoCBOR'; +import { isoCBOR } from './iso'; export function decodeCredentialPublicKey(publicKey: Uint8Array): COSEPublicKey { - return cbor.decodeFirst(publicKey); + return isoCBOR.decodeFirst(publicKey); } diff --git a/packages/server/src/helpers/index.ts b/packages/server/src/helpers/index.ts index ff565bc..b71e0b1 100644 --- a/packages/server/src/helpers/index.ts +++ b/packages/server/src/helpers/index.ts @@ -13,9 +13,7 @@ import { parseAuthenticatorData } from './parseAuthenticatorData'; import { toHash } from './toHash'; import { validateCertificatePath } from './validateCertificatePath'; import { verifySignature } from './verifySignature'; -import * as isoCBOR from './isoCBOR'; -import * as isoBase64URL from './isoBase64URL'; -import * as isoUint8Array from './isoUint8Array'; +import { isoCBOR, isoBase64URL, isoUint8Array } from './iso'; export { convertAAGUIDToString, @@ -33,9 +31,9 @@ export { toHash, validateCertificatePath, verifySignature, - cbor, - base64url, - uint8Array, + isoCBOR, + isoBase64URL, + isoUint8Array, }; import type { diff --git a/packages/server/src/helpers/isCertRevoked.ts b/packages/server/src/helpers/isCertRevoked.ts index 2f5c3a5..1ea3a8a 100644 --- a/packages/server/src/helpers/isCertRevoked.ts +++ b/packages/server/src/helpers/isCertRevoked.ts @@ -4,7 +4,7 @@ import { AsnParser } from '@peculiar/asn1-schema'; import { CertificateList } from '@peculiar/asn1-x509'; import { convertCertBufferToPEM } from './convertCertBufferToPEM'; -import * as isoUint8Array from './isoUint8Array'; +import { isoUint8Array } from './iso'; /** * A cache of revoked cert serial numbers by Authority Key ID @@ -69,7 +69,7 @@ export async function isCertRevoked(cert: X509): Promise { return false; } - const data = AsnParser.parse(uint8Array.fromHex(crlCert.hex), CertificateList); + const data = AsnParser.parse(isoUint8Array.fromHex(crlCert.hex), CertificateList); const newCached: CAAuthorityInfo = { revokedCerts: [], @@ -86,7 +86,7 @@ export async function isCertRevoked(cert: X509): Promise { if (revokedCerts) { for (const cert of revokedCerts) { - const revokedHex = uint8Array.toHex(new Uint8Array(cert.userCertificate)); + const revokedHex = isoUint8Array.toHex(new Uint8Array(cert.userCertificate)); newCached.revokedCerts.push(revokedHex); } diff --git a/packages/server/src/helpers/iso/index.ts b/packages/server/src/helpers/iso/index.ts new file mode 100644 index 0000000..5cb3bab --- /dev/null +++ b/packages/server/src/helpers/iso/index.ts @@ -0,0 +1,10 @@ +/** + * A collection of methods for isomorphic manipulation of trickier data types + * + * The goal with these is to make it easier to replace dependencies later that might not play well + * with specific server-like runtimes that expose global Web APIs (CloudFlare Workers, Deno, Bun, + * etc...), while also supporting execution in Node. + */ +export * as isoBase64URL from './isoBase64URL'; +export * as isoCBOR from './isoCBOR'; +export * as isoUint8Array from './isoUint8Array'; diff --git a/packages/server/src/helpers/iso/isoBase64URL.ts b/packages/server/src/helpers/iso/isoBase64URL.ts new file mode 100644 index 0000000..d03de51 --- /dev/null +++ b/packages/server/src/helpers/iso/isoBase64URL.ts @@ -0,0 +1,62 @@ +import base64 from '@hexagon/base64'; + +/** + * Decode from a Base64URL-encoded string to an ArrayBuffer. Best used when converting a + * credential ID from a JSON string to an ArrayBuffer, like in allowCredentials or + * excludeCredentials. + * + * @param buffer Value to decode from base64 + * @param to (optional) The decoding to use, in case it's desirable to decode from base64 instead + */ +export function toBuffer(base64urlString: string, from: 'base64' | 'base64url' = 'base64url'): Uint8Array { + const _buffer = base64.toArrayBuffer(base64urlString, from === 'base64url'); + return new Uint8Array(_buffer); +} + +/** + * Encode 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. + * + * @param buffer Value to encode to base64 + * @param to (optional) The encoding to use, in case it's desirable to encode to base64 instead + */ +export function fromBuffer(buffer: Uint8Array, to: 'base64' | 'base64url' = 'base64url'): string { + return base64.fromArrayBuffer(buffer, to === 'base64url'); +} + +/** + * Convert a base64url string into base64 + */ +export function toBase64(base64urlString: string): string { + const fromBase64Url = base64.toArrayBuffer(base64urlString, true); + const toBase64 = base64.fromArrayBuffer(fromBase64Url); + return toBase64; +} + +/** + * Encode a string to base64url + */ +export function fromString(ascii: string): string { + return base64.fromString(ascii, true); +} + +/** + * Decode a base64url string into its original string + */ +export function toString(base64urlString: string): string { + return base64.toString(base64urlString, true); +} + +/** + * Confirm that the string is encoded into base64 + */ +export function isBase64(input: string): boolean { + return base64.validate(input, false); +} + +/** + * Confirm that the string is encoded into base64url + */ +export function isBase64url(input: string): boolean { + return base64.validate(input, true); +} diff --git a/packages/server/src/helpers/iso/isoCBOR.ts b/packages/server/src/helpers/iso/isoCBOR.ts new file mode 100644 index 0000000..9f7cbd7 --- /dev/null +++ b/packages/server/src/helpers/iso/isoCBOR.ts @@ -0,0 +1,46 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +import * as cborx from 'cbor-x'; + +/** + * This encoder should keep CBOR data the same length when data is re-encoded + * + * MOST CRITICALLY, this means the following needs to be true of whatever CBOR library we use: + * - CBOR Map type values MUST decode to JavaScript Maps + * - CBOR tag 64 (uint8 Typed Array) MUST NOT be used when encoding Uint8Arrays back to CBOR + * + * So long as these requirements are maintained, then CBOR sequences can be encoded and decoded + * freely while maintaining their lengths for the most accurate pointer movement across them. + */ +const encoder = new cborx.Encoder({ mapsAsObjects: false, tagUint8Array: false }); + +/** + * Decode and return the first item in a sequence of CBOR-encoded values + * + * @param input The CBOR data to decode + * @param asObject (optional) Whether to convert any CBOR Maps into JavaScript Objects. Defaults to + * `false` + */ +export function decodeFirst(input: Uint8Array): Type { + const decoded = encoder.decodeMultiple(input) as undefined | Type[]; + + if (decoded === undefined) { + throw new Error('CBOR input data was empty'); + } + + /** + * Typing on `decoded` is `void | []` which causes TypeScript to think that it's an empty array, + * and thus you can't destructure it. I'm ignoring that because the code works fine in JS, and + * so this should be a valid operation. + */ + // @ts-ignore 2493 + const [first] = decoded; + + return first; +} + +/** + * Encode data to CBOR + */ +export function encode(input: any): Uint8Array { + return encoder.encode(input); +} diff --git a/packages/server/src/helpers/iso/isoUint8Array.ts b/packages/server/src/helpers/iso/isoUint8Array.ts new file mode 100644 index 0000000..5237937 --- /dev/null +++ b/packages/server/src/helpers/iso/isoUint8Array.ts @@ -0,0 +1,90 @@ +/** + * Make sure two Uint8Arrays are deeply equivalent + */ +export function areEqual(array1: Uint8Array, array2: Uint8Array): boolean { + if (array1.length != array2.length) { + return false; + } + + return array1.every((val, i) => val === array2[i]); +} + +/** + * Convert a Uint8Array to Hexadecimal. + * + * A replacement for `Buffer.toString('hex')` + */ +export function toHex(array: Uint8Array): string { + const hexParts = Array.from(array, i => i.toString(16).padStart(2, "0")); + + // adce000235bcc60a648b0b25f1f05503 + return hexParts.join(''); +} + +/** + * Convert a hexadecimal string to isoUint8Array. + * + * A replacement for `Buffer.from('...', 'hex')` + */ +export function fromHex(hex: string): Uint8Array { + if (!hex) { + return Uint8Array.from([]); + } + + const isValid = hex.length !== 0 && hex.length % 2 === 0 && !/[^a-fA-F0-9]/u.test(hex); + + if (!isValid) { + throw new Error('Invalid hex string'); + } + + const byteStrings = hex.match(/.{1,2}/g) ?? []; + + return Uint8Array.from(byteStrings.map((byte) => parseInt(byte, 16))); +} + +/** + * Combine multiple Uint8Arrays into a single Uint8Array + */ +export function concat(arrays: Uint8Array[]): Uint8Array { + let pointer = 0; + const totalLength = arrays.reduce((prev, curr) => prev + curr.length, 0); + + const toReturn = new Uint8Array(totalLength); + + arrays.forEach((arr) => { + toReturn.set(arr, pointer); + pointer += arr.length; + }); + + return toReturn; +} + +/** + * Convert bytes into a UTF-8 string + */ +export function toUTF8String(array: Uint8Array): string { + const decoder = new globalThis.TextDecoder("utf-8"); + return decoder.decode(array); +} + +/** + * Convert a UTF-8 string back into bytes + */ +export function fromUTF8String(utf8String: string): Uint8Array { + const encoder = new globalThis.TextEncoder(); + return encoder.encode(utf8String); +} + +/** + * Convert an ASCII string to Uint8Array + */ +export function fromASCIIString(value: string): Uint8Array { + return Uint8Array.from(value.split("").map(x => x.charCodeAt(0))); +} + +/** + * Prepare a DataView we can slice our way around in as we parse the bytes in a Uint8Array + */ +export function toDataView(array: Uint8Array): DataView { + return new DataView(array.buffer, array.byteOffset, array.length); +} diff --git a/packages/server/src/helpers/isoBase64URL.ts b/packages/server/src/helpers/isoBase64URL.ts deleted file mode 100644 index cb8c71d..0000000 --- a/packages/server/src/helpers/isoBase64URL.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * A collection of isomorphic methods for working with base64url and base64 values - */ -import base64 from '@hexagon/base64'; - -/** - * Decode from a Base64URL-encoded string to an ArrayBuffer. Best used when converting a - * credential ID from a JSON string to an ArrayBuffer, like in allowCredentials or - * excludeCredentials. - * - * @param buffer Value to decode from base64 - * @param to (optional) The decoding to use, in case it's desirable to decode from base64 instead - */ -export function toBuffer(base64urlString: string, from: 'base64' | 'base64url' = 'base64url'): Uint8Array { - const _buffer = base64.toArrayBuffer(base64urlString, from === 'base64url'); - return new Uint8Array(_buffer); -} - -/** - * Encode 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. - * - * @param buffer Value to encode to base64 - * @param to (optional) The encoding to use, in case it's desirable to encode to base64 instead - */ -export function fromBuffer(buffer: Uint8Array, to: 'base64' | 'base64url' = 'base64url'): string { - return base64.fromArrayBuffer(buffer, to === 'base64url'); -} - -/** - * Convert a base64url string into base64 - */ -export function toBase64(base64urlString: string): string { - const fromBase64Url = base64.toArrayBuffer(base64urlString, true); - const toBase64 = base64.fromArrayBuffer(fromBase64Url); - return toBase64; -} - -/** - * Encode a string to base64url - */ -export function fromString(ascii: string): string { - return base64.fromString(ascii, true); -} - -/** - * Decode a base64url string into its original string - */ -export function toString(base64urlString: string): string { - return base64.toString(base64urlString, true); -} - -/** - * Confirm that the string is encoded into base64 - */ -export function isBase64(input: string): boolean { - return base64.validate(input, false); -} - -/** - * Confirm that the string is encoded into base64url - */ -export function isBase64url(input: string): boolean { - return base64.validate(input, true); -} diff --git a/packages/server/src/helpers/isoCBOR.ts b/packages/server/src/helpers/isoCBOR.ts deleted file mode 100644 index 9093ffd..0000000 --- a/packages/server/src/helpers/isoCBOR.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ -/** - * A collection of isomorphic methods for working with CBOR values - */ -import * as cborx from 'cbor-x'; - -/** - * This encoder should keep CBOR data the same length when data is re-encoded - * - * MOST CRITICALLY, this means the following needs to be true of whatever CBOR library we use: - * - CBOR Map type values MUST decode to JavaScript Maps - * - CBOR tag 64 (uint8 Typed Array) MUST NOT be used when encoding Uint8Arrays back to CBOR - * - * So long as these requirements are maintained, then CBOR sequences can be encoded and decoded - * freely while maintaining their lengths for the most accurate pointer movement across them. - */ -const encoder = new cborx.Encoder({ mapsAsObjects: false, tagUint8Array: false }); - -/** - * Decode and return the first item in a sequence of CBOR-encoded values - * - * @param input The CBOR data to decode - * @param asObject (optional) Whether to convert any CBOR Maps into JavaScript Objects. Defaults to - * `false` - */ -export function decodeFirst(input: Uint8Array): Type { - const decoded = encoder.decodeMultiple(input) as undefined | Type[]; - - if (decoded === undefined) { - throw new Error('CBOR input data was empty'); - } - - /** - * Typing on `decoded` is `void | []` which causes TypeScript to think that it's an empty array, - * and thus you can't destructure it. I'm ignoring that because the code works fine in JS, and - * so this should be a valid operation. - */ - // @ts-ignore 2493 - const [first] = decoded; - - return first; -} - -/** - * Encode data to CBOR - */ -export function encode(input: any): Uint8Array { - return encoder.encode(input); -} diff --git a/packages/server/src/helpers/isoUint8Array.ts b/packages/server/src/helpers/isoUint8Array.ts deleted file mode 100644 index 0834bb2..0000000 --- a/packages/server/src/helpers/isoUint8Array.ts +++ /dev/null @@ -1,94 +0,0 @@ -/** - * A collection of isomorphic methods for working with Uint8Array values - */ - -/** - * Make sure two Uint8Arrays are deeply equivalent - */ -export function areEqual(array1: Uint8Array, array2: Uint8Array): boolean { - if (array1.length != array2.length) { - return false; - } - - return array1.every((val, i) => val === array2[i]); -} - -/** - * Convert a Uint8Array to Hexadecimal. - * - * A replacement for `Buffer.toString('hex')` - */ -export function toHex(array: Uint8Array): string { - const hexParts = Array.from(array, i => i.toString(16).padStart(2, "0")); - - // adce000235bcc60a648b0b25f1f05503 - return hexParts.join(''); -} - -/** - * Convert a hexadecimal string to Uint8Array. - * - * A replacement for `Buffer.from('...', 'hex')` - */ -export function fromHex(hex: string): Uint8Array { - if (!hex) { - return Uint8Array.from([]); - } - - const isValid = hex.length !== 0 && hex.length % 2 === 0 && !/[^a-fA-F0-9]/u.test(hex); - - if (!isValid) { - throw new Error('Invalid hex string'); - } - - const byteStrings = hex.match(/.{1,2}/g) ?? []; - - return Uint8Array.from(byteStrings.map((byte) => parseInt(byte, 16))); -} - -/** - * Combine multiple Uint8Arrays into a single Uint8Array - */ -export function concat(arrays: Uint8Array[]): Uint8Array { - let pointer = 0; - const totalLength = arrays.reduce((prev, curr) => prev + curr.length, 0); - - const toReturn = new Uint8Array(totalLength); - - arrays.forEach((arr) => { - toReturn.set(arr, pointer); - pointer += arr.length; - }); - - return toReturn; -} - -/** - * Convert bytes into a UTF-8 string - */ -export function toUTF8String(array: Uint8Array): string { - const decoder = new globalThis.TextDecoder("utf-8"); - return decoder.decode(array); -} - -/** - * Convert a UTF-8 string back into bytes - */ -export function fromUTF8String(utf8String: string): Uint8Array { - const encoder = new globalThis.TextEncoder(); - return encoder.encode(utf8String); -} - -/** - * Convert an ASCII string to Uint8Array - */ -export function fromASCIIString(value: string): Uint8Array { - return Uint8Array.from(value.split("").map(x => x.charCodeAt(0))); -} - -/** - * Prepare a DataView we can slice our way around in as we parse the bytes in a Uint8Array - */ -export function toDataView(array: Uint8Array): DataView { - return new DataView(array.buffer, array.byteOffset, array.length); -} diff --git a/packages/server/src/helpers/matchExpectedRPID.ts b/packages/server/src/helpers/matchExpectedRPID.ts index aa96fee..76991bf 100644 --- a/packages/server/src/helpers/matchExpectedRPID.ts +++ b/packages/server/src/helpers/matchExpectedRPID.ts @@ -1,5 +1,5 @@ import { toHash } from './toHash'; -import * as isoUint8Array from './isoUint8Array'; +import { isoUint8Array } from './iso'; /** * Go through each expected RP ID and try to find one that matches. Raises an Error if no @@ -8,8 +8,8 @@ export async function matchExpectedRPID(rpIDHash: Uint8Array, expectedRPIDs: str try { await Promise.any(expectedRPIDs.map((expected) => { return new Promise((resolve, reject) => { - toHash(uint8Array.fromASCIIString(expected)).then((expectedRPIDHash) => { - if (uint8Array.areEqual(rpIDHash, expectedRPIDHash)) { + toHash(isoUint8Array.fromASCIIString(expected)).then((expectedRPIDHash) => { + if (isoUint8Array.areEqual(rpIDHash, expectedRPIDHash)) { resolve(true); } else { reject(); diff --git a/packages/server/src/helpers/parseAuthenticatorData.test.ts b/packages/server/src/helpers/parseAuthenticatorData.test.ts index e08d95b..7885b32 100644 --- a/packages/server/src/helpers/parseAuthenticatorData.test.ts +++ b/packages/server/src/helpers/parseAuthenticatorData.test.ts @@ -1,8 +1,8 @@ import { parseAuthenticatorData } from './parseAuthenticatorData'; -import * as base64url from './base64url'; +import { isoBase64URL } from './iso'; // Grabbed this from a Conformance test, contains attestation data -const authDataWithAT = base64url.toBuffer('SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2NBAAAAJch83ZdWwUm4niTLNjZU81AAIHa7Ksm5br3hAh3UjxP9+4rqu8BEsD+7SZ2xWe1/yHv6pAEDAzkBACBZAQDcxA7Ehs9goWB2Hbl6e9v+aUub9rvy2M7Hkvf+iCzMGE63e3sCEW5Ru33KNy4um46s9jalcBHtZgtEnyeRoQvszis+ws5o4Da0vQfuzlpBmjWT1dV6LuP+vs9wrfObW4jlA5bKEIhv63+jAxOtdXGVzo75PxBlqxrmrr5IR9n8Fw7clwRsDkjgRHaNcQVbwq/qdNwU5H3hZKu9szTwBS5NGRq01EaDF2014YSTFjwtAmZ3PU1tcO/QD2U2zg6eB5grfWDeAJtRE8cbndDWc8aLL0aeC37Q36+TVsGe6AhBgHEw6eO3I3NW5r9v/26CqMPBDwmEundeq1iGyKfMloobIUMBAAE=', 'base64'); +const authDataWithAT = isoBase64URL.toBuffer('SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2NBAAAAJch83ZdWwUm4niTLNjZU81AAIHa7Ksm5br3hAh3UjxP9+4rqu8BEsD+7SZ2xWe1/yHv6pAEDAzkBACBZAQDcxA7Ehs9goWB2Hbl6e9v+aUub9rvy2M7Hkvf+iCzMGE63e3sCEW5Ru33KNy4um46s9jalcBHtZgtEnyeRoQvszis+ws5o4Da0vQfuzlpBmjWT1dV6LuP+vs9wrfObW4jlA5bKEIhv63+jAxOtdXGVzo75PxBlqxrmrr5IR9n8Fw7clwRsDkjgRHaNcQVbwq/qdNwU5H3hZKu9szTwBS5NGRq01EaDF2014YSTFjwtAmZ3PU1tcO/QD2U2zg6eB5grfWDeAJtRE8cbndDWc8aLL0aeC37Q36+TVsGe6AhBgHEw6eO3I3NW5r9v/26CqMPBDwmEundeq1iGyKfMloobIUMBAAE=', 'base64'); // Grabbed this from a Conformance test, contains extension data const authDataWithED = Buffer.from( @@ -28,11 +28,11 @@ test('should parse attestation data', () => { const { credentialID, credentialPublicKey, aaguid, counter } = parsed; - expect(base64url.fromBuffer(credentialID!)).toEqual('drsqybluveECHdSPE_37iuq7wESwP7tJnbFZ7X_Ie_o'); - expect(base64url.fromBuffer(credentialPublicKey!, 'base64')).toEqual( + expect(isoBase64URL.fromBuffer(credentialID!)).toEqual('drsqybluveECHdSPE_37iuq7wESwP7tJnbFZ7X_Ie_o'); + expect(isoBase64URL.fromBuffer(credentialPublicKey!, 'base64')).toEqual( 'pAEDAzkBACBZAQDcxA7Ehs9goWB2Hbl6e9v+aUub9rvy2M7Hkvf+iCzMGE63e3sCEW5Ru33KNy4um46s9jalcBHtZgtEnyeRoQvszis+ws5o4Da0vQfuzlpBmjWT1dV6LuP+vs9wrfObW4jlA5bKEIhv63+jAxOtdXGVzo75PxBlqxrmrr5IR9n8Fw7clwRsDkjgRHaNcQVbwq/qdNwU5H3hZKu9szTwBS5NGRq01EaDF2014YSTFjwtAmZ3PU1tcO/QD2U2zg6eB5grfWDeAJtRE8cbndDWc8aLL0aeC37Q36+TVsGe6AhBgHEw6eO3I3NW5r9v/26CqMPBDwmEundeq1iGyKfMloobIUMBAAE=', ); - expect(base64url.fromBuffer(aaguid!, 'base64')).toEqual('yHzdl1bBSbieJMs2NlTzUA=='); + expect(isoBase64URL.fromBuffer(aaguid!, 'base64')).toEqual('yHzdl1bBSbieJMs2NlTzUA=='); expect(counter).toEqual(37); }); diff --git a/packages/server/src/helpers/parseAuthenticatorData.ts b/packages/server/src/helpers/parseAuthenticatorData.ts index f45e717..bcc552d 100644 --- a/packages/server/src/helpers/parseAuthenticatorData.ts +++ b/packages/server/src/helpers/parseAuthenticatorData.ts @@ -1,9 +1,8 @@ -import * as isoCBOR from './isoCBOR'; import { decodeAuthenticatorExtensions, AuthenticationExtensionsAuthenticatorOutputs, } from './decodeAuthenticatorExtensions'; -import * as isoUint8Array from './isoUint8Array'; +import { isoCBOR, isoUint8Array } from './iso'; import { COSEPublicKey } from './convertCOSEtoPKCS'; /** @@ -17,7 +16,7 @@ export function parseAuthenticatorData(authData: Uint8Array): ParsedAuthenticato } let pointer = 0; - const dataView = uint8Array.toDataView(authData); + const dataView = isoUint8Array.toDataView(authData); const rpIdHash = authData.slice(pointer, (pointer += 32)); @@ -53,8 +52,8 @@ export function parseAuthenticatorData(authData: Uint8Array): ParsedAuthenticato credentialID = authData.slice(pointer, (pointer += credIDLen)); // Decode the next CBOR item in the buffer, then re-encode it back to a Buffer - const firstDecoded = cbor.decodeFirst(authData.slice(pointer)); - const firstEncoded = Uint8Array.from(cbor.encode(firstDecoded)); + const firstDecoded = isoCBOR.decodeFirst(authData.slice(pointer)); + const firstEncoded = Uint8Array.from(isoCBOR.encode(firstDecoded)); credentialPublicKey = firstEncoded; pointer += firstEncoded.byteLength; @@ -64,8 +63,8 @@ export function parseAuthenticatorData(authData: Uint8Array): ParsedAuthenticato let extensionsDataBuffer: Uint8Array | undefined = undefined; if (flags.ed) { - const firstDecoded = cbor.decodeFirst(authData.slice(pointer)); - extensionsDataBuffer = Uint8Array.from(cbor.encode(firstDecoded)); + const firstDecoded = isoCBOR.decodeFirst(authData.slice(pointer)); + extensionsDataBuffer = Uint8Array.from(isoCBOR.encode(firstDecoded)); extensionsData = decodeAuthenticatorExtensions(extensionsDataBuffer); pointer += extensionsDataBuffer.byteLength; } diff --git a/packages/server/src/helpers/verifySignature.ts b/packages/server/src/helpers/verifySignature.ts index 42a167e..4373d5d 100644 --- a/packages/server/src/helpers/verifySignature.ts +++ b/packages/server/src/helpers/verifySignature.ts @@ -4,7 +4,7 @@ import { verify as ed25519Verify } from '@noble/ed25519'; import { COSEKEYS, COSEKTY, COSEPublicKey } from './convertCOSEtoPKCS'; import { convertCertBufferToPEM } from './convertCertBufferToPEM'; import { convertPublicKeyToPEM } from './convertPublicKeyToPEM'; -import * as isoCBOR from './isoCBOR'; +import { isoCBOR } from './iso'; type VerifySignatureOptsLeafCert = { signature: Uint8Array; @@ -51,7 +51,7 @@ export async function verifySignature( // Decode CBOR to COSE let struct; try { - struct = cbor.decodeFirst(credentialPublicKey); + struct = isoCBOR.decodeFirst(credentialPublicKey); } catch (err) { const _err = err as Error; throw new Error(`Error decoding public key while converting to PEM: ${_err.message}`); diff --git a/packages/server/src/metadata/parseJWT.ts b/packages/server/src/metadata/parseJWT.ts index 879134d..7b1a291 100644 --- a/packages/server/src/metadata/parseJWT.ts +++ b/packages/server/src/metadata/parseJWT.ts @@ -1,4 +1,4 @@ -import * as isoBase64URL from "../helpers/isoBase64URL"; +import { isoBase64URL } from "../helpers/iso"; /** * Process a JWT into Javascript-friendly data structures @@ -6,8 +6,8 @@ import * as isoBase64URL from "../helpers/isoBase64URL"; export function parseJWT(jwt: string): [T1, T2, string] { const parts = jwt.split('.'); return [ - JSON.parse(base64url.toString(parts[0])) as T1, - JSON.parse(base64url.toString(parts[1])) as T2, + JSON.parse(isoBase64URL.toString(parts[0])) as T1, + JSON.parse(isoBase64URL.toString(parts[1])) as T2, parts[2], ]; } diff --git a/packages/server/src/metadata/verifyAttestationWithMetadata.test.ts b/packages/server/src/metadata/verifyAttestationWithMetadata.test.ts index 228a1b7..57751b1 100644 --- a/packages/server/src/metadata/verifyAttestationWithMetadata.test.ts +++ b/packages/server/src/metadata/verifyAttestationWithMetadata.test.ts @@ -1,6 +1,6 @@ import { verifyAttestationWithMetadata } from './verifyAttestationWithMetadata'; import { MetadataStatement } from '../metadata/mdsTypes'; -import * as base64url from '../helpers/base64url'; +import { isoBase64URL } from '../helpers/iso'; test('should verify attestation with metadata (android-safetynet)', async () => { const metadataStatementJSONSafetyNet: MetadataStatement = { @@ -48,7 +48,7 @@ test('should verify attestation with metadata (android-safetynet)', async () => const verified = await verifyAttestationWithMetadata({ statement: metadataStatementJSONSafetyNet, - credentialPublicKey: base64url.toBuffer(credentialPublicKey), + credentialPublicKey: isoBase64URL.toBuffer(credentialPublicKey), x5c, }); @@ -98,7 +98,7 @@ test('should verify attestation with rsa_emsa_pkcs1_sha256_raw authenticator alg const verified = await verifyAttestationWithMetadata({ statement: metadataStatement, - credentialPublicKey: base64url.toBuffer(credentialPublicKey), + credentialPublicKey: isoBase64URL.toBuffer(credentialPublicKey), x5c, }); @@ -155,7 +155,7 @@ test('should not validate certificate path when authenticator is self-referencin const verified = await verifyAttestationWithMetadata({ statement: metadataStatement, - credentialPublicKey: base64url.toBuffer(credentialPublicKey), + credentialPublicKey: isoBase64URL.toBuffer(credentialPublicKey), x5c, }); diff --git a/packages/server/src/registration/generateRegistrationOptions.ts b/packages/server/src/registration/generateRegistrationOptions.ts index 1497ec6..83bfb3c 100644 --- a/packages/server/src/registration/generateRegistrationOptions.ts +++ b/packages/server/src/registration/generateRegistrationOptions.ts @@ -9,8 +9,7 @@ import type { } from '@simplewebauthn/typescript-types'; import { generateChallenge } from '../helpers/generateChallenge'; -import * as isoBase64URL from '../helpers/isoBase64URL'; -import * as isoUint8Array from '../helpers/isoUint8Array'; +import { isoBase64URL, isoUint8Array } from '../helpers/iso'; export type GenerateRegistrationOptionsOpts = { rpName: string; @@ -157,11 +156,11 @@ export function generateRegistrationOptions( */ let _challenge = challenge; if (typeof _challenge === 'string') { - _challenge = uint8Array.fromASCIIString(_challenge); + _challenge = isoUint8Array.fromASCIIString(_challenge); } return { - challenge: base64url.fromBuffer(_challenge), + challenge: isoBase64URL.fromBuffer(_challenge), rp: { name: rpName, id: rpID, @@ -176,7 +175,7 @@ export function generateRegistrationOptions( attestation: attestationType, excludeCredentials: excludeCredentials.map(cred => ({ ...cred, - id: base64url.fromBuffer(cred.id as Uint8Array), + id: isoBase64URL.fromBuffer(cred.id as Uint8Array), })), authenticatorSelection, extensions, diff --git a/packages/server/src/registration/verifications/tpm/parseCertInfo.ts b/packages/server/src/registration/verifications/tpm/parseCertInfo.ts index d36f6f6..ab4798e 100644 --- a/packages/server/src/registration/verifications/tpm/parseCertInfo.ts +++ b/packages/server/src/registration/verifications/tpm/parseCertInfo.ts @@ -1,12 +1,12 @@ import { TPM_ST, TPM_ALG } from './constants'; -import * as isoUint8Array from '../../../helpers/isoUint8Array'; +import { isoUint8Array } from '../../../helpers/iso'; /** * Cut up a TPM attestation's certInfo into intelligible chunks */ export function parseCertInfo(certInfo: Uint8Array): ParsedCertInfo { let pointer = 0; - const dataView = uint8Array.toDataView(certInfo); + const dataView = isoUint8Array.toDataView(certInfo); // Get a magic constant const magic = dataView.getUint32(pointer); @@ -44,7 +44,7 @@ export function parseCertInfo(certInfo: Uint8Array): ParsedCertInfo { const attestedNameLength = dataView.getUint16(pointer); pointer += 2; const attestedName = certInfo.slice(pointer, (pointer += attestedNameLength)); - const attestedNameDataView = uint8Array.toDataView(attestedName); + const attestedNameDataView = isoUint8Array.toDataView(attestedName); // Attested qualified name, can be ignored const qualifiedNameLength = dataView.getUint16(pointer); diff --git a/packages/server/src/registration/verifications/tpm/parsePubArea.ts b/packages/server/src/registration/verifications/tpm/parsePubArea.ts index 593b9a4..514828c 100644 --- a/packages/server/src/registration/verifications/tpm/parsePubArea.ts +++ b/packages/server/src/registration/verifications/tpm/parsePubArea.ts @@ -1,5 +1,5 @@ import { TPM_ALG, TPM_ECC_CURVE } from './constants'; -import * as isoUint8Array from '../../../helpers/isoUint8Array'; +import { isoUint8Array } from '../../../helpers/iso'; /** * Break apart a TPM attestation's pubArea buffer @@ -9,7 +9,7 @@ import * as isoUint8Array from '../../../helpers/isoUint8Array'; */ export function parsePubArea(pubArea: Uint8Array): ParsedPubArea { let pointer = 0; - const dataView = uint8Array.toDataView(pubArea); + const dataView = isoUint8Array.toDataView(pubArea); const type = TPM_ALG[dataView.getUint16(pointer)]; pointer += 2; @@ -99,7 +99,7 @@ export function parsePubArea(pubArea: Uint8Array): ParsedPubArea { const uniqueY = pubArea.slice(pointer, (pointer += uniqueYLength)); - unique = uint8Array.concat([uniqueX, uniqueY]); + unique = isoUint8Array.concat([uniqueX, uniqueY]); } else { throw new Error(`Unexpected type "${type}" (TPM)`); } diff --git a/packages/server/src/registration/verifications/tpm/verifyAttestationTPM.test.ts b/packages/server/src/registration/verifications/tpm/verifyAttestationTPM.test.ts index b69f24c..3f1531c 100644 --- a/packages/server/src/registration/verifications/tpm/verifyAttestationTPM.test.ts +++ b/packages/server/src/registration/verifications/tpm/verifyAttestationTPM.test.ts @@ -1,9 +1,9 @@ -import * as base64url from '../../../helpers/base64url'; +import { isoBase64URL } from '../../../helpers/iso'; import { verifyRegistrationResponse } from '../../verifyRegistrationResponse'; test('should verify TPM response', async () => { const expectedChallenge = 'a4de0d36-057d-4e9d-831a-2c578fa89170'; - jest.spyOn(base64url, 'fromString').mockReturnValueOnce(expectedChallenge); + jest.spyOn(isoBase64URL, 'fromString').mockReturnValueOnce(expectedChallenge); const verification = await verifyRegistrationResponse({ credential: { id: 'SErwRhxIzjPowcnM3e-D-u89EQXLUe1NYewpshd7Mc0', @@ -33,7 +33,7 @@ test('should verify SHA1 TPM response', async () => { */ const expectedChallenge = '9JyUfJkg8PqoKZuD7FHzOE9dbyculC9urGTpGqBnEwnhKmni4rGRXxm3-ZBHK8x6riJQqIpC8qEa-T0qIFTKTQ'; - jest.spyOn(base64url, 'fromString').mockReturnValueOnce(expectedChallenge); + jest.spyOn(isoBase64URL, 'fromString').mockReturnValueOnce(expectedChallenge); const verification = await verifyRegistrationResponse({ credential: { rawId: 'UJDoUJoGiDQF_EEZ3G_z9Lfq16_KFaXtMTjwTUrrRlc', @@ -63,7 +63,7 @@ test('should verify SHA256 TPM response', async () => { */ const expectedChallenge = 'gHrAk4pNe2VlB0HLeKclI2P6QEa83PuGeijTHMtpbhY9KlybyhlwF_VzRe7yhabXagWuY6rkDWfvvhNqgh2o7A'; - jest.spyOn(base64url, 'fromString').mockReturnValueOnce(expectedChallenge); + jest.spyOn(isoBase64URL, 'fromString').mockReturnValueOnce(expectedChallenge); const verification = await verifyRegistrationResponse({ credential: { rawId: 'h9XMhkVePN1Prq9Ks_VfwIsVZvt-jmSRTEnevTc-KB8', @@ -100,7 +100,7 @@ test('should verify TPM response with spec-compliant tcgAtTpm SAN structure', as * ] */ const expectedChallenge = 'VfmZXKDxqdoXFMHXO3SE2Q2b8u5Ki64OL_XICELcGKg'; - jest.spyOn(base64url, 'fromString').mockReturnValueOnce(expectedChallenge); + jest.spyOn(isoBase64URL, 'fromString').mockReturnValueOnce(expectedChallenge); const verification = await verifyRegistrationResponse({ credential: { id: 'LVwzXx0fStkvsos_jdl9DTd6O3-6be8Ua4tcdXc5XeM', @@ -133,7 +133,7 @@ test('should verify TPM response with non-spec-compliant tcgAtTpm SAN structure' * ] */ const expectedChallenge = '4STWgmXrgJxzigqe6nFuIg'; - jest.spyOn(base64url, 'fromString').mockReturnValueOnce(expectedChallenge); + jest.spyOn(isoBase64URL, 'fromString').mockReturnValueOnce(expectedChallenge); const verification = await verifyRegistrationResponse({ credential: { id: 'X7TPi7o8WfiIz1bP0Vciz1xRvSMyiitgOR1sUqY724s', @@ -157,7 +157,7 @@ test('should verify TPM response with non-spec-compliant tcgAtTpm SAN structure' test('should verify TPM response with ECC public area type', async () => { const expectedChallenge = 'uzn9u0Tx-LBdtGgERsbkHRBjiUt5i2rvm2BBTZrWqEo'; - jest.spyOn(base64url, 'fromString').mockReturnValueOnce(expectedChallenge); + jest.spyOn(isoBase64URL, 'fromString').mockReturnValueOnce(expectedChallenge); const verification = await verifyRegistrationResponse({ credential: { 'id': 'hsS2ywFz_LWf9-lC35vC9uJTVD3ZCVdweZvESUbjXnQ', diff --git a/packages/server/src/registration/verifications/tpm/verifyAttestationTPM.ts b/packages/server/src/registration/verifications/tpm/verifyAttestationTPM.ts index 844b97b..ff2d4e5 100644 --- a/packages/server/src/registration/verifications/tpm/verifyAttestationTPM.ts +++ b/packages/server/src/registration/verifications/tpm/verifyAttestationTPM.ts @@ -17,7 +17,7 @@ import { convertCertBufferToPEM } from '../../../helpers/convertCertBufferToPEM' import { validateCertificatePath } from '../../../helpers/validateCertificatePath'; import { getCertificateInfo } from '../../../helpers/getCertificateInfo'; import { verifySignature } from '../../../helpers/verifySignature'; -import * as isoUint8Array from '../../../helpers/isoUint8Array'; +import { isoUint8Array } from '../../../helpers/iso'; import { MetadataService } from '../../../services/metadataService'; import { verifyAttestationWithMetadata } from '../../../metadata/verifyAttestationWithMetadata'; @@ -80,7 +80,7 @@ export async function verifyAttestationTPM(options: AttestationFormatVerifierOpt throw new Error('COSE public key missing e (TPM|RSA)'); } - if (!uint8Array.areEqual(unique, (n as Uint8Array))) { + if (!isoUint8Array.areEqual(unique, (n as Uint8Array))) { throw new Error('PubArea unique is not same as credentialPublicKey (TPM|RSA)'); } @@ -113,7 +113,7 @@ export async function verifyAttestationTPM(options: AttestationFormatVerifierOpt throw new Error('COSE public key missing y (TPM|ECC)'); } - if (!uint8Array.areEqual(unique, uint8Array.concat([x as Uint8Array, y as Uint8Array]))) { + if (!isoUint8Array.areEqual(unique, isoUint8Array.concat([x as Uint8Array, y as Uint8Array]))) { throw new Error('PubArea unique is not same as public key x and y (TPM|ECC)'); } @@ -147,22 +147,22 @@ export async function verifyAttestationTPM(options: AttestationFormatVerifierOpt const pubAreaHash = await toHash(pubArea, attested.nameAlg.replace('TPM_ALG_', '')); // Concatenate attested.nameAlg and pubAreaHash to create attestedName. - const attestedName = uint8Array.concat([attested.nameAlgBuffer, pubAreaHash]); + const attestedName = isoUint8Array.concat([attested.nameAlgBuffer, pubAreaHash]); // Check that certInfo.attested.name is equals to attestedName. - if (!uint8Array.areEqual(attested.name, attestedName)) { + if (!isoUint8Array.areEqual(attested.name, attestedName)) { throw new Error(`Attested name comparison failed (TPM)`); } // Concatenate authData with clientDataHash to create attToBeSigned - const attToBeSigned = uint8Array.concat([authData, clientDataHash]); + const attToBeSigned = isoUint8Array.concat([authData, clientDataHash]); // Hash attToBeSigned using the algorithm specified in attStmt.alg to create attToBeSignedHash const hashAlg: string = COSEALGHASH[alg as number]; const attToBeSignedHash = await toHash(attToBeSigned, hashAlg); // Check that certInfo.extraData is equals to attToBeSignedHash. - if (!uint8Array.areEqual(extraData, attToBeSignedHash)) { + if (!isoUint8Array.areEqual(extraData, attToBeSignedHash)) { throw new Error('CertInfo extra data did not equal hashed attestation (TPM)'); } diff --git a/packages/server/src/registration/verifications/verifyAttestationAndroidKey.test.ts b/packages/server/src/registration/verifications/verifyAttestationAndroidKey.test.ts index 7e9ce37..0e5d27b 100644 --- a/packages/server/src/registration/verifications/verifyAttestationAndroidKey.test.ts +++ b/packages/server/src/registration/verifications/verifyAttestationAndroidKey.test.ts @@ -1,5 +1,5 @@ import { SettingsService } from '../../services/settingsService'; -import * as base64url from '../../helpers/base64url'; +import { isoBase64URL } from '../../helpers/iso'; import { verifyRegistrationResponse } from '../verifyRegistrationResponse'; @@ -11,7 +11,7 @@ SettingsService.setRootCertificates({ identifier: 'android-key', certificates: [ test('should verify Android KeyStore response', async () => { const expectedChallenge = '4ab7dfd1-a695-4777-985f-ad2993828e99'; - jest.spyOn(base64url, 'fromString').mockReturnValueOnce(expectedChallenge); + jest.spyOn(isoBase64URL, 'fromString').mockReturnValueOnce(expectedChallenge); const verification = await verifyRegistrationResponse({ credential: { id: 'V51GE29tGbhby7sbg1cZ_qL8V8njqEsXpAnwQBobvgw', diff --git a/packages/server/src/registration/verifications/verifyAttestationAndroidKey.ts b/packages/server/src/registration/verifications/verifyAttestationAndroidKey.ts index 0c41061..8f29cca 100644 --- a/packages/server/src/registration/verifications/verifyAttestationAndroidKey.ts +++ b/packages/server/src/registration/verifications/verifyAttestationAndroidKey.ts @@ -8,7 +8,7 @@ import { convertCertBufferToPEM } from '../../helpers/convertCertBufferToPEM'; import { validateCertificatePath } from '../../helpers/validateCertificatePath'; import { verifySignature } from '../../helpers/verifySignature'; import { COSEALGHASH, convertCOSEtoPKCS } from '../../helpers/convertCOSEtoPKCS'; -import * as isoUint8Array from '../../helpers/isoUint8Array'; +import { isoUint8Array } from '../../helpers/iso'; import { MetadataService } from '../../services/metadataService'; import { verifyAttestationWithMetadata } from '../../metadata/verifyAttestationWithMetadata'; @@ -46,7 +46,7 @@ export async function verifyAttestationAndroidKey( // Convert the credentialPublicKey to PKCS const credPubKeyPKCS = convertCOSEtoPKCS(credentialPublicKey); - if (!uint8Array.areEqual(credPubKeyPKCS, parsedCertPubKey)) { + if (!isoUint8Array.areEqual(credPubKeyPKCS, parsedCertPubKey)) { throw new Error('Credential public key does not equal leaf cert public key (AndroidKey)'); } @@ -64,7 +64,7 @@ export async function verifyAttestationAndroidKey( // Verify extKeyStore values const { attestationChallenge, teeEnforced, softwareEnforced } = parsedExtKeyStore; - if (!uint8Array.areEqual(new Uint8Array(attestationChallenge.buffer), clientDataHash)) { + if (!isoUint8Array.areEqual(new Uint8Array(attestationChallenge.buffer), clientDataHash)) { throw new Error('Attestation challenge was not equal to client data hash (AndroidKey)'); } @@ -101,7 +101,7 @@ export async function verifyAttestationAndroidKey( } } - const signatureBase = uint8Array.concat([authData, clientDataHash]); + const signatureBase = isoUint8Array.concat([authData, clientDataHash]); const hashAlg = COSEALGHASH[alg as number]; return verifySignature({ diff --git a/packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.test.ts b/packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.test.ts index d46c17f..7c33bb3 100644 --- a/packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.test.ts +++ b/packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.test.ts @@ -6,7 +6,7 @@ import { } from '../../helpers/decodeAttestationObject'; import { parseAuthenticatorData } from '../../helpers/parseAuthenticatorData'; import { toHash } from '../../helpers/toHash'; -import * as base64url from '../../helpers/base64url'; +import { isoBase64URL } from '../../helpers/iso'; import { SettingsService } from '../../services/settingsService'; const rootCertificates = SettingsService.getRootCertificates({ @@ -24,11 +24,11 @@ let spyDate: jest.SpyInstance; beforeEach(async () => { const { attestationObject, clientDataJSON } = attestationAndroidSafetyNet.response; - const decodedAttestationObject = decodeAttestationObject(base64url.toBuffer(attestationObject)); + const decodedAttestationObject = decodeAttestationObject(isoBase64URL.toBuffer(attestationObject)); authData = decodedAttestationObject.get('authData'); attStmt = decodedAttestationObject.get('attStmt'); - clientDataHash = await toHash(base64url.toBuffer(clientDataJSON)); + clientDataHash = await toHash(isoBase64URL.toBuffer(clientDataJSON)); const parsedAuthData = parseAuthenticatorData(authData); aaguid = parsedAuthData.aaguid!; @@ -87,11 +87,11 @@ test('should validate response with cert path completed with GlobalSign R1 root spyDate.mockReturnValue(new Date('2021-11-15T00:00:42.000Z')); const { attestationObject, clientDataJSON } = safetyNetUsingGSR1RootCert.response; - const decodedAttestationObject = decodeAttestationObject(base64url.toBuffer(attestationObject)); + const decodedAttestationObject = decodeAttestationObject(isoBase64URL.toBuffer(attestationObject)); const _authData = decodedAttestationObject.get('authData'); const _attStmt = decodedAttestationObject.get('attStmt'); - const _clientDataHash = await toHash(base64url.toBuffer(clientDataJSON)); + const _clientDataHash = await toHash(isoBase64URL.toBuffer(clientDataJSON)); const parsedAuthData = parseAuthenticatorData(_authData); const _aaguid = parsedAuthData.aaguid!; diff --git a/packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.ts b/packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.ts index 3879f40..285f919 100644 --- a/packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.ts +++ b/packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.ts @@ -5,8 +5,7 @@ import { verifySignature } from '../../helpers/verifySignature'; import { getCertificateInfo } from '../../helpers/getCertificateInfo'; import { validateCertificatePath } from '../../helpers/validateCertificatePath'; import { convertCertBufferToPEM } from '../../helpers/convertCertBufferToPEM'; -import * as isoUint8Array from '../../helpers/isoUint8Array'; -import * as isoBase64URL from '../../helpers/isoBase64URL'; +import { isoUint8Array, isoBase64URL } from '../../helpers/iso'; import { MetadataService } from '../../services/metadataService'; import { verifyAttestationWithMetadata } from '../../metadata/verifyAttestationWithMetadata'; @@ -38,11 +37,11 @@ export async function verifyAttestationAndroidSafetyNet( } // Prepare to verify a JWT - const jwt = uint8Array.toUTF8String(response); + const jwt = isoUint8Array.toUTF8String(response); const jwtParts = jwt.split('.'); - const HEADER: SafetyNetJWTHeader = JSON.parse(base64url.toString(jwtParts[0])); - const PAYLOAD: SafetyNetJWTPayload = JSON.parse(base64url.toString(jwtParts[1])); + const HEADER: SafetyNetJWTHeader = JSON.parse(isoBase64URL.toString(jwtParts[0])); + const PAYLOAD: SafetyNetJWTPayload = JSON.parse(isoBase64URL.toString(jwtParts[1])); const SIGNATURE: SafetyNetJWTSignature = jwtParts[2]; /** @@ -65,9 +64,9 @@ export async function verifyAttestationAndroidSafetyNet( } } - const nonceBase = uint8Array.concat([authData, clientDataHash]); + const nonceBase = isoUint8Array.concat([authData, clientDataHash]); const nonceBuffer = await toHash(nonceBase); - const expectedNonce = base64url.fromBuffer(nonceBuffer, 'base64'); + const expectedNonce = isoBase64URL.fromBuffer(nonceBuffer, 'base64'); if (nonce !== expectedNonce) { throw new Error('Could not verify payload nonce (SafetyNet)'); @@ -84,7 +83,7 @@ export async function verifyAttestationAndroidSafetyNet( * START Verify Header */ // `HEADER.x5c[0]` is definitely a base64 string - const leafCertBuffer = base64url.toBuffer(HEADER.x5c[0], 'base64'); + const leafCertBuffer = isoBase64URL.toBuffer(HEADER.x5c[0], 'base64'); const leafCertInfo = getCertificateInfo(leafCertBuffer); const { subject } = leafCertInfo; @@ -124,8 +123,8 @@ export async function verifyAttestationAndroidSafetyNet( /** * START Verify Signature */ - const signatureBaseBuffer = uint8Array.fromUTF8String(`${jwtParts[0]}.${jwtParts[1]}`); - const signatureBuffer = base64url.toBuffer(SIGNATURE); + const signatureBaseBuffer = isoUint8Array.fromUTF8String(`${jwtParts[0]}.${jwtParts[1]}`); + const signatureBuffer = isoBase64URL.toBuffer(SIGNATURE); const verified = await verifySignature({ signature: signatureBuffer, diff --git a/packages/server/src/registration/verifications/verifyAttestationApple.ts b/packages/server/src/registration/verifications/verifyAttestationApple.ts index d8f5465..bc7ceab 100644 --- a/packages/server/src/registration/verifications/verifyAttestationApple.ts +++ b/packages/server/src/registration/verifications/verifyAttestationApple.ts @@ -7,7 +7,7 @@ import { validateCertificatePath } from '../../helpers/validateCertificatePath'; import { convertCertBufferToPEM } from '../../helpers/convertCertBufferToPEM'; import { toHash } from '../../helpers/toHash'; import { convertCOSEtoPKCS } from '../../helpers/convertCOSEtoPKCS'; -import * as isoUint8Array from '../../helpers/isoUint8Array'; +import { isoUint8Array } from '../../helpers/iso'; export async function verifyAttestationApple( options: AttestationFormatVerifierOpts, @@ -45,7 +45,7 @@ export async function verifyAttestationApple( throw new Error('credCert missing "1.2.840.113635.100.8.2" extension (Apple)'); } - const nonceToHash = uint8Array.concat([authData, clientDataHash]); + const nonceToHash = isoUint8Array.concat([authData, clientDataHash]); const nonce = await toHash(nonceToHash, 'SHA256'); /** * Ignore the first six ASN.1 structure bytes that define the nonce as an OCTET STRING. Should @@ -56,7 +56,7 @@ export async function verifyAttestationApple( */ const extNonce = new Uint8Array(extCertNonce.extnValue.buffer).slice(6); - if (!uint8Array.areEqual(nonce, extNonce)) { + if (!isoUint8Array.areEqual(nonce, extNonce)) { throw new Error(`credCert nonce was not expected value (Apple)`); } @@ -66,7 +66,7 @@ export async function verifyAttestationApple( const credPubKeyPKCS = convertCOSEtoPKCS(credentialPublicKey); const credCertSubjectPublicKey = new Uint8Array(subjectPublicKeyInfo.subjectPublicKey); - if (!uint8Array.areEqual(credPubKeyPKCS, credCertSubjectPublicKey)) { + if (!isoUint8Array.areEqual(credPubKeyPKCS, credCertSubjectPublicKey)) { throw new Error('Credential public key does not equal credCert public key (Apple)'); } diff --git a/packages/server/src/registration/verifications/verifyAttestationFIDOU2F.ts b/packages/server/src/registration/verifications/verifyAttestationFIDOU2F.ts index 7e57ab5..0b681fa 100644 --- a/packages/server/src/registration/verifications/verifyAttestationFIDOU2F.ts +++ b/packages/server/src/registration/verifications/verifyAttestationFIDOU2F.ts @@ -4,7 +4,7 @@ import { convertCOSEtoPKCS } from '../../helpers/convertCOSEtoPKCS'; import { convertCertBufferToPEM } from '../../helpers/convertCertBufferToPEM'; import { validateCertificatePath } from '../../helpers/validateCertificatePath'; import { verifySignature } from '../../helpers/verifySignature'; -import * as isoUint8Array from '../../helpers/isoUint8Array'; +import { isoUint8Array } from '../../helpers/iso'; /** * Verify an attestation response with fmt 'fido-u2f' @@ -25,7 +25,7 @@ export async function verifyAttestationFIDOU2F( const reservedByte = Uint8Array.from([0x00]); const publicKey = convertCOSEtoPKCS(credentialPublicKey); - const signatureBase = uint8Array.concat([ + const signatureBase = isoUint8Array.concat([ reservedByte, rpIdHash, clientDataHash, @@ -45,7 +45,7 @@ export async function verifyAttestationFIDOU2F( } // FIDO spec says that aaguid _must_ equal 0x00 here to be legit - const aaguidToHex = Number.parseInt(uint8Array.toHex(aaguid), 16); + const aaguidToHex = Number.parseInt(isoUint8Array.toHex(aaguid), 16); if (aaguidToHex !== 0x00) { throw new Error(`AAGUID "${aaguidToHex}" was not expected value`); } diff --git a/packages/server/src/registration/verifications/verifyAttestationPacked.ts b/packages/server/src/registration/verifications/verifyAttestationPacked.ts index 4250595..e4fdf92 100644 --- a/packages/server/src/registration/verifications/verifyAttestationPacked.ts +++ b/packages/server/src/registration/verifications/verifyAttestationPacked.ts @@ -5,7 +5,7 @@ import { convertCertBufferToPEM } from '../../helpers/convertCertBufferToPEM'; import { validateCertificatePath } from '../../helpers/validateCertificatePath'; import { getCertificateInfo } from '../../helpers/getCertificateInfo'; import { verifySignature } from '../../helpers/verifySignature'; -import * as isoUint8Array from '../../helpers/isoUint8Array'; +import { isoUint8Array } from '../../helpers/iso'; import { MetadataService } from '../../services/metadataService'; import { verifyAttestationWithMetadata } from '../../metadata/verifyAttestationWithMetadata'; @@ -30,7 +30,7 @@ export async function verifyAttestationPacked( throw new Error(`Attestation Statement alg "${alg}" is not a number (Packed)`); } - const signatureBase = uint8Array.concat([authData, clientDataHash]); + const signatureBase = isoUint8Array.concat([authData, clientDataHash]); let verified = false; diff --git a/packages/server/src/registration/verifyRegistrationResponse.test.ts b/packages/server/src/registration/verifyRegistrationResponse.test.ts index 277aaff..92ea27a 100644 --- a/packages/server/src/registration/verifyRegistrationResponse.test.ts +++ b/packages/server/src/registration/verifyRegistrationResponse.test.ts @@ -7,8 +7,7 @@ import * as esmDecodeClientDataJSON from '../helpers/decodeClientDataJSON'; import * as esmParseAuthenticatorData from '../helpers/parseAuthenticatorData'; import * as esmDecodeCredentialPublicKey from '../helpers/decodeCredentialPublicKey'; import { toHash } from '../helpers/toHash'; -import * as base64url from '../helpers/base64url'; -import * as uint8Array from '../helpers/uint8Array'; +import { isoBase64URL, isoUint8Array } from '../helpers/iso'; import { COSEPublicKey, COSEKEYS } from '../helpers/convertCOSEtoPKCS'; import { SettingsService } from '../services/settingsService'; @@ -54,12 +53,12 @@ test('should verify FIDO U2F attestation', async () => { expect(verification.registrationInfo?.fmt).toEqual('fido-u2f'); expect(verification.registrationInfo?.counter).toEqual(0); expect(verification.registrationInfo?.credentialPublicKey).toEqual( - base64url.toBuffer( + isoBase64URL.toBuffer( 'pQECAyYgASFYIMiRyw5pUoMhBjCrcQND6lJPaRHA0f-XWcKBb5ZwWk1eIlggFJu6aan4o7epl6qa9n9T-6KsIMvZE2PcTnLj8rN58is', ), ); expect(verification.registrationInfo?.credentialID).toEqual( - base64url.toBuffer( + isoBase64URL.toBuffer( 'VHzbxaYaJu2P8m1Y2iHn2gRNHrgK0iYbn9E978L3Qi7Q-chFeicIHwYCRophz5lth2nCgEVKcgWirxlgidgbUQ', ), ); @@ -67,7 +66,7 @@ test('should verify FIDO U2F attestation', async () => { expect(verification.registrationInfo?.credentialType).toEqual('public-key'); expect(verification.registrationInfo?.userVerified).toEqual(false); expect(verification.registrationInfo?.attestationObject).toEqual( - base64url.toBuffer(attestationFIDOU2F.response.attestationObject), + isoBase64URL.toBuffer(attestationFIDOU2F.response.attestationObject), ); }); @@ -83,12 +82,12 @@ test('should verify Packed (EC2) attestation', async () => { expect(verification.registrationInfo?.fmt).toEqual('packed'); expect(verification.registrationInfo?.counter).toEqual(1589874425); expect(verification.registrationInfo?.credentialPublicKey).toEqual( - base64url.toBuffer( + isoBase64URL.toBuffer( 'pQECAyYgASFYIEoxVVqK-oIGmqoDEyO4KjmMx5R2HeMM4LQQXh8sE01PIlggtzuuoMN5fWnAIuuXdlfshOGu1k3ApBUtDJ8eKiuo_6c', ), ); expect(verification.registrationInfo?.credentialID).toEqual( - base64url.toBuffer( + isoBase64URL.toBuffer( 'AYThY1csINY4JrbHyGmqTl1nL_F1zjAF3hSAIngz8kAcjugmAMNVvxZRwqpEH-bNHHAIv291OX5ko9eDf_5mu3U' + 'B2BvsScr2K-ppM4owOpGsqwg5tZglqqmxIm1Q', ), @@ -107,12 +106,12 @@ test('should verify Packed (X5C) attestation', async () => { expect(verification.registrationInfo?.fmt).toEqual('packed'); expect(verification.registrationInfo?.counter).toEqual(28); expect(verification.registrationInfo?.credentialPublicKey).toEqual( - base64url.toBuffer( + isoBase64URL.toBuffer( 'pQECAyYgASFYIGwlsYCNyRb4AD9cyTw6cH5VS-uzflmmO1UldGGe9eIaIlggvadzKD8p6wKLjgYfxRxldjCMGRV0YyM13osWbKIPrF8', ), ); expect(verification.registrationInfo?.credentialID).toEqual( - base64url.toBuffer( + isoBase64URL.toBuffer( '4rrvMciHCkdLQ2HghazIp1sMc8TmV8W8RgoX-x8tqV_1AmlqWACqUK8mBGLandr-htduQKPzgb2yWxOFV56Tlg', ), ); @@ -130,12 +129,12 @@ test('should verify None attestation', async () => { expect(verification.registrationInfo?.fmt).toEqual('none'); expect(verification.registrationInfo?.counter).toEqual(0); expect(verification.registrationInfo?.credentialPublicKey).toEqual( - base64url.toBuffer( + isoBase64URL.toBuffer( 'pQECAyYgASFYID5PQTZQQg6haZFQWFzqfAOyQ_ENsMH8xxQ4GRiNPsqrIlggU8IVUOV8qpgk_Jh-OTaLuZL52KdX1fTht07X4DiQPow', ), ); expect(verification.registrationInfo?.credentialID).toEqual( - base64url.toBuffer( + isoBase64URL.toBuffer( 'AdKXJEch1aV5Wo7bj7qLHskVY4OoNaj9qu8TPdJ7kSAgUeRxWNngXlcNIGt4gexZGKVGcqZpqqWordXb_he1izY', ), ); @@ -165,12 +164,12 @@ test('should verify None attestation w/RSA public key', async () => { expect(verification.registrationInfo?.fmt).toEqual('none'); expect(verification.registrationInfo?.counter).toEqual(0); expect(verification.registrationInfo?.credentialPublicKey).toEqual( - base64url.toBuffer( + isoBase64URL.toBuffer( 'pAEDAzkBACBZAQDxfpXrj0ba_AH30JJ_-W7BHSOPugOD8aEDdNBKc1gjB9AmV3FPl2aL0fwiOMKtM_byI24qXb2FzcyjC7HUVkHRtzkAQnahXckI4wY_01koaY6iwXuIE3Ya0Zjs2iZyz6u4G_abGnWdObqa_kHxc3CHR7Xy5MDkAkKyX6TqU0tgHZcEhDd_Lb5ONJDwg4wvKlZBtZYElfMuZ6lonoRZ7qR_81rGkDZyFaxp6RlyvzEbo4ijeIaHQylqCz-oFm03ifZMOfRHYuF4uTjJDRH-g4BW1f3rdi7DTHk1hJnIw1IyL_VFIQ9NifkAguYjNCySCUNpYli2eMrPhAu5dYJFFjINIUMBAAE', ), ); expect(verification.registrationInfo?.credentialID).toEqual( - base64url.toBuffer('kGXv4RJWLeXRw8Yf3T22K3Gq_GGeDv9OKYmAHLm0Ylo'), + isoBase64URL.toBuffer('kGXv4RJWLeXRw8Yf3T22K3Gq_GGeDv9OKYmAHLm0Ylo'), ); }); @@ -219,7 +218,7 @@ test('should throw when attestation type is not webauthn.create', async () => { test('should throw if an unexpected attestation format is specified', async () => { const realAtteObj = esmDecodeAttestationObject.decodeAttestationObject( - base64url.toBuffer(attestationNone.response.attestationObject), + isoBase64URL.toBuffer(attestationNone.response.attestationObject), ); // Mangle the fmt (realAtteObj as Map).set('fmt', 'fizzbuzz'); @@ -238,7 +237,7 @@ test('should throw if an unexpected attestation format is specified', async () = test('should throw error if assertion RP ID is unexpected value', async () => { const authData = esmDecodeAttestationObject.decodeAttestationObject( - base64url.toBuffer(attestationNone.response.attestationObject), + isoBase64URL.toBuffer(attestationNone.response.attestationObject), ).get('authData'); const actualAuthData = esmParseAuthenticatorData.parseAuthenticatorData(authData); @@ -401,12 +400,12 @@ test('should validate TPM RSA response (SHA256)', async () => { expect(verification.registrationInfo?.fmt).toEqual('tpm'); expect(verification.registrationInfo?.counter).toEqual(30); expect(verification.registrationInfo?.credentialPublicKey).toEqual( - base64url.toBuffer( + isoBase64URL.toBuffer( 'pAEDAzkBACBZAQCtxzw59Wsl8xWP97wPTu2TSDlushwshL8GedHAHO1R62m3nNy21hCLJlQabfLepRUQ_v9mq3PCmV81tBSqtRGU5_YlK0R2yeu756SnT39c6hKC3PBPt_xdjL_ccz4H_73DunfB63QZOtdeAsswV7WPLqMARofuM-LQ_LHnNguCypDcxhADuUqQtogfwZsknTVIPxzGcfqnQ7ERF9D9AOWIQ8YjOsTi_B2zS8SOySKIFUGwwYcPG7DiCE-QJcI-fpydRDnEq6UxbkYgB7XK4BlmPKlwuXkBDX9egl_Ma4B7W2WJvYbKevu6Z8Kc5y-OITpNVDYKbBK3qKyh4yIUpB1NIUMBAAE', ), ); expect(verification.registrationInfo?.credentialID).toEqual( - base64url.toBuffer('lGkWHPe88VpnNYgVBxzon_MRR9-gmgODveQ16uM_bPM'), + isoBase64URL.toBuffer('lGkWHPe88VpnNYgVBxzon_MRR9-gmgODveQ16uM_bPM'), ); }); @@ -434,12 +433,12 @@ test('should validate TPM RSA response (SHA1)', async () => { expect(verification.registrationInfo?.fmt).toEqual('tpm'); expect(verification.registrationInfo?.counter).toEqual(97); expect(verification.registrationInfo?.credentialPublicKey).toEqual( - base64url.toBuffer( + isoBase64URL.toBuffer( 'pAEDAzn__iBZAQCzl_wD24PZ5z-po2FrwoQVdd13got_CkL8p4B_NvJBC5OwAYKDilii_wj-0CA8ManbpSInx9Tdnz6t91OhudwUT0-W_BHSLK_MqFcjZWrR5LYVmVpz1EgH3DrOTra4AlogEq2D2CYktPrPe7joE-oT3vAYXK8vzQDLRyaxI_Z1qS4KLlLCdajW8PGpw1YRjMDw6s69GZU8mXkgNPMCUh1TZ1bnCvJTO9fnmLjDjqdQGRU4bWo8tFjCL8g1-2WD_2n0-twt6n-Uox5VnR1dQJG4awMlanBCkGGpOb3WBDQ8K10YJJ2evPhJKGJahBvu2Dxmq6pLCAXCv0ma3EHj-PmDIUMBAAE', ), ); expect(verification.registrationInfo?.credentialID).toEqual( - base64url.toBuffer('oELnad0f6-g2BtzEn_78iLNoubarlq0xFtOtAMXnflU'), + isoBase64URL.toBuffer('oELnad0f6-g2BtzEn_78iLNoubarlq0xFtOtAMXnflU'), ); }); @@ -467,12 +466,12 @@ test('should validate Android-Key response', async () => { expect(verification.registrationInfo?.fmt).toEqual('android-key'); expect(verification.registrationInfo?.counter).toEqual(108); expect(verification.registrationInfo?.credentialPublicKey).toEqual( - base64url.toBuffer( + isoBase64URL.toBuffer( 'pQECAyYgASFYIEjCq7woGNN_42rbaqMgJvz0nuKTWNRrR29lMX3J239oIlgg6IcAXqPJPIjSrClHDAmbJv_EShYhYq0R9-G3k744n7Y', ), ); expect(verification.registrationInfo?.credentialID).toEqual( - base64url.toBuffer('PPa1spYTB680cQq5q6qBtFuPLLdG1FQ73EastkT8n0o'), + isoBase64URL.toBuffer('PPa1spYTB680cQq5q6qBtFuPLLdG1FQ73EastkT8n0o'), ); }); @@ -538,7 +537,7 @@ test('should pass verification if custom challenge verifier returns true', async }, expectedChallenge: (challenge: string) => { const parsedChallenge: { actualChallenge: string; arbitraryData: string } = JSON.parse( - base64url.toString(challenge), + isoBase64URL.toString(challenge), ); return parsedChallenge.actualChallenge === 'xRsYdCQv5WZOqmxReiZl6C9q5SfrZne4lNSr9QVtPig'; }, @@ -596,15 +595,15 @@ test('should return authenticator extension output', async () => { expect(verification.registrationInfo?.authenticatorExtensionResults).toMatchObject({ devicePubKey: { - dpk: uint8Array.fromHex( + dpk: isoUint8Array.fromHex( 'A5010203262001215820991AABED9DE4271A9EDEAD8806F9DC96D6DCCD0C476253A5510489EC8379BE5B225820A0973CFDEDBB79E27FEF4EE7481673FB3312504DDCA5434CFD23431D6AD29EDA', ), - sig: uint8Array.fromHex( + sig: isoUint8Array.fromHex( '3045022100EFB38074BD15B8C82CF09F87FBC6FB3C7169EA4F1806B7E90937374302345B7A02202B7113040731A0E727D338D48542863CE65880AA79E5EA740AC8CCD94347988E', ), - nonce: uint8Array.fromHex(''), - scope: uint8Array.fromHex('00'), - aaguid: uint8Array.fromHex('00000000000000000000000000000000'), + nonce: isoUint8Array.fromHex(''), + scope: isoUint8Array.fromHex('00'), + aaguid: isoUint8Array.fromHex('00000000000000000000000000000000'), }, }); }); @@ -625,7 +624,7 @@ const attestationFIDOU2F: RegistrationCredentialJSON = { clientExtensionResults: {}, type: 'public-key', }; -const attestationFIDOU2FChallenge = base64url.fromString('totallyUniqueValueEveryAttestation'); +const attestationFIDOU2FChallenge = isoBase64URL.fromString('totallyUniqueValueEveryAttestation'); const attestationPacked: RegistrationCredentialJSON = { id: 'bbb', @@ -646,7 +645,7 @@ const attestationPacked: RegistrationCredentialJSON = { clientExtensionResults: {}, type: 'public-key', }; -const attestationPackedChallenge = base64url.fromString('s6PIbBnPPnrGNSBxNdtDrT7UrVYJK9HM'); +const attestationPackedChallenge = isoBase64URL.fromString('s6PIbBnPPnrGNSBxNdtDrT7UrVYJK9HM'); const attestationPackedX5C: RegistrationCredentialJSON = { // TODO: Grab these from another iPhone attestation @@ -677,7 +676,7 @@ const attestationPackedX5C: RegistrationCredentialJSON = { clientExtensionResults: {}, type: 'public-key', }; -const attestationPackedX5CChallenge = base64url.fromString('totallyUniqueValueEveryTime'); +const attestationPackedX5CChallenge = isoBase64URL.fromString('totallyUniqueValueEveryTime'); const attestationNone: RegistrationCredentialJSON = { id: 'AdKXJEch1aV5Wo7bj7qLHskVY4OoNaj9qu8TPdJ7kSAgUeRxWNngXlcNIGt4gexZGKVGcqZpqqWordXb_he1izY', @@ -696,4 +695,4 @@ const attestationNone: RegistrationCredentialJSON = { clientExtensionResults: {}, type: 'public-key', }; -const attestationNoneChallenge = base64url.fromString('hEccPWuziP00H0p5gxh2_u5_PC4NeYgd'); +const attestationNoneChallenge = isoBase64URL.fromString('hEccPWuziP00H0p5gxh2_u5_PC4NeYgd'); diff --git a/packages/server/src/registration/verifyRegistrationResponse.ts b/packages/server/src/registration/verifyRegistrationResponse.ts index 6ed3ca9..4b1cbc7 100644 --- a/packages/server/src/registration/verifyRegistrationResponse.ts +++ b/packages/server/src/registration/verifyRegistrationResponse.ts @@ -18,8 +18,7 @@ import { COSEKEYS } from '../helpers/convertCOSEtoPKCS'; import { convertAAGUIDToString } from '../helpers/convertAAGUIDToString'; import { parseBackupFlags } from '../helpers/parseBackupFlags'; import { matchExpectedRPID } from '../helpers/matchExpectedRPID'; -import * as isoUint8Array from '../helpers/isoUint8Array'; -import * as isoBase64URL from '../helpers/isoBase64URL'; +import { isoBase64URL } from '../helpers/iso'; import { SettingsService } from '../services/settingsService'; import { supportedCOSEAlgorithmIdentifiers } from './generateRegistrationOptions'; @@ -131,7 +130,7 @@ export async function verifyRegistrationResponse( } } - const attestationObject = base64url.toBuffer(response.attestationObject); + const attestationObject = isoBase64URL.toBuffer(response.attestationObject); const decodedAttestationObject = decodeAttestationObject(attestationObject); const fmt = decodedAttestationObject.get('fmt'); const authData = decodedAttestationObject.get('authData'); @@ -188,7 +187,7 @@ export async function verifyRegistrationResponse( throw new Error(`Unexpected public key alg "${alg}", expected one of "${supported}"`); } - const clientDataHash = await toHash(base64url.toBuffer(response.clientDataJSON)); + const clientDataHash = await toHash(isoBase64URL.toBuffer(response.clientDataJSON)); const rootCertificates = SettingsService.getRootCertificates({ identifier: fmt }); // Prepare arguments to pass to the relevant verification method -- cgit v1.2.3 From c41dead310bdf5e1ce94c551a1f9d2d21ce5f793 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Sun, 20 Nov 2022 00:12:37 -0800 Subject: Run linter over everything --- .../generateAuthenticationOptions.test.ts | 3 +- .../generateAuthenticationOptions.ts | 10 +- .../server/src/helpers/convertCertBufferToPEM.ts | 2 +- .../src/helpers/convertX509PublicKeyToCOSE.ts | 24 ++-- packages/server/src/helpers/cose.ts | 14 ++- .../helpers/decodeAuthenticatorExtensions.test.ts | 10 +- .../server/src/helpers/decodeClientDataJSON.ts | 2 +- packages/server/src/helpers/matchExpectedRPID.ts | 29 +++-- .../src/helpers/parseAuthenticatorData.test.ts | 9 +- packages/server/src/helpers/toHash.ts | 5 +- packages/server/src/metadata/mdsTypes.ts | 2 +- packages/server/src/metadata/parseJWT.ts | 2 +- .../metadata/verifyAttestationWithMetadata.test.ts | 136 +++++++++++---------- .../src/metadata/verifyAttestationWithMetadata.ts | 25 ++-- .../generateRegistrationOptions.test.ts | 4 +- .../verifyRegistrationResponse.test.ts | 6 +- .../src/registration/verifyRegistrationResponse.ts | 2 +- 17 files changed, 159 insertions(+), 126 deletions(-) (limited to 'packages/server/src/helpers/parseAuthenticatorData.test.ts') diff --git a/packages/server/src/authentication/generateAuthenticationOptions.test.ts b/packages/server/src/authentication/generateAuthenticationOptions.test.ts index 65dc66e..9048cf5 100644 --- a/packages/server/src/authentication/generateAuthenticationOptions.test.ts +++ b/packages/server/src/authentication/generateAuthenticationOptions.test.ts @@ -5,10 +5,9 @@ import { isoBase64URL } from '../helpers/iso'; import { generateAuthenticationOptions } from './generateAuthenticationOptions'; const challengeString = 'dG90YWxseXJhbmRvbXZhbHVl'; -const challengeBuffer = isoBase64URL.toBuffer(challengeString) +const challengeBuffer = isoBase64URL.toBuffer(challengeString); test('should generate credential request options suitable for sending via JSON', () => { - const options = generateAuthenticationOptions({ allowCredentials: [ { diff --git a/packages/server/src/authentication/generateAuthenticationOptions.ts b/packages/server/src/authentication/generateAuthenticationOptions.ts index 3847b39..bd517e3 100644 --- a/packages/server/src/authentication/generateAuthenticationOptions.ts +++ b/packages/server/src/authentication/generateAuthenticationOptions.ts @@ -5,7 +5,7 @@ import type { UserVerificationRequirement, } from '@simplewebauthn/typescript-types'; -import { isoBase64URL, isoUint8Array } from '../helpers/iso' +import { isoBase64URL, isoUint8Array } from '../helpers/iso'; import { generateChallenge } from '../helpers/generateChallenge'; export type GenerateAuthenticationOptionsOpts = { @@ -45,10 +45,10 @@ export function generateAuthenticationOptions( /** * Preserve ability to specify `string` values for challenges */ - let _challenge = challenge; - if (typeof _challenge === 'string') { - _challenge = isoUint8Array.fromUTF8String(_challenge); - } + let _challenge = challenge; + if (typeof _challenge === 'string') { + _challenge = isoUint8Array.fromUTF8String(_challenge); + } return { challenge: isoBase64URL.fromBuffer(_challenge), diff --git a/packages/server/src/helpers/convertCertBufferToPEM.ts b/packages/server/src/helpers/convertCertBufferToPEM.ts index 0179eee..adf4201 100644 --- a/packages/server/src/helpers/convertCertBufferToPEM.ts +++ b/packages/server/src/helpers/convertCertBufferToPEM.ts @@ -15,7 +15,7 @@ export function convertCertBufferToPEM(certBuffer: Uint8Array | Base64URLString) if (isoBase64URL.isBase64url(certBuffer)) { b64cert = isoBase64URL.toBase64(certBuffer); } else if (isoBase64URL.isBase64(certBuffer)) { - b64cert = certBuffer + b64cert = certBuffer; } else { throw new Error('Certificate is not a valid base64 or base64url string'); } diff --git a/packages/server/src/helpers/convertX509PublicKeyToCOSE.ts b/packages/server/src/helpers/convertX509PublicKeyToCOSE.ts index de51c7d..cd76146 100644 --- a/packages/server/src/helpers/convertX509PublicKeyToCOSE.ts +++ b/packages/server/src/helpers/convertX509PublicKeyToCOSE.ts @@ -3,7 +3,15 @@ import { Certificate } from '@peculiar/asn1-x509'; import { ECParameters, id_ecPublicKey, id_secp256r1 } from '@peculiar/asn1-ecc'; import { RSAPublicKey } from '@peculiar/asn1-rsa'; -import { COSEPublicKey, COSEKTY, COSECRV, COSEKEYS, COSEPublicKeyEC2, COSEPublicKeyRSA, COSEALG } from "./cose"; +import { + COSEPublicKey, + COSEKTY, + COSECRV, + COSEKEYS, + COSEPublicKeyEC2, + COSEPublicKeyRSA, + COSEALG, +} from './cose'; export function convertX509PublicKeyToCOSE(leafCertificate: Uint8Array): COSEPublicKey { let cosePublicKey: COSEPublicKey = new Map(); @@ -14,10 +22,7 @@ export function convertX509PublicKeyToCOSE(leafCertificate: Uint8Array): COSEPub const x509 = AsnParser.parse(leafCertificate, Certificate); const { tbsCertificate } = x509; - const { - subjectPublicKeyInfo, - signature: _tbsSignature, - } = tbsCertificate; + const { subjectPublicKeyInfo, signature: _tbsSignature } = tbsCertificate; const signatureAlgorithm = _tbsSignature.algorithm; const publicKeyAlgorithmID = subjectPublicKeyInfo.algorithm.algorithm; @@ -30,7 +35,10 @@ export function convertX509PublicKeyToCOSE(leafCertificate: Uint8Array): COSEPub throw new Error('Leaf cert public key missing parameters (EC2)'); } - const ecParameters = AsnParser.parse(new Uint8Array(subjectPublicKeyInfo.algorithm.parameters), ECParameters); + const ecParameters = AsnParser.parse( + new Uint8Array(subjectPublicKeyInfo.algorithm.parameters), + ECParameters, + ); let crv = -999; if (ecParameters.namedCurve === id_secp256r1) { @@ -41,7 +49,7 @@ export function convertX509PublicKeyToCOSE(leafCertificate: Uint8Array): COSEPub ); } - const subjectPublicKey = new Uint8Array(subjectPublicKeyInfo.subjectPublicKey) + const subjectPublicKey = new Uint8Array(subjectPublicKeyInfo.subjectPublicKey); let x: Uint8Array; let y: Uint8Array; @@ -49,7 +57,7 @@ export function convertX509PublicKeyToCOSE(leafCertificate: Uint8Array): COSEPub // Public key is in "uncompressed form", so we can split the remaining bytes in half let pointer = 1; const halfLength = (subjectPublicKey.length - 1) / 2; - x = subjectPublicKey.slice(pointer, pointer += halfLength); + x = subjectPublicKey.slice(pointer, (pointer += halfLength)); y = subjectPublicKey.slice(pointer); } else { throw new Error('TODO: Figure out how to handle public keys in "compressed form"'); diff --git a/packages/server/src/helpers/cose.ts b/packages/server/src/helpers/cose.ts index d4e08f3..2f2e446 100644 --- a/packages/server/src/helpers/cose.ts +++ b/packages/server/src/helpers/cose.ts @@ -8,7 +8,7 @@ * These types are an unorthodox way of saying "these Maps should involve these discrete lists of * keys", but it works. */ - export type COSEPublicKey = { +export type COSEPublicKey = { // Getters get(key: COSEKEYS.kty): COSEKTY | undefined; get(key: COSEKEYS.alg): COSEALG | undefined; @@ -46,17 +46,23 @@ export type COSEPublicKeyRSA = COSEPublicKey & { set(key: COSEKEYS.e, value: Uint8Array): void; }; -export function isCOSEPublicKeyOKP(cosePublicKey: COSEPublicKey): cosePublicKey is COSEPublicKeyOKP { +export function isCOSEPublicKeyOKP( + cosePublicKey: COSEPublicKey, +): cosePublicKey is COSEPublicKeyOKP { const kty = cosePublicKey.get(COSEKEYS.kty); return isCOSEKty(kty) && kty === COSEKTY.OKP; } -export function isCOSEPublicKeyEC2(cosePublicKey: COSEPublicKey): cosePublicKey is COSEPublicKeyEC2 { +export function isCOSEPublicKeyEC2( + cosePublicKey: COSEPublicKey, +): cosePublicKey is COSEPublicKeyEC2 { const kty = cosePublicKey.get(COSEKEYS.kty); return isCOSEKty(kty) && kty === COSEKTY.EC2; } -export function isCOSEPublicKeyRSA(cosePublicKey: COSEPublicKey): cosePublicKey is COSEPublicKeyRSA { +export function isCOSEPublicKeyRSA( + cosePublicKey: COSEPublicKey, +): cosePublicKey is COSEPublicKeyRSA { const kty = cosePublicKey.get(COSEKEYS.kty); return isCOSEKty(kty) && kty === COSEKTY.RSA; } diff --git a/packages/server/src/helpers/decodeAuthenticatorExtensions.test.ts b/packages/server/src/helpers/decodeAuthenticatorExtensions.test.ts index 8afa3d7..6cc5e24 100644 --- a/packages/server/src/helpers/decodeAuthenticatorExtensions.test.ts +++ b/packages/server/src/helpers/decodeAuthenticatorExtensions.test.ts @@ -5,11 +5,11 @@ test('should decode authenticator extensions', () => { const extensions = decodeAuthenticatorExtensions( isoUint8Array.fromHex( 'A16C6465766963655075624B6579A56364706B584DA5010203262001215820991AABED9D' + - 'E4271A9EDEAD8806F9DC96D6DCCD0C476253A5510489EC8379BE5B225820A0973CFDEDBB' + - '79E27FEF4EE7481673FB3312504DDCA5434CFD23431D6AD29EDA63736967584730450221' + - '00EFB38074BD15B8C82CF09F87FBC6FB3C7169EA4F1806B7E90937374302345B7A02202B' + - '7113040731A0E727D338D48542863CE65880AA79E5EA740AC8CCD94347988E656E6F6E63' + - '65406573636F70654100666161677569645000000000000000000000000000000000', + 'E4271A9EDEAD8806F9DC96D6DCCD0C476253A5510489EC8379BE5B225820A0973CFDEDBB' + + '79E27FEF4EE7481673FB3312504DDCA5434CFD23431D6AD29EDA63736967584730450221' + + '00EFB38074BD15B8C82CF09F87FBC6FB3C7169EA4F1806B7E90937374302345B7A02202B' + + '7113040731A0E727D338D48542863CE65880AA79E5EA740AC8CCD94347988E656E6F6E63' + + '65406573636F70654100666161677569645000000000000000000000000000000000', ), ); expect(extensions).toMatchObject({ diff --git a/packages/server/src/helpers/decodeClientDataJSON.ts b/packages/server/src/helpers/decodeClientDataJSON.ts index 36a949e..e0de0a0 100644 --- a/packages/server/src/helpers/decodeClientDataJSON.ts +++ b/packages/server/src/helpers/decodeClientDataJSON.ts @@ -1,4 +1,4 @@ -import { isoBase64URL } from "./iso"; +import { isoBase64URL } from './iso'; /** * Decode an authenticator's base64url-encoded clientDataJSON to JSON diff --git a/packages/server/src/helpers/matchExpectedRPID.ts b/packages/server/src/helpers/matchExpectedRPID.ts index 76991bf..be49fc2 100644 --- a/packages/server/src/helpers/matchExpectedRPID.ts +++ b/packages/server/src/helpers/matchExpectedRPID.ts @@ -4,19 +4,24 @@ import { isoUint8Array } from './iso'; /** * Go through each expected RP ID and try to find one that matches. Raises an Error if no */ -export async function matchExpectedRPID(rpIDHash: Uint8Array, expectedRPIDs: string[]): Promise { +export async function matchExpectedRPID( + rpIDHash: Uint8Array, + expectedRPIDs: string[], +): Promise { try { - await Promise.any(expectedRPIDs.map((expected) => { - return new Promise((resolve, reject) => { - toHash(isoUint8Array.fromASCIIString(expected)).then((expectedRPIDHash) => { - if (isoUint8Array.areEqual(rpIDHash, expectedRPIDHash)) { - resolve(true); - } else { - reject(); - } - }) - }); - })); + await Promise.any( + expectedRPIDs.map(expected => { + return new Promise((resolve, reject) => { + toHash(isoUint8Array.fromASCIIString(expected)).then(expectedRPIDHash => { + if (isoUint8Array.areEqual(rpIDHash, expectedRPIDHash)) { + resolve(true); + } else { + reject(); + } + }); + }); + }), + ); } catch (err) { const _err = err as Error; diff --git a/packages/server/src/helpers/parseAuthenticatorData.test.ts b/packages/server/src/helpers/parseAuthenticatorData.test.ts index 7885b32..1db4bfe 100644 --- a/packages/server/src/helpers/parseAuthenticatorData.test.ts +++ b/packages/server/src/helpers/parseAuthenticatorData.test.ts @@ -2,7 +2,10 @@ import { parseAuthenticatorData } from './parseAuthenticatorData'; import { isoBase64URL } from './iso'; // Grabbed this from a Conformance test, contains attestation data -const authDataWithAT = isoBase64URL.toBuffer('SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2NBAAAAJch83ZdWwUm4niTLNjZU81AAIHa7Ksm5br3hAh3UjxP9+4rqu8BEsD+7SZ2xWe1/yHv6pAEDAzkBACBZAQDcxA7Ehs9goWB2Hbl6e9v+aUub9rvy2M7Hkvf+iCzMGE63e3sCEW5Ru33KNy4um46s9jalcBHtZgtEnyeRoQvszis+ws5o4Da0vQfuzlpBmjWT1dV6LuP+vs9wrfObW4jlA5bKEIhv63+jAxOtdXGVzo75PxBlqxrmrr5IR9n8Fw7clwRsDkjgRHaNcQVbwq/qdNwU5H3hZKu9szTwBS5NGRq01EaDF2014YSTFjwtAmZ3PU1tcO/QD2U2zg6eB5grfWDeAJtRE8cbndDWc8aLL0aeC37Q36+TVsGe6AhBgHEw6eO3I3NW5r9v/26CqMPBDwmEundeq1iGyKfMloobIUMBAAE=', 'base64'); +const authDataWithAT = isoBase64URL.toBuffer( + 'SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2NBAAAAJch83ZdWwUm4niTLNjZU81AAIHa7Ksm5br3hAh3UjxP9+4rqu8BEsD+7SZ2xWe1/yHv6pAEDAzkBACBZAQDcxA7Ehs9goWB2Hbl6e9v+aUub9rvy2M7Hkvf+iCzMGE63e3sCEW5Ru33KNy4um46s9jalcBHtZgtEnyeRoQvszis+ws5o4Da0vQfuzlpBmjWT1dV6LuP+vs9wrfObW4jlA5bKEIhv63+jAxOtdXGVzo75PxBlqxrmrr5IR9n8Fw7clwRsDkjgRHaNcQVbwq/qdNwU5H3hZKu9szTwBS5NGRq01EaDF2014YSTFjwtAmZ3PU1tcO/QD2U2zg6eB5grfWDeAJtRE8cbndDWc8aLL0aeC37Q36+TVsGe6AhBgHEw6eO3I3NW5r9v/26CqMPBDwmEundeq1iGyKfMloobIUMBAAE=', + 'base64', +); // Grabbed this from a Conformance test, contains extension data const authDataWithED = Buffer.from( @@ -28,7 +31,9 @@ test('should parse attestation data', () => { const { credentialID, credentialPublicKey, aaguid, counter } = parsed; - expect(isoBase64URL.fromBuffer(credentialID!)).toEqual('drsqybluveECHdSPE_37iuq7wESwP7tJnbFZ7X_Ie_o'); + expect(isoBase64URL.fromBuffer(credentialID!)).toEqual( + 'drsqybluveECHdSPE_37iuq7wESwP7tJnbFZ7X_Ie_o', + ); expect(isoBase64URL.fromBuffer(credentialPublicKey!, 'base64')).toEqual( 'pAEDAzkBACBZAQDcxA7Ehs9goWB2Hbl6e9v+aUub9rvy2M7Hkvf+iCzMGE63e3sCEW5Ru33KNy4um46s9jalcBHtZgtEnyeRoQvszis+ws5o4Da0vQfuzlpBmjWT1dV6LuP+vs9wrfObW4jlA5bKEIhv63+jAxOtdXGVzo75PxBlqxrmrr5IR9n8Fw7clwRsDkjgRHaNcQVbwq/qdNwU5H3hZKu9szTwBS5NGRq01EaDF2014YSTFjwtAmZ3PU1tcO/QD2U2zg6eB5grfWDeAJtRE8cbndDWc8aLL0aeC37Q36+TVsGe6AhBgHEw6eO3I3NW5r9v/26CqMPBDwmEundeq1iGyKfMloobIUMBAAE=', ); diff --git a/packages/server/src/helpers/toHash.ts b/packages/server/src/helpers/toHash.ts index 2f8dc30..90edd4e 100644 --- a/packages/server/src/helpers/toHash.ts +++ b/packages/server/src/helpers/toHash.ts @@ -5,7 +5,10 @@ import { isoUint8Array, isoCrypto } from './iso'; * Returns hash digest of the given data, using the given algorithm when provided. Defaults to using * SHA-256. */ -export async function toHash(data: Uint8Array | string, algorithm: COSEALG = -7): Promise { +export async function toHash( + data: Uint8Array | string, + algorithm: COSEALG = -7, +): Promise { if (typeof data === 'string') { data = isoUint8Array.fromUTF8String(data); } diff --git a/packages/server/src/metadata/mdsTypes.ts b/packages/server/src/metadata/mdsTypes.ts index 1bf9f80..d86f587 100644 --- a/packages/server/src/metadata/mdsTypes.ts +++ b/packages/server/src/metadata/mdsTypes.ts @@ -292,5 +292,5 @@ export type AuthenticatorGetInfo = { }; maxMsgSize?: number; pinProtocols?: number[]; - algorithms?: { type: 'public-key', alg: number }[]; + algorithms?: { type: 'public-key'; alg: number }[]; }; diff --git a/packages/server/src/metadata/parseJWT.ts b/packages/server/src/metadata/parseJWT.ts index 7b1a291..beb2501 100644 --- a/packages/server/src/metadata/parseJWT.ts +++ b/packages/server/src/metadata/parseJWT.ts @@ -1,4 +1,4 @@ -import { isoBase64URL } from "../helpers/iso"; +import { isoBase64URL } from '../helpers/iso'; /** * Process a JWT into Javascript-friendly data structures diff --git a/packages/server/src/metadata/verifyAttestationWithMetadata.test.ts b/packages/server/src/metadata/verifyAttestationWithMetadata.test.ts index 57751b1..c76fb1d 100644 --- a/packages/server/src/metadata/verifyAttestationWithMetadata.test.ts +++ b/packages/server/src/metadata/verifyAttestationWithMetadata.test.ts @@ -57,44 +57,45 @@ test('should verify attestation with metadata (android-safetynet)', async () => test('should verify attestation with rsa_emsa_pkcs1_sha256_raw authenticator algorithm in metadata', async () => { const metadataStatement: MetadataStatement = { - 'legalHeader': 'https://fidoalliance.org/metadata/metadata-statement-legal-header/', - 'aaguid': '08987058-cadc-4b81-b6e1-30de50dcbe96', - 'description': 'Windows Hello Hardware Authenticator', - 'authenticatorVersion': 1, - 'protocolFamily': 'fido2', - 'schema': 3, - 'upv': [{ 'major': 1, 'minor': 0 }], - 'authenticationAlgorithms': ['rsassa_pkcsv15_sha256_raw'], - 'publicKeyAlgAndEncodings': ['cose'], - 'attestationTypes': ['attca'], - 'userVerificationDetails': [ - [{ 'userVerificationMethod': 'eyeprint_internal' }], - [{ 'userVerificationMethod': 'passcode_internal' }], - [{ 'userVerificationMethod': 'fingerprint_internal' }], - [{ 'userVerificationMethod': 'faceprint_internal' }] + legalHeader: 'https://fidoalliance.org/metadata/metadata-statement-legal-header/', + aaguid: '08987058-cadc-4b81-b6e1-30de50dcbe96', + description: 'Windows Hello Hardware Authenticator', + authenticatorVersion: 1, + protocolFamily: 'fido2', + schema: 3, + upv: [{ major: 1, minor: 0 }], + authenticationAlgorithms: ['rsassa_pkcsv15_sha256_raw'], + publicKeyAlgAndEncodings: ['cose'], + attestationTypes: ['attca'], + userVerificationDetails: [ + [{ userVerificationMethod: 'eyeprint_internal' }], + [{ userVerificationMethod: 'passcode_internal' }], + [{ userVerificationMethod: 'fingerprint_internal' }], + [{ userVerificationMethod: 'faceprint_internal' }], ], - 'keyProtection': ['hardware'], - 'isKeyRestricted': false, - 'matcherProtection': ['software'], - 'attachmentHint': ['internal'], - 'tcDisplay': [], - 'attestationRootCertificates': [ - 'MIIF9TCCA92gAwIBAgIQXbYwTgy/J79JuMhpUB5dyzANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjE2MDQGA1UEAxMtTWljcm9zb2Z0IFRQTSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDE0MB4XDTE0MTIxMDIxMzExOVoXDTM5MTIxMDIxMzkyOFowgYwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xNjA0BgNVBAMTLU1pY3Jvc29mdCBUUE0gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJ+n+bnKt/JHIRC/oI/xgkgsYdPzP0gpvduDA2GbRtth+L4WUyoZKGBw7uz5bjjP8Aql4YExyjR3EZQ4LqnZChMpoCofbeDR4MjCE1TGwWghGpS0mM3GtWD9XiME4rE2K0VW3pdN0CLzkYbvZbs2wQTFfE62yNQiDjyHFWAZ4BQH4eWa8wrDMUxIAneUCpU6zCwM+l6Qh4ohX063BHzXlTSTc1fDsiPaKuMMjWjK9vp5UHFPa+dMAWr6OljQZPFIg3aZ4cUfzS9y+n77Hs1NXPBn6E4Db679z4DThIXyoKeZTv1aaWOWl/exsDLGt2mTMTyykVV8uD1eRjYriFpmoRDwJKAEMOfaURarzp7hka9TOElGyD2gOV4Fscr2MxAYCywLmOLzA4VDSYLuKAhPSp7yawET30AvY1HRfMwBxetSqWP2+yZRNYJlHpor5QTuRDgzR+Zej+aWx6rWNYx43kLthozeVJ3QCsD5iEI/OZlmWn5WYf7O8LB/1A7scrYv44FD8ck3Z+hxXpkklAsjJMsHZa9mBqh+VR1AicX4uZG8m16x65ZU2uUpBa3rn8CTNmw17ZHOiuSWJtS9+PrZVA8ljgf4QgA1g6NPOEiLG2fn8Gm+r5Ak+9tqv72KDd2FPBJ7Xx4stYj/WjNPtEUhW4rcLK3ktLfcy6ea7Rocw5y5AgMBAAGjUTBPMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR6jArOL0hiF+KU0a5VwVLscXSkVjAQBgkrBgEEAYI3FQEEAwIBADANBgkqhkiG9w0BAQsFAAOCAgEAW4ioo1+J9VWC0UntSBXcXRm1ePTVamtsxVy/GpP4EmJd3Ub53JzNBfYdgfUL51CppS3ZY6BoagB+DqoA2GbSL+7sFGHBl5ka6FNelrwsH6VVw4xV/8klIjmqOyfatPYsz0sUdZev+reeiGpKVoXrK6BDnUU27/mgPtem5YKWvHB/soofUrLKzZV3WfGdx9zBr8V0xW6vO3CKaqkqU9y6EsQw34n7eJCbEVVQ8VdFd9iV1pmXwaBAfBwkviPTKEP9Cm+zbFIOLr3V3CL9hJj+gkTUuXWlJJ6wVXEG5i4rIbLAV59UrW4LonP+seqvWMJYUFxu/niF0R3fSGM+NU11DtBVkhRZt1u0kFhZqjDz1dWyfT/N7Hke3WsDqUFsBi+8SEw90rWx2aUkLvKo83oU4Mx4na+2I3l9F2a2VNGk4K7l3a00g51miPiq0Da0jqw30PaLluTMTGY5+RnZVh50JD6nk+Ea3wRkU8aiYFnpIxfKBZ72whmYYa/egj9IKeqpR0vuLebbU0fJBf880K1jWD3Z5SFyJXo057Mv0OPw5mttytE585ZIy5JsaRXlsOoWGRXE3kUT/MKR1UoAgR54c8Bsh+9Dq2wqIK9mRn15zvBDeyHG6+czurLopziOUeWokxZN1syrEdKlhFoPYavm6t+PzIcpdxZwHA+V3jLJPfI=' + keyProtection: ['hardware'], + isKeyRestricted: false, + matcherProtection: ['software'], + attachmentHint: ['internal'], + tcDisplay: [], + attestationRootCertificates: [ + 'MIIF9TCCA92gAwIBAgIQXbYwTgy/J79JuMhpUB5dyzANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjE2MDQGA1UEAxMtTWljcm9zb2Z0IFRQTSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDE0MB4XDTE0MTIxMDIxMzExOVoXDTM5MTIxMDIxMzkyOFowgYwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xNjA0BgNVBAMTLU1pY3Jvc29mdCBUUE0gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJ+n+bnKt/JHIRC/oI/xgkgsYdPzP0gpvduDA2GbRtth+L4WUyoZKGBw7uz5bjjP8Aql4YExyjR3EZQ4LqnZChMpoCofbeDR4MjCE1TGwWghGpS0mM3GtWD9XiME4rE2K0VW3pdN0CLzkYbvZbs2wQTFfE62yNQiDjyHFWAZ4BQH4eWa8wrDMUxIAneUCpU6zCwM+l6Qh4ohX063BHzXlTSTc1fDsiPaKuMMjWjK9vp5UHFPa+dMAWr6OljQZPFIg3aZ4cUfzS9y+n77Hs1NXPBn6E4Db679z4DThIXyoKeZTv1aaWOWl/exsDLGt2mTMTyykVV8uD1eRjYriFpmoRDwJKAEMOfaURarzp7hka9TOElGyD2gOV4Fscr2MxAYCywLmOLzA4VDSYLuKAhPSp7yawET30AvY1HRfMwBxetSqWP2+yZRNYJlHpor5QTuRDgzR+Zej+aWx6rWNYx43kLthozeVJ3QCsD5iEI/OZlmWn5WYf7O8LB/1A7scrYv44FD8ck3Z+hxXpkklAsjJMsHZa9mBqh+VR1AicX4uZG8m16x65ZU2uUpBa3rn8CTNmw17ZHOiuSWJtS9+PrZVA8ljgf4QgA1g6NPOEiLG2fn8Gm+r5Ak+9tqv72KDd2FPBJ7Xx4stYj/WjNPtEUhW4rcLK3ktLfcy6ea7Rocw5y5AgMBAAGjUTBPMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR6jArOL0hiF+KU0a5VwVLscXSkVjAQBgkrBgEEAYI3FQEEAwIBADANBgkqhkiG9w0BAQsFAAOCAgEAW4ioo1+J9VWC0UntSBXcXRm1ePTVamtsxVy/GpP4EmJd3Ub53JzNBfYdgfUL51CppS3ZY6BoagB+DqoA2GbSL+7sFGHBl5ka6FNelrwsH6VVw4xV/8klIjmqOyfatPYsz0sUdZev+reeiGpKVoXrK6BDnUU27/mgPtem5YKWvHB/soofUrLKzZV3WfGdx9zBr8V0xW6vO3CKaqkqU9y6EsQw34n7eJCbEVVQ8VdFd9iV1pmXwaBAfBwkviPTKEP9Cm+zbFIOLr3V3CL9hJj+gkTUuXWlJJ6wVXEG5i4rIbLAV59UrW4LonP+seqvWMJYUFxu/niF0R3fSGM+NU11DtBVkhRZt1u0kFhZqjDz1dWyfT/N7Hke3WsDqUFsBi+8SEw90rWx2aUkLvKo83oU4Mx4na+2I3l9F2a2VNGk4K7l3a00g51miPiq0Da0jqw30PaLluTMTGY5+RnZVh50JD6nk+Ea3wRkU8aiYFnpIxfKBZ72whmYYa/egj9IKeqpR0vuLebbU0fJBf880K1jWD3Z5SFyJXo057Mv0OPw5mttytE585ZIy5JsaRXlsOoWGRXE3kUT/MKR1UoAgR54c8Bsh+9Dq2wqIK9mRn15zvBDeyHG6+czurLopziOUeWokxZN1syrEdKlhFoPYavm6t+PzIcpdxZwHA+V3jLJPfI=', ], - 'icon': 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAACkUlEQVR42uyai3GDMAyGQyegGzACnaCMkBHoBhkhnSAj0A2SDaAT0E6QbEA3cOXW6XEpBtnImMv9utOllxjF/qKHLTdRSm0gdnkAAgACIAACIAACIAACIAgAARAAARAAARAAARBEAFCSJINKkpLuSTtSZbQz76W25zhKkpFWPbtaz6Q75vPuoluuPmqxlZK2yi76s9RznjlpN2K7CrFWaUAHNS0HT0Atw3YpDSjxbdoPuaziG3uk579cvIdeWsbQD7L7NAYoWpKmLy8chueO5reB7KKKrQnQJdDYn9AJZHc5QBT7enINY2hjxrqItsvJWSdxFxKuYlOlWJmE6zPPcsJuN7WFiF7me5DOAws4OyZyG6TOsr/KQziDaJm/mcy2V1V0+T0JeXxqqlrWC9mGGy3O6wwFaI0SdR+EMg9AEAACIAByqViZb+/prgFdN6qb306j3lTWs0BJ76Qjw0ktO+3ad60PQhMrfM9YwqK7lUPe4j+/OR40cDaqJeJ+xo80JsWih1WTBAcb8ysKrb+TfowQKy3v55wbBkk49FJbQusqr4snadL9hEtXC3nO1G1HG6UfxIj5oDnJlHPOVVAerWGmvYQxwc70hiTh7Bidy3/3ZFE6isxf8epNhUCl4n5ftYqWKzMP3IIquaFnquXO0sZ1yn/RWq69SuK6GdPXORfSz4HPnk1bNXO0+UZze5HqKIodNYwnHVVcOUivNcStxj4CGFYhWAWgXgmuF4JzdMhn6wDUm1DpmFyVY7IvQqeTRdod2v2F8lNn/gcpW+rUsOi9mAmFwlSo3Pw9JQ3p+8bhgnAMkPM613BxOBQqc2FEB4SmPQSAAAiAAAiAAAiAAAiAIAAEQAAEQAAEQPco3wIMADOXgFhOTghuAAAAAElFTkSuQmCC', - 'authenticatorGetInfo': { - 'versions': ['FIDO_2_0'], - 'aaguid': '08987058cadc4b81b6e130de50dcbe96', - 'options': { 'plat': true, 'rk': true, 'up': true }, + icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAACkUlEQVR42uyai3GDMAyGQyegGzACnaCMkBHoBhkhnSAj0A2SDaAT0E6QbEA3cOXW6XEpBtnImMv9utOllxjF/qKHLTdRSm0gdnkAAgACIAACIAACIAACIAgAARAAARAAARAAARBEAFCSJINKkpLuSTtSZbQz76W25zhKkpFWPbtaz6Q75vPuoluuPmqxlZK2yi76s9RznjlpN2K7CrFWaUAHNS0HT0Atw3YpDSjxbdoPuaziG3uk579cvIdeWsbQD7L7NAYoWpKmLy8chueO5reB7KKKrQnQJdDYn9AJZHc5QBT7enINY2hjxrqItsvJWSdxFxKuYlOlWJmE6zPPcsJuN7WFiF7me5DOAws4OyZyG6TOsr/KQziDaJm/mcy2V1V0+T0JeXxqqlrWC9mGGy3O6wwFaI0SdR+EMg9AEAACIAByqViZb+/prgFdN6qb306j3lTWs0BJ76Qjw0ktO+3ad60PQhMrfM9YwqK7lUPe4j+/OR40cDaqJeJ+xo80JsWih1WTBAcb8ysKrb+TfowQKy3v55wbBkk49FJbQusqr4snadL9hEtXC3nO1G1HG6UfxIj5oDnJlHPOVVAerWGmvYQxwc70hiTh7Bidy3/3ZFE6isxf8epNhUCl4n5ftYqWKzMP3IIquaFnquXO0sZ1yn/RWq69SuK6GdPXORfSz4HPnk1bNXO0+UZze5HqKIodNYwnHVVcOUivNcStxj4CGFYhWAWgXgmuF4JzdMhn6wDUm1DpmFyVY7IvQqeTRdod2v2F8lNn/gcpW+rUsOi9mAmFwlSo3Pw9JQ3p+8bhgnAMkPM613BxOBQqc2FEB4SmPQSAAAiAAAiAAAiAAAiAIAAEQAAEQAAEQPco3wIMADOXgFhOTghuAAAAAElFTkSuQmCC', + authenticatorGetInfo: { + versions: ['FIDO_2_0'], + aaguid: '08987058cadc4b81b6e130de50dcbe96', + options: { plat: true, rk: true, up: true }, }, }; // Extracted from an actual TPM|ECC response const x5c = [ 'MIIFuTCCA6GgAwIBAgIQAM86nt2LQk-si1Q75opOtjANBgkqhkiG9w0BAQsFADBCMUAwPgYDVQQDEzdOQ1UtSU5UQy1LRVlJRC0xN0EwMDU3NUQwNUU1OEUzODgxMjEwQkI5OEIxMDQ1QkI0QzMwNjM5MB4XDTIxMTIwMTA3MTMwOFoXDTI3MDYwMzE3NTExOFowADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN42zmd-TJwY8b8KKakCP_Jmq46s9qIcae5EObWRtWqw-qXBM9fH15vJ3UrE1mHv9mjCsV384_TJP7snP7MHy93jQOZNvR-T8JGNXR1Zhzg1MOjsZlv69w-shGZBF3lWXKKrdyS4q5KP8WbC6A30LVM_Ic0uAxkOeS-z4CdwWC4au2i8TkCTsUSenc98SFEksNOQONdNLA5qQInYCWppdT2lzEi-BbTV2GyropPgL3PCHGKVNt73XWzWZD_e9zuPNrOG9gfhh1hJaQS82TIul59Qp4C6AbIzH5uvhSh3_mhK2YU7Je6-FE_cvFLiTLt4vVimxd5uNGO4Oth_nfUm_sECAwEAAaOCAeswggHnMA4GA1UdDwEB_wQEAwIHgDAMBgNVHRMBAf8EAjAAMG0GA1UdIAEB_wRjMGEwXwYJKwYBBAGCNxUfMFIwUAYIKwYBBQUHAgIwRB5CAFQAQwBQAEEAIAAgAFQAcgB1AHMAdABlAGQAIAAgAFAAbABhAHQAZgBvAHIAbQAgACAASQBkAGUAbgB0AGkAdAB5MBAGA1UdJQQJMAcGBWeBBQgDMFAGA1UdEQEB_wRGMESkQjBAMRYwFAYFZ4EFAgEMC2lkOjQ5NEU1NDQzMQ4wDAYFZ4EFAgIMA0NOTDEWMBQGBWeBBQIDDAtpZDowMDAyMDAwMDAfBgNVHSMEGDAWgBTg0USwFsuPP50VHiH8i_DHd-1qLjAdBgNVHQ4EFgQU99bEZ0-Oi7GG2f-i68p7Xf1-diQwgbMGCCsGAQUFBwEBBIGmMIGjMIGgBggrBgEFBQcwAoaBk2h0dHA6Ly9hemNzcHJvZG5jdWFpa3B1Ymxpc2guYmxvYi5jb3JlLndpbmRvd3MubmV0L25jdS1pbnRjLWtleWlkLTE3YTAwNTc1ZDA1ZTU4ZTM4ODEyMTBiYjk4YjEwNDViYjRjMzA2MzkvYTdjNjk5MjUtZjM4Yi00ZmQwLWExZWMtMmYzMjI1MjA1YmM4LmNlcjANBgkqhkiG9w0BAQsFAAOCAgEAMwXq91wHH27AiR6rrWH3L7xEJ6o-wnoP808WisQcQ5gCUh4o0E3eeICh1IjPpr-n5CCMwU8GSzX5vQGF3VKa8FoEBNrhT4IuD-3qNv939NW1k4VPVQGTwgXy8YHiAlGnLmAIiqmEAgsn9fKLzBDhT448CJWyWzmtA5TflBX_jeL5V94hTvOMDtdtPQOpdGKlpYyArz3_sU8_XyOZad3DAbQbKOiFfzJoyr4CUDjZy1wHcO5ouwW33syPyrQwlqgnS8whBYXPK2M9Y-qT2--VutBAZIWI2wdiqMhY-RTm9OIbURZWmqVZ2DPn7dEGMow9TgdNYHL9m3CYsvRQejWyBffU0l8aLRzt330FqjHIK1x8kvk25V-mF10bTIejS6F516k3iZ2FbH5UeiZVE9ofVgN_lJ8KwyeOUjyG66VuH6dmnRfn4gg_2Uyj9TrDF0dJpoCKTspShuIaPD2-H-pkDQlDkldXo-bHlrGXJJGRBbhutxbBxozRsvkYhgoR4TbSzyDcFzFnDJd1ib_Z9C9q5KwaUiREX0b1rLCd1BZ-JXYGiQTrfnMZDvbHSXuZ-HXhcF9t5TZ8f4xDZX4gfsyj75uGJ34e4ThWxnNvdY7HkhFSXJzmvT6dIlIW1UorbYYm-UtbW4e8GwEVXquG0bpmWIXmL2k9D_WCSkyzkR7tPvw', - 'MIIG7DCCBNSgAwIBAgITMwAAA-Y6aLPA71ZHOwAAAAAD5jANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjE2MDQGA1UEAxMtTWljcm9zb2Z0IFRQTSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDE0MB4XDTIxMDYwMzE3NTExOFoXDTI3MDYwMzE3NTExOFowQjFAMD4GA1UEAxM3TkNVLUlOVEMtS0VZSUQtMTdBMDA1NzVEMDVFNThFMzg4MTIxMEJCOThCMTA0NUJCNEMzMDYzOTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAO26HxYkAnL4SBpcIIDBFYw95P18eBVzl0owJPKtEwqtJhRArv6DQMDGKPw9HGy3Vtmh5VvrGgKh6LPyTbqN2xITi-wgPUIv7Mn-WtvzPO70dnhdRvw1vDY8LeulOh2l9zU2o2jII0HzLTl_LJqKmrN3yZpq1NneSQ49l3sbXvsW0eKSj2iCtgvOk2FhY-Os3cyexx6phX5I26BmoP-Y-W5kYqtNw2o8rxol_I0v51PVzNcLBwseGOpHNYtRF0m0QdoudCKZKk0hWzKPA4BE35wSSWGjgUbp91Pjzva33tYmOlk0UOLoIT2rZ2Y5feG3QpBuacD1ImDEUQ01-kJ1S2bATRR3BoaJtRbOCRoz41MS-2XfbXhcnzZxbT5TY7dlbX4oKYZn2Wqw-TYmfBiPYBX-Mo6wObruVOs6Lk04XzznXvx5lLKLNdvDBJxG3dZIzgepo9fLrp7hTiKw0T1EdYn6-MjUO7utoq7RmKA_AzFI1VLTfVJxPn_RahYPJmt8a8F2X7WlYPg5vayPDyWtmXtuuoxoAclNp3ViC9ko5LVr7M78C2RA1T94yk2eAEm_ueCuqn8mrmqQjFo3fMAfvRB2nL66tQhBZwmWyRIjuycRCJRdtSrwjSXRywA_VHLajhVutGzPrizmFcygT3ozL1NB6s5Ill5o4DpQsE9qNIOHAgMBAAGjggGOMIIBijAOBgNVHQ8BAf8EBAMCAoQwGwYDVR0lBBQwEgYJKwYBBAGCNxUkBgVngQUIAzAWBgNVHSAEDzANMAsGCSsGAQQBgjcVHzASBgNVHRMBAf8ECDAGAQH_AgEAMB0GA1UdDgQWBBTg0USwFsuPP50VHiH8i_DHd-1qLjAfBgNVHSMEGDAWgBR6jArOL0hiF-KU0a5VwVLscXSkVjBwBgNVHR8EaTBnMGWgY6Bhhl9odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBUUE0lMjBSb290JTIwQ2VydGlmaWNhdGUlMjBBdXRob3JpdHklMjAyMDE0LmNybDB9BggrBgEFBQcBAQRxMG8wbQYIKwYBBQUHMAKGYWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwVFBNJTIwUm9vdCUyMENlcnRpZmljYXRlJTIwQXV0aG9yaXR5JTIwMjAxNC5jcnQwDQYJKoZIhvcNAQELBQADggIBAGW4yKQ4HaO4JdNMVjVO4mCM0lbLMmXQ0YJtyDHCIE6hsywTYv30DeUDm7Nmmhap9nWp26mSihb7qKQuyhdZkfhA10sp4wDbNpcXjQjdEaE2T1rcgKfounCPQRSW1V42DUgX_Bzuh0flbLYGOJIvugR46gBMUuKVQWmMQKyOMwmofFI8xG_z3VaLMcsgQ8Fl0cvJ6XZ2Jly-QRbZ2v44KNItTTuQKYJCL4kx2b50I4CkrRBaq2LAB-npikLN6xxHqsPvulA0t2WRfF9QzzDZhkVVZ5iCP1fAu5dnHvq0ArBlY2W29OIH_zviW2av88wxZ7FSQzIHu6B8GL45s6skvPa7E9lU6hG186LjrJtHJd0Qad3KYzZQyLKT78m1YiZXLFM02vsctM7nXqtndDjbDPVCota3mg8Jgi2s7-Aq59TL9ZBnRMEvJ5m1Rze1ofFwfO21ktBtLB8vXhzkHjtXy5ld0UQXmdbcs32uaqx6Q3_jVzXlXNNjuG6YBW9iBNL2ar3MtFt66LogL1gmOkyrjGK2Cdyzy1lEupr_SKtggthTyubemmf9G6hJtUZuT_gdFxVZm-MOvCtdNsqdi4HaU8VTCPB999upaEc5vv5KeEQ2xQk0wNmffMlGXGHJrQw8WBwCKkm3TW8hjnhZ9e6ePQvdMEzPhefsxjiQirzpf6lB' + 'MIIG7DCCBNSgAwIBAgITMwAAA-Y6aLPA71ZHOwAAAAAD5jANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjE2MDQGA1UEAxMtTWljcm9zb2Z0IFRQTSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDE0MB4XDTIxMDYwMzE3NTExOFoXDTI3MDYwMzE3NTExOFowQjFAMD4GA1UEAxM3TkNVLUlOVEMtS0VZSUQtMTdBMDA1NzVEMDVFNThFMzg4MTIxMEJCOThCMTA0NUJCNEMzMDYzOTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAO26HxYkAnL4SBpcIIDBFYw95P18eBVzl0owJPKtEwqtJhRArv6DQMDGKPw9HGy3Vtmh5VvrGgKh6LPyTbqN2xITi-wgPUIv7Mn-WtvzPO70dnhdRvw1vDY8LeulOh2l9zU2o2jII0HzLTl_LJqKmrN3yZpq1NneSQ49l3sbXvsW0eKSj2iCtgvOk2FhY-Os3cyexx6phX5I26BmoP-Y-W5kYqtNw2o8rxol_I0v51PVzNcLBwseGOpHNYtRF0m0QdoudCKZKk0hWzKPA4BE35wSSWGjgUbp91Pjzva33tYmOlk0UOLoIT2rZ2Y5feG3QpBuacD1ImDEUQ01-kJ1S2bATRR3BoaJtRbOCRoz41MS-2XfbXhcnzZxbT5TY7dlbX4oKYZn2Wqw-TYmfBiPYBX-Mo6wObruVOs6Lk04XzznXvx5lLKLNdvDBJxG3dZIzgepo9fLrp7hTiKw0T1EdYn6-MjUO7utoq7RmKA_AzFI1VLTfVJxPn_RahYPJmt8a8F2X7WlYPg5vayPDyWtmXtuuoxoAclNp3ViC9ko5LVr7M78C2RA1T94yk2eAEm_ueCuqn8mrmqQjFo3fMAfvRB2nL66tQhBZwmWyRIjuycRCJRdtSrwjSXRywA_VHLajhVutGzPrizmFcygT3ozL1NB6s5Ill5o4DpQsE9qNIOHAgMBAAGjggGOMIIBijAOBgNVHQ8BAf8EBAMCAoQwGwYDVR0lBBQwEgYJKwYBBAGCNxUkBgVngQUIAzAWBgNVHSAEDzANMAsGCSsGAQQBgjcVHzASBgNVHRMBAf8ECDAGAQH_AgEAMB0GA1UdDgQWBBTg0USwFsuPP50VHiH8i_DHd-1qLjAfBgNVHSMEGDAWgBR6jArOL0hiF-KU0a5VwVLscXSkVjBwBgNVHR8EaTBnMGWgY6Bhhl9odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBUUE0lMjBSb290JTIwQ2VydGlmaWNhdGUlMjBBdXRob3JpdHklMjAyMDE0LmNybDB9BggrBgEFBQcBAQRxMG8wbQYIKwYBBQUHMAKGYWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwVFBNJTIwUm9vdCUyMENlcnRpZmljYXRlJTIwQXV0aG9yaXR5JTIwMjAxNC5jcnQwDQYJKoZIhvcNAQELBQADggIBAGW4yKQ4HaO4JdNMVjVO4mCM0lbLMmXQ0YJtyDHCIE6hsywTYv30DeUDm7Nmmhap9nWp26mSihb7qKQuyhdZkfhA10sp4wDbNpcXjQjdEaE2T1rcgKfounCPQRSW1V42DUgX_Bzuh0flbLYGOJIvugR46gBMUuKVQWmMQKyOMwmofFI8xG_z3VaLMcsgQ8Fl0cvJ6XZ2Jly-QRbZ2v44KNItTTuQKYJCL4kx2b50I4CkrRBaq2LAB-npikLN6xxHqsPvulA0t2WRfF9QzzDZhkVVZ5iCP1fAu5dnHvq0ArBlY2W29OIH_zviW2av88wxZ7FSQzIHu6B8GL45s6skvPa7E9lU6hG186LjrJtHJd0Qad3KYzZQyLKT78m1YiZXLFM02vsctM7nXqtndDjbDPVCota3mg8Jgi2s7-Aq59TL9ZBnRMEvJ5m1Rze1ofFwfO21ktBtLB8vXhzkHjtXy5ld0UQXmdbcs32uaqx6Q3_jVzXlXNNjuG6YBW9iBNL2ar3MtFt66LogL1gmOkyrjGK2Cdyzy1lEupr_SKtggthTyubemmf9G6hJtUZuT_gdFxVZm-MOvCtdNsqdi4HaU8VTCPB999upaEc5vv5KeEQ2xQk0wNmffMlGXGHJrQw8WBwCKkm3TW8hjnhZ9e6ePQvdMEzPhefsxjiQirzpf6lB', ]; - const credentialPublicKey = 'pAEDAzkBACBZAQC3X5SKwYUkxFxxyvCnz_37Z57eSdsgQuiBLDaBOd1R6VEZReAV3nVr_7jiRgmWfu1C-S3Aro65eSG5shcDCgIvY3KdEI8K5ENEPlmucjnFILBAE_MZtPmZlkEDmVCDcVspHX2iKqiVWYV6IFzVX1QUf0SAlWijV9NEfKDbij34ddV0qfG2nEMA0_xVpN2OK2BVXonFg6tS3T00XlFh4MdzIauIHTDT63eAdHlkFrMqU53T5IqDvL3VurBmBjYRJ3VDT9mA2sm7fSrJNXhSVLPst-ZsiOioVKrpzFE9sJmyCQvq2nGZ2RhDo8FfAKiw0kvJRkCSSe1ddxryk9_VSCprIUMBAAE'; + const credentialPublicKey = + 'pAEDAzkBACBZAQC3X5SKwYUkxFxxyvCnz_37Z57eSdsgQuiBLDaBOd1R6VEZReAV3nVr_7jiRgmWfu1C-S3Aro65eSG5shcDCgIvY3KdEI8K5ENEPlmucjnFILBAE_MZtPmZlkEDmVCDcVspHX2iKqiVWYV6IFzVX1QUf0SAlWijV9NEfKDbij34ddV0qfG2nEMA0_xVpN2OK2BVXonFg6tS3T00XlFh4MdzIauIHTDT63eAdHlkFrMqU53T5IqDvL3VurBmBjYRJ3VDT9mA2sm7fSrJNXhSVLPst-ZsiOioVKrpzFE9sJmyCQvq2nGZ2RhDo8FfAKiw0kvJRkCSSe1ddxryk9_VSCprIUMBAAE'; const verified = await verifyAttestationWithMetadata({ statement: metadataStatement, @@ -107,51 +108,56 @@ test('should verify attestation with rsa_emsa_pkcs1_sha256_raw authenticator alg test('should not validate certificate path when authenticator is self-referencing its attestation statement certificates', async () => { const metadataStatement: MetadataStatement = { - "legalHeader": "https://fidoalliance.org/metadata/metadata-statement-legal-header/", - "description": "Virtual Secp256R1 FIDO2 Conformance Testing CTAP2 Authenticator with Self Batch Referencing", - "aaguid": "5b65dac1-7af4-46e6-8a4f-8701fcc4f3b4", - "alternativeDescriptions": { - "ru-RU": "Виртуальный Secp256R1 CTAP2 аутентификатор для тестирование серверов на соответсвие спецификации FIDO2 с одинаковыми сертификатами" + legalHeader: 'https://fidoalliance.org/metadata/metadata-statement-legal-header/', + description: + 'Virtual Secp256R1 FIDO2 Conformance Testing CTAP2 Authenticator with Self Batch Referencing', + aaguid: '5b65dac1-7af4-46e6-8a4f-8701fcc4f3b4', + alternativeDescriptions: { + 'ru-RU': + 'Виртуальный Secp256R1 CTAP2 аутентификатор для тестирование серверов на соответсвие спецификации FIDO2 с одинаковыми сертификатами', }, - "protocolFamily": "fido2", - "authenticatorVersion": 2, - "upv": [{ "major": 1, "minor": 0 }], - "authenticationAlgorithms": ["secp256r1_ecdsa_sha256_raw"], - "publicKeyAlgAndEncodings": ["cose"], - "attestationTypes": ["basic_full"], - "schema": 3, - "userVerificationDetails": [ - [{ "userVerificationMethod": "none" }], - [{ "userVerificationMethod": "presence_internal" }], - [{ "userVerificationMethod": "passcode_external", "caDesc": { "base": 10, "minLength": 4 } }], + protocolFamily: 'fido2', + authenticatorVersion: 2, + upv: [{ major: 1, minor: 0 }], + authenticationAlgorithms: ['secp256r1_ecdsa_sha256_raw'], + publicKeyAlgAndEncodings: ['cose'], + attestationTypes: ['basic_full'], + schema: 3, + userVerificationDetails: [ + [{ userVerificationMethod: 'none' }], + [{ userVerificationMethod: 'presence_internal' }], + [{ userVerificationMethod: 'passcode_external', caDesc: { base: 10, minLength: 4 } }], [ - { "userVerificationMethod": "passcode_external", "caDesc": { "base": 10, "minLength": 4 } }, - { "userVerificationMethod": "presence_internal" } - ] + { userVerificationMethod: 'passcode_external', caDesc: { base: 10, minLength: 4 } }, + { userVerificationMethod: 'presence_internal' }, + ], ], - "keyProtection": ["hardware", "secure_element"], - "matcherProtection": ["on_chip"], - "cryptoStrength": 128, - "attachmentHint": ["external", "wired", "wireless", "nfc"], - "tcDisplay": [], - "attestationRootCertificates": [ - "MIIEQTCCAimgAwIBAgIBATANBgkqhkiG9w0BAQsFADCBoTEYMBYGA1UEAwwPRklETzIgVEVTVCBST09UMTEwLwYJKoZIhvcNAQkBFiJjb25mb3JtYW5jZS10b29sc0BmaWRvYWxsaWFuY2Uub3JnMRYwFAYDVQQKDA1GSURPIEFsbGlhbmNlMQwwCgYDVQQLDANDV0cxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJNWTESMBAGA1UEBwwJV2FrZWZpZWxkMB4XDTE4MDUyMzE0Mzk0M1oXDTI4MDUyMDE0Mzk0M1owgcIxIzAhBgNVBAMMGkZJRE8yIEJBVENIIEtFWSBwcmltZTI1NnYxMTEwLwYJKoZIhvcNAQkBFiJjb25mb3JtYW5jZS10b29sc0BmaWRvYWxsaWFuY2Uub3JnMRYwFAYDVQQKDA1GSURPIEFsbGlhbmNlMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTVkxEjAQBgNVBAcMCVdha2VmaWVsZDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABE86Xl6rbB+8rpf232RJlnYse+9yAEAqdsbyMPZVbxeqmZtZf8S/UIqvjp7wzQE/Wrm9J5FL8IBDeMvMsRuJtUajLDAqMAkGA1UdEwQCMAAwHQYDVR0OBBYEFFZN98D4xlW2oR9sTRnzv0Hi/QF5MA0GCSqGSIb3DQEBCwUAA4ICAQCH3aCf+CCJBdEtQc4JpOnUelwGGw7DxnBMokHHBgrzJxDn9BFcFwxGLxrFV7EfYehQNOD+74OS8fZRgZiNf9EDGAYiHh0+CspfBWd20zCIjlCdDBcyhwq3PLJ65JC/og3CT9AK4kvks4DI+01RYxNv9S8Jx1haO1lgU55hBIr1P/p21ZKnpcCEhPjB/cIFrHJqL5iJGfed+LXni9Suq24OHnp44Mrv4h7OD2elu5yWfdfFb+RGG2TYURFIGYGijsii093w0ZMBOfBS+3Xq/DrHeZbZrrNkY455gJCZ5eV83Nrt9J9/UF0VZHl/hwnSAUC/b3tN/l0ZlC9kPcNzJD04l4ndFBD2KdfQ2HGTX7pybWLZ7yH2BM3ui2OpiacaOzd7OE91rHYB2uZyQ7jdg25yF9M8QI9NHM/itCjdBvAYt4QCT8dX6gmZiIGR2F/YXZAsybtJ16pnUmODVbW80lPbzy+PUQYX79opeD9u6MBorzr9g08Elpb1F3DgSd8VSLlsR2QPllKl4AcJDMIOfZHOQGOzatMV7ipEVRa0L5FnjAWpHHvSNcsjD4Cul562mO3MlI2pCyo+US+nIzG5XZmOeu4Db/Kw/dEPOo2ztHwlU0qKJ7REBsbt63jdQtlwLuiLHwkpiwnrAOZfwbLLu9Yz4tL1eJlQffuwS/Aolsz7HA==" + keyProtection: ['hardware', 'secure_element'], + matcherProtection: ['on_chip'], + cryptoStrength: 128, + attachmentHint: ['external', 'wired', 'wireless', 'nfc'], + tcDisplay: [], + attestationRootCertificates: [ + 'MIIEQTCCAimgAwIBAgIBATANBgkqhkiG9w0BAQsFADCBoTEYMBYGA1UEAwwPRklETzIgVEVTVCBST09UMTEwLwYJKoZIhvcNAQkBFiJjb25mb3JtYW5jZS10b29sc0BmaWRvYWxsaWFuY2Uub3JnMRYwFAYDVQQKDA1GSURPIEFsbGlhbmNlMQwwCgYDVQQLDANDV0cxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJNWTESMBAGA1UEBwwJV2FrZWZpZWxkMB4XDTE4MDUyMzE0Mzk0M1oXDTI4MDUyMDE0Mzk0M1owgcIxIzAhBgNVBAMMGkZJRE8yIEJBVENIIEtFWSBwcmltZTI1NnYxMTEwLwYJKoZIhvcNAQkBFiJjb25mb3JtYW5jZS10b29sc0BmaWRvYWxsaWFuY2Uub3JnMRYwFAYDVQQKDA1GSURPIEFsbGlhbmNlMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTVkxEjAQBgNVBAcMCVdha2VmaWVsZDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABE86Xl6rbB+8rpf232RJlnYse+9yAEAqdsbyMPZVbxeqmZtZf8S/UIqvjp7wzQE/Wrm9J5FL8IBDeMvMsRuJtUajLDAqMAkGA1UdEwQCMAAwHQYDVR0OBBYEFFZN98D4xlW2oR9sTRnzv0Hi/QF5MA0GCSqGSIb3DQEBCwUAA4ICAQCH3aCf+CCJBdEtQc4JpOnUelwGGw7DxnBMokHHBgrzJxDn9BFcFwxGLxrFV7EfYehQNOD+74OS8fZRgZiNf9EDGAYiHh0+CspfBWd20zCIjlCdDBcyhwq3PLJ65JC/og3CT9AK4kvks4DI+01RYxNv9S8Jx1haO1lgU55hBIr1P/p21ZKnpcCEhPjB/cIFrHJqL5iJGfed+LXni9Suq24OHnp44Mrv4h7OD2elu5yWfdfFb+RGG2TYURFIGYGijsii093w0ZMBOfBS+3Xq/DrHeZbZrrNkY455gJCZ5eV83Nrt9J9/UF0VZHl/hwnSAUC/b3tN/l0ZlC9kPcNzJD04l4ndFBD2KdfQ2HGTX7pybWLZ7yH2BM3ui2OpiacaOzd7OE91rHYB2uZyQ7jdg25yF9M8QI9NHM/itCjdBvAYt4QCT8dX6gmZiIGR2F/YXZAsybtJ16pnUmODVbW80lPbzy+PUQYX79opeD9u6MBorzr9g08Elpb1F3DgSd8VSLlsR2QPllKl4AcJDMIOfZHOQGOzatMV7ipEVRa0L5FnjAWpHHvSNcsjD4Cul562mO3MlI2pCyo+US+nIzG5XZmOeu4Db/Kw/dEPOo2ztHwlU0qKJ7REBsbt63jdQtlwLuiLHwkpiwnrAOZfwbLLu9Yz4tL1eJlQffuwS/Aolsz7HA==', ], - "supportedExtensions": [{ "id": "hmac-secret", "fail_if_unknown": false }, { "id": "credProtect", "fail_if_unknown": false } + supportedExtensions: [ + { id: 'hmac-secret', fail_if_unknown: false }, + { id: 'credProtect', fail_if_unknown: false }, ], - "authenticatorGetInfo": { - "versions": ["U2F_V2", "FIDO_2_0"], - "extensions": ["credProtect", "hmac-secret"], - "aaguid": "5b65dac17af446e68a4f8701fcc4f3b4", - "options": { "plat": false, "rk": true, "clientPin": true, "up": true, "uv": true }, - "maxMsgSize": 1200, - } + authenticatorGetInfo: { + versions: ['U2F_V2', 'FIDO_2_0'], + extensions: ['credProtect', 'hmac-secret'], + aaguid: '5b65dac17af446e68a4f8701fcc4f3b4', + options: { plat: false, rk: true, clientPin: true, up: true, uv: true }, + maxMsgSize: 1200, + }, }; const x5c = [ - 'MIIEQTCCAimgAwIBAgIBATANBgkqhkiG9w0BAQsFADCBoTEYMBYGA1UEAwwPRklETzIgVEVTVCBST09UMTEwLwYJKoZIhvcNAQkBFiJjb25mb3JtYW5jZS10b29sc0BmaWRvYWxsaWFuY2Uub3JnMRYwFAYDVQQKDA1GSURPIEFsbGlhbmNlMQwwCgYDVQQLDANDV0cxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJNWTESMBAGA1UEBwwJV2FrZWZpZWxkMB4XDTE4MDUyMzE0Mzk0M1oXDTI4MDUyMDE0Mzk0M1owgcIxIzAhBgNVBAMMGkZJRE8yIEJBVENIIEtFWSBwcmltZTI1NnYxMTEwLwYJKoZIhvcNAQkBFiJjb25mb3JtYW5jZS10b29sc0BmaWRvYWxsaWFuY2Uub3JnMRYwFAYDVQQKDA1GSURPIEFsbGlhbmNlMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTVkxEjAQBgNVBAcMCVdha2VmaWVsZDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABE86Xl6rbB-8rpf232RJlnYse-9yAEAqdsbyMPZVbxeqmZtZf8S_UIqvjp7wzQE_Wrm9J5FL8IBDeMvMsRuJtUajLDAqMAkGA1UdEwQCMAAwHQYDVR0OBBYEFFZN98D4xlW2oR9sTRnzv0Hi_QF5MA0GCSqGSIb3DQEBCwUAA4ICAQCH3aCf-CCJBdEtQc4JpOnUelwGGw7DxnBMokHHBgrzJxDn9BFcFwxGLxrFV7EfYehQNOD-74OS8fZRgZiNf9EDGAYiHh0-CspfBWd20zCIjlCdDBcyhwq3PLJ65JC_og3CT9AK4kvks4DI-01RYxNv9S8Jx1haO1lgU55hBIr1P_p21ZKnpcCEhPjB_cIFrHJqL5iJGfed-LXni9Suq24OHnp44Mrv4h7OD2elu5yWfdfFb-RGG2TYURFIGYGijsii093w0ZMBOfBS-3Xq_DrHeZbZrrNkY455gJCZ5eV83Nrt9J9_UF0VZHl_hwnSAUC_b3tN_l0ZlC9kPcNzJD04l4ndFBD2KdfQ2HGTX7pybWLZ7yH2BM3ui2OpiacaOzd7OE91rHYB2uZyQ7jdg25yF9M8QI9NHM_itCjdBvAYt4QCT8dX6gmZiIGR2F_YXZAsybtJ16pnUmODVbW80lPbzy-PUQYX79opeD9u6MBorzr9g08Elpb1F3DgSd8VSLlsR2QPllKl4AcJDMIOfZHOQGOzatMV7ipEVRa0L5FnjAWpHHvSNcsjD4Cul562mO3MlI2pCyo-US-nIzG5XZmOeu4Db_Kw_dEPOo2ztHwlU0qKJ7REBsbt63jdQtlwLuiLHwkpiwnrAOZfwbLLu9Yz4tL1eJlQffuwS_Aolsz7HA' + 'MIIEQTCCAimgAwIBAgIBATANBgkqhkiG9w0BAQsFADCBoTEYMBYGA1UEAwwPRklETzIgVEVTVCBST09UMTEwLwYJKoZIhvcNAQkBFiJjb25mb3JtYW5jZS10b29sc0BmaWRvYWxsaWFuY2Uub3JnMRYwFAYDVQQKDA1GSURPIEFsbGlhbmNlMQwwCgYDVQQLDANDV0cxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJNWTESMBAGA1UEBwwJV2FrZWZpZWxkMB4XDTE4MDUyMzE0Mzk0M1oXDTI4MDUyMDE0Mzk0M1owgcIxIzAhBgNVBAMMGkZJRE8yIEJBVENIIEtFWSBwcmltZTI1NnYxMTEwLwYJKoZIhvcNAQkBFiJjb25mb3JtYW5jZS10b29sc0BmaWRvYWxsaWFuY2Uub3JnMRYwFAYDVQQKDA1GSURPIEFsbGlhbmNlMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTVkxEjAQBgNVBAcMCVdha2VmaWVsZDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABE86Xl6rbB-8rpf232RJlnYse-9yAEAqdsbyMPZVbxeqmZtZf8S_UIqvjp7wzQE_Wrm9J5FL8IBDeMvMsRuJtUajLDAqMAkGA1UdEwQCMAAwHQYDVR0OBBYEFFZN98D4xlW2oR9sTRnzv0Hi_QF5MA0GCSqGSIb3DQEBCwUAA4ICAQCH3aCf-CCJBdEtQc4JpOnUelwGGw7DxnBMokHHBgrzJxDn9BFcFwxGLxrFV7EfYehQNOD-74OS8fZRgZiNf9EDGAYiHh0-CspfBWd20zCIjlCdDBcyhwq3PLJ65JC_og3CT9AK4kvks4DI-01RYxNv9S8Jx1haO1lgU55hBIr1P_p21ZKnpcCEhPjB_cIFrHJqL5iJGfed-LXni9Suq24OHnp44Mrv4h7OD2elu5yWfdfFb-RGG2TYURFIGYGijsii093w0ZMBOfBS-3Xq_DrHeZbZrrNkY455gJCZ5eV83Nrt9J9_UF0VZHl_hwnSAUC_b3tN_l0ZlC9kPcNzJD04l4ndFBD2KdfQ2HGTX7pybWLZ7yH2BM3ui2OpiacaOzd7OE91rHYB2uZyQ7jdg25yF9M8QI9NHM_itCjdBvAYt4QCT8dX6gmZiIGR2F_YXZAsybtJ16pnUmODVbW80lPbzy-PUQYX79opeD9u6MBorzr9g08Elpb1F3DgSd8VSLlsR2QPllKl4AcJDMIOfZHOQGOzatMV7ipEVRa0L5FnjAWpHHvSNcsjD4Cul562mO3MlI2pCyo-US-nIzG5XZmOeu4Db_Kw_dEPOo2ztHwlU0qKJ7REBsbt63jdQtlwLuiLHwkpiwnrAOZfwbLLu9Yz4tL1eJlQffuwS_Aolsz7HA', ]; - const credentialPublicKey = 'pQECAyYgASFYIBdmUVOxrn-OOtkVwGP_vAspH3VkgzcGXVlu3-acb7EZIlggKgDTs0fr2d51sLR6uL3KP2cqR3iIUkKMCjyMJhYOkf4'; + const credentialPublicKey = + 'pQECAyYgASFYIBdmUVOxrn-OOtkVwGP_vAspH3VkgzcGXVlu3-acb7EZIlggKgDTs0fr2d51sLR6uL3KP2cqR3iIUkKMCjyMJhYOkf4'; const verified = await verifyAttestationWithMetadata({ statement: metadataStatement, diff --git a/packages/server/src/metadata/verifyAttestationWithMetadata.ts b/packages/server/src/metadata/verifyAttestationWithMetadata.ts index dea3417..9b4c471 100644 --- a/packages/server/src/metadata/verifyAttestationWithMetadata.ts +++ b/packages/server/src/metadata/verifyAttestationWithMetadata.ts @@ -4,7 +4,14 @@ import type { MetadataStatement, AlgSign } from '../metadata/mdsTypes'; import { convertCertBufferToPEM } from '../helpers/convertCertBufferToPEM'; import { validateCertificatePath } from '../helpers/validateCertificatePath'; import { decodeCredentialPublicKey } from '../helpers/decodeCredentialPublicKey'; -import { COSEALG, COSECRV, COSEKEYS, COSEKTY, COSEPublicKeyEC2, isCOSEPublicKeyEC2 } from '../helpers/cose'; +import { + COSEALG, + COSECRV, + COSEKEYS, + COSEKTY, + COSEPublicKeyEC2, + isCOSEPublicKeyEC2, +} from '../helpers/cose'; /** * Match properties of the authenticator's attestation statement against expected values as @@ -21,11 +28,7 @@ export async function verifyAttestationWithMetadata({ x5c: Uint8Array[] | Base64URLString[]; attestationStatementAlg?: number; }): Promise { - const { - authenticationAlgorithms, - authenticatorGetInfo, - attestationRootCertificates, - } = statement; + const { authenticationAlgorithms, authenticatorGetInfo, attestationRootCertificates } = statement; // Make sure the alg in the attestation statement matches one of the ones specified in metadata const keypairCOSEAlgs: Set = new Set(); @@ -104,8 +107,9 @@ export async function verifyAttestationWithMetadata({ * ] * ``` */ - const debugMDSAlgs = authenticationAlgorithms - .map((algSign) => `'${algSign}' (COSE info: ${stringifyCOSEInfo(algSignToCOSEInfoMap[algSign])})`); + const debugMDSAlgs = authenticationAlgorithms.map( + algSign => `'${algSign}' (COSE info: ${stringifyCOSEInfo(algSignToCOSEInfoMap[algSign])})`, + ); const strMDSAlgs = JSON.stringify(debugMDSAlgs, null, 2).replace(/"/g, ''); /** @@ -140,10 +144,7 @@ export async function verifyAttestationWithMetadata({ * certificate chain validation. */ let authenticatorIsSelfReferencing = false; - if ( - authenticatorCerts.length === 1 && - statementRootCerts.indexOf(authenticatorCerts[0]) >= 0 - ) { + if (authenticatorCerts.length === 1 && statementRootCerts.indexOf(authenticatorCerts[0]) >= 0) { authenticatorIsSelfReferencing = true; } diff --git a/packages/server/src/registration/generateRegistrationOptions.test.ts b/packages/server/src/registration/generateRegistrationOptions.test.ts index c67a8b2..25f9d30 100644 --- a/packages/server/src/registration/generateRegistrationOptions.test.ts +++ b/packages/server/src/registration/generateRegistrationOptions.test.ts @@ -175,7 +175,7 @@ test('should require resident key if residentKey option is absent but requireRes userName: 'usernameHere', authenticatorSelection: { requireResidentKey: true, - } + }, }); expect(options.authenticatorSelection?.requireResidentKey).toEqual(true); @@ -190,7 +190,7 @@ test('should discourage resident key if residentKey option is absent but require userName: 'usernameHere', authenticatorSelection: { requireResidentKey: false, - } + }, }); expect(options.authenticatorSelection?.requireResidentKey).toEqual(false); diff --git a/packages/server/src/registration/verifyRegistrationResponse.test.ts b/packages/server/src/registration/verifyRegistrationResponse.test.ts index 776be4c..09d748b 100644 --- a/packages/server/src/registration/verifyRegistrationResponse.test.ts +++ b/packages/server/src/registration/verifyRegistrationResponse.test.ts @@ -237,9 +237,9 @@ test('should throw if an unexpected attestation format is specified', async () = }); test('should throw error if assertion RP ID is unexpected value', async () => { - const authData = esmDecodeAttestationObject.decodeAttestationObject( - isoBase64URL.toBuffer(attestationNone.response.attestationObject), - ).get('authData'); + const authData = esmDecodeAttestationObject + .decodeAttestationObject(isoBase64URL.toBuffer(attestationNone.response.attestationObject)) + .get('authData'); const actualAuthData = esmParseAuthenticatorData.parseAuthenticatorData(authData); mockParseAuthData.mockReturnValue({ diff --git a/packages/server/src/registration/verifyRegistrationResponse.ts b/packages/server/src/registration/verifyRegistrationResponse.ts index c937bee..0c1351f 100644 --- a/packages/server/src/registration/verifyRegistrationResponse.ts +++ b/packages/server/src/registration/verifyRegistrationResponse.ts @@ -18,7 +18,7 @@ import { COSEKEYS } from '../helpers/cose'; import { convertAAGUIDToString } from '../helpers/convertAAGUIDToString'; import { parseBackupFlags } from '../helpers/parseBackupFlags'; import { matchExpectedRPID } from '../helpers/matchExpectedRPID'; -import { isoBase64URL } from '../helpers/iso'; +import { isoBase64URL } from '../helpers/iso'; import { SettingsService } from '../services/settingsService'; import { supportedCOSEAlgorithmIdentifiers } from './generateRegistrationOptions'; -- cgit v1.2.3