summaryrefslogtreecommitdiffhomepage
path: root/packages/server/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/server/src')
-rw-r--r--packages/server/src/authentication/generateAuthenticationOptions.test.ts19
-rw-r--r--packages/server/src/authentication/generateAuthenticationOptions.ts16
-rw-r--r--packages/server/src/authentication/verifyAuthenticationResponse.test.ts57
-rw-r--r--packages/server/src/authentication/verifyAuthenticationResponse.ts38
-rw-r--r--packages/server/src/helpers/__mocks__/generateChallenge.ts4
-rw-r--r--packages/server/src/helpers/convertAAGUIDToString.ts6
-rw-r--r--packages/server/src/helpers/convertCOSEtoPKCS.test.ts9
-rw-r--r--packages/server/src/helpers/convertCOSEtoPKCS.ts94
-rw-r--r--packages/server/src/helpers/convertCertBufferToPEM.ts15
-rw-r--r--packages/server/src/helpers/convertPublicKeyToPEM.test.ts83
-rw-r--r--packages/server/src/helpers/convertPublicKeyToPEM.ts69
-rw-r--r--packages/server/src/helpers/convertX509PublicKeyToCOSE.ts124
-rw-r--r--packages/server/src/helpers/cose.ts139
-rw-r--r--packages/server/src/helpers/decodeAttestationObject.test.ts14
-rw-r--r--packages/server/src/helpers/decodeAttestationObject.ts33
-rw-r--r--packages/server/src/helpers/decodeAuthenticatorExtensions.test.ts26
-rw-r--r--packages/server/src/helpers/decodeAuthenticatorExtensions.ts39
-rw-r--r--packages/server/src/helpers/decodeCbor.ts24
-rw-r--r--packages/server/src/helpers/decodeClientDataJSON.ts4
-rw-r--r--packages/server/src/helpers/decodeCredentialPublicKey.ts8
-rw-r--r--packages/server/src/helpers/generateChallenge.ts10
-rw-r--r--packages/server/src/helpers/getCertificateInfo.ts2
-rw-r--r--packages/server/src/helpers/index.ts15
-rw-r--r--packages/server/src/helpers/isBase64URLString.test.ts16
-rw-r--r--packages/server/src/helpers/isBase64URLString.ts13
-rw-r--r--packages/server/src/helpers/isCertRevoked.ts11
-rw-r--r--packages/server/src/helpers/iso/index.ts11
-rw-r--r--packages/server/src/helpers/iso/isoBase64URL.ts67
-rw-r--r--packages/server/src/helpers/iso/isoCBOR.ts46
-rw-r--r--packages/server/src/helpers/iso/isoCrypto/digest.ts18
-rw-r--r--packages/server/src/helpers/iso/isoCrypto/getRandomValues.ts11
-rw-r--r--packages/server/src/helpers/iso/isoCrypto/importKey.ts10
-rw-r--r--packages/server/src/helpers/iso/isoCrypto/index.ts3
-rw-r--r--packages/server/src/helpers/iso/isoCrypto/mapCoseAlgToWebCryptoAlg.ts19
-rw-r--r--packages/server/src/helpers/iso/isoCrypto/mapCoseAlgToWebCryptoKeyAlgName.ts19
-rw-r--r--packages/server/src/helpers/iso/isoCrypto/structs.ts3
-rw-r--r--packages/server/src/helpers/iso/isoCrypto/verify.ts36
-rw-r--r--packages/server/src/helpers/iso/isoCrypto/verifyEC2.ts117
-rw-r--r--packages/server/src/helpers/iso/isoCrypto/verifyOKP.test.ts41
-rw-r--r--packages/server/src/helpers/iso/isoCrypto/verifyOKP.ts67
-rw-r--r--packages/server/src/helpers/iso/isoCrypto/verifyRSA.ts104
-rw-r--r--packages/server/src/helpers/iso/isoUint8Array.ts90
-rw-r--r--packages/server/src/helpers/matchExpectedRPID.ts44
-rw-r--r--packages/server/src/helpers/parseAuthenticatorData.test.ts17
-rw-r--r--packages/server/src/helpers/parseAuthenticatorData.ts50
-rw-r--r--packages/server/src/helpers/toHash.test.ts8
-rw-r--r--packages/server/src/helpers/toHash.ts21
-rw-r--r--packages/server/src/helpers/validateCertificatePath.ts1
-rw-r--r--packages/server/src/helpers/verifySignature.ts114
-rw-r--r--packages/server/src/metadata/mdsTypes.ts2
-rw-r--r--packages/server/src/metadata/parseJWT.ts6
-rw-r--r--packages/server/src/metadata/verifyAttestationWithMetadata.test.ts145
-rw-r--r--packages/server/src/metadata/verifyAttestationWithMetadata.ts63
-rw-r--r--packages/server/src/registration/generateRegistrationOptions.test.ts4
-rw-r--r--packages/server/src/registration/generateRegistrationOptions.ts16
-rw-r--r--packages/server/src/registration/verifications/tpm/constants.ts4
-rw-r--r--packages/server/src/registration/verifications/tpm/parseCertInfo.ts56
-rw-r--r--packages/server/src/registration/verifications/tpm/parsePubArea.ts78
-rw-r--r--packages/server/src/registration/verifications/tpm/verifyAttestationTPM.test.ts36
-rw-r--r--packages/server/src/registration/verifications/tpm/verifyAttestationTPM.ts89
-rw-r--r--packages/server/src/registration/verifications/verifyAttestationAndroidKey.test.ts6
-rw-r--r--packages/server/src/registration/verifications/verifyAttestationAndroidKey.ts27
-rw-r--r--packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.test.ts39
-rw-r--r--packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.ts30
-rw-r--r--packages/server/src/registration/verifications/verifyAttestationApple.test.ts9
-rw-r--r--packages/server/src/registration/verifications/verifyAttestationApple.ts15
-rw-r--r--packages/server/src/registration/verifications/verifyAttestationFIDOU2F.ts18
-rw-r--r--packages/server/src/registration/verifications/verifyAttestationPacked.test.ts1
-rw-r--r--packages/server/src/registration/verifications/verifyAttestationPacked.ts27
-rw-r--r--packages/server/src/registration/verifyRegistrationResponse.test.ts190
-rw-r--r--packages/server/src/registration/verifyRegistrationResponse.ts51
-rw-r--r--packages/server/src/services/metadataService.test.ts4
-rw-r--r--packages/server/src/services/metadataService.ts8
-rw-r--r--packages/server/src/services/settingsService.ts4
-rw-r--r--packages/server/src/setupTests.ts14
75 files changed, 1827 insertions, 920 deletions
diff --git a/packages/server/src/authentication/generateAuthenticationOptions.test.ts b/packages/server/src/authentication/generateAuthenticationOptions.test.ts
index 78c6473..9048cf5 100644
--- a/packages/server/src/authentication/generateAuthenticationOptions.test.ts
+++ b/packages/server/src/authentication/generateAuthenticationOptions.test.ts
@@ -1,10 +1,13 @@
jest.mock('../helpers/generateChallenge');
+import { isoBase64URL } from '../helpers/iso';
+
import { generateAuthenticationOptions } from './generateAuthenticationOptions';
-test('should generate credential request options suitable for sending via JSON', () => {
- const challenge = 'totallyrandomvalue';
+const challengeString = 'dG90YWxseXJhbmRvbXZhbHVl';
+const challengeBuffer = isoBase64URL.toBuffer(challengeString);
+test('should generate credential request options suitable for sending via JSON', () => {
const options = generateAuthenticationOptions({
allowCredentials: [
{
@@ -19,12 +22,12 @@ test('should generate credential request options suitable for sending via JSON',
},
],
timeout: 1,
- challenge,
+ challenge: challengeBuffer,
});
expect(options).toEqual({
// base64url-encoded
- challenge: 'dG90YWxseXJhbmRvbXZhbHVl',
+ challenge: challengeString,
allowCredentials: [
{
id: 'MTIzNA',
@@ -43,7 +46,7 @@ test('should generate credential request options suitable for sending via JSON',
test('defaults to 60 seconds if no timeout is specified', () => {
const options = generateAuthenticationOptions({
- challenge: 'totallyrandomvalue',
+ challenge: challengeBuffer,
allowCredentials: [
{ id: Buffer.from('1234', 'ascii'), type: 'public-key' },
{ id: Buffer.from('5678', 'ascii'), type: 'public-key' },
@@ -55,7 +58,7 @@ test('defaults to 60 seconds if no timeout is specified', () => {
test('should not set userVerification if not specified', () => {
const options = generateAuthenticationOptions({
- challenge: 'totallyrandomvalue',
+ challenge: challengeBuffer,
allowCredentials: [
{ id: Buffer.from('1234', 'ascii'), type: 'public-key' },
{ id: Buffer.from('5678', 'ascii'), type: 'public-key' },
@@ -86,7 +89,7 @@ test('should generate without params', () => {
test('should set userVerification if specified', () => {
const options = generateAuthenticationOptions({
- challenge: 'totallyrandomvalue',
+ challenge: challengeBuffer,
allowCredentials: [
{ id: Buffer.from('1234', 'ascii'), type: 'public-key' },
{ id: Buffer.from('5678', 'ascii'), type: 'public-key' },
@@ -99,7 +102,7 @@ test('should set userVerification if specified', () => {
test('should set extensions if specified', () => {
const options = generateAuthenticationOptions({
- challenge: 'totallyrandomvalue',
+ challenge: challengeBuffer,
allowCredentials: [
{ id: Buffer.from('1234', 'ascii'), type: 'public-key' },
{ id: Buffer.from('5678', 'ascii'), type: 'public-key' },
diff --git a/packages/server/src/authentication/generateAuthenticationOptions.ts b/packages/server/src/authentication/generateAuthenticationOptions.ts
index b80473e..bd517e3 100644
--- a/packages/server/src/authentication/generateAuthenticationOptions.ts
+++ b/packages/server/src/authentication/generateAuthenticationOptions.ts
@@ -4,13 +4,13 @@ import type {
PublicKeyCredentialDescriptorFuture,
UserVerificationRequirement,
} from '@simplewebauthn/typescript-types';
-import base64url from 'base64url';
+import { isoBase64URL, isoUint8Array } from '../helpers/iso';
import { generateChallenge } from '../helpers/generateChallenge';
export type GenerateAuthenticationOptionsOpts = {
allowCredentials?: PublicKeyCredentialDescriptorFuture[];
- challenge?: string | Buffer;
+ challenge?: string | Uint8Array;
timeout?: number;
userVerification?: UserVerificationRequirement;
extensions?: AuthenticationExtensionsClientInputs;
@@ -42,11 +42,19 @@ export function generateAuthenticationOptions(
rpID,
} = options;
+ /**
+ * Preserve ability to specify `string` values for challenges
+ */
+ let _challenge = challenge;
+ if (typeof _challenge === 'string') {
+ _challenge = isoUint8Array.fromUTF8String(_challenge);
+ }
+
return {
- challenge: base64url.encode(challenge),
+ challenge: isoBase64URL.fromBuffer(_challenge),
allowCredentials: allowCredentials?.map(cred => ({
...cred,
- id: base64url.encode(cred.id as Buffer),
+ 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 3b8e7b6..547d953 100644
--- a/packages/server/src/authentication/verifyAuthenticationResponse.test.ts
+++ b/packages/server/src/authentication/verifyAuthenticationResponse.test.ts
@@ -1,4 +1,3 @@
-import base64url from 'base64url';
import { verifyAuthenticationResponse } from './verifyAuthenticationResponse';
import * as esmDecodeClientDataJSON from '../helpers/decodeClientDataJSON';
@@ -8,6 +7,7 @@ import {
AuthenticatorDevice,
AuthenticationCredentialJSON,
} from '@simplewebauthn/typescript-types';
+import { isoUint8Array, isoBase64URL } from '../helpers/iso';
let mockDecodeClientData: jest.SpyInstance;
let mockParseAuthData: jest.SpyInstance;
@@ -92,7 +92,7 @@ test('should throw when assertion type is not webauthn.create', async () => {
test('should throw error if user was not present', async () => {
mockParseAuthData.mockReturnValue({
- rpIdHash: toHash(Buffer.from('dev.dontneeda.pw', 'ascii')),
+ rpIdHash: await toHash(Buffer.from('dev.dontneeda.pw', 'ascii')),
flags: 0,
});
@@ -128,7 +128,7 @@ test('should throw error if previous counter value is not less than in response'
test('should throw error if assertion RP ID is unexpected value', async () => {
mockParseAuthData.mockReturnValue({
- rpIdHash: toHash(Buffer.from('bad.url', 'ascii')),
+ rpIdHash: await toHash(Buffer.from('bad.url', 'ascii')),
flags: 0,
});
@@ -157,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({
@@ -183,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, 'encode').mockReturnValueOnce(expectedChallenge);
+ jest.spyOn(isoBase64URL, 'toString').mockReturnValueOnce(expectedChallenge);
const verification = await verifyAuthenticationResponse({
credential: {
id: 'YJ8FMM-AmcUt73XPX341WXWd7ypBMylGjjhu0g3VzME',
@@ -198,13 +198,14 @@ test.skip('should verify TPM assertion', async () => {
},
type: 'public-key',
clientExtensionResults: {},
+ authenticatorAttachment: '',
},
expectedChallenge,
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,
},
});
@@ -276,20 +277,21 @@ test('should pass verification if custom challenge verifier returns true', async
},
type: 'public-key',
clientExtensionResults: {},
+ authenticatorAttachment: '',
},
expectedChallenge: (challenge: string) => {
const parsedChallenge: { actualChallenge: string; arbitraryData: string } = JSON.parse(
- base64url.decode(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,
@@ -318,7 +320,7 @@ test('should return authenticator extension output', async () => {
clientDataJSON:
'eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiaVpzVkN6dHJEVzdEMlVfR0hDSWxZS0x3VjJiQ3NCVFJxVlFVbkpYbjlUayIsIm9yaWdpbiI6ImFuZHJvaWQ6YXBrLWtleS1oYXNoOmd4N3NxX3B4aHhocklRZEx5ZkcwcHhLd2lKN2hPazJESlE0eHZLZDQzOFEiLCJhbmRyb2lkUGFja2FnZU5hbWUiOiJjb20uZmlkby5leGFtcGxlLmZpZG8yYXBpZXhhbXBsZSJ9',
authenticatorData:
- 'DXX8xWP9p3nbLjQ-6kiYiHWLeFSdSTpP2-oc2WqjHMSFAAAAAKFvZGV2aWNlUHVibGljS2V5pWNkcGtYTaUBAgMmIAEhWCCZGqvtneQnGp7erYgG-dyW1tzNDEdiU6VRBInsg3m-WyJYIKCXPP3tu3nif-9O50gWc_szElBN3KVDTP0jQx1q0p7aY3NpZ1hHMEUCIElSbNKK72tOYhp9WTbStQSVL8CuIxOk8DV6r_-uqWR0AiEAnVE6yu-wsyx2Wq5v66jClGhe_2P_HL8R7PIQevT-uPhlbm9uY2VAZXNjb3BlQQBmYWFndWlkULk_2WHy5kYvsSKCACJH3ng=',
+ 'DXX8xWP9p3nbLjQ-6kiYiHWLeFSdSTpP2-oc2WqjHMSFAAAAAKFsZGV2aWNlUHViS2V5pWNkcGtYTaUBAgMmIAEhWCCZGqvtneQnGp7erYgG-dyW1tzNDEdiU6VRBInsg3m-WyJYIKCXPP3tu3nif-9O50gWc_szElBN3KVDTP0jQx1q0p7aY3NpZ1hHMEUCIElSbNKK72tOYhp9WTbStQSVL8CuIxOk8DV6r_-uqWR0AiEAnVE6yu-wsyx2Wq5v66jClGhe_2P_HL8R7PIQevT-uPhlbm9uY2VAZXNjb3BlQQBmYWFndWlkULk_2WHy5kYvsSKCACJH3ng',
signature:
'MEYCIQDlRuxY7cYre0sb3T6TovQdfYIUb72cRZYOQv_zS9wN_wIhAOvN-fwjtyIhWRceqJV4SX74-z6oALERbC7ohk8EdVPO',
userHandle: 'b2FPajFxcmM4MWo3QkFFel9RN2lEakh5RVNlU2RLNDF0Sl92eHpQYWV5UQ==',
@@ -327,15 +329,16 @@ test('should return authenticator extension output', async () => {
rawId: 'E_Pko4wN1BXE23S0ftN3eQ',
type: 'public-key',
clientExtensionResults: {},
+ authenticatorAttachment: '',
},
expectedOrigin: 'android:apk-key-hash:gx7sq_pxhxhrIQdLyfG0pxKwiJ7hOk2DJQ4xvKd438Q',
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,
@@ -343,18 +346,16 @@ test('should return authenticator extension output', async () => {
});
expect(verification.authenticationInfo?.authenticatorExtensionResults).toMatchObject({
- devicePublicKey: {
- dpk: Buffer.from(
+ devicePubKey: {
+ dpk: isoUint8Array.fromHex(
'A5010203262001215820991AABED9DE4271A9EDEAD8806F9DC96D6DCCD0C476253A5510489EC8379BE5B225820A0973CFDEDBB79E27FEF4EE7481673FB3312504DDCA5434CFD23431D6AD29EDA',
- 'hex',
),
- sig: Buffer.from(
+ sig: isoUint8Array.fromHex(
'3045022049526CD28AEF6B4E621A7D5936D2B504952FC0AE2313A4F0357AAFFFAEA964740221009D513ACAEFB0B32C765AAE6FEBA8C294685EFF63FF1CBF11ECF2107AF4FEB8F8',
- 'hex',
),
- nonce: Buffer.from('', 'hex'),
- scope: Buffer.from('00', 'hex'),
- aaguid: Buffer.from('B93FD961F2E6462FB12282002247DE78', 'hex'),
+ nonce: isoUint8Array.fromHex(''),
+ scope: isoUint8Array.fromHex('00'),
+ aaguid: isoUint8Array.fromHex('B93FD961F2E6462FB12282002247DE78'),
},
});
});
@@ -391,15 +392,16 @@ const assertionResponse: AuthenticationCredentialJSON = {
},
clientExtensionResults: {},
type: 'public-key',
+ authenticatorAttachment: '',
};
-const assertionChallenge = base64url.encode('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,14 +422,15 @@ const assertionFirstTimeUsedResponse: AuthenticationCredentialJSON = {
},
type: 'public-key',
clientExtensionResults: {},
+ authenticatorAttachment: '',
};
-const assertionFirstTimeUsedChallenge = base64url.encode('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 6bb6e98..c99013e 100644
--- a/packages/server/src/authentication/verifyAuthenticationResponse.ts
+++ b/packages/server/src/authentication/verifyAuthenticationResponse.ts
@@ -1,4 +1,3 @@
-import base64url from 'base64url';
import {
AuthenticationCredentialJSON,
AuthenticatorDevice,
@@ -10,9 +9,10 @@ import { decodeClientDataJSON } from '../helpers/decodeClientDataJSON';
import { toHash } from '../helpers/toHash';
import { verifySignature } from '../helpers/verifySignature';
import { parseAuthenticatorData } from '../helpers/parseAuthenticatorData';
-import { isBase64URLString } from '../helpers/isBase64URLString';
import { parseBackupFlags } from '../helpers/parseBackupFlags';
import { AuthenticationExtensionsAuthenticatorOutputs } from '../helpers/decodeAuthenticatorExtensions';
+import { matchExpectedRPID } from '../helpers/matchExpectedRPID';
+import { isoUint8Array, isoBase64URL } from '../helpers/iso';
export type VerifyAuthenticationResponseOpts = {
credential: AuthenticationCredentialJSON;
@@ -120,11 +120,11 @@ export async function verifyAuthenticationResponse(
}
}
- if (!isBase64URLString(response.authenticatorData)) {
+ if (!isoBase64URL.isBase64url(response.authenticatorData)) {
throw new Error('Credential response authenticatorData was not a base64url string');
}
- if (!isBase64URLString(response.signature)) {
+ if (!isoBase64URL.isBase64url(response.signature)) {
throw new Error('Credential response signature was not a base64url string');
}
@@ -142,28 +142,20 @@ 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;
// Make sure the response's RP ID is ours
+ let expectedRPIDs: string[] = [];
if (typeof expectedRPID === 'string') {
- const expectedRPIDHash = toHash(Buffer.from(expectedRPID, 'ascii'));
- if (!rpIdHash.equals(expectedRPIDHash)) {
- throw new Error(`Unexpected RP ID hash`);
- }
+ expectedRPIDs = [expectedRPID];
} else {
- // Go through each expected RP ID and try to find one that matches
- const foundMatch = expectedRPID.some(expected => {
- const expectedRPIDHash = toHash(Buffer.from(expected, 'ascii'));
- return rpIdHash.equals(expectedRPIDHash);
- });
-
- if (!foundMatch) {
- throw new Error(`Unexpected RP ID hash`);
- }
+ expectedRPIDs = expectedRPID;
}
+ await matchExpectedRPID(rpIdHash, expectedRPIDs);
+
if (advancedFIDOConfig !== undefined) {
const { userVerification: fidoUserVerification } = advancedFIDOConfig;
@@ -193,10 +185,10 @@ export async function verifyAuthenticationResponse(
}
}
- const clientDataHash = toHash(base64url.toBuffer(response.clientDataJSON));
- const signatureBase = Buffer.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
@@ -213,7 +205,7 @@ export async function verifyAuthenticationResponse(
const toReturn: VerifiedAuthenticationResponse = {
verified: await verifySignature({
signature,
- signatureBase,
+ data: signatureBase,
credentialPublicKey: authenticator.credentialPublicKey,
}),
authenticationInfo: {
@@ -250,7 +242,7 @@ export async function verifyAuthenticationResponse(
export type VerifiedAuthenticationResponse = {
verified: boolean;
authenticationInfo: {
- credentialID: Buffer;
+ credentialID: Uint8Array;
newCounter: number;
userVerified: boolean;
credentialDeviceType: CredentialDeviceType;
diff --git a/packages/server/src/helpers/__mocks__/generateChallenge.ts b/packages/server/src/helpers/__mocks__/generateChallenge.ts
index a339e56..d9d866e 100644
--- a/packages/server/src/helpers/__mocks__/generateChallenge.ts
+++ b/packages/server/src/helpers/__mocks__/generateChallenge.ts
@@ -1,3 +1,3 @@
-export function generateChallenge(): Buffer {
- return Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
+export function generateChallenge(): Uint8Array {
+ return Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
}
diff --git a/packages/server/src/helpers/convertAAGUIDToString.ts b/packages/server/src/helpers/convertAAGUIDToString.ts
index 0fb8356..db9622a 100644
--- a/packages/server/src/helpers/convertAAGUIDToString.ts
+++ b/packages/server/src/helpers/convertAAGUIDToString.ts
@@ -1,9 +1,11 @@
+import { isoUint8Array } from './iso';
+
/**
* Convert the aaguid buffer in authData into a UUID string
*/
-export function convertAAGUIDToString(aaguid: Buffer): string {
+export function convertAAGUIDToString(aaguid: Uint8Array): string {
// Raw Hex: adce000235bcc60a648b0b25f1f05503
- const hex = aaguid.toString('hex');
+ 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 de2d10f..761382f 100644
--- a/packages/server/src/helpers/convertCOSEtoPKCS.test.ts
+++ b/packages/server/src/helpers/convertCOSEtoPKCS.test.ts
@@ -1,13 +1,14 @@
-import * as esmDecodeCbor from './decodeCbor';
+import { isoCBOR } from './iso';
-import { convertCOSEtoPKCS, COSEKEYS } from './convertCOSEtoPKCS';
+import { convertCOSEtoPKCS } from './convertCOSEtoPKCS';
+import { COSEKEYS } from './cose';
test('should throw an error curve if, somehow, curve coordinate x is missing', () => {
const mockCOSEKey = new Map<number, number | Buffer>();
mockCOSEKey.set(COSEKEYS.y, 1);
- jest.spyOn(esmDecodeCbor, 'decodeCborFirst').mockReturnValue(mockCOSEKey);
+ jest.spyOn(isoCBOR, 'decodeFirst').mockReturnValue(mockCOSEKey);
expect(() => {
convertCOSEtoPKCS(Buffer.from('123', 'ascii'));
@@ -19,7 +20,7 @@ test('should throw an error curve if, somehow, curve coordinate y is missing', (
mockCOSEKey.set(COSEKEYS.x, 1);
- jest.spyOn(esmDecodeCbor, 'decodeCborFirst').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 618a0dc..761fae6 100644
--- a/packages/server/src/helpers/convertCOSEtoPKCS.ts
+++ b/packages/server/src/helpers/convertCOSEtoPKCS.ts
@@ -1,13 +1,16 @@
-import { COSEAlgorithmIdentifier } from '@simplewebauthn/typescript-types';
-import { decodeCborFirst } from './decodeCbor';
+import { isoCBOR, isoUint8Array } from './iso';
+import { COSEPublicKeyEC2, COSEKEYS } from './cose';
/**
* Takes COSE-encoded public key and converts it to PKCS key
*/
-export function convertCOSEtoPKCS(cosePublicKey: Buffer): Buffer {
- const struct: COSEPublicKey = decodeCborFirst(cosePublicKey);
+export function convertCOSEtoPKCS(cosePublicKey: Uint8Array): Uint8Array {
+ // This is a little sloppy, I'm using COSEPublicKeyEC2 since it could have both x and y, but when
+ // there's no y it means it's probably better typed as COSEPublicKeyOKP. I'll leave this for now
+ // and revisit it later if it ever becomes an actual problem.
+ const struct = isoCBOR.decodeFirst<COSEPublicKeyEC2>(cosePublicKey);
- const tag = Buffer.from([0x04]);
+ const tag = Uint8Array.from([0x04]);
const x = struct.get(COSEKEYS.x);
const y = struct.get(COSEKEYS.y);
@@ -16,85 +19,8 @@ export function convertCOSEtoPKCS(cosePublicKey: Buffer): Buffer {
}
if (y) {
- return Buffer.concat([tag, x as Buffer, y as Buffer]);
+ return isoUint8Array.concat([tag, x, y]);
}
- return Buffer.concat([tag, x as Buffer]);
+ return isoUint8Array.concat([tag, x]);
}
-
-export type COSEPublicKey = Map<COSEAlgorithmIdentifier, number | Buffer>;
-
-export enum COSEKEYS {
- kty = 1,
- alg = 3,
- crv = -1,
- x = -2,
- y = -3,
- n = -1,
- e = -2,
-}
-
-export enum COSEKTY {
- OKP = 1,
- EC2 = 2,
- RSA = 3,
-}
-
-export const COSERSASCHEME: { [key: string]: SigningSchemeHash } = {
- '-3': 'pss-sha256',
- '-39': 'pss-sha512',
- '-38': 'pss-sha384',
- '-65535': 'pkcs1-sha1',
- '-257': 'pkcs1-sha256',
- '-258': 'pkcs1-sha384',
- '-259': 'pkcs1-sha512',
-};
-
-// See https://w3c.github.io/webauthn/#sctn-alg-identifier
-export const COSECRV: { [key: number]: string } = {
- // alg: -7
- 1: 'p256',
- // alg: -35
- 2: 'p384',
- // alg: -36
- 3: 'p521',
- // alg: -8
- 6: 'ed25519',
-};
-
-export const COSEALGHASH: { [key: string]: string } = {
- '-65535': 'sha1',
- '-259': 'sha512',
- '-258': 'sha384',
- '-257': 'sha256',
- '-39': 'sha512',
- '-38': 'sha384',
- '-37': 'sha256',
- '-36': 'sha512',
- '-35': 'sha384',
- '-8': 'sha512',
- '-7': 'sha256',
-};
-
-/**
- * Imported from node-rsa's types
- */
-type SigningSchemeHash =
- | 'pkcs1-ripemd160'
- | 'pkcs1-md4'
- | 'pkcs1-md5'
- | 'pkcs1-sha'
- | 'pkcs1-sha1'
- | 'pkcs1-sha224'
- | 'pkcs1-sha256'
- | 'pkcs1-sha384'
- | 'pkcs1-sha512'
- | 'pss-ripemd160'
- | 'pss-md4'
- | 'pss-md5'
- | 'pss-sha'
- | 'pss-sha1'
- | 'pss-sha224'
- | 'pss-sha256'
- | 'pss-sha384'
- | 'pss-sha512';
diff --git a/packages/server/src/helpers/convertCertBufferToPEM.ts b/packages/server/src/helpers/convertCertBufferToPEM.ts
index b6949c4..adf4201 100644
--- a/packages/server/src/helpers/convertCertBufferToPEM.ts
+++ b/packages/server/src/helpers/convertCertBufferToPEM.ts
@@ -1,19 +1,26 @@
-import base64url from 'base64url';
import type { Base64URLString } from '@simplewebauthn/typescript-types';
+import { isoBase64URL } from './iso';
+
/**
* Convert buffer to an OpenSSL-compatible PEM text format.
*/
-export function convertCertBufferToPEM(certBuffer: Buffer | Base64URLString): string {
+export function convertCertBufferToPEM(certBuffer: Uint8Array | Base64URLString): string {
let b64cert: string;
/**
* Get certBuffer to a base64 representation
*/
if (typeof certBuffer === 'string') {
- b64cert = base64url.toBase64(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 = certBuffer.toString('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
deleted file mode 100644
index 353a9eb..0000000
--- a/packages/server/src/helpers/convertPublicKeyToPEM.test.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import base64url from 'base64url';
-import cbor from 'cbor';
-import { COSEKEYS } from './convertCOSEtoPKCS';
-import { convertPublicKeyToPEM } from './convertPublicKeyToPEM';
-
-test('should return pem when input is base64URLString', () => {
- const mockCOSEKey = new Map<number, number | Buffer>();
-
- const x = Buffer.from('gh9MmXjtmcHFesofqWZ6iuxSdAYgoPVvfJqpv1818lo', 'base64');
- const y = Buffer.from('3BDZHsNvKUb5VbyGPqcAFf4FGuPhJ2Xy215oWDw_1jc', 'base64');
- mockCOSEKey.set(COSEKEYS.kty, 2);
- mockCOSEKey.set(COSEKEYS.alg, -7);
- mockCOSEKey.set(COSEKEYS.crv, 1);
- mockCOSEKey.set(COSEKEYS.x, x);
- mockCOSEKey.set(COSEKEYS.y, y);
-
- jest.spyOn(cbor, 'decodeAllSync').mockReturnValueOnce([mockCOSEKey]);
- const input = base64url.toBuffer('test');
- const actual = convertPublicKeyToPEM(input);
- expect(actual).toEqual(`-----BEGIN PUBLIC KEY-----
-MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgh9MmXjtmcHFesofqWZ6iuxSdAYg\noPVvfJqpv1818lrcENkew28pRvlVvIY+pwAV/gUa4+EnZfLbXmhYPD/WNw==
------END PUBLIC KEY-----
-`);
-});
-
-test('should return pem when input is base64URLString', () => {
- const mockCOSEKey = new Map<number, number | Buffer>();
-
- const n = Buffer.from(
- '0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw',
- 'base64',
- );
- const e = Buffer.from('AQAB', 'base64');
- mockCOSEKey.set(COSEKEYS.kty, 3);
- mockCOSEKey.set(COSEKEYS.alg, -7);
- mockCOSEKey.set(COSEKEYS.crv, 1);
- mockCOSEKey.set(COSEKEYS.n, n);
- mockCOSEKey.set(COSEKEYS.e, e);
-
- jest.spyOn(cbor, 'decodeAllSync').mockReturnValueOnce([mockCOSEKey]);
- const input = base64url.toBuffer('test');
- const actual = convertPublicKeyToPEM(input);
- expect(actual).toEqual(`-----BEGIN PUBLIC KEY-----
-MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0vx7agoebGcQSuuPiLJX
-ZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tS
-oc/BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ/2W+5JsGY4Hc5n9yBXArwl93lqt
-7/RN5w6Cf0h4QyQ5v+65YGjQR0/FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0
-zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt+bFTWhAI4vMQFh6WeZu0f
-M4lFd2NcRwr3XPksINHaQ+G/xBniIqbw0Ls1jF44+csFCur+kEgU8awapJzKnqDK
-gwIDAQAB
------END PUBLIC KEY-----
-`);
-});
-
-test('should return pem when input is base64URLString', () => {
- const mockCOSEKey = new Map<number, number | Buffer>();
-
- mockCOSEKey.set(COSEKEYS.kty, 0);
- mockCOSEKey.set(COSEKEYS.alg, -7);
-
- jest.spyOn(cbor, 'decodeAllSync').mockReturnValueOnce([mockCOSEKey]);
- const input = base64url.toBuffer('test');
- try {
- convertPublicKeyToPEM(input);
- } catch (err) {
- expect((err as Error).message).toEqual('Public key was missing kty');
- }
-});
-
-test('should raise error when kty is OKP (1)', () => {
- const mockCOSEKey = new Map<number, number | Buffer>();
-
- mockCOSEKey.set(COSEKEYS.kty, 1);
- mockCOSEKey.set(COSEKEYS.alg, -7);
-
- jest.spyOn(cbor, 'decodeAllSync').mockReturnValueOnce([mockCOSEKey]);
- const input = base64url.toBuffer('test');
- try {
- convertPublicKeyToPEM(input);
- } catch (err) {
- expect((err as Error).message).toEqual('Could not convert public key type 1 to PEM');
- }
-});
diff --git a/packages/server/src/helpers/convertPublicKeyToPEM.ts b/packages/server/src/helpers/convertPublicKeyToPEM.ts
deleted file mode 100644
index 5c0e39a..0000000
--- a/packages/server/src/helpers/convertPublicKeyToPEM.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-import cbor from 'cbor';
-import jwkToPem from 'jwk-to-pem';
-
-import { COSEKEYS, COSEKTY, COSECRV } from './convertCOSEtoPKCS';
-
-export function convertPublicKeyToPEM(publicKey: Buffer): string {
- let struct;
- try {
- struct = cbor.decodeAllSync(publicKey)[0];
- } catch (err) {
- const _err = err as Error;
- throw new Error(`Error decoding public key while converting to PEM: ${_err.message}`);
- }
-
- const kty = struct.get(COSEKEYS.kty);
-
- if (!kty) {
- throw new Error('Public key was missing kty');
- }
-
- if (kty === COSEKTY.EC2) {
- const crv = struct.get(COSEKEYS.crv);
- const x = struct.get(COSEKEYS.x);
- const y = struct.get(COSEKEYS.y);
-
- if (!crv) {
- throw new Error('Public key was missing crv (EC2)');
- }
-
- if (!x) {
- throw new Error('Public key was missing x (EC2)');
- }
-
- if (!y) {
- throw new Error('Public key was missing y (EC2)');
- }
-
- const ecPEM = jwkToPem({
- kty: 'EC',
- // Specify curve as "P-256" from "p256"
- crv: COSECRV[crv as number].replace('p', 'P-'),
- x: (x as Buffer).toString('base64'),
- y: (y as Buffer).toString('base64'),
- });
-
- return ecPEM;
- } else if (kty === COSEKTY.RSA) {
- const n = struct.get(COSEKEYS.n);
- const e = struct.get(COSEKEYS.e);
-
- if (!n) {
- throw new Error('Public key was missing n (RSA)');
- }
-
- if (!e) {
- throw new Error('Public key was missing e (RSA)');
- }
-
- const rsaPEM = jwkToPem({
- kty: 'RSA',
- n: (n as Buffer).toString('base64'),
- e: (e as Buffer).toString('base64'),
- });
-
- return rsaPEM;
- }
-
- throw new Error(`Could not convert public key type ${kty} to PEM`);
-}
diff --git a/packages/server/src/helpers/convertX509PublicKeyToCOSE.ts b/packages/server/src/helpers/convertX509PublicKeyToCOSE.ts
new file mode 100644
index 0000000..cd76146
--- /dev/null
+++ b/packages/server/src/helpers/convertX509PublicKeyToCOSE.ts
@@ -0,0 +1,124 @@
+import { AsnParser } from '@peculiar/asn1-schema';
+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';
+
+export function convertX509PublicKeyToCOSE(leafCertificate: Uint8Array): COSEPublicKey {
+ let cosePublicKey: COSEPublicKey = new Map();
+
+ /**
+ * Time to extract the public key from an X.509 leaf certificate
+ */
+ const x509 = AsnParser.parse(leafCertificate, Certificate);
+
+ const { tbsCertificate } = x509;
+ const { subjectPublicKeyInfo, signature: _tbsSignature } = tbsCertificate;
+
+ const signatureAlgorithm = _tbsSignature.algorithm;
+ const publicKeyAlgorithmID = subjectPublicKeyInfo.algorithm.algorithm;
+
+ if (publicKeyAlgorithmID === id_ecPublicKey) {
+ /**
+ * EC2 Public Key
+ */
+ if (!subjectPublicKeyInfo.algorithm.parameters) {
+ throw new Error('Leaf cert public key missing parameters (EC2)');
+ }
+
+ const ecParameters = AsnParser.parse(
+ new Uint8Array(subjectPublicKeyInfo.algorithm.parameters),
+ ECParameters,
+ );
+
+ let crv = -999;
+ if (ecParameters.namedCurve === id_secp256r1) {
+ crv = COSECRV.P256;
+ } else {
+ throw new Error(
+ `Leaf cert public key contained unexpected namedCurve ${ecParameters.namedCurve} (EC2)`,
+ );
+ }
+
+ const subjectPublicKey = new Uint8Array(subjectPublicKeyInfo.subjectPublicKey);
+
+ let x: Uint8Array;
+ let y: Uint8Array;
+ if (subjectPublicKey[0] === 0x04) {
+ // 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));
+ y = subjectPublicKey.slice(pointer);
+ } else {
+ throw new Error('TODO: Figure out how to handle public keys in "compressed form"');
+ }
+
+ const coseEC2PubKey: COSEPublicKeyEC2 = new Map();
+ coseEC2PubKey.set(COSEKEYS.kty, COSEKTY.EC2);
+ coseEC2PubKey.set(COSEKEYS.alg, signatureAlgorithmToCOSEAlg(signatureAlgorithm));
+ coseEC2PubKey.set(COSEKEYS.crv, crv);
+ coseEC2PubKey.set(COSEKEYS.x, x);
+ coseEC2PubKey.set(COSEKEYS.y, y);
+
+ cosePublicKey = coseEC2PubKey;
+ } else if (publicKeyAlgorithmID === '1.2.840.113549.1.1.1') {
+ /**
+ * RSA public key
+ */
+ const rsaPublicKey = AsnParser.parse(subjectPublicKeyInfo.subjectPublicKey, RSAPublicKey);
+
+ const coseRSAPubKey: COSEPublicKeyRSA = new Map();
+ coseRSAPubKey.set(COSEKEYS.kty, COSEKTY.RSA);
+ coseRSAPubKey.set(COSEKEYS.alg, signatureAlgorithmToCOSEAlg(signatureAlgorithm));
+ coseRSAPubKey.set(COSEKEYS.n, new Uint8Array(rsaPublicKey.modulus));
+ coseRSAPubKey.set(COSEKEYS.e, new Uint8Array(rsaPublicKey.publicExponent));
+
+ cosePublicKey = coseRSAPubKey;
+ } else {
+ throw new Error(`Unexpected leaf cert public key algorithm ${publicKeyAlgorithmID}`);
+ }
+
+ return cosePublicKey;
+}
+
+/**
+ * Map X.509 signature algorithm OIDs to COSE algorithm IDs
+ *
+ * - EC2 OIDs: https://oidref.com/1.2.840.10045.4.3
+ * - RSA OIDs: https://oidref.com/1.2.840.113549.1.1
+ */
+function signatureAlgorithmToCOSEAlg(signatureAlgorithm: string): COSEALG {
+ let alg: COSEALG;
+
+ if (signatureAlgorithm === '1.2.840.10045.4.3.2') {
+ alg = COSEALG.ES256;
+ } else if (signatureAlgorithm === '1.2.840.10045.4.3.3') {
+ alg = COSEALG.ES384;
+ } else if (signatureAlgorithm === '1.2.840.10045.4.3.4') {
+ alg = COSEALG.ES512;
+ } else if (signatureAlgorithm === '1.2.840.113549.1.1.11') {
+ alg = COSEALG.RS256;
+ } else if (signatureAlgorithm === '1.2.840.113549.1.1.12') {
+ alg = COSEALG.RS384;
+ } else if (signatureAlgorithm === '1.2.840.113549.1.1.13') {
+ alg = COSEALG.RS512;
+ } else if (signatureAlgorithm === '1.2.840.113549.1.1.5') {
+ alg = COSEALG.RS1;
+ } else {
+ throw new Error(
+ `Leaf cert contained unexpected signature algorithm ${signatureAlgorithm} (EC2)`,
+ );
+ }
+
+ return alg;
+}
diff --git a/packages/server/src/helpers/cose.ts b/packages/server/src/helpers/cose.ts
new file mode 100644
index 0000000..2f2e446
--- /dev/null
+++ b/packages/server/src/helpers/cose.ts
@@ -0,0 +1,139 @@
+/**
+ * Fundamental values that are needed to discern the more specific COSE public key types below.
+ *
+ * The use of `Maps` here is due to CBOR encoding being used with public keys, and the CBOR "Map"
+ * type is being decoded to JavaScript's `Map` type instead of, say, a basic Object as us JS
+ * developers might prefer.
+ *
+ * These types are an unorthodox way of saying "these Maps should involve these discrete lists of
+ * keys", but it works.
+ */
+export type COSEPublicKey = {
+ // Getters
+ get(key: COSEKEYS.kty): COSEKTY | undefined;
+ get(key: COSEKEYS.alg): COSEALG | undefined;
+ // Setters
+ set(key: COSEKEYS.kty, value: COSEKTY): void;
+ set(key: COSEKEYS.alg, value: COSEALG): void;
+};
+
+export type COSEPublicKeyOKP = COSEPublicKey & {
+ // Getters
+ get(key: COSEKEYS.crv): number | undefined;
+ get(key: COSEKEYS.x): Uint8Array | undefined;
+ // Setters
+ set(key: COSEKEYS.crv, value: number): void;
+ set(key: COSEKEYS.x, value: Uint8Array): void;
+};
+
+export type COSEPublicKeyEC2 = COSEPublicKey & {
+ // Getters
+ get(key: COSEKEYS.crv): number | undefined;
+ get(key: COSEKEYS.x): Uint8Array | undefined;
+ get(key: COSEKEYS.y): Uint8Array | undefined;
+ // Setters
+ set(key: COSEKEYS.crv, value: number): void;
+ set(key: COSEKEYS.x, value: Uint8Array): void;
+ set(key: COSEKEYS.y, value: Uint8Array): void;
+};
+
+export type COSEPublicKeyRSA = COSEPublicKey & {
+ // Getters
+ get(key: COSEKEYS.n): Uint8Array | undefined;
+ get(key: COSEKEYS.e): Uint8Array | undefined;
+ // Setters
+ set(key: COSEKEYS.n, value: Uint8Array): void;
+ set(key: COSEKEYS.e, value: Uint8Array): void;
+};
+
+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 {
+ const kty = cosePublicKey.get(COSEKEYS.kty);
+ return isCOSEKty(kty) && kty === COSEKTY.EC2;
+}
+
+export function isCOSEPublicKeyRSA(
+ cosePublicKey: COSEPublicKey,
+): cosePublicKey is COSEPublicKeyRSA {
+ const kty = cosePublicKey.get(COSEKEYS.kty);
+ return isCOSEKty(kty) && kty === COSEKTY.RSA;
+}
+
+/**
+ * COSE Keys
+ *
+ * https://www.iana.org/assignments/cose/cose.xhtml#key-common-parameters
+ * https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters
+ */
+export enum COSEKEYS {
+ kty = 1,
+ alg = 3,
+ crv = -1,
+ x = -2,
+ y = -3,
+ n = -1,
+ e = -2,
+}
+
+/**
+ * COSE Key Types
+ *
+ * https://www.iana.org/assignments/cose/cose.xhtml#key-type
+ */
+export enum COSEKTY {
+ OKP = 1,
+ EC2 = 2,
+ RSA = 3,
+}
+
+export function isCOSEKty(kty: number | undefined): kty is COSEKTY {
+ return Object.values(COSEKTY).indexOf(kty as COSEKTY) >= 0;
+}
+
+/**
+ * COSE Curves
+ *
+ * https://www.iana.org/assignments/cose/cose.xhtml#elliptic-curves
+ */
+export enum COSECRV {
+ P256 = 1,
+ P384 = 2,
+ P521 = 3,
+ ED25519 = 6,
+}
+
+export function isCOSECrv(crv: number | undefined): crv is COSECRV {
+ return Object.values(COSECRV).indexOf(crv as COSECRV) >= 0;
+}
+
+/**
+ * COSE Algorithms
+ *
+ * https://www.iana.org/assignments/cose/cose.xhtml#algorithms
+ */
+export enum COSEALG {
+ ES256 = -7,
+ EdDSA = -8,
+ ES384 = -35,
+ ES512 = -36,
+ PS256 = -37,
+ PS384 = -38,
+ PS512 = -39,
+ ES256K = -47,
+ RS256 = -257,
+ RS384 = -258,
+ RS512 = -259,
+ RS1 = -65535,
+}
+
+export function isCOSEAlg(alg: number | undefined): alg is COSEALG {
+ return Object.values(COSEALG).indexOf(alg as COSEALG) >= 0;
+}
diff --git a/packages/server/src/helpers/decodeAttestationObject.test.ts b/packages/server/src/helpers/decodeAttestationObject.test.ts
index 1ba6bd0..b37d137 100644
--- a/packages/server/src/helpers/decodeAttestationObject.test.ts
+++ b/packages/server/src/helpers/decodeAttestationObject.test.ts
@@ -11,9 +11,9 @@ test('should decode base64url-encoded indirect attestationObject', () => {
),
);
- expect(decoded.fmt).toEqual('none');
- expect(decoded.attStmt).toEqual({});
- expect(decoded.authData).toBeDefined();
+ expect(decoded.get('fmt')).toEqual('none');
+ expect(decoded.get('attStmt')).toEqual(new Map());
+ expect(decoded.get('authData')).toBeDefined();
});
test('should decode base64url-encoded direct attestationObject', () => {
@@ -38,8 +38,8 @@ test('should decode base64url-encoded direct attestationObject', () => {
),
);
- expect(decoded.fmt).toEqual('fido-u2f');
- expect(decoded.attStmt.sig).toBeDefined();
- expect(decoded.attStmt.x5c).toBeDefined();
- expect(decoded.authData).toBeDefined();
+ expect(decoded.get('fmt')).toEqual('fido-u2f');
+ expect(decoded.get('attStmt').get('sig')).toBeDefined();
+ expect(decoded.get('attStmt').get('x5c')).toBeDefined();
+ expect(decoded.get('authData')).toBeDefined();
});
diff --git a/packages/server/src/helpers/decodeAttestationObject.ts b/packages/server/src/helpers/decodeAttestationObject.ts
index 5385106..afdd7a4 100644
--- a/packages/server/src/helpers/decodeAttestationObject.ts
+++ b/packages/server/src/helpers/decodeAttestationObject.ts
@@ -1,13 +1,12 @@
-import cbor from 'cbor';
+import { isoCBOR } from './iso';
/**
* Convert an AttestationObject buffer to a proper object
*
* @param base64AttestationObject Attestation Object buffer
*/
-export function decodeAttestationObject(attestationObject: Buffer): AttestationObject {
- const toCBOR: AttestationObject = cbor.decodeAllSync(attestationObject)[0];
- return toCBOR;
+export function decodeAttestationObject(attestationObject: Uint8Array): AttestationObject {
+ return isoCBOR.decodeFirst<AttestationObject>(attestationObject);
}
export type AttestationFormat =
@@ -20,17 +19,23 @@ export type AttestationFormat =
| 'none';
export type AttestationObject = {
- fmt: AttestationFormat;
- attStmt: AttestationStatement;
- authData: Buffer;
+ get(key: 'fmt'): AttestationFormat;
+ get(key: 'attStmt'): AttestationStatement;
+ get(key: 'authData'): Uint8Array;
};
+/**
+ * `AttestationStatement` will be an instance of `Map`, but these keys help make finite the list of
+ * possible values within it.
+ */
export type AttestationStatement = {
- sig?: Buffer;
- x5c?: Buffer[];
- response?: Buffer;
- alg?: number;
- ver?: string;
- certInfo?: Buffer;
- pubArea?: Buffer;
+ get(key: 'sig'): Uint8Array | undefined;
+ get(key: 'x5c'): Uint8Array[] | undefined;
+ get(key: 'response'): Uint8Array | undefined;
+ get(key: 'alg'): number | undefined;
+ get(key: 'ver'): string | undefined;
+ get(key: 'certInfo'): Uint8Array | undefined;
+ get(key: 'pubArea'): Uint8Array | undefined;
+ // `Map` properties
+ get size(): number;
};
diff --git a/packages/server/src/helpers/decodeAuthenticatorExtensions.test.ts b/packages/server/src/helpers/decodeAuthenticatorExtensions.test.ts
index b9e66b5..6cc5e24 100644
--- a/packages/server/src/helpers/decodeAuthenticatorExtensions.test.ts
+++ b/packages/server/src/helpers/decodeAuthenticatorExtensions.test.ts
@@ -1,30 +1,28 @@
import { decodeAuthenticatorExtensions } from './decodeAuthenticatorExtensions';
+import { isoUint8Array } from './iso';
test('should decode authenticator extensions', () => {
const extensions = decodeAuthenticatorExtensions(
- Buffer.from(
+ isoUint8Array.fromHex(
'A16C6465766963655075624B6579A56364706B584DA5010203262001215820991AABED9D' +
- 'E4271A9EDEAD8806F9DC96D6DCCD0C476253A5510489EC8379BE5B225820A0973CFDEDBB' +
- '79E27FEF4EE7481673FB3312504DDCA5434CFD23431D6AD29EDA63736967584730450221' +
- '00EFB38074BD15B8C82CF09F87FBC6FB3C7169EA4F1806B7E90937374302345B7A02202B' +
- '7113040731A0E727D338D48542863CE65880AA79E5EA740AC8CCD94347988E656E6F6E63' +
- '65406573636F70654100666161677569645000000000000000000000000000000000',
- 'hex',
+ 'E4271A9EDEAD8806F9DC96D6DCCD0C476253A5510489EC8379BE5B225820A0973CFDEDBB' +
+ '79E27FEF4EE7481673FB3312504DDCA5434CFD23431D6AD29EDA63736967584730450221' +
+ '00EFB38074BD15B8C82CF09F87FBC6FB3C7169EA4F1806B7E90937374302345B7A02202B' +
+ '7113040731A0E727D338D48542863CE65880AA79E5EA740AC8CCD94347988E656E6F6E63' +
+ '65406573636F70654100666161677569645000000000000000000000000000000000',
),
);
expect(extensions).toMatchObject({
devicePubKey: {
- dpk: Buffer.from(
+ dpk: isoUint8Array.fromHex(
'A5010203262001215820991AABED9DE4271A9EDEAD8806F9DC96D6DCCD0C476253A5510489EC8379BE5B225820A0973CFDEDBB79E27FEF4EE7481673FB3312504DDCA5434CFD23431D6AD29EDA',
- 'hex',
),
- sig: Buffer.from(
+ sig: isoUint8Array.fromHex(
'3045022100EFB38074BD15B8C82CF09F87FBC6FB3C7169EA4F1806B7E90937374302345B7A02202B7113040731A0E727D338D48542863CE65880AA79E5EA740AC8CCD94347988E',
- 'hex',
),
- nonce: Buffer.from('', 'hex'),
- scope: Buffer.from('00', 'hex'),
- aaguid: Buffer.from('00000000000000000000000000000000', 'hex'),
+ 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 a889879..7bd583c 100644
--- a/packages/server/src/helpers/decodeAuthenticatorExtensions.ts
+++ b/packages/server/src/helpers/decodeAuthenticatorExtensions.ts
@@ -1,4 +1,4 @@
-import cbor from 'cbor';
+import { isoCBOR } from './iso';
/**
* Convert authenticator extension data buffer to a proper object
@@ -6,16 +6,17 @@ import cbor from 'cbor';
* @param extensionData Authenticator Extension Data buffer
*/
export function decodeAuthenticatorExtensions(
- extensionData: Buffer,
+ extensionData: Uint8Array,
): AuthenticationExtensionsAuthenticatorOutputs | undefined {
- let toCBOR: AuthenticationExtensionsAuthenticatorOutputs | undefined;
+ let toCBOR: Map<string, unknown>;
try {
- toCBOR = cbor.decodeAllSync(extensionData)[0];
+ toCBOR = isoCBOR.decodeFirst(extensionData);
} catch (err) {
const _err = err as Error;
throw new Error(`Error decoding authenticator extensions: ${_err.message}`);
}
- return toCBOR;
+
+ return convertMapToObjectDeep(toCBOR);
}
export type AuthenticationExtensionsAuthenticatorOutputs = {
@@ -24,14 +25,34 @@ export type AuthenticationExtensionsAuthenticatorOutputs = {
};
export type DevicePublicKeyAuthenticatorOutput = {
- dpk?: Buffer;
- scp?: Buffer;
+ dpk?: Uint8Array;
sig?: string;
- aaguid?: Buffer;
+ nonce?: Uint8Array;
+ scope?: Uint8Array;
+ aaguid?: Uint8Array;
};
// TODO: Need to verify this format
// https://w3c.github.io/webauthn/#sctn-uvm-extension.
export type UVMAuthenticatorOutput = {
- uvm?: Buffer[];
+ uvm?: Uint8Array[];
};
+
+/**
+ * CBOR-encoded extensions can be deeply-nested Maps, which are too deep for a simple
+ * `Object.entries()`. This method will recursively make sure that all Maps are converted into
+ * basic objects.
+ */
+function convertMapToObjectDeep(input: Map<string, unknown>): { [key: string]: unknown } {
+ const mapped: { [key: string]: unknown } = {};
+
+ for (const [key, value] of input) {
+ if (value instanceof Map) {
+ mapped[key] = convertMapToObjectDeep(value);
+ } else {
+ mapped[key] = value;
+ }
+ }
+
+ return mapped;
+}
diff --git a/packages/server/src/helpers/decodeCbor.ts b/packages/server/src/helpers/decodeCbor.ts
deleted file mode 100644
index 37e8ab2..0000000
--- a/packages/server/src/helpers/decodeCbor.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import cbor from 'cbor';
-
-export function decodeCborFirst(input: string | Buffer | ArrayBufferView): any {
- try {
- // throws if there are extra bytes
- return cbor.decodeFirstSync(input);
- } catch (err) {
- const _err = err as CborDecoderError;
- // if the error was due to extra bytes, return the unpacked value
- if (_err.value) {
- return _err.value;
- }
- throw err;
- }
-}
-
-/**
- * Intuited from a quick scan of `cbor.decodeFirstSync()` here:
- *
- * https://github.com/hildjj/node-cbor/blob/v5.1.0/lib/decoder.js#L189
- */
-class CborDecoderError extends Error {
- value: any;
-}
diff --git a/packages/server/src/helpers/decodeClientDataJSON.ts b/packages/server/src/helpers/decodeClientDataJSON.ts
index b3094db..e0de0a0 100644
--- a/packages/server/src/helpers/decodeClientDataJSON.ts
+++ b/packages/server/src/helpers/decodeClientDataJSON.ts
@@ -1,10 +1,10 @@
-import base64url from 'base64url';
+import { isoBase64URL } from './iso';
/**
* Decode an authenticator's base64url-encoded clientDataJSON to JSON
*/
export function decodeClientDataJSON(data: string): ClientDataJSON {
- const toString = base64url.decode(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 cd7a4a2..32f4199 100644
--- a/packages/server/src/helpers/decodeCredentialPublicKey.ts
+++ b/packages/server/src/helpers/decodeCredentialPublicKey.ts
@@ -1,6 +1,6 @@
-import { COSEPublicKey } from './convertCOSEtoPKCS';
-import { decodeCborFirst } from './decodeCbor';
+import { COSEPublicKey } from './cose';
+import { isoCBOR } from './iso';
-export function decodeCredentialPublicKey(publicKey: Buffer): COSEPublicKey {
- return decodeCborFirst(publicKey);
+export function decodeCredentialPublicKey(publicKey: Uint8Array): COSEPublicKey {
+ return isoCBOR.decodeFirst<COSEPublicKey>(publicKey);
}
diff --git a/packages/server/src/helpers/generateChallenge.ts b/packages/server/src/helpers/generateChallenge.ts
index 4acecf3..8277674 100644
--- a/packages/server/src/helpers/generateChallenge.ts
+++ b/packages/server/src/helpers/generateChallenge.ts
@@ -1,9 +1,9 @@
-import crypto from 'crypto';
+import { isoCrypto } from './iso';
/**
* Generate a suitably random value to be used as an attestation or assertion challenge
*/
-export function generateChallenge(): Buffer {
+export function generateChallenge(): Uint8Array {
/**
* WebAuthn spec says that 16 bytes is a good minimum:
*
@@ -12,5 +12,9 @@ export function generateChallenge(): Buffer {
*
* Just in case, let's double it
*/
- return crypto.randomBytes(32);
+ const challenge = new Uint8Array(32);
+
+ isoCrypto.getRandomValues(challenge);
+
+ return challenge;
}
diff --git a/packages/server/src/helpers/getCertificateInfo.ts b/packages/server/src/helpers/getCertificateInfo.ts
index 02183c5..e503f70 100644
--- a/packages/server/src/helpers/getCertificateInfo.ts
+++ b/packages/server/src/helpers/getCertificateInfo.ts
@@ -36,7 +36,7 @@ const issuerSubjectIDKey: { [key: string]: 'C' | 'O' | 'OU' | 'CN' } = {
*
* @param pemCertificate Result from call to `convertASN1toPEM(x5c[0])`
*/
-export function getCertificateInfo(leafCertBuffer: Buffer): CertificateInfo {
+export function getCertificateInfo(leafCertBuffer: Uint8Array): CertificateInfo {
const asnx509 = AsnParser.parse(leafCertBuffer, Certificate);
const parsedCert = asnx509.tbsCertificate;
diff --git a/packages/server/src/helpers/index.ts b/packages/server/src/helpers/index.ts
index d0c4f42..fec9838 100644
--- a/packages/server/src/helpers/index.ts
+++ b/packages/server/src/helpers/index.ts
@@ -1,37 +1,38 @@
import { convertAAGUIDToString } from './convertAAGUIDToString';
import { convertCertBufferToPEM } from './convertCertBufferToPEM';
import { convertCOSEtoPKCS } from './convertCOSEtoPKCS';
-import { convertPublicKeyToPEM } from './convertPublicKeyToPEM';
import { decodeAttestationObject } from './decodeAttestationObject';
-import { decodeCborFirst } from './decodeCbor';
import { decodeClientDataJSON } from './decodeClientDataJSON';
import { decodeCredentialPublicKey } from './decodeCredentialPublicKey';
import { generateChallenge } from './generateChallenge';
import { getCertificateInfo } from './getCertificateInfo';
-import { isBase64URLString } from './isBase64URLString';
import { isCertRevoked } from './isCertRevoked';
import { parseAuthenticatorData } from './parseAuthenticatorData';
import { toHash } from './toHash';
import { validateCertificatePath } from './validateCertificatePath';
import { verifySignature } from './verifySignature';
+import { isoCBOR, isoBase64URL, isoUint8Array, isoCrypto } from './iso';
+import * as cose from './cose';
export {
convertAAGUIDToString,
convertCertBufferToPEM,
convertCOSEtoPKCS,
- convertPublicKeyToPEM,
decodeAttestationObject,
- decodeCborFirst,
decodeClientDataJSON,
decodeCredentialPublicKey,
generateChallenge,
getCertificateInfo,
- isBase64URLString,
isCertRevoked,
parseAuthenticatorData,
toHash,
validateCertificatePath,
verifySignature,
+ isoCBOR,
+ isoCrypto,
+ isoBase64URL,
+ isoUint8Array,
+ cose,
};
import type {
@@ -41,7 +42,7 @@ import type {
} from './decodeAttestationObject';
import type { CertificateInfo } from './getCertificateInfo';
import type { ClientDataJSON } from './decodeClientDataJSON';
-import type { COSEPublicKey } from './convertCOSEtoPKCS';
+import type { COSEPublicKey } from './cose';
import type { ParsedAuthenticatorData } from './parseAuthenticatorData';
export type {
diff --git a/packages/server/src/helpers/isBase64URLString.test.ts b/packages/server/src/helpers/isBase64URLString.test.ts
deleted file mode 100644
index 358c420..0000000
--- a/packages/server/src/helpers/isBase64URLString.test.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { isBase64URLString } from './isBase64URLString';
-
-test('should return true when input is base64URLString', () => {
- const actual = isBase64URLString('U2ltcGxlV2ViQXV0aG4');
- expect(actual).toEqual(true);
-});
-
-test('should return false when input is not base64URLString', () => {
- const actual = isBase64URLString('U2ltcGxlV2ViQXV0aG4+');
- expect(actual).toEqual(false);
-});
-
-test('should return false when input is blank', () => {
- const actual = isBase64URLString('');
- expect(actual).toEqual(false);
-});
diff --git a/packages/server/src/helpers/isBase64URLString.ts b/packages/server/src/helpers/isBase64URLString.ts
deleted file mode 100644
index f229bf3..0000000
--- a/packages/server/src/helpers/isBase64URLString.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-// Base64URL, with optional padding
-const base64urlRegEx = /^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}=?))?$/;
-
-/**
- * Check to see if a string only contains valid Base64URL values
- */
-export function isBase64URLString(value: string): boolean {
- if (!value) {
- return false;
- }
-
- return base64urlRegEx.test(value);
-}
diff --git a/packages/server/src/helpers/isCertRevoked.ts b/packages/server/src/helpers/isCertRevoked.ts
index cc8c3f1..1ea3a8a 100644
--- a/packages/server/src/helpers/isCertRevoked.ts
+++ b/packages/server/src/helpers/isCertRevoked.ts
@@ -1,9 +1,10 @@
import { X509 } from 'jsrsasign';
-import fetch from 'node-fetch';
+import fetch from 'cross-fetch';
import { AsnParser } from '@peculiar/asn1-schema';
import { CertificateList } from '@peculiar/asn1-x509';
import { convertCertBufferToPEM } from './convertCertBufferToPEM';
+import { isoUint8Array } from './iso';
/**
* A cache of revoked cert serial numbers by Authority Key ID
@@ -61,14 +62,14 @@ export async function isCertRevoked(cert: X509): Promise<boolean> {
const crlCert = new X509();
try {
const respCRL = await fetch(crlURL[0]);
- const dataCRL = await respCRL.buffer();
- const dataPEM = convertCertBufferToPEM(dataCRL);
+ const dataCRL = await respCRL.arrayBuffer();
+ const dataPEM = convertCertBufferToPEM(new Uint8Array(dataCRL));
crlCert.readCertPEM(dataPEM);
} catch (err) {
return false;
}
- const data = AsnParser.parse(Buffer.from(crlCert.hex, 'hex'), CertificateList);
+ const data = AsnParser.parse(isoUint8Array.fromHex(crlCert.hex), CertificateList);
const newCached: CAAuthorityInfo = {
revokedCerts: [],
@@ -85,7 +86,7 @@ export async function isCertRevoked(cert: X509): Promise<boolean> {
if (revokedCerts) {
for (const cert of revokedCerts) {
- const revokedHex = Buffer.from(cert.userCertificate).toString('hex');
+ 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..49f19e4
--- /dev/null
+++ b/packages/server/src/helpers/iso/index.ts
@@ -0,0 +1,11 @@
+/**
+ * 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 isoCrypto from './isoCrypto';
+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..1dfd522
--- /dev/null
+++ b/packages/server/src/helpers/iso/isoBase64URL.ts
@@ -0,0 +1,67 @@
+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, with support for optional padding
+ */
+export function isBase64url(input: string): boolean {
+ // Trim padding characters from the string if present
+ input = input.replace(/=/g, '');
+ 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<Type>(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/isoCrypto/digest.ts b/packages/server/src/helpers/iso/isoCrypto/digest.ts
new file mode 100644
index 0000000..05260a3
--- /dev/null
+++ b/packages/server/src/helpers/iso/isoCrypto/digest.ts
@@ -0,0 +1,18 @@
+import WebCrypto from '@simplewebauthn/iso-webcrypto';
+
+import { COSEALG } from '../../cose';
+import { mapCoseAlgToWebCryptoAlg } from './mapCoseAlgToWebCryptoAlg';
+
+/**
+ * Generate a digest of the provided data.
+ *
+ * @param data The data to generate a digest of
+ * @param algorithm A COSE algorithm ID that maps to a desired SHA algorithm
+ */
+export async function digest(data: Uint8Array, algorithm: COSEALG): Promise<Uint8Array> {
+ const subtleAlgorithm = mapCoseAlgToWebCryptoAlg(algorithm);
+
+ const hashed = await WebCrypto.subtle.digest(subtleAlgorithm, data);
+
+ return new Uint8Array(hashed);
+}
diff --git a/packages/server/src/helpers/iso/isoCrypto/getRandomValues.ts b/packages/server/src/helpers/iso/isoCrypto/getRandomValues.ts
new file mode 100644
index 0000000..ab7454b
--- /dev/null
+++ b/packages/server/src/helpers/iso/isoCrypto/getRandomValues.ts
@@ -0,0 +1,11 @@
+import WebCrypto from '@simplewebauthn/iso-webcrypto';
+
+/**
+ * Fill up the provided bytes array with random bytes equal to its length.
+ *
+ * @returns the same bytes array passed into the method
+ */
+export function getRandomValues(array: Uint8Array): Uint8Array {
+ WebCrypto.getRandomValues(array);
+ return array;
+}
diff --git a/packages/server/src/helpers/iso/isoCrypto/importKey.ts b/packages/server/src/helpers/iso/isoCrypto/importKey.ts
new file mode 100644
index 0000000..4d2ef2b
--- /dev/null
+++ b/packages/server/src/helpers/iso/isoCrypto/importKey.ts
@@ -0,0 +1,10 @@
+import WebCrypto from '@simplewebauthn/iso-webcrypto';
+
+export async function importKey(opts: {
+ keyData: JsonWebKey;
+ algorithm: AlgorithmIdentifier | RsaHashedImportParams | EcKeyImportParams;
+}): Promise<CryptoKey> {
+ const { keyData, algorithm } = opts;
+
+ return WebCrypto.subtle.importKey('jwk', keyData, algorithm, false, ['verify']);
+}
diff --git a/packages/server/src/helpers/iso/isoCrypto/index.ts b/packages/server/src/helpers/iso/isoCrypto/index.ts
new file mode 100644
index 0000000..7850722
--- /dev/null
+++ b/packages/server/src/helpers/iso/isoCrypto/index.ts
@@ -0,0 +1,3 @@
+export { digest } from './digest';
+export { getRandomValues } from './getRandomValues';
+export { verify } from './verify';
diff --git a/packages/server/src/helpers/iso/isoCrypto/mapCoseAlgToWebCryptoAlg.ts b/packages/server/src/helpers/iso/isoCrypto/mapCoseAlgToWebCryptoAlg.ts
new file mode 100644
index 0000000..3394b90
--- /dev/null
+++ b/packages/server/src/helpers/iso/isoCrypto/mapCoseAlgToWebCryptoAlg.ts
@@ -0,0 +1,19 @@
+import { SubtleCryptoAlg } from './structs';
+import { COSEALG } from '../../cose';
+
+/**
+ * Convert a COSE alg ID into a corresponding string value that WebCrypto APIs expect
+ */
+export function mapCoseAlgToWebCryptoAlg(alg: COSEALG): SubtleCryptoAlg {
+ if ([COSEALG.RS1].indexOf(alg) >= 0) {
+ return 'SHA-1';
+ } else if ([COSEALG.ES256, COSEALG.PS256, COSEALG.RS256].indexOf(alg) >= 0) {
+ return 'SHA-256';
+ } else if ([COSEALG.ES384, COSEALG.PS384, COSEALG.RS384].indexOf(alg) >= 0) {
+ return 'SHA-384';
+ } else if ([COSEALG.ES512, COSEALG.PS512, COSEALG.RS512, COSEALG.EdDSA].indexOf(alg) >= 0) {
+ return 'SHA-512';
+ }
+
+ throw new Error(`Unexpected COSE alg value of ${alg}`);
+}
diff --git a/packages/server/src/helpers/iso/isoCrypto/mapCoseAlgToWebCryptoKeyAlgName.ts b/packages/server/src/helpers/iso/isoCrypto/mapCoseAlgToWebCryptoKeyAlgName.ts
new file mode 100644
index 0000000..8be875c
--- /dev/null
+++ b/packages/server/src/helpers/iso/isoCrypto/mapCoseAlgToWebCryptoKeyAlgName.ts
@@ -0,0 +1,19 @@
+import { COSEALG } from '../../cose';
+import { SubtleCryptoKeyAlgName } from './structs';
+
+/**
+ * Convert a COSE alg ID into a corresponding key algorithm string value that WebCrypto APIs expect
+ */
+export function mapCoseAlgToWebCryptoKeyAlgName(alg: COSEALG): SubtleCryptoKeyAlgName {
+ if ([COSEALG.EdDSA].indexOf(alg) >= 0) {
+ return 'Ed25519';
+ } else if ([COSEALG.ES256, COSEALG.ES384, COSEALG.ES512, COSEALG.ES256K].indexOf(alg) >= 0) {
+ return 'ECDSA';
+ } else if ([COSEALG.RS256, COSEALG.RS384, COSEALG.RS512].indexOf(alg) >= 0) {
+ return 'RSASSA-PKCS1-v1_5';
+ } else if ([COSEALG.PS256, COSEALG.PS384, COSEALG.PS512].indexOf(alg) >= 0) {
+ return 'RSA-PSS';
+ }
+
+ throw new Error(`Unexpected COSE alg value of ${alg}`);
+}
diff --git a/packages/server/src/helpers/iso/isoCrypto/structs.ts b/packages/server/src/helpers/iso/isoCrypto/structs.ts
new file mode 100644
index 0000000..b6880c4
--- /dev/null
+++ b/packages/server/src/helpers/iso/isoCrypto/structs.ts
@@ -0,0 +1,3 @@
+export type SubtleCryptoAlg = 'SHA-1' | 'SHA-256' | 'SHA-384' | 'SHA-512';
+export type SubtleCryptoCrv = 'P-256' | 'P-384' | 'P-521' | 'Ed25519';
+export type SubtleCryptoKeyAlgName = 'ECDSA' | 'Ed25519' | 'RSASSA-PKCS1-v1_5' | 'RSA-PSS';
diff --git a/packages/server/src/helpers/iso/isoCrypto/verify.ts b/packages/server/src/helpers/iso/isoCrypto/verify.ts
new file mode 100644
index 0000000..b995e7a
--- /dev/null
+++ b/packages/server/src/helpers/iso/isoCrypto/verify.ts
@@ -0,0 +1,36 @@
+import {
+ COSEALG,
+ COSEKEYS,
+ COSEPublicKey,
+ isCOSEPublicKeyEC2,
+ isCOSEPublicKeyOKP,
+ isCOSEPublicKeyRSA,
+} from '../../cose';
+import { verifyEC2 } from './verifyEC2';
+import { verifyRSA } from './verifyRSA';
+import { verifyOKP } from './verifyOKP';
+
+/**
+ * Verify signatures with their public key. Supports EC2 and RSA public keys.
+ */
+export async function verify(opts: {
+ cosePublicKey: COSEPublicKey;
+ signature: Uint8Array;
+ data: Uint8Array;
+ shaHashOverride?: COSEALG;
+}): Promise<boolean> {
+ const { cosePublicKey, signature, data, shaHashOverride } = opts;
+
+ if (isCOSEPublicKeyEC2(cosePublicKey)) {
+ return verifyEC2({ cosePublicKey, signature, data, shaHashOverride });
+ } else if (isCOSEPublicKeyRSA(cosePublicKey)) {
+ return verifyRSA({ cosePublicKey, signature, data, shaHashOverride });
+ } else if (isCOSEPublicKeyOKP(cosePublicKey)) {
+ return verifyOKP({ cosePublicKey, signature, data });
+ }
+
+ const kty = cosePublicKey.get(COSEKEYS.kty);
+ throw new Error(
+ `Signature verification with public key of kty ${kty} is not supported by this method`,
+ );
+}
diff --git a/packages/server/src/helpers/iso/isoCrypto/verifyEC2.ts b/packages/server/src/helpers/iso/isoCrypto/verifyEC2.ts
new file mode 100644
index 0000000..b86f57f
--- /dev/null
+++ b/packages/server/src/helpers/iso/isoCrypto/verifyEC2.ts
@@ -0,0 +1,117 @@
+import WebCrypto from '@simplewebauthn/iso-webcrypto';
+import { ECDSASigValue } from '@peculiar/asn1-ecc';
+import { AsnParser } from '@peculiar/asn1-schema';
+
+import { COSEALG, COSECRV, COSEKEYS, COSEPublicKeyEC2 } from '../../cose';
+import { mapCoseAlgToWebCryptoAlg } from './mapCoseAlgToWebCryptoAlg';
+import { importKey } from './importKey';
+import { isoBase64URL, isoUint8Array } from '../index';
+import { SubtleCryptoCrv } from './structs';
+
+/**
+ * Verify a signature using an EC2 public key
+ */
+export async function verifyEC2(opts: {
+ cosePublicKey: COSEPublicKeyEC2;
+ signature: Uint8Array;
+ data: Uint8Array;
+ shaHashOverride?: COSEALG;
+}): Promise<boolean> {
+ const { cosePublicKey, signature, data, shaHashOverride } = opts;
+
+ // Import the public key
+ const alg = cosePublicKey.get(COSEKEYS.alg);
+ const crv = cosePublicKey.get(COSEKEYS.crv);
+ const x = cosePublicKey.get(COSEKEYS.x);
+ const y = cosePublicKey.get(COSEKEYS.y);
+
+ if (!alg) {
+ throw new Error('Public key was missing alg (EC2)');
+ }
+
+ if (!crv) {
+ throw new Error('Public key was missing crv (EC2)');
+ }
+
+ if (!x) {
+ throw new Error('Public key was missing x (EC2)');
+ }
+
+ if (!y) {
+ throw new Error('Public key was missing y (EC2)');
+ }
+
+ let _crv: SubtleCryptoCrv;
+ if (crv === COSECRV.P256) {
+ _crv = 'P-256';
+ } else if (crv === COSECRV.P384) {
+ _crv = 'P-384';
+ } else if (crv === COSECRV.P521) {
+ _crv = 'P-521';
+ } else {
+ throw new Error(`Unexpected COSE crv value of ${crv} (EC2)`);
+ }
+
+ const keyData: JsonWebKey = {
+ kty: 'EC',
+ crv: _crv,
+ x: isoBase64URL.fromBuffer(x),
+ y: isoBase64URL.fromBuffer(y),
+ ext: false,
+ };
+
+ const keyAlgorithm: EcKeyImportParams = {
+ /**
+ * Note to future self: you can't use `mapCoseAlgToWebCryptoKeyAlgName()` here because some
+ * leaf certs from actual devices specified an RSA SHA value for `alg` (e.g. `-257`) which
+ * would then map here to `'RSASSA-PKCS1-v1_5'`. We always want `'ECDSA'` here so we'll
+ * hard-code this.
+ */
+ name: 'ECDSA',
+ namedCurve: _crv,
+ };
+
+ const key = await importKey({
+ keyData,
+ algorithm: keyAlgorithm,
+ });
+
+ // Determine which SHA algorithm to use for signature verification
+ let subtleAlg = mapCoseAlgToWebCryptoAlg(alg);
+ if (shaHashOverride) {
+ subtleAlg = mapCoseAlgToWebCryptoAlg(shaHashOverride);
+ }
+
+ const verifyAlgorithm: EcdsaParams = {
+ name: 'ECDSA',
+ hash: { name: subtleAlg },
+ };
+
+ // The signature is wrapped in ASN.1 structure, so we need to peel it apart
+ const parsedSignature = AsnParser.parse(signature, ECDSASigValue);
+ let rBytes = new Uint8Array(parsedSignature.r);
+ let sBytes = new Uint8Array(parsedSignature.s);
+
+ if (shouldRemoveLeadingZero(rBytes)) {
+ rBytes = rBytes.slice(1);
+ }
+
+ if (shouldRemoveLeadingZero(sBytes)) {
+ sBytes = sBytes.slice(1);
+ }
+
+ const finalSignature = isoUint8Array.concat([rBytes, sBytes]);
+
+ return WebCrypto.subtle.verify(verifyAlgorithm, key, finalSignature, data);
+}
+
+/**
+ * Determine if the DER-specific `00` byte at the start of an ECDSA signature byte sequence
+ * should be removed based on the following logic:
+ *
+ * "If the leading byte is 0x0, and the the high order bit on the second byte is not set to 0,
+ * then remove the leading 0x0 byte"
+ */
+function shouldRemoveLeadingZero(bytes: Uint8Array): boolean {
+ return bytes[0] === 0x0 && (bytes[1] & (1 << 7)) !== 0;
+}
diff --git a/packages/server/src/helpers/iso/isoCrypto/verifyOKP.test.ts b/packages/server/src/helpers/iso/isoCrypto/verifyOKP.test.ts
new file mode 100644
index 0000000..ccdcb00
--- /dev/null
+++ b/packages/server/src/helpers/iso/isoCrypto/verifyOKP.test.ts
@@ -0,0 +1,41 @@
+import { COSEALG, COSECRV, COSEKEYS, COSEKTY, COSEPublicKeyOKP } from '../../cose';
+import { verifyOKP } from './verifyOKP';
+
+test('should verify a signature signed with an Ed25519 public key', async () => {
+ const cosePublicKey: COSEPublicKeyOKP = new Map();
+ cosePublicKey.set(COSEKEYS.kty, COSEKTY.OKP);
+ cosePublicKey.set(COSEKEYS.alg, COSEALG.EdDSA);
+ cosePublicKey.set(COSEKEYS.crv, COSECRV.ED25519);
+ cosePublicKey.set(
+ COSEKEYS.x,
+ new Uint8Array([
+ 108, 223, 182, 117, 49, 249, 221, 119, 212, 171, 158, 83, 213, 25, 47, 92, 202, 112, 29, 93,
+ 29, 69, 89, 204, 4, 252, 110, 56, 25, 181, 250, 242,
+ ]),
+ );
+
+ const data = new Uint8Array([
+ 73, 150, 13, 229, 136, 14, 140, 104, 116, 52, 23, 15, 100, 118, 96, 91, 143, 228, 174, 185, 162,
+ 134, 50, 199, 153, 92, 243, 186, 131, 29, 151, 99, 65, 0, 0, 0, 50, 145, 223, 234, 215, 149,
+ 158, 68, 117, 173, 38, 155, 13, 72, 43, 224, 137, 0, 32, 26, 165, 170, 88, 196, 173, 98, 22, 89,
+ 49, 152, 159, 162, 234, 142, 198, 252, 167, 119, 99, 175, 187, 21, 101, 110, 214, 98, 129, 2,
+ 202, 30, 113, 164, 1, 1, 3, 39, 32, 6, 33, 88, 32, 108, 223, 182, 117, 49, 249, 221, 119, 212,
+ 171, 158, 83, 213, 25, 47, 92, 202, 112, 29, 93, 29, 69, 89, 204, 4, 252, 110, 56, 25, 181, 250,
+ 242, 180, 65, 206, 26, 160, 29, 17, 43, 138, 105, 200, 52, 116, 140, 10, 89, 241, 15, 241, 83,
+ 248, 162, 190, 130, 32, 220, 100, 15, 154, 150, 65, 140,
+ ]);
+ const signature = new Uint8Array([
+ 29, 218, 16, 150, 129, 34, 25, 37, 7, 127, 215, 73, 93, 181, 115, 201, 99, 91, 14, 29, 10, 219,
+ 155, 105, 53, 4, 41, 143, 152, 107, 146, 16, 156, 117, 252, 244, 164, 32, 79, 182, 160, 161,
+ 145, 175, 248, 145, 242, 27, 133, 254, 137, 201, 141, 68, 24, 11, 159, 246, 148, 29, 194, 162,
+ 85, 5,
+ ]);
+
+ const verified = await verifyOKP({
+ cosePublicKey,
+ data,
+ signature,
+ });
+
+ expect(verified).toBe(true);
+});
diff --git a/packages/server/src/helpers/iso/isoCrypto/verifyOKP.ts b/packages/server/src/helpers/iso/isoCrypto/verifyOKP.ts
new file mode 100644
index 0000000..84679b3
--- /dev/null
+++ b/packages/server/src/helpers/iso/isoCrypto/verifyOKP.ts
@@ -0,0 +1,67 @@
+import WebCrypto from '@simplewebauthn/iso-webcrypto';
+
+import { COSEPublicKeyOKP, COSEKEYS, isCOSEAlg, COSECRV } from '../../cose';
+import { isoBase64URL } from '../../index';
+import { SubtleCryptoCrv } from './structs';
+import { importKey } from './importKey';
+
+export async function verifyOKP(opts: {
+ cosePublicKey: COSEPublicKeyOKP;
+ signature: Uint8Array;
+ data: Uint8Array;
+}): Promise<boolean> {
+ const { cosePublicKey, signature, data } = opts;
+
+ const alg = cosePublicKey.get(COSEKEYS.alg);
+ const crv = cosePublicKey.get(COSEKEYS.crv);
+ const x = cosePublicKey.get(COSEKEYS.x);
+
+ if (!alg) {
+ throw new Error('Public key was missing alg (OKP)');
+ }
+
+ if (!isCOSEAlg(alg)) {
+ throw new Error(`Public key had invalid alg ${alg} (OKP)`);
+ }
+
+ if (!crv) {
+ throw new Error('Public key was missing crv (OKP)');
+ }
+
+ if (!x) {
+ throw new Error('Public key was missing x (OKP)');
+ }
+
+ // Pulled key import steps from here:
+ // https://wicg.github.io/webcrypto-secure-curves/#ed25519-operations
+ let _crv: SubtleCryptoCrv;
+ if (crv === COSECRV.ED25519) {
+ _crv = 'Ed25519';
+ } else {
+ throw new Error(`Unexpected COSE crv value of ${crv} (OKP)`);
+ }
+
+ const keyData: JsonWebKey = {
+ kty: 'OKP',
+ crv: _crv,
+ alg: 'EdDSA',
+ x: isoBase64URL.fromBuffer(x),
+ ext: false,
+ };
+
+ const keyAlgorithm: EcKeyImportParams = {
+ name: _crv,
+ namedCurve: _crv,
+ };
+
+ const key = await importKey({
+ keyData,
+ algorithm: keyAlgorithm,
+ });
+
+ const verifyAlgorithm: AlgorithmIdentifier = {
+ name: _crv,
+ };
+
+ return WebCrypto.subtle.verify(verifyAlgorithm, key, signature, data);
+}
diff --git a/packages/server/src/helpers/iso/isoCrypto/verifyRSA.ts b/packages/server/src/helpers/iso/isoCrypto/verifyRSA.ts
new file mode 100644
index 0000000..9d07aab
--- /dev/null
+++ b/packages/server/src/helpers/iso/isoCrypto/verifyRSA.ts
@@ -0,0 +1,104 @@
+import WebCrypto from '@simplewebauthn/iso-webcrypto';
+
+import { COSEALG, COSEKEYS, COSEPublicKeyRSA, isCOSEAlg } from '../../cose';
+import { mapCoseAlgToWebCryptoAlg } from './mapCoseAlgToWebCryptoAlg';
+import { importKey } from './importKey';
+import { isoBase64URL } from '../index';
+import { mapCoseAlgToWebCryptoKeyAlgName } from './mapCoseAlgToWebCryptoKeyAlgName';
+
+/**
+ * Verify a signature using an RSA public key
+ */
+export async function verifyRSA(opts: {
+ cosePublicKey: COSEPublicKeyRSA;
+ signature: Uint8Array;
+ data: Uint8Array;
+ shaHashOverride?: COSEALG;
+}): Promise<boolean> {
+ const { cosePublicKey, signature, data, shaHashOverride } = opts;
+
+ const alg = cosePublicKey.get(COSEKEYS.alg);
+ const n = cosePublicKey.get(COSEKEYS.n);
+ const e = cosePublicKey.get(COSEKEYS.e);
+
+ if (!alg) {
+ throw new Error('Public key was missing alg (RSA)');
+ }
+
+ if (!isCOSEAlg(alg)) {
+ throw new Error(`Public key had invalid alg ${alg} (RSA)`);
+ }
+
+ if (!n) {
+ throw new Error('Public key was missing n (RSA)');
+ }
+
+ if (!e) {
+ throw new Error('Public key was missing e (RSA)');
+ }
+
+ const keyData: JsonWebKey = {
+ kty: 'RSA',
+ alg: '',
+ n: isoBase64URL.fromBuffer(n),
+ e: isoBase64URL.fromBuffer(e),
+ ext: false,
+ };
+
+ const keyAlgorithm = {
+ name: mapCoseAlgToWebCryptoKeyAlgName(alg),
+ hash: { name: mapCoseAlgToWebCryptoAlg(alg) },
+ };
+
+ const verifyAlgorithm: AlgorithmIdentifier | RsaPssParams = {
+ name: mapCoseAlgToWebCryptoKeyAlgName(alg),
+ };
+
+ if (shaHashOverride) {
+ keyAlgorithm.hash.name = mapCoseAlgToWebCryptoAlg(shaHashOverride);
+ }
+
+ if (keyAlgorithm.name === 'RSASSA-PKCS1-v1_5') {
+ if (keyAlgorithm.hash.name === 'SHA-256') {
+ keyData.alg = 'RS256';
+ } else if (keyAlgorithm.hash.name === 'SHA-384') {
+ keyData.alg = 'RS384';
+ } else if (keyAlgorithm.hash.name === 'SHA-512') {
+ keyData.alg = 'RS512';
+ } else if (keyAlgorithm.hash.name === 'SHA-1') {
+ keyData.alg = 'RS1';
+ }
+ } else if (keyAlgorithm.name === 'RSA-PSS') {
+ /**
+ * salt length. The default value is 20 but the convention is to use hLen, the length of the
+ * output of the hash function in bytes. A salt length of zero is permitted and will result in
+ * a deterministic signature value. The actual salt length used can be determined from the
+ * signature value.
+ *
+ * From https://www.cryptosys.net/pki/manpki/pki_rsaschemes.html
+ */
+ let saltLength = 0;
+
+ if (keyAlgorithm.hash.name === 'SHA-256') {
+ keyData.alg = 'PS256';
+ saltLength = 32; // 256 bits => 32 bytes
+ } else if (keyAlgorithm.hash.name === 'SHA-384') {
+ keyData.alg = 'PS384';
+ saltLength = 48; // 384 bits => 48 bytes
+ } else if (keyAlgorithm.hash.name === 'SHA-512') {
+ keyData.alg = 'PS512';
+ saltLength = 64; // 512 bits => 64 bytes
+ }
+
+ (verifyAlgorithm as RsaPssParams).saltLength = saltLength;
+ } else {
+ throw new Error(`Unexpected RSA key algorithm ${alg} (${keyAlgorithm.name})`);
+ }
+
+ const key = await importKey({
+ keyData,
+ algorithm: keyAlgorithm,
+ });
+
+ return WebCrypto.subtle.verify(verifyAlgorithm, key, signature, data);
+}
diff --git a/packages/server/src/helpers/iso/isoUint8Array.ts b/packages/server/src/helpers/iso/isoUint8Array.ts
new file mode 100644
index 0000000..7dc163e
--- /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/matchExpectedRPID.ts b/packages/server/src/helpers/matchExpectedRPID.ts
new file mode 100644
index 0000000..be49fc2
--- /dev/null
+++ b/packages/server/src/helpers/matchExpectedRPID.ts
@@ -0,0 +1,44 @@
+import { toHash } from './toHash';
+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<void> {
+ 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();
+ }
+ });
+ });
+ }),
+ );
+ } catch (err) {
+ const _err = err as Error;
+
+ // This means no matches were found
+ if (_err.name === 'AggregateError') {
+ throw new UnexpectedRPIDHash();
+ }
+
+ // An unexpected error occurred
+ throw err;
+ }
+}
+
+class UnexpectedRPIDHash extends Error {
+ constructor() {
+ const message = 'Unexpected RP ID hash';
+ super(message);
+ this.name = 'UnexpectedRPIDHash';
+ }
+}
diff --git a/packages/server/src/helpers/parseAuthenticatorData.test.ts b/packages/server/src/helpers/parseAuthenticatorData.test.ts
index a706718..1db4bfe 100644
--- a/packages/server/src/helpers/parseAuthenticatorData.test.ts
+++ b/packages/server/src/helpers/parseAuthenticatorData.test.ts
@@ -1,12 +1,12 @@
-import cbor from 'cbor';
-
import { parseAuthenticatorData } from './parseAuthenticatorData';
+import { isoBase64URL } from './iso';
// Grabbed this from a Conformance test, contains attestation data
-const authDataWithAT = Buffer.from(
+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(
'SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2OBAAAAjaFxZXhhbXBsZS5leHRlbnNpb254dlRoaXMgaXMgYW4gZXhhbXBsZSBleHRlbnNpb24hIElmIHlvdSByZWFkIHRoaXMgbWVzc2FnZSwgeW91IHByb2JhYmx5IHN1Y2Nlc3NmdWxseSBwYXNzaW5nIGNvbmZvcm1hbmNlIHRlc3RzLiBHb29kIGpvYiE=',
@@ -29,13 +29,16 @@ 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(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(aaguid?.toString('base64')).toEqual('yHzdl1bBSbieJMs2NlTzUA==');
+ expect(isoBase64URL.fromBuffer(aaguid!, 'base64')).toEqual('yHzdl1bBSbieJMs2NlTzUA==');
+ expect(counter).toEqual(37);
});
test('should parse extension data', () => {
diff --git a/packages/server/src/helpers/parseAuthenticatorData.ts b/packages/server/src/helpers/parseAuthenticatorData.ts
index c2128e0..4e3bb0b 100644
--- a/packages/server/src/helpers/parseAuthenticatorData.ts
+++ b/packages/server/src/helpers/parseAuthenticatorData.ts
@@ -1,14 +1,14 @@
-import cbor from 'cbor';
-import { decodeCborFirst } from './decodeCbor';
import {
decodeAuthenticatorExtensions,
AuthenticationExtensionsAuthenticatorOutputs,
} from './decodeAuthenticatorExtensions';
+import { isoCBOR, isoUint8Array } from './iso';
+import { COSEPublicKey } from './cose';
/**
* Make sense of the authData buffer contained in an Attestation
*/
-export function parseAuthenticatorData(authData: Buffer): ParsedAuthenticatorData {
+export function parseAuthenticatorData(authData: Uint8Array): ParsedAuthenticatorData {
if (authData.byteLength < 37) {
throw new Error(
`Authenticator data was ${authData.byteLength} bytes, expected at least 37 bytes`,
@@ -16,6 +16,7 @@ export function parseAuthenticatorData(authData: Buffer): ParsedAuthenticatorDat
}
let pointer = 0;
+ const dataView = isoUint8Array.toDataView(authData);
const rpIdHash = authData.slice(pointer, (pointer += 32));
@@ -34,37 +35,38 @@ export function parseAuthenticatorData(authData: Buffer): ParsedAuthenticatorDat
flagsInt,
};
- const counterBuf = authData.slice(pointer, (pointer += 4));
- const counter = counterBuf.readUInt32BE(0);
+ const counterBuf = authData.slice(pointer, pointer + 4);
+ const counter = dataView.getUint32(pointer, false);
+ pointer += 4;
- let aaguid: Buffer | undefined = undefined;
- let credentialID: Buffer | undefined = undefined;
- let credentialPublicKey: Buffer | undefined = undefined;
+ let aaguid: Uint8Array | undefined = undefined;
+ let credentialID: Uint8Array | undefined = undefined;
+ let credentialPublicKey: Uint8Array | undefined = undefined;
if (flags.at) {
aaguid = authData.slice(pointer, (pointer += 16));
- const credIDLenBuf = authData.slice(pointer, (pointer += 2));
- const credIDLen = credIDLenBuf.readUInt16BE(0);
+ const credIDLen = dataView.getUint16(pointer);
+ pointer += 2;
credentialID = authData.slice(pointer, (pointer += credIDLen));
// Decode the next CBOR item in the buffer, then re-encode it back to a Buffer
- const firstDecoded = decodeCborFirst(authData.slice(pointer));
- const firstEncoded = Buffer.from(cbor.encode(firstDecoded) as ArrayBuffer);
+ const firstDecoded = isoCBOR.decodeFirst<COSEPublicKey>(authData.slice(pointer));
+ const firstEncoded = Uint8Array.from(isoCBOR.encode(firstDecoded));
+
credentialPublicKey = firstEncoded;
pointer += firstEncoded.byteLength;
}
let extensionsData: AuthenticationExtensionsAuthenticatorOutputs | undefined = undefined;
- let extensionsDataBuffer: Buffer | undefined = undefined;
+ let extensionsDataBuffer: Uint8Array | undefined = undefined;
if (flags.ed) {
- const firstDecoded = decodeCborFirst(authData.slice(pointer));
- const firstEncoded = Buffer.from(cbor.encode(firstDecoded) as ArrayBuffer);
- extensionsDataBuffer = firstEncoded;
+ const firstDecoded = isoCBOR.decodeFirst(authData.slice(pointer));
+ extensionsDataBuffer = Uint8Array.from(isoCBOR.encode(firstDecoded));
extensionsData = decodeAuthenticatorExtensions(extensionsDataBuffer);
- pointer += firstEncoded.byteLength;
+ pointer += extensionsDataBuffer.byteLength;
}
// Pointer should be at the end of the authenticator data, otherwise too much data was sent
@@ -87,8 +89,8 @@ export function parseAuthenticatorData(authData: Buffer): ParsedAuthenticatorDat
}
export type ParsedAuthenticatorData = {
- rpIdHash: Buffer;
- flagsBuf: Buffer;
+ rpIdHash: Uint8Array;
+ flagsBuf: Uint8Array;
flags: {
up: boolean;
uv: boolean;
@@ -99,10 +101,10 @@ export type ParsedAuthenticatorData = {
flagsInt: number;
};
counter: number;
- counterBuf: Buffer;
- aaguid?: Buffer;
- credentialID?: Buffer;
- credentialPublicKey?: Buffer;
+ counterBuf: Uint8Array;
+ aaguid?: Uint8Array;
+ credentialID?: Uint8Array;
+ credentialPublicKey?: Uint8Array;
extensionsData?: AuthenticationExtensionsAuthenticatorOutputs;
- extensionsDataBuffer?: Buffer;
+ extensionsDataBuffer?: Uint8Array;
};
diff --git a/packages/server/src/helpers/toHash.test.ts b/packages/server/src/helpers/toHash.test.ts
index df0c50d..8893c51 100644
--- a/packages/server/src/helpers/toHash.test.ts
+++ b/packages/server/src/helpers/toHash.test.ts
@@ -1,11 +1,11 @@
import { toHash } from './toHash';
-test('should return a buffer of at 32 bytes for input string', () => {
- const hash = toHash('string');
+test('should return a buffer of at 32 bytes for input string', async () => {
+ const hash = await toHash('string');
expect(hash.byteLength).toEqual(32);
});
-test('should return a buffer of at 32 bytes for input Buffer', () => {
- const hash = toHash(Buffer.alloc(10));
+test('should return a buffer of at 32 bytes for input Buffer', async () => {
+ const hash = await toHash(Buffer.alloc(10));
expect(hash.byteLength).toEqual(32);
});
diff --git a/packages/server/src/helpers/toHash.ts b/packages/server/src/helpers/toHash.ts
index 007b1ab..90edd4e 100644
--- a/packages/server/src/helpers/toHash.ts
+++ b/packages/server/src/helpers/toHash.ts
@@ -1,10 +1,19 @@
-import crypto from 'crypto';
+import { COSEALG } from './cose';
+import { isoUint8Array, isoCrypto } from './iso';
/**
- * Returns hash digest of the given data using the given algorithm.
- * @param data Data to hash
- * @return The hash
+ * Returns hash digest of the given data, using the given algorithm when provided. Defaults to using
+ * SHA-256.
*/
-export function toHash(data: Buffer | string, algo = 'SHA256'): Buffer {
- return crypto.createHash(algo).update(data).digest();
+export async function toHash(
+ data: Uint8Array | string,
+ algorithm: COSEALG = -7,
+): Promise<Uint8Array> {
+ if (typeof data === 'string') {
+ data = isoUint8Array.fromUTF8String(data);
+ }
+
+ const digest = isoCrypto.digest(data, algorithm);
+
+ return digest;
}
diff --git a/packages/server/src/helpers/validateCertificatePath.ts b/packages/server/src/helpers/validateCertificatePath.ts
index d98b16b..ed82eac 100644
--- a/packages/server/src/helpers/validateCertificatePath.ts
+++ b/packages/server/src/helpers/validateCertificatePath.ts
@@ -117,6 +117,7 @@ async function _validatePath(certificates: string[]): Promise<boolean> {
const Signature = new crypto.Signature({ alg });
Signature.init(issuerPem);
+ // TODO: `updateHex()` takes approximately two seconds per execution, can we improve this?
Signature.updateHex(subjectCertStruct);
if (!Signature.verify(signatureHex)) {
diff --git a/packages/server/src/helpers/verifySignature.ts b/packages/server/src/helpers/verifySignature.ts
index de8a56e..ff4e73b 100644
--- a/packages/server/src/helpers/verifySignature.ts
+++ b/packages/server/src/helpers/verifySignature.ts
@@ -1,102 +1,40 @@
-import crypto from 'crypto';
-import cbor from 'cbor';
-import { verify as ed25519Verify } from '@noble/ed25519';
-
-import { COSEKEYS, COSEKTY } from './convertCOSEtoPKCS';
-import { convertCertBufferToPEM } from './convertCertBufferToPEM';
-import { convertPublicKeyToPEM } from './convertPublicKeyToPEM';
-
-type VerifySignatureOptsLeafCert = {
- signature: Buffer;
- signatureBase: Buffer;
- leafCert: Buffer;
- hashAlgorithm?: string;
-};
-
-type VerifySignatureOptsCredentialPublicKey = {
- signature: Buffer;
- signatureBase: Buffer;
- credentialPublicKey: Buffer;
- hashAlgorithm?: string;
-};
+import { COSEALG, COSEPublicKey } from './cose';
+import { isoCrypto } from './iso';
+import { decodeCredentialPublicKey } from './decodeCredentialPublicKey';
+import { convertX509PublicKeyToCOSE } from './convertX509PublicKeyToCOSE';
/**
* Verify an authenticator's signature
- *
- * @param signature attStmt.sig
- * @param signatureBase Output from Buffer.concat()
- * @param publicKey Authenticator's public key as a PEM certificate
- * @param algo Which algorithm to use to verify the signature (default: `'sha256'`)
*/
-export async function verifySignature(
- opts: VerifySignatureOptsLeafCert | VerifySignatureOptsCredentialPublicKey,
-): Promise<boolean> {
- const { signature, signatureBase, hashAlgorithm = 'sha256' } = opts;
- const _isLeafcertOpts = isLeafCertOpts(opts);
- const _isCredPubKeyOpts = isCredPubKeyOpts(opts);
-
- if (!_isLeafcertOpts && !_isCredPubKeyOpts) {
+export async function verifySignature(opts: {
+ signature: Uint8Array;
+ data: Uint8Array;
+ credentialPublicKey?: Uint8Array;
+ leafCertificate?: Uint8Array;
+ attestationHashAlgorithm?: COSEALG;
+}): Promise<boolean> {
+ const { signature, data, credentialPublicKey, leafCertificate, attestationHashAlgorithm } = opts;
+
+ if (!leafCertificate && !credentialPublicKey) {
throw new Error('Must declare either "leafCert" or "credentialPublicKey"');
}
- if (_isLeafcertOpts && _isCredPubKeyOpts) {
+ if (leafCertificate && credentialPublicKey) {
throw new Error('Must not declare both "leafCert" and "credentialPublicKey"');
}
- let publicKeyPEM = '';
-
- if (_isCredPubKeyOpts) {
- const { credentialPublicKey } = opts;
-
- // Decode CBOR to COSE
- let struct;
- try {
- struct = cbor.decodeAllSync(credentialPublicKey)[0];
- } catch (err) {
- const _err = err as Error;
- throw new Error(`Error decoding public key while converting to PEM: ${_err.message}`);
- }
-
- const kty = struct.get(COSEKEYS.kty);
-
- if (!kty) {
- throw new Error('Public key was missing kty');
- }
+ let cosePublicKey: COSEPublicKey = new Map();
- // Check key type
- if (kty === COSEKTY.OKP) {
- // Verify Ed25519 slightly differently
- const x = struct.get(COSEKEYS.x);
-
- if (!x) {
- throw new Error('Public key was missing x (OKP)');
- }
-
- return ed25519Verify(signature, signatureBase, x);
- } else {
- // Convert pubKey to PEM for ECC and RSA
- publicKeyPEM = convertPublicKeyToPEM(credentialPublicKey);
- }
- }
-
- if (_isLeafcertOpts) {
- const { leafCert } = opts;
- publicKeyPEM = convertCertBufferToPEM(leafCert);
+ if (credentialPublicKey) {
+ cosePublicKey = decodeCredentialPublicKey(credentialPublicKey);
+ } else if (leafCertificate) {
+ cosePublicKey = convertX509PublicKeyToCOSE(leafCertificate);
}
- return crypto.createVerify(hashAlgorithm).update(signatureBase).verify(publicKeyPEM, signature);
-}
-
-function isLeafCertOpts(
- opts: VerifySignatureOptsLeafCert | VerifySignatureOptsCredentialPublicKey,
-): opts is VerifySignatureOptsLeafCert {
- return Object.keys(opts as VerifySignatureOptsLeafCert).indexOf('leafCert') >= 0;
-}
-
-function isCredPubKeyOpts(
- opts: VerifySignatureOptsLeafCert | VerifySignatureOptsCredentialPublicKey,
-): opts is VerifySignatureOptsCredentialPublicKey {
- return (
- Object.keys(opts as VerifySignatureOptsCredentialPublicKey).indexOf('credentialPublicKey') >= 0
- );
+ return isoCrypto.verify({
+ cosePublicKey,
+ signature,
+ data,
+ shaHashOverride: attestationHashAlgorithm,
+ });
}
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 254e14e..beb2501 100644
--- a/packages/server/src/metadata/parseJWT.ts
+++ b/packages/server/src/metadata/parseJWT.ts
@@ -1,4 +1,4 @@
-import base64url from 'base64url';
+import { isoBase64URL } from '../helpers/iso';
/**
* Process a JWT into Javascript-friendly data structures
@@ -6,8 +6,8 @@ import base64url from 'base64url';
export function parseJWT<T1, T2>(jwt: string): [T1, T2, string] {
const parts = jwt.split('.');
return [
- JSON.parse(base64url.decode(parts[0])) as T1,
- JSON.parse(base64url.decode(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 b48ef2e..c76fb1d 100644
--- a/packages/server/src/metadata/verifyAttestationWithMetadata.test.ts
+++ b/packages/server/src/metadata/verifyAttestationWithMetadata.test.ts
@@ -1,7 +1,6 @@
-import base64url from 'base64url';
-
import { verifyAttestationWithMetadata } from './verifyAttestationWithMetadata';
import { MetadataStatement } from '../metadata/mdsTypes';
+import { isoBase64URL } from '../helpers/iso';
test('should verify attestation with metadata (android-safetynet)', async () => {
const metadataStatementJSONSafetyNet: MetadataStatement = {
@@ -49,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,
});
@@ -58,48 +57,49 @@ 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,
- credentialPublicKey: base64url.toBuffer(credentialPublicKey),
+ credentialPublicKey: isoBase64URL.toBuffer(credentialPublicKey),
x5c,
});
@@ -108,55 +108,60 @@ 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,
- credentialPublicKey: base64url.toBuffer(credentialPublicKey),
+ credentialPublicKey: isoBase64URL.toBuffer(credentialPublicKey),
x5c,
});
diff --git a/packages/server/src/metadata/verifyAttestationWithMetadata.ts b/packages/server/src/metadata/verifyAttestationWithMetadata.ts
index 5193135..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 { COSEKEYS, COSEKTY } from '../helpers/convertCOSEtoPKCS';
+import {
+ COSEALG,
+ COSECRV,
+ COSEKEYS,
+ COSEKTY,
+ COSEPublicKeyEC2,
+ isCOSEPublicKeyEC2,
+} from '../helpers/cose';
/**
* Match properties of the authenticator's attestation statement against expected values as
@@ -17,15 +24,11 @@ export async function verifyAttestationWithMetadata({
attestationStatementAlg,
}: {
statement: MetadataStatement;
- credentialPublicKey: Buffer;
- x5c: Buffer[] | Base64URLString[];
+ credentialPublicKey: Uint8Array;
+ x5c: Uint8Array[] | Base64URLString[];
attestationStatementAlg?: number;
}): Promise<boolean> {
- 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<COSEInfo> = new Set();
@@ -41,14 +44,28 @@ export async function verifyAttestationWithMetadata({
// Extract the public key's COSE info for comparison
const decodedPublicKey = decodeCredentialPublicKey(credentialPublicKey);
+
+ const kty = decodedPublicKey.get(COSEKEYS.kty);
+ const alg = decodedPublicKey.get(COSEKEYS.alg);
+
+ if (!kty) {
+ throw new Error('Credential public key was missing kty');
+ }
+
+ if (!alg) {
+ throw new Error('Credential public key was missing alg');
+ }
+
+ if (!kty) {
+ throw new Error('Credential public key was missing kty');
+ }
+
// Assume everything is a number because these values should be
- const publicKeyCOSEInfo: COSEInfo = {
- kty: decodedPublicKey.get(COSEKEYS.kty) as number,
- alg: decodedPublicKey.get(COSEKEYS.alg) as number,
- crv: decodedPublicKey.get(COSEKEYS.crv) as number,
- };
- if (!publicKeyCOSEInfo.crv) {
- delete publicKeyCOSEInfo.crv;
+ const publicKeyCOSEInfo: COSEInfo = { kty, alg };
+
+ if (isCOSEPublicKeyEC2(decodedPublicKey)) {
+ const crv = decodedPublicKey.get(COSEKEYS.crv);
+ publicKeyCOSEInfo.crv = crv;
}
/**
@@ -90,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, '');
/**
@@ -126,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;
}
@@ -148,9 +163,9 @@ export async function verifyAttestationWithMetadata({
}
type COSEInfo = {
- kty: number;
- alg: number;
- crv?: number;
+ kty: COSEKTY;
+ alg: COSEALG;
+ crv?: COSECRV;
};
/**
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/generateRegistrationOptions.ts b/packages/server/src/registration/generateRegistrationOptions.ts
index 20b3283..83bfb3c 100644
--- a/packages/server/src/registration/generateRegistrationOptions.ts
+++ b/packages/server/src/registration/generateRegistrationOptions.ts
@@ -7,16 +7,16 @@ import type {
PublicKeyCredentialDescriptorFuture,
PublicKeyCredentialParameters,
} from '@simplewebauthn/typescript-types';
-import base64url from 'base64url';
import { generateChallenge } from '../helpers/generateChallenge';
+import { isoBase64URL, isoUint8Array } from '../helpers/iso';
export type GenerateRegistrationOptionsOpts = {
rpName: string;
rpID: string;
userID: string;
userName: string;
- challenge?: string | Buffer;
+ challenge?: string | Uint8Array;
userDisplayName?: string;
timeout?: number;
attestationType?: AttestationConveyancePreference;
@@ -151,8 +151,16 @@ export function generateRegistrationOptions(
authenticatorSelection.requireResidentKey = authenticatorSelection.residentKey === 'required';
}
+ /**
+ * Preserve ability to specify `string` values for challenges
+ */
+ let _challenge = challenge;
+ if (typeof _challenge === 'string') {
+ _challenge = isoUint8Array.fromASCIIString(_challenge);
+ }
+
return {
- challenge: base64url.encode(challenge),
+ challenge: isoBase64URL.fromBuffer(_challenge),
rp: {
name: rpName,
id: rpID,
@@ -167,7 +175,7 @@ export function generateRegistrationOptions(
attestation: attestationType,
excludeCredentials: excludeCredentials.map(cred => ({
...cred,
- id: base64url.encode(cred.id as Buffer),
+ id: isoBase64URL.fromBuffer(cred.id as Uint8Array),
})),
authenticatorSelection,
extensions,
diff --git a/packages/server/src/registration/verifications/tpm/constants.ts b/packages/server/src/registration/verifications/tpm/constants.ts
index c470d5b..324f013 100644
--- a/packages/server/src/registration/verifications/tpm/constants.ts
+++ b/packages/server/src/registration/verifications/tpm/constants.ts
@@ -187,6 +187,6 @@ export const TPM_ECC_CURVE_COSE_CRV_MAP: { [key: string]: number } = {
TPM_ECC_NIST_P256: 1, // p256
TPM_ECC_NIST_P384: 2, // p384
TPM_ECC_NIST_P521: 3, // p521
- TPM_ECC_BN_P256: 1, // p256
- TPM_ECC_SM2_P256: 1, // p256
+ TPM_ECC_BN_P256: 1, // p256
+ TPM_ECC_SM2_P256: 1, // p256
};
diff --git a/packages/server/src/registration/verifications/tpm/parseCertInfo.ts b/packages/server/src/registration/verifications/tpm/parseCertInfo.ts
index 6e3d4c3..bf28418 100644
--- a/packages/server/src/registration/verifications/tpm/parseCertInfo.ts
+++ b/packages/server/src/registration/verifications/tpm/parseCertInfo.ts
@@ -1,48 +1,58 @@
import { TPM_ST, TPM_ALG } from './constants';
+import { isoUint8Array } from '../../../helpers/iso';
/**
* Cut up a TPM attestation's certInfo into intelligible chunks
*/
-export function parseCertInfo(certInfo: Buffer): ParsedCertInfo {
+export function parseCertInfo(certInfo: Uint8Array): ParsedCertInfo {
let pointer = 0;
+ const dataView = isoUint8Array.toDataView(certInfo);
// Get a magic constant
- const magic = certInfo.slice(pointer, (pointer += 4)).readUInt32BE(0);
+ const magic = dataView.getUint32(pointer);
+ pointer += 4;
// Determine the algorithm used for attestation
- const typeBuffer = certInfo.slice(pointer, (pointer += 2));
- const type = TPM_ST[typeBuffer.readUInt16BE(0)];
+ const typeBuffer = dataView.getUint16(pointer);
+ pointer += 2;
+ const type = TPM_ST[typeBuffer];
// The name of a parent entity, can be ignored
- const qualifiedSignerLength = certInfo.slice(pointer, (pointer += 2)).readUInt16BE(0);
+ const qualifiedSignerLength = dataView.getUint16(pointer);
+ pointer += 2;
const qualifiedSigner = certInfo.slice(pointer, (pointer += qualifiedSignerLength));
// Get the expected hash of `attsToBeSigned`
- const extraDataLength = certInfo.slice(pointer, (pointer += 2)).readUInt16BE(0);
+ const extraDataLength = dataView.getUint16(pointer);
+ pointer += 2;
const extraData = certInfo.slice(pointer, (pointer += extraDataLength));
// Information about the TPM device's internal clock, can be ignored
- const clockInfoBuffer = certInfo.slice(pointer, (pointer += 17));
- const clockInfo = {
- clock: clockInfoBuffer.slice(0, 8),
- resetCount: clockInfoBuffer.slice(8, 12).readUInt32BE(0),
- restartCount: clockInfoBuffer.slice(12, 16).readUInt32BE(0),
- safe: !!clockInfoBuffer[16],
- };
+ const clock = certInfo.slice(pointer, (pointer += 8));
+ const resetCount = dataView.getUint32(pointer);
+ pointer += 4;
+ const restartCount = dataView.getUint32(pointer);
+ pointer += 4;
+ const safe = !!certInfo.slice(pointer, (pointer += 1));
+
+ const clockInfo = { clock, resetCount, restartCount, safe };
// TPM device firmware version
const firmwareVersion = certInfo.slice(pointer, (pointer += 8));
// Attested Name
- const attestedNameLength = certInfo.slice(pointer, (pointer += 2)).readUInt16BE(0);
+ const attestedNameLength = dataView.getUint16(pointer);
+ pointer += 2;
const attestedName = certInfo.slice(pointer, (pointer += attestedNameLength));
+ const attestedNameDataView = isoUint8Array.toDataView(attestedName);
// Attested qualified name, can be ignored
- const qualifiedNameLength = certInfo.slice(pointer, (pointer += 2)).readUInt16BE(0);
+ const qualifiedNameLength = dataView.getUint16(pointer);
+ pointer += 2;
const qualifiedName = certInfo.slice(pointer, (pointer += qualifiedNameLength));
const attested = {
- nameAlg: TPM_ALG[attestedName.slice(0, 2).readUInt16BE(0)],
+ nameAlg: TPM_ALG[attestedNameDataView.getUint16(0)],
nameAlgBuffer: attestedName.slice(0, 2),
name: attestedName,
qualifiedName,
@@ -62,19 +72,19 @@ export function parseCertInfo(certInfo: Buffer): ParsedCertInfo {
type ParsedCertInfo = {
magic: number;
type: string;
- qualifiedSigner: Buffer;
- extraData: Buffer;
+ qualifiedSigner: Uint8Array;
+ extraData: Uint8Array;
clockInfo: {
- clock: Buffer;
+ clock: Uint8Array;
resetCount: number;
restartCount: number;
safe: boolean;
};
- firmwareVersion: Buffer;
+ firmwareVersion: Uint8Array;
attested: {
nameAlg: string;
- nameAlgBuffer: Buffer;
- name: Buffer;
- qualifiedName: Buffer;
+ nameAlgBuffer: Uint8Array;
+ name: Uint8Array;
+ qualifiedName: Uint8Array;
};
};
diff --git a/packages/server/src/registration/verifications/tpm/parsePubArea.ts b/packages/server/src/registration/verifications/tpm/parsePubArea.ts
index ca61ddc..514828c 100644
--- a/packages/server/src/registration/verifications/tpm/parsePubArea.ts
+++ b/packages/server/src/registration/verifications/tpm/parsePubArea.ts
@@ -1,4 +1,5 @@
import { TPM_ALG, TPM_ECC_CURVE } from './constants';
+import { isoUint8Array } from '../../../helpers/iso';
/**
* Break apart a TPM attestation's pubArea buffer
@@ -6,17 +7,20 @@ import { TPM_ALG, TPM_ECC_CURVE } from './constants';
* See 12.2.4 TPMT_PUBLIC here:
* https://trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-00.96-130315.pdf
*/
-export function parsePubArea(pubArea: Buffer): ParsedPubArea {
+export function parsePubArea(pubArea: Uint8Array): ParsedPubArea {
let pointer = 0;
+ const dataView = isoUint8Array.toDataView(pubArea);
- const typeBuffer = pubArea.slice(pointer, (pointer += 2));
- const type = TPM_ALG[typeBuffer.readUInt16BE(0)];
+ const type = TPM_ALG[dataView.getUint16(pointer)];
+ pointer += 2;
- const nameAlgBuffer = pubArea.slice(pointer, (pointer += 2));
- const nameAlg = TPM_ALG[nameAlgBuffer.readUInt16BE(0)];
+ const nameAlg = TPM_ALG[dataView.getUint16(pointer)];
+ pointer += 2;
// Get some authenticator attributes(?)
- const objectAttributesInt = pubArea.slice(pointer, (pointer += 4)).readUInt32BE(0);
+ // const objectAttributesInt = pubArea.slice(pointer, (pointer += 4)).readUInt32BE(0);
+ const objectAttributesInt = dataView.getUint32(pointer);
+ pointer += 4;
const objectAttributes = {
fixedTPM: !!(objectAttributesInt & 1),
stClear: !!(objectAttributesInt & 2),
@@ -32,52 +36,70 @@ export function parsePubArea(pubArea: Buffer): ParsedPubArea {
};
// Slice out the authPolicy of dynamic length
- const authPolicyLength = pubArea.slice(pointer, (pointer += 2)).readUInt16BE(0);
+ const authPolicyLength = dataView.getUint16(pointer);
+ pointer += 2;
const authPolicy = pubArea.slice(pointer, (pointer += authPolicyLength));
// Extract additional curve params according to type
const parameters: { rsa?: RSAParameters; ecc?: ECCParameters } = {};
- let unique = Buffer.from([]);
+ let unique = Uint8Array.from([]);
if (type === 'TPM_ALG_RSA') {
- const rsaBuffer = pubArea.slice(pointer, (pointer += 10));
+ const symmetric = TPM_ALG[dataView.getUint16(pointer)];
+ pointer += 2;
- parameters.rsa = {
- symmetric: TPM_ALG[rsaBuffer.slice(0, 2).readUInt16BE(0)],
- scheme: TPM_ALG[rsaBuffer.slice(2, 4).readUInt16BE(0)],
- keyBits: rsaBuffer.slice(4, 6).readUInt16BE(0),
- exponent: rsaBuffer.slice(6, 10).readUInt32BE(0),
- };
+ const scheme = TPM_ALG[dataView.getUint16(pointer)];
+ pointer += 2;
+
+ const keyBits = dataView.getUint16(pointer);
+ pointer += 2;
+
+ const exponent = dataView.getUint32(pointer);
+ pointer += 4;
+
+ parameters.rsa = { symmetric, scheme, keyBits, exponent };
/**
* See 11.2.4.5 TPM2B_PUBLIC_KEY_RSA here:
* https://trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-00.96-130315.pdf
*/
- const uniqueLength = pubArea.slice(pointer, (pointer += 2)).readUInt16BE(0);
+ // const uniqueLength = pubArea.slice(pointer, (pointer += 2)).readUInt16BE(0);
+ const uniqueLength = dataView.getUint16(pointer);
+ pointer += 2;
unique = pubArea.slice(pointer, (pointer += uniqueLength));
} else if (type === 'TPM_ALG_ECC') {
- const eccBuffer = pubArea.slice(pointer, (pointer += 8));
+ const symmetric = TPM_ALG[dataView.getUint16(pointer)];
+ pointer += 2;
+
+ const scheme = TPM_ALG[dataView.getUint16(pointer)];
+ pointer += 2;
+
+ const curveID = TPM_ECC_CURVE[dataView.getUint16(pointer)];
+ pointer += 2;
- parameters.ecc = {
- symmetric: TPM_ALG[eccBuffer.slice(0, 2).readUInt16BE(0)],
- scheme: TPM_ALG[eccBuffer.slice(2, 4).readUInt16BE(0)],
- curveID: TPM_ECC_CURVE[eccBuffer.slice(4, 6).readUInt16BE(0)],
- kdf: TPM_ALG[eccBuffer.slice(6, 8).readUInt16BE(0)],
- };
+ const kdf = TPM_ALG[dataView.getUint16(pointer)];
+ pointer += 2;
+
+ parameters.ecc = { symmetric, scheme, curveID, kdf };
/**
* See 11.2.5.1 TPM2B_ECC_PARAMETER here:
* https://trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-00.96-130315.pdf
*/
// Retrieve X
- const uniqueXLength = pubArea.slice(pointer, (pointer += 2)).readUInt16BE(0);
+ const uniqueXLength = dataView.getUint16(pointer);
+ pointer += 2;
+
const uniqueX = pubArea.slice(pointer, (pointer += uniqueXLength));
+
// Retrieve Y
- const uniqueYLength = pubArea.slice(pointer, (pointer += 2)).readUInt16BE(0);
+ const uniqueYLength = dataView.getUint16(pointer);
+ pointer += 2;
+
const uniqueY = pubArea.slice(pointer, (pointer += uniqueYLength));
- unique = Buffer.concat([uniqueX, uniqueY]);
+ unique = isoUint8Array.concat([uniqueX, uniqueY]);
} else {
throw new Error(`Unexpected type "${type}" (TPM)`);
}
@@ -108,12 +130,12 @@ type ParsedPubArea = {
decrypt: boolean;
signOrEncrypt: boolean;
};
- authPolicy: Buffer;
+ authPolicy: Uint8Array;
parameters: {
rsa?: RSAParameters;
ecc?: ECCParameters;
};
- unique: Buffer;
+ unique: Uint8Array;
};
type RSAParameters = {
diff --git a/packages/server/src/registration/verifications/tpm/verifyAttestationTPM.test.ts b/packages/server/src/registration/verifications/tpm/verifyAttestationTPM.test.ts
index 5652640..0625b9e 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 { isoBase64URL } from '../../../helpers/iso';
import { verifyRegistrationResponse } from '../../verifyRegistrationResponse';
-import base64url from 'base64url';
test('should verify TPM response', async () => {
const expectedChallenge = 'a4de0d36-057d-4e9d-831a-2c578fa89170';
- jest.spyOn(base64url, 'encode').mockReturnValueOnce(expectedChallenge);
+ jest.spyOn(isoBase64URL, 'fromString').mockReturnValueOnce(expectedChallenge);
const verification = await verifyRegistrationResponse({
credential: {
id: 'SErwRhxIzjPowcnM3e-D-u89EQXLUe1NYewpshd7Mc0',
@@ -16,6 +16,7 @@ test('should verify TPM response', async () => {
},
type: 'public-key',
clientExtensionResults: {},
+ authenticatorAttachment: '',
},
expectedChallenge,
expectedOrigin: 'https://dev.dontneeda.pw',
@@ -33,7 +34,7 @@ test('should verify SHA1 TPM response', async () => {
*/
const expectedChallenge =
'9JyUfJkg8PqoKZuD7FHzOE9dbyculC9urGTpGqBnEwnhKmni4rGRXxm3-ZBHK8x6riJQqIpC8qEa-T0qIFTKTQ';
- jest.spyOn(base64url, 'encode').mockReturnValueOnce(expectedChallenge);
+ jest.spyOn(isoBase64URL, 'fromString').mockReturnValueOnce(expectedChallenge);
const verification = await verifyRegistrationResponse({
credential: {
rawId: 'UJDoUJoGiDQF_EEZ3G_z9Lfq16_KFaXtMTjwTUrrRlc',
@@ -46,6 +47,7 @@ test('should verify SHA1 TPM response', async () => {
},
type: 'public-key',
clientExtensionResults: {},
+ authenticatorAttachment: '',
},
expectedChallenge,
expectedOrigin: 'https://localhost:44329',
@@ -63,7 +65,7 @@ test('should verify SHA256 TPM response', async () => {
*/
const expectedChallenge =
'gHrAk4pNe2VlB0HLeKclI2P6QEa83PuGeijTHMtpbhY9KlybyhlwF_VzRe7yhabXagWuY6rkDWfvvhNqgh2o7A';
- jest.spyOn(base64url, 'encode').mockReturnValueOnce(expectedChallenge);
+ jest.spyOn(isoBase64URL, 'fromString').mockReturnValueOnce(expectedChallenge);
const verification = await verifyRegistrationResponse({
credential: {
rawId: 'h9XMhkVePN1Prq9Ks_VfwIsVZvt-jmSRTEnevTc-KB8',
@@ -76,6 +78,7 @@ test('should verify SHA256 TPM response', async () => {
},
type: 'public-key',
clientExtensionResults: {},
+ authenticatorAttachment: '',
},
expectedChallenge,
expectedOrigin: 'https://localhost:44329',
@@ -100,7 +103,7 @@ test('should verify TPM response with spec-compliant tcgAtTpm SAN structure', as
* ]
*/
const expectedChallenge = 'VfmZXKDxqdoXFMHXO3SE2Q2b8u5Ki64OL_XICELcGKg';
- jest.spyOn(base64url, 'encode').mockReturnValueOnce(expectedChallenge);
+ jest.spyOn(isoBase64URL, 'fromString').mockReturnValueOnce(expectedChallenge);
const verification = await verifyRegistrationResponse({
credential: {
id: 'LVwzXx0fStkvsos_jdl9DTd6O3-6be8Ua4tcdXc5XeM',
@@ -113,6 +116,7 @@ test('should verify TPM response with spec-compliant tcgAtTpm SAN structure', as
},
type: 'public-key',
clientExtensionResults: {},
+ authenticatorAttachment: '',
},
expectedChallenge,
expectedOrigin: 'https://dev.netpassport.io',
@@ -133,7 +137,7 @@ test('should verify TPM response with non-spec-compliant tcgAtTpm SAN structure'
* ]
*/
const expectedChallenge = '4STWgmXrgJxzigqe6nFuIg';
- jest.spyOn(base64url, 'encode').mockReturnValueOnce(expectedChallenge);
+ jest.spyOn(isoBase64URL, 'fromString').mockReturnValueOnce(expectedChallenge);
const verification = await verifyRegistrationResponse({
credential: {
id: 'X7TPi7o8WfiIz1bP0Vciz1xRvSMyiitgOR1sUqY724s',
@@ -146,6 +150,7 @@ test('should verify TPM response with non-spec-compliant tcgAtTpm SAN structure'
},
type: 'public-key',
clientExtensionResults: {},
+ authenticatorAttachment: '',
},
expectedChallenge,
expectedOrigin: 'https://localhost:44329',
@@ -157,17 +162,20 @@ 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, 'encode').mockReturnValueOnce(expectedChallenge);
+ jest.spyOn(isoBase64URL, 'fromString').mockReturnValueOnce(expectedChallenge);
const verification = await verifyRegistrationResponse({
credential: {
- 'id': 'hsS2ywFz_LWf9-lC35vC9uJTVD3ZCVdweZvESUbjXnQ',
- 'rawId': 'hsS2ywFz_LWf9-lC35vC9uJTVD3ZCVdweZvESUbjXnQ',
- 'type': 'public-key',
- 'response': {
- 'attestationObject': 'o2NmbXRjdHBtZ2F0dFN0bXSmY2FsZzn__mNzaWdZAQCqAcGoi2IFXCF5xxokjR5yOAwK_11iCOqt8hCkpHE9rW602J3KjhcRQzoFf1UxZvadwmYcHHMxDQDmVuOhH-yW-DfARVT7O3MzlhhzrGTNO_-jhGFsGeEdz0RgNsviDdaVP5lNsV6Pe4bMhgBv1aTkk0zx1T8sxK8B7gKT6x80RIWg89_aYY4gHR4n65SRDp2gOGI2IHDvqTwidyeaAHVPbDrF8iDbQ88O-GH_fheAtFtgjbIq-XQbwVdzQhYdWyL0XVUwGLSSuABuB4seRPkyZCKoOU6VuuQzfWNpH2Nl05ybdXi27HysUexgfPxihB3PbR8LJdi1j04tRg3JvBUvY3ZlcmMyLjBjeDVjglkFuzCCBbcwggOfoAMCAQICEGEZiaSlAkKpqaQOKDYmWPkwDQYJKoZIhvcNAQELBQAwQTE_MD0GA1UEAxM2RVVTLU5UQy1LRVlJRC1FNEE4NjY2RjhGNEM2RDlDMzkzMkE5NDg4NDc3ODBBNjgxMEM0MjEzMB4XDTIyMDExMjIyMTUxOFoXDTI3MDYxMDE4NTQzNlowADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKo-7DHdiipZTzfA9fpTaIMVK887zM0nXAVIvU0kmGAsPpTYbf7dn1DAl6BhcDkXs2WrwYP02K8RxXWOF4jf7esMAIkr65zPWqLys8WRNM60d7g9GOADwbN8qrY0hepSsaJwjhswbNJI6L8vJwnnrQ6UWVCm3xHqn8CB2iSWNSUnshgTQTkJ1ZEdToeD51sFXUE0fSxXjyIiSAAD4tCIZkmHFVqchzfqUgiiM_mbbKzUnxEZ6c6r39ccHzbm4Ir-u62repQnVXKTpzFBbJ-Eg15REvw6xuYaGtpItk27AXVcEodfAylf7pgQPfExWkoMZfb8faqbQAj5x29mBJvlzj0CAwEAAaOCAeowggHmMA4GA1UdDwEB_wQEAwIHgDAMBgNVHRMBAf8EAjAAMG0GA1UdIAEB_wRjMGEwXwYJKwYBBAGCNxUfMFIwUAYIKwYBBQUHAgIwRB5CAFQAQwBQAEEAIAAgAFQAcgB1AHMAdABlAGQAIAAgAFAAbABhAHQAZgBvAHIAbQAgACAASQBkAGUAbgB0AGkAdAB5MBAGA1UdJQQJMAcGBWeBBQgDMFAGA1UdEQEB_wRGMESkQjBAMT4wEAYFZ4EFAgIMB05QQ1Q3NXgwFAYFZ4EFAgEMC2lkOjRFNTQ0MzAwMBQGBWeBBQIDDAtpZDowMDA3MDAwMjAfBgNVHSMEGDAWgBQ3yjAtSXrnaSNOtzy1PEXxOO1ZUDAdBgNVHQ4EFgQU1ml3H5Tzrs0Nev69tFNhPZnhaV0wgbIGCCsGAQUFBwEBBIGlMIGiMIGfBggrBgEFBQcwAoaBkmh0dHA6Ly9hemNzcHJvZGV1c2Fpa3B1Ymxpc2guYmxvYi5jb3JlLndpbmRvd3MubmV0L2V1cy1udGMta2V5aWQtZTRhODY2NmY4ZjRjNmQ5YzM5MzJhOTQ4ODQ3NzgwYTY4MTBjNDIxMy9lMDFjMjA2Mi1mYmRjLTQwYTUtYTQwZi1jMzc3YzBmNzY1MWMuY2VyMA0GCSqGSIb3DQEBCwUAA4ICAQAz-YGrj0S841gyMZuit-qsKpKNdxbkaEhyB1baexHGcMzC2y1O1kpTrpaH3I80hrIZFtYoA2xKQ1j67uoC6vm1PhsJB6qhs9T7zmWZ1VtleJTYGNZ_bYY2wo65qJHFB5TXkevJUVe2G39kB_W1TKB6g_GSwb4a5e4D_Sjp7b7RZpyIKHT1_UE1H4RXgR9Qi68K4WVaJXJUS6T4PHrRc4PeGUoJLQFUGxYokWIf456G32GwGgvUSX76K77pVv4Y-kT3v5eEJdYxlS4EVT13a17KWd0DdLje0Ae69q_DQSlrHVLUrADvuZMeM8jxyPQvDb7ETKLsSUeHm73KOCGLStcGQ3pB49nt3d9XdWCcUwUrmbBF2G7HsRgTNbj16G6QUcWroQEqNrBG49aO9mMZ0NwSn5d3oNuXSXjLdGBXM1ukLZ-GNrZDYw5KXU102_5VpHpjIHrZh0dXg3Q9eucKe6EkFbH65-O5VaQWUnR5WJpt6-fl_l0iHqHnKXbgL6tjeerCqZWDvFsOak05R-hosAoQs_Ni0EsgZqHwR_VlG86fsSwCVU3_sDKTNs_Je08ewJ_bbMB5Tq6k1Sxs8Aw8R96EwjQLp3z-Zva1myU-KerYYVDl5BdvgPqbD8Xmst-z6vrP3CJbtr8jgqVS7RWy_cJOA8KCZ6IS_75QT7Gblq6UGFkG7zCCBuswggTToAMCAQICEzMAAAbTtnznKsOrB-gAAAAABtMwDQYJKoZIhvcNAQELBQAwgYwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xNjA0BgNVBAMTLU1pY3Jvc29mdCBUUE0gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxNDAeFw0yMTA2MTAxODU0MzZaFw0yNzA2MTAxODU0MzZaMEExPzA9BgNVBAMTNkVVUy1OVEMtS0VZSUQtRTRBODY2NkY4RjRDNkQ5QzM5MzJBOTQ4ODQ3NzgwQTY4MTBDNDIxMzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJA7GLwHWWbn2H8DRppxQfre4zll1sgE3Wxt9DTYWt5-v-xKwCQb6z_7F1py7LMe58qLqglAgVhS6nEvN2puZ1GzejdsFFxz2gyEfH1y-X3RGp0dxS6UKwEtmksaMEKIRQn2GgKdUkiuvkaxaoznuExoTPyu0aXk6yFsX5KEDu9UZCgt66bRy6m3KIRnn1VK2frZfqGYi8C8x9Q69oGG316tUwAIm3ypDtv3pREXsDLYE1U5Irdv32hzJ4CqqPyau-qJS18b8CsjvgOppwXRSwpOmU7S3xqo-F7h1eeFw2tgHc7PEPt8MSSKeba8Fz6QyiLhgFr8jFUvKRzk4B41HFUMqXYawbhAtfIBiGGsGrrdNKb7MxISnH1E6yLVCQGGhXiN9U7V0h8Gn56eKzopGlubw7yMmgu8Cu2wBX_a_jFmIBHnn8YgwcRm6NvT96KclDHnFqPVm3On12bG31F7EYkIRGLbaTT6avEu9rL6AJn7Xr245Sa6dC_OSMRKqLSufxp6O6f2TH2g4kvT0Go9SeyM2_acBjIiQ0rFeBOm49H4E4VcJepf79FkljovD68imeZ5MXjxepcCzS138374Jeh7k28JePwJnjDxS8n9Dr6xOU3_wxS1gN5cW6cXSoiPGe0JM4CEyAcUtKrvpUWoTajxxnylZuvS8ou2thfH2PQlAgMBAAGjggGOMIIBijAOBgNVHQ8BAf8EBAMCAoQwGwYDVR0lBBQwEgYJKwYBBAGCNxUkBgVngQUIAzAWBgNVHSAEDzANMAsGCSsGAQQBgjcVHzASBgNVHRMBAf8ECDAGAQH_AgEAMB0GA1UdDgQWBBQ3yjAtSXrnaSNOtzy1PEXxOO1ZUDAfBgNVHSMEGDAWgBR6jArOL0hiF-KU0a5VwVLscXSkVjBwBgNVHR8EaTBnMGWgY6Bhhl9odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBUUE0lMjBSb290JTIwQ2VydGlmaWNhdGUlMjBBdXRob3JpdHklMjAyMDE0LmNybDB9BggrBgEFBQcBAQRxMG8wbQYIKwYBBQUHMAKGYWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwVFBNJTIwUm9vdCUyMENlcnRpZmljYXRlJTIwQXV0aG9yaXR5JTIwMjAxNC5jcnQwDQYJKoZIhvcNAQELBQADggIBAFZTSitCISvll6i6rPUPd8Wt2mogRw6I_c-dWQzdc9-SY9iaIGXqVSPKKOlAYU2ju7nvN6AvrIba6sngHeU0AUTeg1UZ5-bDFOWdSgPaGyH_EN_l-vbV6SJPzOmZHJOHfw2WT8hjlFaTaKYRXxzFH7PUR4nxGRbWtdIGgQhUlWg5oo_FO4bvLKfssPSONn684qkAVierq-ly1WeqJzOYhd4EylgVJ9NL3YUhg8dYcHAieptDzF7OcDqffbuZLZUx6xcyibhWQcntAh7a3xPwqXxENsHhme_bqw_kqa-NVk-Wz4zdoiNNLRvUmCSL1WLc4JPsFJ08Ekn1kW7f9ZKnie5aw-29jEf6KIBt4lGDD3tXTfaOVvWcDbu92jMOO1dhEIj63AwQiDJgZhqnrpjlyWU_X0IVQlaPBg80AE0Y3sw1oMrY0XwdeQUjSpH6e5fTYKrNB6NMT1jXGjKIzVg8XbPWlnebP2wEhq8rYiDR31b9B9Sw_naK7Xb-Cqi-VQdUtknSjeljusrBpxGUx-EIJci0-dzeXRT5_376vyKSuYxA1Xd2jd4EknJLIAVLT3rb10DCuKGLDgafbsfTBxVoEa9hSjYOZUr_m3WV6t6I9WPYjVyhyi7fCEIG4JE7YbM4na4jg5q3DM8ibE8jyufAq0PfJZTJyi7c2Q2N_9NgnCNwZ3B1YkFyZWFYdgAjAAsABAByACCd_8vzbDg65pn7mGjcbcuJ1xU4hL4oA5IsEkFYv60irgAQABAAAwAQACAek7g2C8TeORRoKxuN7HrJ5OinVGuHzEgYODyUsF9D1wAggXPPXn-Pm_4IF0c4XVaJjmHO3EB2KBwdg_L60N0IL9xoY2VydEluZm9Yof9UQ0eAFwAiAAvQNGTLa2wT6u8SKDDdwkgaq5Cmh6jcD_6ULvM9ZmvdbwAUtMInD3WtGSdWHPWijMrW_TfYo-gAAAABPuBems3Sywu4aQsGAe85iOosjtXIACIAC5FPRiZSJzjYMNnAz9zFtM62o57FJwv8F5gNEcioqhHwACIACyVXxq1wZhDsqTqdYr7vQUUJ3vwWVrlN0ZQv5HFnHqWdaGF1dGhEYXRhWKR0puqSE8mcL3SyJJKzIM9AJiqUwalQoDl_KSULYIQe8EUAAAAACJhwWMrcS4G24TDeUNy-lgAghsS2ywFz_LWf9-lC35vC9uJTVD3ZCVdweZvESUbjXnSlAQIDJiABIVggHpO4NgvE3jkUaCsbjex6yeTop1Rrh8xIGDg8lLBfQ9ciWCCBc89ef4-b_ggXRzhdVomOYc7cQHYoHB2D8vrQ3Qgv3A',
- 'clientDataJSON': 'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoidXpuOXUwVHgtTEJkdEdnRVJzYmtIUkJqaVV0NWkycnZtMkJCVFpyV3FFbyIsIm9yaWdpbiI6Imh0dHBzOi8vd2ViYXV0aG4uaW8iLCJjcm9zc09yaWdpbiI6ZmFsc2V9'
+ id: 'hsS2ywFz_LWf9-lC35vC9uJTVD3ZCVdweZvESUbjXnQ',
+ rawId: 'hsS2ywFz_LWf9-lC35vC9uJTVD3ZCVdweZvESUbjXnQ',
+ type: 'public-key',
+ response: {
+ attestationObject:
+ 'o2NmbXRjdHBtZ2F0dFN0bXSmY2FsZzn__mNzaWdZAQCqAcGoi2IFXCF5xxokjR5yOAwK_11iCOqt8hCkpHE9rW602J3KjhcRQzoFf1UxZvadwmYcHHMxDQDmVuOhH-yW-DfARVT7O3MzlhhzrGTNO_-jhGFsGeEdz0RgNsviDdaVP5lNsV6Pe4bMhgBv1aTkk0zx1T8sxK8B7gKT6x80RIWg89_aYY4gHR4n65SRDp2gOGI2IHDvqTwidyeaAHVPbDrF8iDbQ88O-GH_fheAtFtgjbIq-XQbwVdzQhYdWyL0XVUwGLSSuABuB4seRPkyZCKoOU6VuuQzfWNpH2Nl05ybdXi27HysUexgfPxihB3PbR8LJdi1j04tRg3JvBUvY3ZlcmMyLjBjeDVjglkFuzCCBbcwggOfoAMCAQICEGEZiaSlAkKpqaQOKDYmWPkwDQYJKoZIhvcNAQELBQAwQTE_MD0GA1UEAxM2RVVTLU5UQy1LRVlJRC1FNEE4NjY2RjhGNEM2RDlDMzkzMkE5NDg4NDc3ODBBNjgxMEM0MjEzMB4XDTIyMDExMjIyMTUxOFoXDTI3MDYxMDE4NTQzNlowADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKo-7DHdiipZTzfA9fpTaIMVK887zM0nXAVIvU0kmGAsPpTYbf7dn1DAl6BhcDkXs2WrwYP02K8RxXWOF4jf7esMAIkr65zPWqLys8WRNM60d7g9GOADwbN8qrY0hepSsaJwjhswbNJI6L8vJwnnrQ6UWVCm3xHqn8CB2iSWNSUnshgTQTkJ1ZEdToeD51sFXUE0fSxXjyIiSAAD4tCIZkmHFVqchzfqUgiiM_mbbKzUnxEZ6c6r39ccHzbm4Ir-u62repQnVXKTpzFBbJ-Eg15REvw6xuYaGtpItk27AXVcEodfAylf7pgQPfExWkoMZfb8faqbQAj5x29mBJvlzj0CAwEAAaOCAeowggHmMA4GA1UdDwEB_wQEAwIHgDAMBgNVHRMBAf8EAjAAMG0GA1UdIAEB_wRjMGEwXwYJKwYBBAGCNxUfMFIwUAYIKwYBBQUHAgIwRB5CAFQAQwBQAEEAIAAgAFQAcgB1AHMAdABlAGQAIAAgAFAAbABhAHQAZgBvAHIAbQAgACAASQBkAGUAbgB0AGkAdAB5MBAGA1UdJQQJMAcGBWeBBQgDMFAGA1UdEQEB_wRGMESkQjBAMT4wEAYFZ4EFAgIMB05QQ1Q3NXgwFAYFZ4EFAgEMC2lkOjRFNTQ0MzAwMBQGBWeBBQIDDAtpZDowMDA3MDAwMjAfBgNVHSMEGDAWgBQ3yjAtSXrnaSNOtzy1PEXxOO1ZUDAdBgNVHQ4EFgQU1ml3H5Tzrs0Nev69tFNhPZnhaV0wgbIGCCsGAQUFBwEBBIGlMIGiMIGfBggrBgEFBQcwAoaBkmh0dHA6Ly9hemNzcHJvZGV1c2Fpa3B1Ymxpc2guYmxvYi5jb3JlLndpbmRvd3MubmV0L2V1cy1udGMta2V5aWQtZTRhODY2NmY4ZjRjNmQ5YzM5MzJhOTQ4ODQ3NzgwYTY4MTBjNDIxMy9lMDFjMjA2Mi1mYmRjLTQwYTUtYTQwZi1jMzc3YzBmNzY1MWMuY2VyMA0GCSqGSIb3DQEBCwUAA4ICAQAz-YGrj0S841gyMZuit-qsKpKNdxbkaEhyB1baexHGcMzC2y1O1kpTrpaH3I80hrIZFtYoA2xKQ1j67uoC6vm1PhsJB6qhs9T7zmWZ1VtleJTYGNZ_bYY2wo65qJHFB5TXkevJUVe2G39kB_W1TKB6g_GSwb4a5e4D_Sjp7b7RZpyIKHT1_UE1H4RXgR9Qi68K4WVaJXJUS6T4PHrRc4PeGUoJLQFUGxYokWIf456G32GwGgvUSX76K77pVv4Y-kT3v5eEJdYxlS4EVT13a17KWd0DdLje0Ae69q_DQSlrHVLUrADvuZMeM8jxyPQvDb7ETKLsSUeHm73KOCGLStcGQ3pB49nt3d9XdWCcUwUrmbBF2G7HsRgTNbj16G6QUcWroQEqNrBG49aO9mMZ0NwSn5d3oNuXSXjLdGBXM1ukLZ-GNrZDYw5KXU102_5VpHpjIHrZh0dXg3Q9eucKe6EkFbH65-O5VaQWUnR5WJpt6-fl_l0iHqHnKXbgL6tjeerCqZWDvFsOak05R-hosAoQs_Ni0EsgZqHwR_VlG86fsSwCVU3_sDKTNs_Je08ewJ_bbMB5Tq6k1Sxs8Aw8R96EwjQLp3z-Zva1myU-KerYYVDl5BdvgPqbD8Xmst-z6vrP3CJbtr8jgqVS7RWy_cJOA8KCZ6IS_75QT7Gblq6UGFkG7zCCBuswggTToAMCAQICEzMAAAbTtnznKsOrB-gAAAAABtMwDQYJKoZIhvcNAQELBQAwgYwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xNjA0BgNVBAMTLU1pY3Jvc29mdCBUUE0gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxNDAeFw0yMTA2MTAxODU0MzZaFw0yNzA2MTAxODU0MzZaMEExPzA9BgNVBAMTNkVVUy1OVEMtS0VZSUQtRTRBODY2NkY4RjRDNkQ5QzM5MzJBOTQ4ODQ3NzgwQTY4MTBDNDIxMzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJA7GLwHWWbn2H8DRppxQfre4zll1sgE3Wxt9DTYWt5-v-xKwCQb6z_7F1py7LMe58qLqglAgVhS6nEvN2puZ1GzejdsFFxz2gyEfH1y-X3RGp0dxS6UKwEtmksaMEKIRQn2GgKdUkiuvkaxaoznuExoTPyu0aXk6yFsX5KEDu9UZCgt66bRy6m3KIRnn1VK2frZfqGYi8C8x9Q69oGG316tUwAIm3ypDtv3pREXsDLYE1U5Irdv32hzJ4CqqPyau-qJS18b8CsjvgOppwXRSwpOmU7S3xqo-F7h1eeFw2tgHc7PEPt8MSSKeba8Fz6QyiLhgFr8jFUvKRzk4B41HFUMqXYawbhAtfIBiGGsGrrdNKb7MxISnH1E6yLVCQGGhXiN9U7V0h8Gn56eKzopGlubw7yMmgu8Cu2wBX_a_jFmIBHnn8YgwcRm6NvT96KclDHnFqPVm3On12bG31F7EYkIRGLbaTT6avEu9rL6AJn7Xr245Sa6dC_OSMRKqLSufxp6O6f2TH2g4kvT0Go9SeyM2_acBjIiQ0rFeBOm49H4E4VcJepf79FkljovD68imeZ5MXjxepcCzS138374Jeh7k28JePwJnjDxS8n9Dr6xOU3_wxS1gN5cW6cXSoiPGe0JM4CEyAcUtKrvpUWoTajxxnylZuvS8ou2thfH2PQlAgMBAAGjggGOMIIBijAOBgNVHQ8BAf8EBAMCAoQwGwYDVR0lBBQwEgYJKwYBBAGCNxUkBgVngQUIAzAWBgNVHSAEDzANMAsGCSsGAQQBgjcVHzASBgNVHRMBAf8ECDAGAQH_AgEAMB0GA1UdDgQWBBQ3yjAtSXrnaSNOtzy1PEXxOO1ZUDAfBgNVHSMEGDAWgBR6jArOL0hiF-KU0a5VwVLscXSkVjBwBgNVHR8EaTBnMGWgY6Bhhl9odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBUUE0lMjBSb290JTIwQ2VydGlmaWNhdGUlMjBBdXRob3JpdHklMjAyMDE0LmNybDB9BggrBgEFBQcBAQRxMG8wbQYIKwYBBQUHMAKGYWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwVFBNJTIwUm9vdCUyMENlcnRpZmljYXRlJTIwQXV0aG9yaXR5JTIwMjAxNC5jcnQwDQYJKoZIhvcNAQELBQADggIBAFZTSitCISvll6i6rPUPd8Wt2mogRw6I_c-dWQzdc9-SY9iaIGXqVSPKKOlAYU2ju7nvN6AvrIba6sngHeU0AUTeg1UZ5-bDFOWdSgPaGyH_EN_l-vbV6SJPzOmZHJOHfw2WT8hjlFaTaKYRXxzFH7PUR4nxGRbWtdIGgQhUlWg5oo_FO4bvLKfssPSONn684qkAVierq-ly1WeqJzOYhd4EylgVJ9NL3YUhg8dYcHAieptDzF7OcDqffbuZLZUx6xcyibhWQcntAh7a3xPwqXxENsHhme_bqw_kqa-NVk-Wz4zdoiNNLRvUmCSL1WLc4JPsFJ08Ekn1kW7f9ZKnie5aw-29jEf6KIBt4lGDD3tXTfaOVvWcDbu92jMOO1dhEIj63AwQiDJgZhqnrpjlyWU_X0IVQlaPBg80AE0Y3sw1oMrY0XwdeQUjSpH6e5fTYKrNB6NMT1jXGjKIzVg8XbPWlnebP2wEhq8rYiDR31b9B9Sw_naK7Xb-Cqi-VQdUtknSjeljusrBpxGUx-EIJci0-dzeXRT5_376vyKSuYxA1Xd2jd4EknJLIAVLT3rb10DCuKGLDgafbsfTBxVoEa9hSjYOZUr_m3WV6t6I9WPYjVyhyi7fCEIG4JE7YbM4na4jg5q3DM8ibE8jyufAq0PfJZTJyi7c2Q2N_9NgnCNwZ3B1YkFyZWFYdgAjAAsABAByACCd_8vzbDg65pn7mGjcbcuJ1xU4hL4oA5IsEkFYv60irgAQABAAAwAQACAek7g2C8TeORRoKxuN7HrJ5OinVGuHzEgYODyUsF9D1wAggXPPXn-Pm_4IF0c4XVaJjmHO3EB2KBwdg_L60N0IL9xoY2VydEluZm9Yof9UQ0eAFwAiAAvQNGTLa2wT6u8SKDDdwkgaq5Cmh6jcD_6ULvM9ZmvdbwAUtMInD3WtGSdWHPWijMrW_TfYo-gAAAABPuBems3Sywu4aQsGAe85iOosjtXIACIAC5FPRiZSJzjYMNnAz9zFtM62o57FJwv8F5gNEcioqhHwACIACyVXxq1wZhDsqTqdYr7vQUUJ3vwWVrlN0ZQv5HFnHqWdaGF1dGhEYXRhWKR0puqSE8mcL3SyJJKzIM9AJiqUwalQoDl_KSULYIQe8EUAAAAACJhwWMrcS4G24TDeUNy-lgAghsS2ywFz_LWf9-lC35vC9uJTVD3ZCVdweZvESUbjXnSlAQIDJiABIVggHpO4NgvE3jkUaCsbjex6yeTop1Rrh8xIGDg8lLBfQ9ciWCCBc89ef4-b_ggXRzhdVomOYc7cQHYoHB2D8vrQ3Qgv3A',
+ clientDataJSON:
+ 'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoidXpuOXUwVHgtTEJkdEdnRVJzYmtIUkJqaVV0NWkycnZtMkJCVFpyV3FFbyIsIm9yaWdpbiI6Imh0dHBzOi8vd2ViYXV0aG4uaW8iLCJjcm9zc09yaWdpbiI6ZmFsc2V9',
},
- 'clientExtensionResults': {},
+ clientExtensionResults: {},
+ authenticatorAttachment: '',
},
expectedChallenge,
expectedOrigin: 'https://webauthn.io',
diff --git a/packages/server/src/registration/verifications/tpm/verifyAttestationTPM.ts b/packages/server/src/registration/verifications/tpm/verifyAttestationTPM.ts
index fd2375c..c665be3 100644
--- a/packages/server/src/registration/verifications/tpm/verifyAttestationTPM.ts
+++ b/packages/server/src/registration/verifications/tpm/verifyAttestationTPM.ts
@@ -11,12 +11,20 @@ import {
import type { AttestationFormatVerifierOpts } from '../../verifyRegistrationResponse';
import { decodeCredentialPublicKey } from '../../../helpers/decodeCredentialPublicKey';
-import { COSEKEYS, COSEALGHASH } from '../../../helpers/convertCOSEtoPKCS';
+import {
+ COSEKEYS,
+ isCOSEAlg,
+ COSEKTY,
+ isCOSEPublicKeyRSA,
+ isCOSEPublicKeyEC2,
+ COSEALG,
+} from '../../../helpers/cose';
import { toHash } from '../../../helpers/toHash';
import { convertCertBufferToPEM } from '../../../helpers/convertCertBufferToPEM';
import { validateCertificatePath } from '../../../helpers/validateCertificatePath';
import { getCertificateInfo } from '../../../helpers/getCertificateInfo';
import { verifySignature } from '../../../helpers/verifySignature';
+import { isoUint8Array } from '../../../helpers/iso';
import { MetadataService } from '../../../services/metadataService';
import { verifyAttestationWithMetadata } from '../../../metadata/verifyAttestationWithMetadata';
@@ -24,10 +32,17 @@ import { TPM_MANUFACTURERS, TPM_ECC_CURVE_COSE_CRV_MAP } from './constants';
import { parseCertInfo } from './parseCertInfo';
import { parsePubArea } from './parsePubArea';
-export async function verifyAttestationTPM(options: AttestationFormatVerifierOpts): Promise<boolean> {
+export async function verifyAttestationTPM(
+ options: AttestationFormatVerifierOpts,
+): Promise<boolean> {
const { aaguid, attStmt, authData, credentialPublicKey, clientDataHash, rootCertificates } =
options;
- const { ver, sig, alg, x5c, pubArea, certInfo } = attStmt;
+ const ver = attStmt.get('ver');
+ const sig = attStmt.get('sig');
+ const alg = attStmt.get('alg');
+ const x5c = attStmt.get('x5c');
+ const pubArea = attStmt.get('pubArea');
+ const certInfo = attStmt.get('certInfo');
/**
* Verify structures
@@ -44,6 +59,10 @@ export async function verifyAttestationTPM(options: AttestationFormatVerifierOpt
throw new Error(`Attestation statement did not contain alg (TPM)`);
}
+ if (!isCOSEAlg(alg)) {
+ throw new Error(`Attestation statement contained invalid alg ${alg} (TPM)`);
+ }
+
if (!x5c) {
throw new Error('No attestation certificate provided in attestation statement (TPM)');
}
@@ -64,6 +83,14 @@ export async function verifyAttestationTPM(options: AttestationFormatVerifierOpt
const cosePublicKey = decodeCredentialPublicKey(credentialPublicKey);
if (pubType === 'TPM_ALG_RSA') {
+ if (!isCOSEPublicKeyRSA(cosePublicKey)) {
+ throw new Error(
+ `Credential public key with kty ${cosePublicKey.get(
+ COSEKEYS.kty,
+ )} did not match ${pubType}`,
+ );
+ }
+
const n = cosePublicKey.get(COSEKEYS.n);
const e = cosePublicKey.get(COSEKEYS.e);
@@ -74,7 +101,7 @@ export async function verifyAttestationTPM(options: AttestationFormatVerifierOpt
throw new Error('COSE public key missing e (TPM|RSA)');
}
- if (!unique.equals(n as Buffer)) {
+ if (!isoUint8Array.areEqual(unique, n)) {
throw new Error('PubArea unique is not same as credentialPublicKey (TPM|RSA)');
}
@@ -82,7 +109,7 @@ export async function verifyAttestationTPM(options: AttestationFormatVerifierOpt
throw new Error(`Parsed pubArea type is RSA, but missing parameters.rsa (TPM|RSA)`);
}
- const eBuffer = e as Buffer;
+ const eBuffer = e as Uint8Array;
// If `exponent` is equal to 0x00, then exponent is the default RSA exponent of 2^16+1 (65537)
const pubAreaExponent = parameters.rsa.exponent || 65537;
@@ -93,6 +120,14 @@ export async function verifyAttestationTPM(options: AttestationFormatVerifierOpt
throw new Error(`Unexpected public key exp ${eSum}, expected ${pubAreaExponent} (TPM|RSA)`);
}
} else if (pubType === 'TPM_ALG_ECC') {
+ if (!isCOSEPublicKeyEC2(cosePublicKey)) {
+ throw new Error(
+ `Credential public key with kty ${cosePublicKey.get(
+ COSEKEYS.kty,
+ )} did not match ${pubType}`,
+ );
+ }
+
const crv = cosePublicKey.get(COSEKEYS.crv);
const x = cosePublicKey.get(COSEKEYS.x);
const y = cosePublicKey.get(COSEKEYS.y);
@@ -107,7 +142,7 @@ export async function verifyAttestationTPM(options: AttestationFormatVerifierOpt
throw new Error('COSE public key missing y (TPM|ECC)');
}
- if (!unique.equals(Buffer.concat([x as Buffer, y as Buffer]))) {
+ if (!isoUint8Array.areEqual(unique, isoUint8Array.concat([x, y]))) {
throw new Error('PubArea unique is not same as public key x and y (TPM|ECC)');
}
@@ -116,7 +151,7 @@ export async function verifyAttestationTPM(options: AttestationFormatVerifierOpt
}
const pubAreaCurveID = parameters.ecc.curveID;
- const pubAreaCurveIDMapToCOSECRV = TPM_ECC_CURVE_COSE_CRV_MAP[pubAreaCurveID]
+ const pubAreaCurveIDMapToCOSECRV = TPM_ECC_CURVE_COSE_CRV_MAP[pubAreaCurveID];
if (pubAreaCurveIDMapToCOSECRV !== crv) {
throw new Error(
`Public area key curve ID "${pubAreaCurveID}" mapped to "${pubAreaCurveIDMapToCOSECRV}" which did not match public key crv of "${crv}" (TPM|ECC)`,
@@ -138,25 +173,24 @@ export async function verifyAttestationTPM(options: AttestationFormatVerifierOpt
}
// Hash pubArea to create pubAreaHash using the nameAlg in attested
- const pubAreaHash = toHash(pubArea, attested.nameAlg.replace('TPM_ALG_', ''));
+ const pubAreaHash = await toHash(pubArea, attestedNameAlgToCOSEAlg(attested.nameAlg));
// Concatenate attested.nameAlg and pubAreaHash to create attestedName.
- const attestedName = Buffer.concat([attested.nameAlgBuffer, pubAreaHash]);
+ const attestedName = isoUint8Array.concat([attested.nameAlgBuffer, pubAreaHash]);
// Check that certInfo.attested.name is equals to attestedName.
- if (!attested.name.equals(attestedName)) {
+ if (!isoUint8Array.areEqual(attested.name, attestedName)) {
throw new Error(`Attested name comparison failed (TPM)`);
}
// Concatenate authData with clientDataHash to create attToBeSigned
- const attToBeSigned = Buffer.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 = toHash(attToBeSigned, hashAlg);
+ const attToBeSignedHash = await toHash(attToBeSigned, alg);
// Check that certInfo.extraData is equals to attToBeSignedHash.
- if (!extraData.equals(attToBeSignedHash)) {
+ if (!isoUint8Array.areEqual(extraData, attToBeSignedHash)) {
throw new Error('CertInfo extra data did not equal hashed attestation (TPM)');
}
@@ -281,9 +315,9 @@ export async function verifyAttestationTPM(options: AttestationFormatVerifierOpt
// In the wise words of Yuriy Ackermann: "Get Martini friend, you are done!"
return verifySignature({
signature: sig,
- signatureBase: certInfo,
- leafCert: x5c[0],
- hashAlgorithm: hashAlg
+ data: certInfo,
+ leafCertificate: x5c[0],
+ attestationHashAlgorithm: alg,
});
}
@@ -349,3 +383,24 @@ function getTcgAtTpmValues(root: Name): {
tcgAtTpmVersion,
};
}
+
+/**
+ * Convert TPM-specific SHA algorithm ID's with COSE-specific equivalents. Note that the choice to
+ * use ECDSA SHA IDs is arbitrary; any such COSEALG that would map to SHA-256 in
+ * `mapCoseAlgToWebCryptoAlg()`
+ *
+ * SHA IDs referenced from here:
+ *
+ * https://trustedcomputinggroup.org/wp-content/uploads/TCG_TPM2_r1p59_Part2_Structures_pub.pdf
+ */
+function attestedNameAlgToCOSEAlg(alg: string): COSEALG {
+ if (alg === 'TPM_ALG_SHA256') {
+ return COSEALG.ES256;
+ } else if (alg === 'TPM_ALG_SHA384') {
+ return COSEALG.ES384;
+ } else if (alg === 'TPM_ALG_SHA512') {
+ return COSEALG.ES512;
+ }
+
+ throw new Error(`Unexpected TPM attested name alg ${alg}`);
+}
diff --git a/packages/server/src/registration/verifications/verifyAttestationAndroidKey.test.ts b/packages/server/src/registration/verifications/verifyAttestationAndroidKey.test.ts
index f7cdd4f..dd0a488 100644
--- a/packages/server/src/registration/verifications/verifyAttestationAndroidKey.test.ts
+++ b/packages/server/src/registration/verifications/verifyAttestationAndroidKey.test.ts
@@ -1,6 +1,5 @@
-import base64url from 'base64url';
-
import { SettingsService } from '../../services/settingsService';
+import { isoBase64URL } from '../../helpers/iso';
import { verifyRegistrationResponse } from '../verifyRegistrationResponse';
@@ -12,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, 'encode').mockReturnValueOnce(expectedChallenge);
+ jest.spyOn(isoBase64URL, 'fromString').mockReturnValueOnce(expectedChallenge);
const verification = await verifyRegistrationResponse({
credential: {
id: 'V51GE29tGbhby7sbg1cZ_qL8V8njqEsXpAnwQBobvgw',
@@ -25,6 +24,7 @@ test('should verify Android KeyStore response', async () => {
},
type: 'public-key',
clientExtensionResults: {},
+ authenticatorAttachment: '',
},
expectedChallenge,
expectedOrigin: 'https://dev.dontneeda.pw',
diff --git a/packages/server/src/registration/verifications/verifyAttestationAndroidKey.ts b/packages/server/src/registration/verifications/verifyAttestationAndroidKey.ts
index 55a0612..1f3eb83 100644
--- a/packages/server/src/registration/verifications/verifyAttestationAndroidKey.ts
+++ b/packages/server/src/registration/verifications/verifyAttestationAndroidKey.ts
@@ -7,7 +7,9 @@ import type { AttestationFormatVerifierOpts } from '../verifyRegistrationRespons
import { convertCertBufferToPEM } from '../../helpers/convertCertBufferToPEM';
import { validateCertificatePath } from '../../helpers/validateCertificatePath';
import { verifySignature } from '../../helpers/verifySignature';
-import { COSEALGHASH, convertCOSEtoPKCS } from '../../helpers/convertCOSEtoPKCS';
+import { convertCOSEtoPKCS } from '../../helpers/convertCOSEtoPKCS';
+import { isCOSEAlg } from '../../helpers/cose';
+import { isoUint8Array } from '../../helpers/iso';
import { MetadataService } from '../../services/metadataService';
import { verifyAttestationWithMetadata } from '../../metadata/verifyAttestationWithMetadata';
@@ -19,7 +21,9 @@ export async function verifyAttestationAndroidKey(
): Promise<boolean> {
const { authData, clientDataHash, attStmt, credentialPublicKey, aaguid, rootCertificates } =
options;
- const { x5c, sig, alg } = attStmt;
+ const x5c = attStmt.get('x5c');
+ const sig = attStmt.get('sig');
+ const alg = attStmt.get('alg');
if (!x5c) {
throw new Error('No attestation certificate provided in attestation statement (AndroidKey)');
@@ -33,17 +37,21 @@ export async function verifyAttestationAndroidKey(
throw new Error(`Attestation statement did not contain alg (AndroidKey)`);
}
+ if (!isCOSEAlg(alg)) {
+ throw new Error(`Attestation statement contained invalid alg ${alg} (AndroidKey)`);
+ }
+
// Check that credentialPublicKey matches the public key in the attestation certificate
// Find the public cert in the certificate as PKCS
const parsedCert = AsnParser.parse(x5c[0], Certificate);
- const parsedCertPubKey = Buffer.from(
+ const parsedCertPubKey = new Uint8Array(
parsedCert.tbsCertificate.subjectPublicKeyInfo.subjectPublicKey,
);
// Convert the credentialPublicKey to PKCS
const credPubKeyPKCS = convertCOSEtoPKCS(credentialPublicKey);
- if (!credPubKeyPKCS.equals(parsedCertPubKey)) {
+ if (!isoUint8Array.areEqual(credPubKeyPKCS, parsedCertPubKey)) {
throw new Error('Credential public key does not equal leaf cert public key (AndroidKey)');
}
@@ -61,7 +69,7 @@ export async function verifyAttestationAndroidKey(
// Verify extKeyStore values
const { attestationChallenge, teeEnforced, softwareEnforced } = parsedExtKeyStore;
- if (!Buffer.from(attestationChallenge.buffer).equals(clientDataHash)) {
+ if (!isoUint8Array.areEqual(new Uint8Array(attestationChallenge.buffer), clientDataHash)) {
throw new Error('Attestation challenge was not equal to client data hash (AndroidKey)');
}
@@ -98,13 +106,12 @@ export async function verifyAttestationAndroidKey(
}
}
- const signatureBase = Buffer.concat([authData, clientDataHash]);
- const hashAlg = COSEALGHASH[alg as number];
+ const signatureBase = isoUint8Array.concat([authData, clientDataHash]);
return verifySignature({
signature: sig,
- signatureBase,
- leafCert: x5c[0],
- hashAlgorithm: hashAlg
+ data: signatureBase,
+ leafCertificate: x5c[0],
+ attestationHashAlgorithm: alg,
});
}
diff --git a/packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.test.ts b/packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.test.ts
index 5df3bee..0e7edb3 100644
--- a/packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.test.ts
+++ b/packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.test.ts
@@ -1,5 +1,3 @@
-import base64url from 'base64url';
-
import { verifyAttestationAndroidSafetyNet } from './verifyAttestationAndroidSafetyNet';
import {
@@ -8,35 +6,38 @@ import {
} from '../../helpers/decodeAttestationObject';
import { parseAuthenticatorData } from '../../helpers/parseAuthenticatorData';
import { toHash } from '../../helpers/toHash';
+import { isoBase64URL } from '../../helpers/iso';
import { SettingsService } from '../../services/settingsService';
const rootCertificates = SettingsService.getRootCertificates({
identifier: 'android-safetynet',
});
-let authData: Buffer;
+let authData: Uint8Array;
let attStmt: AttestationStatement;
-let clientDataHash: Buffer;
-let aaguid: Buffer;
-let credentialID: Buffer;
-let credentialPublicKey: Buffer;
-let rpIdHash: Buffer;
+let clientDataHash: Uint8Array;
+let aaguid: Uint8Array;
+let credentialID: Uint8Array;
+let credentialPublicKey: Uint8Array;
+let rpIdHash: Uint8Array;
let spyDate: jest.SpyInstance;
-beforeEach(() => {
+beforeEach(async () => {
const { attestationObject, clientDataJSON } = attestationAndroidSafetyNet.response;
- const decodedAttestationObject = decodeAttestationObject(base64url.toBuffer(attestationObject));
+ const decodedAttestationObject = decodeAttestationObject(
+ isoBase64URL.toBuffer(attestationObject),
+ );
- authData = decodedAttestationObject.authData;
- attStmt = decodedAttestationObject.attStmt;
- clientDataHash = toHash(base64url.toBuffer(clientDataJSON));
+ authData = decodedAttestationObject.get('authData');
+ attStmt = decodedAttestationObject.get('attStmt');
+ clientDataHash = await toHash(isoBase64URL.toBuffer(clientDataJSON));
const parsedAuthData = parseAuthenticatorData(authData);
aaguid = parsedAuthData.aaguid!;
credentialID = parsedAuthData.credentialID!;
credentialPublicKey = parsedAuthData.credentialPublicKey!;
- spyDate = jest.spyOn(global.Date, 'now');
+ spyDate = jest.spyOn(globalThis.Date, 'now');
});
afterEach(() => {
@@ -88,11 +89,13 @@ 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.authData;
- const _attStmt = decodedAttestationObject.attStmt;
- const _clientDataHash = toHash(base64url.toBuffer(clientDataJSON));
+ const _authData = decodedAttestationObject.get('authData');
+ const _attStmt = decodedAttestationObject.get('attStmt');
+ 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 4c1e685..d47dd70 100644
--- a/packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.ts
+++ b/packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.ts
@@ -1,5 +1,3 @@
-import base64url from 'base64url';
-
import type { AttestationFormatVerifierOpts } from '../verifyRegistrationResponse';
import { toHash } from '../../helpers/toHash';
@@ -7,6 +5,7 @@ import { verifySignature } from '../../helpers/verifySignature';
import { getCertificateInfo } from '../../helpers/getCertificateInfo';
import { validateCertificatePath } from '../../helpers/validateCertificatePath';
import { convertCertBufferToPEM } from '../../helpers/convertCertBufferToPEM';
+import { isoUint8Array, isoBase64URL } from '../../helpers/iso';
import { MetadataService } from '../../services/metadataService';
import { verifyAttestationWithMetadata } from '../../metadata/verifyAttestationWithMetadata';
@@ -25,7 +24,9 @@ export async function verifyAttestationAndroidSafetyNet(
verifyTimestampMS = true,
credentialPublicKey,
} = options;
- const { alg, response, ver } = attStmt;
+ const alg = attStmt.get('alg');
+ const response = attStmt.get('response');
+ const ver = attStmt.get('ver');
if (!ver) {
throw new Error('No ver value in attestation (SafetyNet)');
@@ -36,11 +37,11 @@ export async function verifyAttestationAndroidSafetyNet(
}
// Prepare to verify a JWT
- const jwt = response.toString('utf8');
+ const jwt = isoUint8Array.toUTF8String(response);
const jwtParts = jwt.split('.');
- const HEADER: SafetyNetJWTHeader = JSON.parse(base64url.decode(jwtParts[0]));
- const PAYLOAD: SafetyNetJWTPayload = JSON.parse(base64url.decode(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];
/**
@@ -63,9 +64,9 @@ export async function verifyAttestationAndroidSafetyNet(
}
}
- const nonceBase = Buffer.concat([authData, clientDataHash]);
- const nonceBuffer = toHash(nonceBase);
- const expectedNonce = nonceBuffer.toString('base64');
+ const nonceBase = isoUint8Array.concat([authData, clientDataHash]);
+ const nonceBuffer = await toHash(nonceBase);
+ const expectedNonce = isoBase64URL.fromBuffer(nonceBuffer, 'base64');
if (nonce !== expectedNonce) {
throw new Error('Could not verify payload nonce (SafetyNet)');
@@ -81,7 +82,8 @@ export async function verifyAttestationAndroidSafetyNet(
/**
* START Verify Header
*/
- const leafCertBuffer = base64url.toBuffer(HEADER.x5c[0]);
+ // `HEADER.x5c[0]` is definitely a base64 string
+ const leafCertBuffer = isoBase64URL.toBuffer(HEADER.x5c[0], 'base64');
const leafCertInfo = getCertificateInfo(leafCertBuffer);
const { subject } = leafCertInfo;
@@ -121,13 +123,13 @@ export async function verifyAttestationAndroidSafetyNet(
/**
* START Verify Signature
*/
- const signatureBaseBuffer = Buffer.from(`${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,
- signatureBase: signatureBaseBuffer,
- leafCert: leafCertBuffer,
+ data: signatureBaseBuffer,
+ leafCertificate: leafCertBuffer,
});
/**
* END Verify Signature
diff --git a/packages/server/src/registration/verifications/verifyAttestationApple.test.ts b/packages/server/src/registration/verifications/verifyAttestationApple.test.ts
index c2d4a49..1459df1 100644
--- a/packages/server/src/registration/verifications/verifyAttestationApple.test.ts
+++ b/packages/server/src/registration/verifications/verifyAttestationApple.test.ts
@@ -1,10 +1,10 @@
-import base64url from 'base64url';
+// TODO: This test can take upwards of 7 seconds to complete locally, more in CI...need to figure
+// out why
+jest.setTimeout(30000);
import { verifyRegistrationResponse } from '../verifyRegistrationResponse';
test('should verify Apple attestation', async () => {
- const expectedChallenge = 'h5xSyIRMx2IQPr1mQk6GD98XSQOBHgMHVpJIkMV9Nkc';
- jest.spyOn(base64url, 'encode').mockReturnValueOnce(expectedChallenge);
const verification = await verifyRegistrationResponse({
credential: {
id: 'J4lAqPXhefDrUD7oh5LQMbBH5TE',
@@ -17,8 +17,9 @@ test('should verify Apple attestation', async () => {
},
type: 'public-key',
clientExtensionResults: {},
+ authenticatorAttachment: '',
},
- expectedChallenge,
+ expectedChallenge: 'h5xSyIRMx2IQPr1mQk6GD98XSQOBHgMHVpJIkMV9Nkc',
expectedOrigin: 'https://dev.dontneeda.pw',
expectedRPID: 'dev.dontneeda.pw',
});
diff --git a/packages/server/src/registration/verifications/verifyAttestationApple.ts b/packages/server/src/registration/verifications/verifyAttestationApple.ts
index e0c7890..4aae99b 100644
--- a/packages/server/src/registration/verifications/verifyAttestationApple.ts
+++ b/packages/server/src/registration/verifications/verifyAttestationApple.ts
@@ -7,12 +7,13 @@ import { validateCertificatePath } from '../../helpers/validateCertificatePath';
import { convertCertBufferToPEM } from '../../helpers/convertCertBufferToPEM';
import { toHash } from '../../helpers/toHash';
import { convertCOSEtoPKCS } from '../../helpers/convertCOSEtoPKCS';
+import { isoUint8Array } from '../../helpers/iso';
export async function verifyAttestationApple(
options: AttestationFormatVerifierOpts,
): Promise<boolean> {
const { attStmt, authData, clientDataHash, credentialPublicKey, rootCertificates } = options;
- const { x5c } = attStmt;
+ const x5c = attStmt.get('x5c');
if (!x5c) {
throw new Error('No attestation certificate provided in attestation statement (Apple)');
@@ -44,8 +45,8 @@ export async function verifyAttestationApple(
throw new Error('credCert missing "1.2.840.113635.100.8.2" extension (Apple)');
}
- const nonceToHash = Buffer.concat([authData, clientDataHash]);
- const nonce = toHash(nonceToHash, 'SHA256');
+ const nonceToHash = isoUint8Array.concat([authData, clientDataHash]);
+ const nonce = await toHash(nonceToHash);
/**
* Ignore the first six ASN.1 structure bytes that define the nonce as an OCTET STRING. Should
* trim off <Buffer 30 24 a1 22 04 20>
@@ -53,9 +54,9 @@ export async function verifyAttestationApple(
* TODO: Try and get @peculiar (GitHub) to add a schema for "1.2.840.113635.100.8.2" when we
* find out where it's defined (doesn't seem to be publicly documented at the moment...)
*/
- const extNonce = Buffer.from(extCertNonce.extnValue.buffer).slice(6);
+ const extNonce = new Uint8Array(extCertNonce.extnValue.buffer).slice(6);
- if (!nonce.equals(extNonce)) {
+ if (!isoUint8Array.areEqual(nonce, extNonce)) {
throw new Error(`credCert nonce was not expected value (Apple)`);
}
@@ -63,9 +64,9 @@ export async function verifyAttestationApple(
* Verify credential public key matches the Subject Public Key of credCert
*/
const credPubKeyPKCS = convertCOSEtoPKCS(credentialPublicKey);
- const credCertSubjectPublicKey = Buffer.from(subjectPublicKeyInfo.subjectPublicKey);
+ const credCertSubjectPublicKey = new Uint8Array(subjectPublicKeyInfo.subjectPublicKey);
- if (!credPubKeyPKCS.equals(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 3c79b9e..e271e48 100644
--- a/packages/server/src/registration/verifications/verifyAttestationFIDOU2F.ts
+++ b/packages/server/src/registration/verifications/verifyAttestationFIDOU2F.ts
@@ -4,6 +4,8 @@ import { convertCOSEtoPKCS } from '../../helpers/convertCOSEtoPKCS';
import { convertCertBufferToPEM } from '../../helpers/convertCertBufferToPEM';
import { validateCertificatePath } from '../../helpers/validateCertificatePath';
import { verifySignature } from '../../helpers/verifySignature';
+import { isoUint8Array } from '../../helpers/iso';
+import { COSEALG } from '../../helpers/cose';
/**
* Verify an attestation response with fmt 'fido-u2f'
@@ -17,14 +19,14 @@ export async function verifyAttestationFIDOU2F(
rpIdHash,
credentialID,
credentialPublicKey,
- aaguid = '',
+ aaguid,
rootCertificates,
} = options;
- const reservedByte = Buffer.from([0x00]);
+ const reservedByte = Uint8Array.from([0x00]);
const publicKey = convertCOSEtoPKCS(credentialPublicKey);
- const signatureBase = Buffer.concat([
+ const signatureBase = isoUint8Array.concat([
reservedByte,
rpIdHash,
clientDataHash,
@@ -32,7 +34,8 @@ export async function verifyAttestationFIDOU2F(
publicKey,
]);
- const { sig, x5c } = attStmt;
+ const sig = attStmt.get('sig');
+ const x5c = attStmt.get('x5c');
if (!x5c) {
throw new Error('No attestation certificate provided in attestation statement (FIDOU2F)');
@@ -43,7 +46,7 @@ export async function verifyAttestationFIDOU2F(
}
// FIDO spec says that aaguid _must_ equal 0x00 here to be legit
- const aaguidToHex = Number.parseInt(aaguid.toString('hex'), 16);
+ const aaguidToHex = Number.parseInt(isoUint8Array.toHex(aaguid), 16);
if (aaguidToHex !== 0x00) {
throw new Error(`AAGUID "${aaguidToHex}" was not expected value`);
}
@@ -58,7 +61,8 @@ export async function verifyAttestationFIDOU2F(
return verifySignature({
signature: sig,
- signatureBase,
- leafCert: x5c[0],
+ data: signatureBase,
+ leafCertificate: x5c[0],
+ attestationHashAlgorithm: COSEALG.ES256,
});
}
diff --git a/packages/server/src/registration/verifications/verifyAttestationPacked.test.ts b/packages/server/src/registration/verifications/verifyAttestationPacked.test.ts
index b38a0e6..f554ae4 100644
--- a/packages/server/src/registration/verifications/verifyAttestationPacked.test.ts
+++ b/packages/server/src/registration/verifications/verifyAttestationPacked.test.ts
@@ -24,6 +24,7 @@ test('should verify (broken) Packed response from Chrome virtual authenticator',
type: 'public-key',
clientExtensionResults: {},
transports: ['usb'],
+ authenticatorAttachment: '',
},
expectedChallenge: '9GIs0QQBna16ycw4stSnAqh2Ab6AiH7SS0_Xm4yJ1zk',
expectedOrigin: 'https://dev.dontneeda.pw',
diff --git a/packages/server/src/registration/verifications/verifyAttestationPacked.ts b/packages/server/src/registration/verifications/verifyAttestationPacked.ts
index 02beebc..a57bf13 100644
--- a/packages/server/src/registration/verifications/verifyAttestationPacked.ts
+++ b/packages/server/src/registration/verifications/verifyAttestationPacked.ts
@@ -1,10 +1,11 @@
import type { AttestationFormatVerifierOpts } from '../verifyRegistrationResponse';
-import { COSEALGHASH } from '../../helpers/convertCOSEtoPKCS';
+import { isCOSEAlg } from '../../helpers/cose';
import { convertCertBufferToPEM } from '../../helpers/convertCertBufferToPEM';
import { validateCertificatePath } from '../../helpers/validateCertificatePath';
import { getCertificateInfo } from '../../helpers/getCertificateInfo';
import { verifySignature } from '../../helpers/verifySignature';
+import { isoUint8Array } from '../../helpers/iso';
import { MetadataService } from '../../services/metadataService';
import { verifyAttestationWithMetadata } from '../../metadata/verifyAttestationWithMetadata';
@@ -17,17 +18,23 @@ export async function verifyAttestationPacked(
const { attStmt, clientDataHash, authData, credentialPublicKey, aaguid, rootCertificates } =
options;
- const { sig, x5c, alg } = attStmt;
+ const sig = attStmt.get('sig');
+ const x5c = attStmt.get('x5c');
+ const alg = attStmt.get('alg');
if (!sig) {
throw new Error('No attestation signature provided in attestation statement (Packed)');
}
- if (typeof alg !== 'number') {
- throw new Error(`Attestation Statement alg "${alg}" is not a number (Packed)`);
+ if (!alg) {
+ throw new Error('Attestation statement did not contain alg (Packed)');
}
- const signatureBase = Buffer.concat([authData, clientDataHash]);
+ if (!isCOSEAlg(alg)) {
+ throw new Error(`Attestation statement contained invalid alg ${alg} (Packed)`);
+ }
+
+ const signatureBase = isoUint8Array.concat([authData, clientDataHash]);
let verified = false;
@@ -107,17 +114,15 @@ export async function verifyAttestationPacked(
verified = await verifySignature({
signature: sig,
- signatureBase,
- leafCert: x5c[0],
+ data: signatureBase,
+ leafCertificate: x5c[0],
});
} else {
- const hashAlg: string = COSEALGHASH[alg as number];
-
verified = await verifySignature({
signature: sig,
- signatureBase,
+ data: signatureBase,
credentialPublicKey,
- hashAlgorithm: hashAlg
+ attestationHashAlgorithm: alg,
});
}
diff --git a/packages/server/src/registration/verifyRegistrationResponse.test.ts b/packages/server/src/registration/verifyRegistrationResponse.test.ts
index 21562bf..df4b1dd 100644
--- a/packages/server/src/registration/verifyRegistrationResponse.test.ts
+++ b/packages/server/src/registration/verifyRegistrationResponse.test.ts
@@ -1,4 +1,4 @@
-import base64url from 'base64url';
+import { RegistrationCredentialJSON } from '@simplewebauthn/typescript-types';
import { verifyRegistrationResponse } from './verifyRegistrationResponse';
@@ -6,23 +6,23 @@ import * as esmDecodeAttestationObject from '../helpers/decodeAttestationObject'
import * as esmDecodeClientDataJSON from '../helpers/decodeClientDataJSON';
import * as esmParseAuthenticatorData from '../helpers/parseAuthenticatorData';
import * as esmDecodeCredentialPublicKey from '../helpers/decodeCredentialPublicKey';
+import { toHash } from '../helpers/toHash';
+import { isoBase64URL, isoUint8Array } from '../helpers/iso';
+import { COSEPublicKey, COSEKEYS } from '../helpers/cose';
import { SettingsService } from '../services/settingsService';
import * as esmVerifyAttestationFIDOU2F from './verifications/verifyAttestationFIDOU2F';
-import { toHash } from '../helpers/toHash';
-import { RegistrationCredentialJSON } from '@simplewebauthn/typescript-types';
-
/**
* Clear out root certs for android-key since responses were captured from FIDO Conformance testing
* and have cert paths that can't be validated with known root certs from Google
*/
SettingsService.setRootCertificates({ identifier: 'android-key', certificates: [] });
-let mockDecodeAttestation: jest.SpyInstance;
+let mockDecodeAttestation: jest.SpyInstance<esmDecodeAttestationObject.AttestationObject>;
let mockDecodeClientData: jest.SpyInstance;
let mockParseAuthData: jest.SpyInstance;
-let mockDecodePubKey: jest.SpyInstance;
+let mockDecodePubKey: jest.SpyInstance<COSEPublicKey>;
let mockVerifyFIDOU2F: jest.SpyInstance;
beforeEach(() => {
@@ -53,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',
),
);
@@ -66,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),
);
});
@@ -82,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',
),
@@ -106,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',
),
);
@@ -129,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',
),
);
@@ -154,6 +154,7 @@ test('should verify None attestation w/RSA public key', async () => {
},
type: 'public-key',
clientExtensionResults: {},
+ authenticatorAttachment: '',
},
expectedChallenge,
expectedOrigin: 'https://dev.dontneeda.pw',
@@ -164,12 +165,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'),
);
});
@@ -217,17 +218,13 @@ test('should throw when attestation type is not webauthn.create', async () => {
});
test('should throw if an unexpected attestation format is specified', async () => {
- const fmt = 'fizzbuzz';
-
const realAtteObj = esmDecodeAttestationObject.decodeAttestationObject(
- base64url.toBuffer(attestationNone.response.attestationObject),
+ isoBase64URL.toBuffer(attestationNone.response.attestationObject),
);
+ // Mangle the fmt
+ (realAtteObj as Map<unknown, unknown>).set('fmt', 'fizzbuzz');
- mockDecodeAttestation.mockReturnValue({
- ...realAtteObj,
- // @ts-ignore 2322
- fmt,
- });
+ mockDecodeAttestation.mockReturnValue(realAtteObj);
await expect(
verifyRegistrationResponse({
@@ -240,14 +237,14 @@ 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),
- );
+ const authData = esmDecodeAttestationObject
+ .decodeAttestationObject(isoBase64URL.toBuffer(attestationNone.response.attestationObject))
+ .get('authData');
const actualAuthData = esmParseAuthenticatorData.parseAuthenticatorData(authData);
mockParseAuthData.mockReturnValue({
...actualAuthData,
- rpIdHash: toHash(Buffer.from('bad.url', 'ascii')),
+ rpIdHash: await toHash(Buffer.from('bad.url', 'ascii')),
});
await expect(
@@ -262,7 +259,7 @@ test('should throw error if assertion RP ID is unexpected value', async () => {
test('should throw error if user was not present', async () => {
mockParseAuthData.mockReturnValue({
- rpIdHash: toHash(Buffer.from('dev.dontneeda.pw', 'ascii')),
+ rpIdHash: await toHash(Buffer.from('dev.dontneeda.pw', 'ascii')),
flags: {
up: false,
},
@@ -280,7 +277,7 @@ test('should throw error if user was not present', async () => {
test('should throw if the authenticator does not give back credential ID', async () => {
mockParseAuthData.mockReturnValue({
- rpIdHash: toHash(Buffer.from('dev.dontneeda.pw', 'ascii')),
+ rpIdHash: await toHash(Buffer.from('dev.dontneeda.pw', 'ascii')),
flags: {
up: true,
},
@@ -299,7 +296,7 @@ test('should throw if the authenticator does not give back credential ID', async
test('should throw if the authenticator does not give back credential public key', async () => {
mockParseAuthData.mockReturnValue({
- rpIdHash: toHash(Buffer.from('dev.dontneeda.pw', 'ascii')),
+ rpIdHash: await toHash(Buffer.from('dev.dontneeda.pw', 'ascii')),
flags: {
up: true,
},
@@ -318,11 +315,8 @@ test('should throw if the authenticator does not give back credential public key
});
test('should throw error if no alg is specified in public key', async () => {
- mockDecodePubKey.mockReturnValue({
- get: () => undefined,
- credentialID: '',
- credentialPublicKey: '',
- });
+ const pubKey = new Map();
+ mockDecodePubKey.mockReturnValue(pubKey);
await expect(
verifyRegistrationResponse({
@@ -335,11 +329,9 @@ test('should throw error if no alg is specified in public key', async () => {
});
test('should throw error if unsupported alg is used', async () => {
- mockDecodePubKey.mockReturnValue({
- get: () => -999,
- credentialID: '',
- credentialPublicKey: '',
- });
+ const pubKey = new Map();
+ pubKey.set(COSEKEYS.alg, -999);
+ mockDecodePubKey.mockReturnValue(pubKey);
await expect(
verifyRegistrationResponse({
@@ -367,7 +359,7 @@ test('should not include authenticator info if not verified', async () => {
test('should throw an error if user verification is required but user was not verified', async () => {
mockParseAuthData.mockReturnValue({
- rpIdHash: toHash(Buffer.from('dev.dontneeda.pw', 'ascii')),
+ rpIdHash: await toHash(Buffer.from('dev.dontneeda.pw', 'ascii')),
flags: {
up: true,
uv: false,
@@ -399,6 +391,7 @@ test('should validate TPM RSA response (SHA256)', async () => {
},
type: 'public-key',
clientExtensionResults: {},
+ authenticatorAttachment: '',
},
expectedChallenge: expectedChallenge,
expectedOrigin: 'https://dev.dontneeda.pw',
@@ -409,12 +402,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'),
);
});
@@ -432,6 +425,7 @@ test('should validate TPM RSA response (SHA1)', async () => {
},
type: 'public-key',
clientExtensionResults: {},
+ authenticatorAttachment: '',
},
expectedChallenge,
expectedOrigin: 'https://dev.dontneeda.pw',
@@ -442,12 +436,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'),
);
});
@@ -465,6 +459,7 @@ test('should validate Android-Key response', async () => {
},
type: 'public-key',
clientExtensionResults: {},
+ authenticatorAttachment: '',
},
expectedChallenge,
expectedOrigin: 'https://dev.dontneeda.pw',
@@ -475,12 +470,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'),
);
});
@@ -543,10 +538,11 @@ test('should pass verification if custom challenge verifier returns true', async
type: 'public-key',
clientExtensionResults: {},
transports: ['internal'],
+ authenticatorAttachment: '',
},
expectedChallenge: (challenge: string) => {
const parsedChallenge: { actualChallenge: string; arbitraryData: string } = JSON.parse(
- base64url.decode(challenge),
+ isoBase64URL.toString(challenge),
);
return parsedChallenge.actualChallenge === 'xRsYdCQv5WZOqmxReiZl6C9q5SfrZne4lNSr9QVtPig';
},
@@ -594,6 +590,7 @@ test('should return authenticator extension output', async () => {
'ZkcwcHhLd2lKN2hPazJESlE0eHZLZDQzOFEiLCJhbmRyb2lkUGFja2FnZU5hbWUiOiJjb20uZmlkby5leGFtcGxl' +
'LmZpZG8yYXBpZXhhbXBsZSJ9',
},
+ authenticatorAttachment: '',
clientExtensionResults: {},
type: 'public-key',
},
@@ -604,18 +601,79 @@ test('should return authenticator extension output', async () => {
expect(verification.registrationInfo?.authenticatorExtensionResults).toMatchObject({
devicePubKey: {
- dpk: Buffer.from(
+ dpk: isoUint8Array.fromHex(
'A5010203262001215820991AABED9DE4271A9EDEAD8806F9DC96D6DCCD0C476253A5510489EC8379BE5B225820A0973CFDEDBB79E27FEF4EE7481673FB3312504DDCA5434CFD23431D6AD29EDA',
- 'hex',
),
- sig: Buffer.from(
+ sig: isoUint8Array.fromHex(
'3045022100EFB38074BD15B8C82CF09F87FBC6FB3C7169EA4F1806B7E90937374302345B7A02202B7113040731A0E727D338D48542863CE65880AA79E5EA740AC8CCD94347988E',
- 'hex',
),
- nonce: Buffer.from('', 'hex'),
- scope: Buffer.from('00', 'hex'),
- aaguid: Buffer.from('00000000000000000000000000000000', 'hex'),
+ nonce: isoUint8Array.fromHex(''),
+ scope: isoUint8Array.fromHex('00'),
+ aaguid: isoUint8Array.fromHex('00000000000000000000000000000000'),
+ },
+ });
+});
+
+test('should verify FIDO U2F attestation that specifies SHA-1 in its leaf cert public key', async () => {
+ const verified = await verifyRegistrationResponse({
+ credential: {
+ id: '7wQcUWO9gG6mi2IktoZUogs8opnghY01DPYwaerMZms',
+ rawId: '7wQcUWO9gG6mi2IktoZUogs8opnghY01DPYwaerMZms',
+ response: {
+ attestationObject:
+ 'o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEgwRgIhAN2iKnT1qcZPVab9eiXw6kmMqAsCjR8FMdx8DWCfc6h1AiEA8Hp4Fv2eWsokC8g3sL3tEgNEpsopz-G7l30-czGkuvBjeDVjgVkELzCCBCswggIToAMCAQICAQEwDQYJKoZIhvcNAQEFBQAwgaExGDAWBgNVBAMMD0ZJRE8yIFRFU1QgUk9PVDExMC8GCSqGSIb3DQEJARYiY29uZm9ybWFuY2UtdG9vbHNAZmlkb2FsbGlhbmNlLm9yZzEWMBQGA1UECgwNRklETyBBbGxpYW5jZTEMMAoGA1UECwwDQ1dHMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTVkxEjAQBgNVBAcMCVdha2VmaWVsZDAeFw0xODAzMTYxNDM1MjdaFw0yODAzMTMxNDM1MjdaMIGsMSMwIQYDVQQDDBpGSURPMiBCQVRDSCBLRVkgcHJpbWUyNTZ2MTExMC8GCSqGSIb3DQEJARYiY29uZm9ybWFuY2UtdG9vbHNAZmlkb2FsbGlhbmNlLm9yZzEWMBQGA1UECgwNRklETyBBbGxpYW5jZTEMMAoGA1UECwwDQ1dHMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTVkxEjAQBgNVBAcMCVdha2VmaWVsZDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABE86Xl6rbB-8rpf232RJlnYse-9yAEAqdsbyMPZVbxeqmZtZf8S_UIqvjp7wzQE_Wrm9J5FL8IBDeMvMsRuJtUajLDAqMAkGA1UdEwQCMAAwHQYDVR0OBBYEFFZN98D4xlW2oR9sTRnzv0Hi_QF5MA0GCSqGSIb3DQEBBQUAA4ICAQCPv4yN9RQfvCdl8cwVzLiOGIPrwLatOwARyap0KVJrfJaTs5rydAjinMLav-26bIElQSdus4Z8lnJtavFdGW8VLzdpB_De57XiBp_giTiZBwyCPiG4h-Pk1EAiY7ggednblFi9HxlcNkddyelfiu1Oa9Dlgc5rZsMIkVU4IFW4w6W8dqKhgMM7qRt0ZgRQ19TPdrN7YMsJy6_nujWWpecmXUvFW5SRo7MA2W3WPkKG6Ngwjer8b5-U1ZLpAB4gK46QQaQJrkHymudr6kgmEaUwpue30FGdXNZ9vTrLw8NcfXJMh_I__V4JNABvjJUPUXYN4Qm-y5Ej7wv82A3ktgo_8hcOjlmoZ5yEcDureFLS7kQJC64z9U-55NM7tcIcI-2BMLb2uOZ4lloeq3coP0mZX7KYd6PzGTeQ8Cmkq1GhDum_p7phCx-Rlo44j4H4DypCKH_g-NMWilBQaTSc6K0JAGQiVrh710aQWVhVYf1ITZRoV9Joc9shZQa7o2GvQYLyJHSfCnqJOqnwJ_q-RBBV3EiPLxmOzhBdNUCl1abvPhVtLksbUPfdQHBQ-io70edZe3utb4rFIHboWUSKvW2M3giMZyuSYZt6PzSRNmzqdjZlcFXuJI7iV_O8KNwWuNW14MCKXYi1sliYUhz5iSP9Ym0U2eVzvdsWzz0p55F6xWhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NBAAAAAgAAAAAAAAAAAAAAAAAAAAAAIO8EHFFjvYBupotiJLaGVKILPKKZ4IWNNQz2MGnqzGZrpQECAyYgASFYIMmWvjddCcHDGxX5F8qRMl1FccFW5R8VQuZOTey6LqA8IlggZLJ8OVPsX-NPDEUjyjzkV1YLW8Nglp1Ea4qgb2n-O88',
+ clientDataJSON:
+ 'eyJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjgwMDAiLCJjaGFsbGVuZ2UiOiJ3SjZtclpua2I2OUdENWQ5X2ZVejktTmdSSEUwejEwcXVYVUJTYTl4SzVvIiwidHlwZSI6IndlYmF1dGhuLmNyZWF0ZSJ9',
+ },
+ authenticatorAttachment: '',
+ clientExtensionResults: {},
+ type: 'public-key',
},
+ expectedChallenge: 'wJ6mrZnkb69GD5d9_fUz9-NgRHE0z10quXUBSa9xK5o',
+ expectedOrigin: 'http://localhost:8000',
+ expectedRPID: 'localhost',
+ });
+});
+
+test('should verify Packed attestation with RSA-PSS SHA-256 public key', async () => {
+ const verified = await verifyRegistrationResponse({
+ credential: {
+ id: 'n_dmFmW9UL7678vS4A3XSQLXvxWjefEkYVzEB5cNc_Q',
+ rawId: 'n_dmFmW9UL7678vS4A3XSQLXvxWjefEkYVzEB5cNc_Q',
+ response: {
+ attestationObject:
+ 'o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZzgkY3NpZ1kBAEaJQ9f_DWVWGJMJrHymDCRP7v2cOzeEA8Z1IUsd4GTq65qqg2khO05tKe6QK_NvpWbiLCRJ2E9QiMUu3xGTl7RIrIRp4T2WCjk5tLbLNwsHuFAPyjcuvIlcX2ZsKNL27tTroIz_zbzDk07vf0jhghoS3ec-qKrSZQ-B0ULgyDJf0omzgDRlH6uon7mErtunes9hVDUTn9pG9UJSL-jDptoJyu87NnBFGnlpu-Iur1lMKIEW27m5E7wYxF7IqIF2lylZGqXxh7ji93Bs7Hhik6y1T9KiGmn58rrYMxmBXzprxNQMF7rJxXbSZ9ZfjaZYamMDaoKDyKEhfAiOHXCm8AVoYXV0aERhdGFZAWZJlg3liA6MaHQ0Fw9kdmBbj-SuuaKGMseZXPO6gx2XY0EAAAB1qWxJcH1fTWqB93Yyt64CQAAgn_dmFmW9UL7678vS4A3XSQLXvxWjefEkYVzEB5cNc_SkAQMDOCQgWQEArEwu_kUDitzDgKOTthwbNnBGfGeUEwv8ksLGvqyRbTNClHnrR9fpaffqQeNor3ndNSReFnZ_3i468d677NMJC4-qoLKu7JP2FIDpt2reDCxg7-XvsaCcDIOucvKR-KIKg9CGiNpkHMhq2auXc4aqYrRjRyuoNYkzpWGENn34govaQQqC5Gdc0yHSeFJLrc9rbQoxMiZY1Ujpe3p9me0VXL4QdNmH_NlnzRclt38Rl8HqQOhrLo6rJOuRc_Ws-BjT0xh8HL8STgTxwb9aKquFkPxylztEy4TAgmOsFv-ukfGwbGO4fszqQKtpsf5-ulO8mfszgY1VrCLmuDzBzdGsdSFDAQAB',
+ clientDataJSON:
+ 'eyJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjgwMDAiLCJjaGFsbGVuZ2UiOiI0MHZfaXpNcHpYLUxPTklHekdxMFlieER3TUtNZmRfWHhRenBlNld2NjRZIiwidHlwZSI6IndlYmF1dGhuLmNyZWF0ZSJ9',
+ },
+ authenticatorAttachment: '',
+ clientExtensionResults: {},
+ type: 'public-key',
+ },
+ expectedChallenge: '40v_izMpzX-LONIGzGq0YbxDwMKMfd_XxQzpe6Wv64Y',
+ expectedOrigin: 'http://localhost:8000',
+ expectedRPID: 'localhost',
+ });
+});
+
+test('should verify Packed attestation with RSA-PSS SHA-384 public key', async () => {
+ const verified = await verifyRegistrationResponse({
+ credential: {
+ id: 'BCwirFmTkTdTUjVqn_uSy-UOSK-iMBgzpfFunE-Hnb0',
+ rawId: 'BCwirFmTkTdTUjVqn_uSy-UOSK-iMBgzpfFunE-Hnb0',
+ response: {
+ attestationObject:
+ 'o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZzglY3NpZ1kBAB7Tn5jK2sn5U4SBuxYzmR-Rg6iU5nox23mUxw6c10RsWcCw0h3aSKaon3gcn_Sfy8cov1YSsJVeUy9jVYJSpfQSS9ZMZXD5btGPf_YKH34j9YSGyTyutquZRxJ01mou2krDIaiXJOGLFpCJfVUBe-ben68MESby_Q2VFA6u3pjayC6Tu_iUJKPwdWPPaJM2P2KwyYtPy2jGIKqn6UFekfHOKpIDInW7QmzZF6JKUXNWqmwddq0vfzBpHlcyCBRDKmbGv667lkOUz9d7h_Lw0ho2HBrqEQuXhfmog5viDsezgHjQ196JZTwIgAO20vWioXiDWwJKjXGUmQxt9OGlQ1doYXV0aERhdGFZAWZJlg3liA6MaHQ0Fw9kdmBbj-SuuaKGMseZXPO6gx2XY0EAAABjBuy6aWZcQpm9f0NUYyTRzQAgBCwirFmTkTdTUjVqn_uSy-UOSK-iMBgzpfFunE-Hnb2kAQMDOCUgWQEApgFt6NaWotNSJIfFKOsdNlOtc7vdG7b78Rrnk7oCyUYg9PFVXRhgwSNAKBwimjeRILxcra5roznykpbcv3RIWNaej-tfxG2KYINh5ts8V2I3R2PgtlgwMfSSH9tv65gAzAFRk7tyizHelODhhNUbMVPMc-qTmnBzZANd06w0PN8xnWgCHPaG2MHZkFAOqiNkL4Kv0PPFbQTpy9HZd9ofdQhpKL71iXU4pMFJSSLG8jhY-HM2EwBM2HBTqb06qDjt6UOThCqCqd-ltNRllKWfstkUKQT0XOB-NpZ88037onupO2qDaMSudwolToh3-muuGAYCSANRS3TcNPuYP-s-6yFDAQAB',
+ clientDataJSON:
+ 'eyJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjgwMDAiLCJjaGFsbGVuZ2UiOiJwLWphWEhmWUpkbGQ2eTVucklzYTZyblpmNnJnU0MtRm8xcTdBU01VN2s4IiwidHlwZSI6IndlYmF1dGhuLmNyZWF0ZSJ9',
+ },
+ clientExtensionResults: {},
+ authenticatorAttachment: '',
+ type: 'public-key',
+ },
+ expectedChallenge: 'p-jaXHfYJdld6y5nrIsa6rnZf6rgSC-Fo1q7ASMU7k8',
+ expectedOrigin: 'http://localhost:8000',
+ expectedRPID: 'localhost',
});
});
@@ -632,10 +690,11 @@ const attestationFIDOU2F: RegistrationCredentialJSON = {
clientDataJSON:
'eyJjaGFsbGVuZ2UiOiJkRzkwWVd4c2VWVnVhWEYxWlZaaGJIVmxSWFpsY25sQmRIUmxjM1JoZEdsdmJnIiwiY2xpZW50RXh0ZW5zaW9ucyI6e30sImhhc2hBbGdvcml0aG0iOiJTSEEtMjU2Iiwib3JpZ2luIjoiaHR0cHM6Ly9kZXYuZG9udG5lZWRhLnB3IiwidHlwZSI6IndlYmF1dGhuLmNyZWF0ZSJ9',
},
- clientExtensionResults: {},
type: 'public-key',
+ clientExtensionResults: {},
+ authenticatorAttachment: '',
};
-const attestationFIDOU2FChallenge = base64url.encode('totallyUniqueValueEveryAttestation');
+const attestationFIDOU2FChallenge = isoBase64URL.fromString('totallyUniqueValueEveryAttestation');
const attestationPacked: RegistrationCredentialJSON = {
id: 'bbb',
@@ -655,8 +714,9 @@ const attestationPacked: RegistrationCredentialJSON = {
},
clientExtensionResults: {},
type: 'public-key',
+ authenticatorAttachment: '',
};
-const attestationPackedChallenge = base64url.encode('s6PIbBnPPnrGNSBxNdtDrT7UrVYJK9HM');
+const attestationPackedChallenge = isoBase64URL.fromString('s6PIbBnPPnrGNSBxNdtDrT7UrVYJK9HM');
const attestationPackedX5C: RegistrationCredentialJSON = {
// TODO: Grab these from another iPhone attestation
@@ -684,10 +744,11 @@ const attestationPackedX5C: RegistrationCredentialJSON = {
'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiZEc5MFlXeHNlVlZ1YVhG' +
'MVpWWmhiSFZsUlhabGNubFVhVzFsIiwib3JpZ2luIjoiaHR0cHM6Ly9kZXYuZG9udG5lZWRhLnB3In0=',
},
- clientExtensionResults: {},
type: 'public-key',
+ clientExtensionResults: {},
+ authenticatorAttachment: '',
};
-const attestationPackedX5CChallenge = base64url.encode('totallyUniqueValueEveryTime');
+const attestationPackedX5CChallenge = isoBase64URL.fromString('totallyUniqueValueEveryTime');
const attestationNone: RegistrationCredentialJSON = {
id: 'AdKXJEch1aV5Wo7bj7qLHskVY4OoNaj9qu8TPdJ7kSAgUeRxWNngXlcNIGt4gexZGKVGcqZpqqWordXb_he1izY',
@@ -703,7 +764,8 @@ const attestationNone: RegistrationCredentialJSON = {
'VURBd1NEQndOV2Q0YURKZmRUVmZVRU0wVG1WWloyUSIsIm9yaWdpbiI6Imh0dHBzOlwvXC9kZXYuZG9udG5lZWRh' +
'LnB3IiwiYW5kcm9pZFBhY2thZ2VOYW1lIjoib3JnLm1vemlsbGEuZmlyZWZveCJ9',
},
- clientExtensionResults: {},
type: 'public-key',
+ clientExtensionResults: {},
+ authenticatorAttachment: '',
};
-const attestationNoneChallenge = base64url.encode('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 14c2110..0c1351f 100644
--- a/packages/server/src/registration/verifyRegistrationResponse.ts
+++ b/packages/server/src/registration/verifyRegistrationResponse.ts
@@ -1,4 +1,3 @@
-import base64url from 'base64url';
import {
RegistrationCredentialJSON,
COSEAlgorithmIdentifier,
@@ -15,9 +14,11 @@ import { decodeClientDataJSON } from '../helpers/decodeClientDataJSON';
import { parseAuthenticatorData } from '../helpers/parseAuthenticatorData';
import { toHash } from '../helpers/toHash';
import { decodeCredentialPublicKey } from '../helpers/decodeCredentialPublicKey';
-import { COSEKEYS } from '../helpers/convertCOSEtoPKCS';
+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 { SettingsService } from '../services/settingsService';
import { supportedCOSEAlgorithmIdentifiers } from './generateRegistrationOptions';
@@ -129,9 +130,11 @@ export async function verifyRegistrationResponse(
}
}
- const attestationObject = base64url.toBuffer(response.attestationObject);
+ const attestationObject = isoBase64URL.toBuffer(response.attestationObject);
const decodedAttestationObject = decodeAttestationObject(attestationObject);
- const { fmt, authData, attStmt } = decodedAttestationObject;
+ const fmt = decodedAttestationObject.get('fmt');
+ const authData = decodedAttestationObject.get('authData');
+ const attStmt = decodedAttestationObject.get('attStmt');
const parsedAuthData = parseAuthenticatorData(authData);
const { aaguid, rpIdHash, flags, credentialID, counter, credentialPublicKey, extensionsData } =
@@ -139,22 +142,14 @@ export async function verifyRegistrationResponse(
// Make sure the response's RP ID is ours
if (expectedRPID) {
+ let expectedRPIDs: string[] = [];
if (typeof expectedRPID === 'string') {
- const expectedRPIDHash = toHash(Buffer.from(expectedRPID, 'ascii'));
- if (!rpIdHash.equals(expectedRPIDHash)) {
- throw new Error(`Unexpected RP ID hash`);
- }
+ expectedRPIDs = [expectedRPID];
} else {
- // Go through each expected RP ID and try to find one that matches
- const foundMatch = expectedRPID.some(expected => {
- const expectedRPIDHash = toHash(Buffer.from(expected, 'ascii'));
- return rpIdHash.equals(expectedRPIDHash);
- });
-
- if (!foundMatch) {
- throw new Error(`Unexpected RP ID hash`);
- }
+ expectedRPIDs = expectedRPID;
}
+
+ await matchExpectedRPID(rpIdHash, expectedRPIDs);
}
// Make sure someone was physically present
@@ -192,7 +187,7 @@ export async function verifyRegistrationResponse(
throw new Error(`Unexpected public key alg "${alg}", expected one of "${supported}"`);
}
- const clientDataHash = 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
@@ -224,7 +219,7 @@ export async function verifyRegistrationResponse(
} else if (fmt === 'apple') {
verified = await verifyAttestationApple(verifierOpts);
} else if (fmt === 'none') {
- if (Object.keys(attStmt).length > 0) {
+ if (attStmt.size > 0) {
throw new Error('None attestation had unexpected attestation statement');
}
// This is the weaker of the attestations, so there's nothing else to really check
@@ -287,10 +282,10 @@ export type VerifiedRegistrationResponse = {
fmt: AttestationFormat;
counter: number;
aaguid: string;
- credentialID: Buffer;
- credentialPublicKey: Buffer;
+ credentialID: Uint8Array;
+ credentialPublicKey: Uint8Array;
credentialType: 'public-key';
- attestationObject: Buffer;
+ attestationObject: Uint8Array;
userVerified: boolean;
credentialDeviceType: CredentialDeviceType;
credentialBackedUp: boolean;
@@ -302,13 +297,13 @@ export type VerifiedRegistrationResponse = {
* Values passed to all attestation format verifiers, from which they are free to use as they please
*/
export type AttestationFormatVerifierOpts = {
- aaguid: Buffer;
+ aaguid: Uint8Array;
attStmt: AttestationStatement;
- authData: Buffer;
- clientDataHash: Buffer;
- credentialID: Buffer;
- credentialPublicKey: Buffer;
+ authData: Uint8Array;
+ clientDataHash: Uint8Array;
+ credentialID: Uint8Array;
+ credentialPublicKey: Uint8Array;
rootCertificates: string[];
- rpIdHash: Buffer;
+ rpIdHash: Uint8Array;
verifyTimestampMS?: boolean;
};
diff --git a/packages/server/src/services/metadataService.test.ts b/packages/server/src/services/metadataService.test.ts
index fdf0979..8e12abc 100644
--- a/packages/server/src/services/metadataService.test.ts
+++ b/packages/server/src/services/metadataService.test.ts
@@ -1,5 +1,5 @@
-jest.mock('node-fetch');
-import fetch from 'node-fetch';
+jest.mock('cross-fetch');
+import fetch from 'cross-fetch';
import { MetadataService, BaseMetadataService } from './metadataService';
import type { MetadataStatement } from '../metadata/mdsTypes';
diff --git a/packages/server/src/services/metadataService.ts b/packages/server/src/services/metadataService.ts
index daed3cb..c532f11 100644
--- a/packages/server/src/services/metadataService.ts
+++ b/packages/server/src/services/metadataService.ts
@@ -1,4 +1,4 @@
-import fetch from 'node-fetch';
+import fetch from 'cross-fetch';
import { KJUR } from 'jsrsasign';
import { validateCertificatePath } from '../helpers/validateCertificatePath';
@@ -139,7 +139,7 @@ export class BaseMetadataService {
* This method will coordinate updating the cache as per the `nextUpdate` property in the initial
* BLOB download.
*/
- async getStatement(aaguid: string | Buffer): Promise<MetadataStatement | undefined> {
+ async getStatement(aaguid: string | Uint8Array): Promise<MetadataStatement | undefined> {
if (this.state === SERVICE_STATE.DISABLED) {
return;
}
@@ -148,7 +148,7 @@ export class BaseMetadataService {
return;
}
- if (aaguid instanceof Buffer) {
+ if (aaguid instanceof Uint8Array) {
aaguid = convertAAGUIDToString(aaguid);
}
@@ -286,7 +286,7 @@ export class BaseMetadataService {
let iterations = totalTimeoutMS / intervalMS;
// Check service state every `intervalMS` milliseconds
- const intervalID: NodeJS.Timeout = global.setInterval(() => {
+ const intervalID: NodeJS.Timeout = globalThis.setInterval(() => {
if (iterations < 1) {
clearInterval(intervalID);
reject(`State did not become ready in ${totalTimeoutMS / 1000} seconds`);
diff --git a/packages/server/src/services/settingsService.ts b/packages/server/src/services/settingsService.ts
index 6dce26b..ee1779b 100644
--- a/packages/server/src/services/settingsService.ts
+++ b/packages/server/src/services/settingsService.ts
@@ -28,13 +28,13 @@ class BaseSettingsService {
*/
setRootCertificates(opts: {
identifier: RootCertIdentifier;
- certificates: (Buffer | string)[];
+ certificates: (Uint8Array | string)[];
}): void {
const { identifier, certificates } = opts;
const newCertificates: string[] = [];
for (const cert of certificates) {
- if (cert instanceof Buffer) {
+ if (cert instanceof Uint8Array) {
newCertificates.push(convertCertBufferToPEM(cert));
} else {
newCertificates.push(cert);
diff --git a/packages/server/src/setupTests.ts b/packages/server/src/setupTests.ts
index 103e5fa..b23ac59 100644
--- a/packages/server/src/setupTests.ts
+++ b/packages/server/src/setupTests.ts
@@ -1,4 +1,18 @@
+import { webcrypto } from 'node:crypto';
// Silence some console output
// jest.spyOn(console, 'log').mockImplementation();
// jest.spyOn(console, 'debug').mockImplementation();
// jest.spyOn(console, 'error').mockImplementation();
+
+/**
+ * We can use this to test runtimes in which the WebCrypto API is available
+ * on `globalThis.crypto`
+ *
+ * This shouldn't be needed anymore once we move support to Node 19+ See here:
+ * https://nodejs.org/docs/latest-v19.x/api/webcrypto.html#web-crypto-api
+ */
+// Object.defineProperty(globalThis, 'crypto', {
+// get(){
+// return webcrypto;
+// },
+// });