summaryrefslogtreecommitdiffhomepage
path: root/packages/server/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/server/src')
-rw-r--r--packages/server/src/attestation/verifications/tpm/verifyTPM.ts34
-rw-r--r--packages/server/src/attestation/verifications/verifyAndroidKey.test.ts11
-rw-r--r--packages/server/src/attestation/verifications/verifyAndroidKey.ts53
-rw-r--r--packages/server/src/attestation/verifications/verifyAndroidSafetyNet.test.ts188
-rw-r--r--packages/server/src/attestation/verifications/verifyAndroidSafetyNet.ts55
-rw-r--r--packages/server/src/attestation/verifications/verifyApple.test.ts3
-rw-r--r--packages/server/src/attestation/verifications/verifyApple.ts45
-rw-r--r--packages/server/src/attestation/verifications/verifyFIDOU2F.ts28
-rw-r--r--packages/server/src/attestation/verifications/verifyPacked.ts32
-rw-r--r--packages/server/src/attestation/verifyAttestationResponse.test.ts7
-rw-r--r--packages/server/src/attestation/verifyAttestationResponse.ts97
-rw-r--r--packages/server/src/helpers/convertCertBufferToPEM.ts (renamed from packages/server/src/helpers/convertX509CertToPEM.ts)4
-rw-r--r--packages/server/src/helpers/decodeAttestationObject.ts19
-rw-r--r--packages/server/src/helpers/validateCertificatePath.ts49
-rw-r--r--packages/server/src/index.ts6
-rw-r--r--packages/server/src/metadata/verifyAttestationWithMetadata.ts34
-rw-r--r--packages/server/src/services/defaultRootCerts/android-key.ts86
-rw-r--r--packages/server/src/services/defaultRootCerts/android-safetynet.ts66
-rw-r--r--packages/server/src/services/defaultRootCerts/apple.ts25
-rw-r--r--packages/server/src/services/metadataService.ts (renamed from packages/server/src/metadata/metadataService.ts)6
-rw-r--r--packages/server/src/services/settingsService.test.ts47
-rw-r--r--packages/server/src/services/settingsService.ts71
22 files changed, 716 insertions, 250 deletions
diff --git a/packages/server/src/attestation/verifications/tpm/verifyTPM.ts b/packages/server/src/attestation/verifications/tpm/verifyTPM.ts
index fc549ff..e2fb772 100644
--- a/packages/server/src/attestation/verifications/tpm/verifyTPM.ts
+++ b/packages/server/src/attestation/verifications/tpm/verifyTPM.ts
@@ -8,30 +8,25 @@ import {
Name,
} from '@peculiar/asn1-x509';
-import type { AttestationStatement } from '../../../helpers/decodeAttestationObject';
+import type { AttestationFormatVerifierOpts } from '../../verifyAttestationResponse';
+
import decodeCredentialPublicKey from '../../../helpers/decodeCredentialPublicKey';
import { COSEKEYS, COSEALGHASH } from '../../../helpers/convertCOSEtoPKCS';
import toHash from '../../../helpers/toHash';
-import convertX509CertToPEM from '../../../helpers/convertX509CertToPEM';
+import convertCertBufferToPEM from '../../../helpers/convertCertBufferToPEM';
+import validateCertificatePath from '../../../helpers/validateCertificatePath';
import getCertificateInfo from '../../../helpers/getCertificateInfo';
import verifySignature from '../../../helpers/verifySignature';
-import MetadataService from '../../../metadata/metadataService';
+import MetadataService from '../../../services/metadataService';
import verifyAttestationWithMetadata from '../../../metadata/verifyAttestationWithMetadata';
import { TPM_ECC_CURVE, TPM_MANUFACTURERS } from './constants';
import parseCertInfo from './parseCertInfo';
import parsePubArea from './parsePubArea';
-type Options = {
- aaguid: Buffer;
- attStmt: AttestationStatement;
- authData: Buffer;
- credentialPublicKey: Buffer;
- clientDataHash: Buffer;
-};
-
-export default async function verifyTPM(options: Options): Promise<boolean> {
- const { aaguid, attStmt, authData, credentialPublicKey, clientDataHash } = options;
+export default async function verifyTPM(options: AttestationFormatVerifierOpts): Promise<boolean> {
+ const { aaguid, attStmt, authData, credentialPublicKey, clientDataHash, rootCertificates } =
+ options;
const { ver, sig, alg, x5c, pubArea, certInfo } = attStmt;
/**
@@ -270,20 +265,25 @@ export default async function verifyTPM(options: Options): Promise<boolean> {
} catch (err) {
throw new Error(`${err.message} (TPM)`);
}
+ } else {
+ try {
+ // Try validating the certificate path using the root certificates set via SettingsService
+ await validateCertificatePath(x5c.map(convertCertBufferToPEM), rootCertificates);
+ } catch (err) {
+ throw new Error(`${err.message} (TPM)`);
+ }
}
// Verify signature over certInfo with the public key extracted from AIK certificate.
// In the wise words of Yuriy Ackermann: "Get Martini friend, you are done!"
- const leafCertPEM = convertX509CertToPEM(x5c[0]);
+ const leafCertPEM = convertCertBufferToPEM(x5c[0]);
return verifySignature(sig, certInfo, leafCertPEM, hashAlg);
}
/**
* Contain logic for pulling TPM-specific values out of subjectAlternativeName extension
*/
-function getTcgAtTpmValues(
- root: Name,
-): {
+function getTcgAtTpmValues(root: Name): {
tcgAtTpmManufacturer?: string;
tcgAtTpmModel?: string;
tcgAtTpmVersion?: string;
diff --git a/packages/server/src/attestation/verifications/verifyAndroidKey.test.ts b/packages/server/src/attestation/verifications/verifyAndroidKey.test.ts
index a2dfc59..f249066 100644
--- a/packages/server/src/attestation/verifications/verifyAndroidKey.test.ts
+++ b/packages/server/src/attestation/verifications/verifyAndroidKey.test.ts
@@ -1,6 +1,15 @@
-import verifyAttestationResponse from '../verifyAttestationResponse';
import base64url from 'base64url';
+import SettingsService from '../../services/settingsService';
+
+import verifyAttestationResponse from '../verifyAttestationResponse';
+
+/**
+ * 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({ attestationFormat: 'android-key', certificates: [] });
+
test('should verify Android KeyStore response', async () => {
const expectedChallenge = '4ab7dfd1-a695-4777-985f-ad2993828e99';
jest.spyOn(base64url, 'encode').mockReturnValueOnce(expectedChallenge);
diff --git a/packages/server/src/attestation/verifications/verifyAndroidKey.ts b/packages/server/src/attestation/verifications/verifyAndroidKey.ts
index f917fa5..29d184e 100644
--- a/packages/server/src/attestation/verifications/verifyAndroidKey.ts
+++ b/packages/server/src/attestation/verifications/verifyAndroidKey.ts
@@ -2,23 +2,23 @@ import { AsnParser } from '@peculiar/asn1-schema';
import { Certificate } from '@peculiar/asn1-x509';
import { KeyDescription, id_ce_keyDescription } from '@peculiar/asn1-android';
-import type { AttestationStatement } from '../../helpers/decodeAttestationObject';
-import convertX509CertToPEM from '../../helpers/convertX509CertToPEM';
+import type { AttestationFormatVerifierOpts } from '../verifyAttestationResponse';
+
+import convertCertBufferToPEM from '../../helpers/convertCertBufferToPEM';
+import validateCertificatePath from '../../helpers/validateCertificatePath';
import verifySignature from '../../helpers/verifySignature';
import convertCOSEtoPKCS, { COSEALGHASH } from '../../helpers/convertCOSEtoPKCS';
-import MetadataService from '../../metadata/metadataService';
+import MetadataService from '../../services/metadataService';
import verifyAttestationWithMetadata from '../../metadata/verifyAttestationWithMetadata';
-type Options = {
- authData: Buffer;
- clientDataHash: Buffer;
- attStmt: AttestationStatement;
- credentialPublicKey: Buffer;
- aaguid: Buffer;
-};
-
-export default async function verifyAttestationAndroidKey(options: Options): Promise<boolean> {
- const { authData, clientDataHash, attStmt, credentialPublicKey, aaguid } = options;
+/**
+ * Verify an attestation response with fmt 'android-key'
+ */
+export default async function verifyAttestationAndroidKey(
+ options: AttestationFormatVerifierOpts,
+): Promise<boolean> {
+ const { authData, clientDataHash, attStmt, credentialPublicKey, aaguid, rootCertificates } =
+ options;
const { x5c, sig, alg } = attStmt;
if (!x5c) {
@@ -75,14 +75,6 @@ export default async function verifyAttestationAndroidKey(options: Options): Pro
throw new Error('teeEnforced contained "allApplications [600]" tag (AndroidKey)');
}
- // TODO: Confirm that the root certificate is an expected certificate
- // const rootCertPEM = convertX509CertToPEM(x5c[x5c.length - 1]);
- // console.log(rootCertPEM);
-
- // if (rootCertPEM !== expectedRootCert) {
- // throw new Error('Root certificate was not expected certificate (AndroidKey)');
- // }
-
const statement = await MetadataService.getStatement(aaguid);
if (statement) {
try {
@@ -90,21 +82,18 @@ export default async function verifyAttestationAndroidKey(options: Options): Pro
} catch (err) {
throw new Error(`${err.message} (AndroidKey)`);
}
+ } else {
+ try {
+ // Try validating the certificate path using the root certificates set via SettingsService
+ await validateCertificatePath(x5c.map(convertCertBufferToPEM), rootCertificates);
+ } catch (err) {
+ throw new Error(`${err.message} (AndroidKey)`);
+ }
}
const signatureBase = Buffer.concat([authData, clientDataHash]);
- const leafCertPEM = convertX509CertToPEM(x5c[0]);
+ const leafCertPEM = convertCertBufferToPEM(x5c[0]);
const hashAlg = COSEALGHASH[alg as number];
return verifySignature(sig, signatureBase, leafCertPEM, hashAlg);
}
-
-type KeyStoreExtensionDescription = {
- attestationVersion: number;
- attestationChallenge: Buffer;
- softwareEnforced: string[];
- teeEnforced: string[];
-};
-
-// TODO: Find the most up-to-date expected root cert, the one from Yuriy's article doesn't match
-const expectedRootCert = ``;
diff --git a/packages/server/src/attestation/verifications/verifyAndroidSafetyNet.test.ts b/packages/server/src/attestation/verifications/verifyAndroidSafetyNet.test.ts
index ea16b1d..6a754d3 100644
--- a/packages/server/src/attestation/verifications/verifyAndroidSafetyNet.test.ts
+++ b/packages/server/src/attestation/verifications/verifyAndroidSafetyNet.test.ts
@@ -7,11 +7,19 @@ import decodeAttestationObject, {
} from '../../helpers/decodeAttestationObject';
import parseAuthenticatorData from '../../helpers/parseAuthenticatorData';
import toHash from '../../helpers/toHash';
+import settingsService from '../../services/settingsService';
+
+const rootCertificates = settingsService.getRootCertificates({
+ attestationFormat: 'android-safetynet',
+});
let authData: Buffer;
let attStmt: AttestationStatement;
let clientDataHash: Buffer;
let aaguid: Buffer;
+let credentialID: Buffer;
+let credentialPublicKey: Buffer;
+let rpIdHash: Buffer;
beforeEach(() => {
const { attestationObject, clientDataJSON } = attestationAndroidSafetyNet.response;
@@ -23,6 +31,8 @@ beforeEach(() => {
const parsedAuthData = parseAuthenticatorData(authData);
aaguid = parsedAuthData.aaguid!;
+ credentialID = parsedAuthData.credentialID!;
+ credentialPublicKey = parsedAuthData.credentialPublicKey!;
});
/**
@@ -36,6 +46,10 @@ test('should verify Android SafetyNet attestation', async () => {
clientDataHash,
verifyTimestampMS: false,
aaguid,
+ rootCertificates,
+ credentialID,
+ credentialPublicKey,
+ rpIdHash,
});
expect(verified).toEqual(true);
@@ -48,10 +62,40 @@ test('should throw error when timestamp is not within one minute of now', async
authData,
clientDataHash,
aaguid,
+ rootCertificates,
+ credentialID,
+ credentialPublicKey,
+ rpIdHash,
}),
).rejects.toThrow(/has expired/i);
});
+test('should validate response with cert path completed with GlobalSign R1 root cert', async () => {
+ const { attestationObject, clientDataJSON } = safetyNetUsingGSR1RootCert.response;
+ const decodedAttestationObject = decodeAttestationObject(base64url.toBuffer(attestationObject));
+
+ const _authData = decodedAttestationObject.authData;
+ const _attStmt = decodedAttestationObject.attStmt;
+ const _clientDataHash = toHash(base64url.toBuffer(clientDataJSON));
+
+ const parsedAuthData = parseAuthenticatorData(_authData);
+ const _aaguid = parsedAuthData.aaguid!;
+
+ const verified = await verifyAndroidSafetyNet({
+ attStmt: _attStmt,
+ authData: _authData,
+ clientDataHash: _clientDataHash,
+ verifyTimestampMS: false,
+ aaguid: _aaguid,
+ rootCertificates,
+ credentialID,
+ credentialPublicKey,
+ rpIdHash,
+ });
+
+ expect(verified).toEqual(true);
+});
+
const attestationAndroidSafetyNet = {
id: 'AQy9gSmVYQXGuzd492rA2qEqwN7SYE_xOCjduU4QVagRwnX30mbfW75Lu4TwXHe-gc1O2PnJF7JVJA9dyJm83Xs',
rawId: 'AQy9gSmVYQXGuzd492rA2qEqwN7SYE_xOCjduU4QVagRwnX30mbfW75Lu4TwXHe-gc1O2PnJF7JVJA9dyJm83Xs',
@@ -150,4 +194,146 @@ const attestationAndroidSafetyNet = {
getClientExtensionResults: () => ({}),
type: 'public-key',
};
-const attestationAndroidSafetyNetChallenge = '_vVPoE42Dh-wk3bvHmaktiVvEYC-LwBX';
+
+const safetyNetUsingGSR1RootCert = {
+ id: 'AQsMmnEQ8OxpZxijXBMT4tyamgkqC_3hr18_e8KeK8nG69ijcTaXNKX_CRmYiW0fegPE0N_3NVHEaj_kit7LPNM',
+ rawId: 'AQsMmnEQ8OxpZxijXBMT4tyamgkqC_3hr18_e8KeK8nG69ijcTaXNKX_CRmYiW0fegPE0N_3NVHEaj_kit7LPNM',
+ response: {
+ attestationObject:
+ 'o2NmbXRxYW5kcm9pZC1zYWZldHluZXRnYXR0U3RtdKJjdmVyaTIxMjQxODA0NmhyZXNwb25zZVkgcmV5SmhiR2Np' +
+ 'T2lKU1V6STFOaUlzSW5nMVl5STZXeUpOU1VsR1dIcERRMEpGWldkQmQwbENRV2RKVVdadE9HbFpXbnAxY1RCRlNr' +
+ 'RkJRVUZCU0RkMVVsUkJUa0puYTNGb2EybEhPWGN3UWtGUmMwWkJSRUpIVFZGemQwTlJXVVJXVVZGSFJYZEtWbFY2' +
+ 'UldsTlEwRkhRVEZWUlVOb1RWcFNNamwyV2pKNGJFbEdVbmxrV0U0d1NVWk9iR051V25CWk1sWjZTVVY0VFZGNlJW' +
+ 'Uk5Ra1ZIUVRGVlJVRjRUVXRTTVZKVVNVVk9Ra2xFUmtWT1JFRmxSbmN3ZVUxVVFUTk5WR3Q0VFhwRmVrNUVTbUZH' +
+ 'ZHpCNVRWUkZkMDFVWTNoTmVrVjZUa1JHWVUxQ01IaEhla0ZhUW1kT1ZrSkJUVlJGYlVZd1pFZFdlbVJETldoaWJW' +
+ 'SjVZakpzYTB4dFRuWmlWRU5EUVZOSmQwUlJXVXBMYjFwSmFIWmpUa0ZSUlVKQ1VVRkVaMmRGVUVGRVEwTkJVVzlE' +
+ 'WjJkRlFrRkxaazVUUWxsNE0wMDJTbkpKYVRCTVVVUkdORlZhYUhSemVUZ3lRMjgwVG5aM2NpOUdTVzQzTHpsbksz' +
+ 'aHpWM3BEV1dkU04xRnpSMjF5ZVVjNWRsQkdja2Q1VVhKRlpHcERVWFZDVTFGVGQyOXZOR2R3YVVocGR6RllibkZH' +
+ 'Wm5KT1l6SjNURkpQTDFCVWRTdGhhMFpFU1UwMlozVXpaR1JuZDFGWFIwZGFjbFpRZWt0RmFrOTVUbE5HVFVKTU1G' +
+ 'ZEJTMmwxZFZsQ2RqRTBVWFp1YmxjeFJXdFpZbkZLWkZSb05reFhabVYyWTFkU1N5dFVkRlpoT1hwelIyNUZibWMz' +
+ 'YTAxUVYxQkNTekJPTUdKUVozaGlOR3B1ZUdGSWNXeE1lSEV2UTJwRWJreHJSRVZrZFdabFZEVlZaM0pzVkc1M09W' +
+ 'VnRXbTFOZUdGUWRHRXZkbm93WTJnMlpteERkM2xwZG1wSGFqSjRWRWhMVmxsMmJWbHdORlJtVEdjd1kxVk9VRVV4' +
+ 'WkV0cVRrbGlTMWxEZUZGSlZucHVlSFY0WlhCVVUxWnBXWFZqVUVZMFZuZHVLelpFT1ZwNFVVcEtLeTlsTmt0TVNX' +
+ 'dERRWGRGUVVGaFQwTkJia0YzWjJkS2MwMUJORWRCTVZWa1JIZEZRaTkzVVVWQmQwbEdiMFJCVkVKblRsWklVMVZG' +
+ 'UkVSQlMwSm5aM0pDWjBWR1FsRmpSRUZVUVUxQ1owNVdTRkpOUWtGbU9FVkJha0ZCVFVJd1IwRXhWV1JFWjFGWFFr' +
+ 'SlVUWE5VU1RWeFowRlBVbXRCWkROTlVFd3dOV2cwTm1KdlZsaEVRV1pDWjA1V1NGTk5SVWRFUVZkblFsRnNOR2hu' +
+ 'VDNOc1pWSnNRM0pzTVVZeVIydEpVR1ZWTjA4MGEycENkRUpuWjNKQ1owVkdRbEZqUWtGUlVtaE5SamgzUzJkWlNV' +
+ 'dDNXVUpDVVZWSVRVRkhSMGh0YURCa1NFRTJUSGs1ZGxrelRuZE1ia0p5WVZNMWJtSXlPVzVNTW1Rd1kzcEdhMDVI' +
+ 'YkhWa1JFRjRRbWRuY2tKblJVWkNVV04zUVc5WmJHRklVakJqUkc5MlRETkNjbUZUTlc1aU1qbHVURE5LYkdOSE9I' +
+ 'WlpNbFo1WkVoTmRsb3pVbnBOVjFFd1RHMVNiR05xUVdSQ1owNVdTRkpGUlVacVFWVm5hRXBvWkVoU2JHTXpVWFZa' +
+ 'VnpWclkyMDVjRnBETldwaU1qQjNTVkZaUkZaU01HZENRbTkzUjBSQlNVSm5XbTVuVVhkQ1FXZEZkMFJCV1V0TGQx' +
+ 'bENRa0ZJVjJWUlNVWkJla0V2UW1kT1ZraFNPRVZQUkVFeVRVUlRaMDF4UVhkb2FUVnZaRWhTZDA5cE9IWlpNMHB6' +
+ 'WTNrMWQyRXlhM1ZhTWpsMlduazVibVJJVFhoYVJGSndZbTVSZGxnd1dsRmpXRVpLVTBka1dVNXFaM1ZaTTBwelRV' +
+ 'bEpRa0YzV1V0TGQxbENRa0ZJVjJWUlNVVkJaMU5DT1VGVFFqaFJSSFpCU0ZWQldFNTRSR3QyTjIxeE1GWkZjMVky' +
+ 'WVRGR1ltMUZSR1kzTVdad1NETkxSbnBzVEVwbE5YWmlTRVJ6YjBGQlFVWTJkbmd5VHpGblFVRkNRVTFCVW1wQ1JV' +
+ 'RnBRa3AxVjFCU2JWSk5kbXBqVkZWd1NXSnlUa3RvT0hONFlrZDRUbEJOWm14aWNuWXhaSGhVYWtwM1EyZEpaMU01' +
+ 'ZDJkTVZVcGxVWEZNVFZJNFdHVnVSMDVtZVZsb1lYRnNjbEo0ZUUwNGMxQTRWa2x3VVVkVFV6QkJaR2RDT1ZCMlRE' +
+ 'UnFMeXRKVmxkbmEzZHpSRXR1YkV0S1pWTjJSa1J1WjBwbWVUVnhiREpwV21acFRIY3hkMEZCUVZoeEwwaFpLMHRC' +
+ 'UVVGRlFYZENTRTFGVlVOSlJESk1NbkpJUW14S2FUbFNSbTlQWmtWQ00yUjRTR1ZJVjFSS2QzTndORFpKWmtscU5t' +
+ 'OUxTM0JZWWtGcFJVRXlOVk5aUmswNFp6RlVLMGRKVlhKVlRUQjRZMDVVZDJrdmJISnhhRmxyVVUxSEswWnpNbVp0' +
+ 'Um1SSmQwUlJXVXBMYjFwSmFIWmpUa0ZSUlV4Q1VVRkVaMmRGUWtGRU5qaG1lRWhNZUU5REsxWnNUakZTVGtONVMy' +
+ 'UlVjV1pJWWxKQlFXUk9XVmczTjBoWEwyMVFRbTVWUXpGb2NtVlVSM2hIZUZOT01VUm9hazF4Tkhwb09GQkRiVEI2' +
+ 'TDNKQ00zQkVkMmxuYldsTmRtRllVRVZFYXpaRWJHbE5VMFY1WkRCak5ua3dPV2cxVjA1WFRpOWplR3BITDNWUk1E' +
+ 'SjZSRU12UldrdlptUkZaM1V5TVVobmVITTNRMFZVZFROMFpUWkNiekZTZUM5NFIxRnRLMnRvTlhZd2NIWXJhVmw2' +
+ 'Y25oVmJFOHZUV1J2YjJsa2VqbENRMWhYT0haeVRVbzJVbk5SVmxKUWVUUjVSbGN2TXpjeU4yeDFSRnBaTUVoME5X' +
+ 'MUZSa2xLUTNCV1EybENUSE5wZURCd2JWUnNhMXBhZFhSRWFDOHZUV1JOTlVFME56RldRVU14VTBsNGVrTXpUMkYw' +
+ 'ZEZoV1RGTnRTWFpuZDFoWFlsbzVhekpzZWtwcGVrRnNiRkpMVld0TlRGUmtjMDlFY0RVek0yNVBhMlJXVTFvMlpp' +
+ 'dEljbkZKYzFSTVRuTTFVVk5MWWtVMGNuaHlkbFpPS3pROUlpd2lUVWxKUm1wRVEwTkJNMU5uUVhkSlFrRm5TVTVC' +
+ 'WjBOUGMyZEplazV0VjB4YVRUTmliWHBCVGtKbmEzRm9hMmxIT1hjd1FrRlJjMFpCUkVKSVRWRnpkME5SV1VSV1VW' +
+ 'RkhSWGRLVmxWNlJXbE5RMEZIUVRGVlJVTm9UVnBTTWpsMldqSjRiRWxHVW5sa1dFNHdTVVpPYkdOdVduQlpNbFo2' +
+ 'U1VWNFRWRjZSVlZOUWtsSFFURlZSVUY0VFV4U01WSlVTVVpLZG1JelVXZFZha1YzU0doalRrMXFRWGRQUkVWNlRV' +
+ 'UkJkMDFFVVhsWGFHTk9UV3BqZDA5VVRYZE5SRUYzVFVSUmVWZHFRa2ROVVhOM1ExRlpSRlpSVVVkRmQwcFdWWHBG' +
+ 'YVUxRFFVZEJNVlZGUTJoTldsSXlPWFphTW5oc1NVWlNlV1JZVGpCSlJrNXNZMjVhY0ZreVZucEpSWGhOVVhwRlZF' +
+ 'MUNSVWRCTVZWRlFYaE5TMUl4VWxSSlJVNUNTVVJHUlU1RVEwTkJVMGwzUkZGWlNrdHZXa2xvZG1OT1FWRkZRa0pS' +
+ 'UVVSblowVlFRVVJEUTBGUmIwTm5aMFZDUVV0MlFYRnhVRU5GTWpkc01IYzVla000WkZSUVNVVTRPV0pCSzNoVWJV' +
+ 'UmhSemQ1TjFabVVUUmpLMjFQVjJoc1ZXVmlWVkZ3U3pCNWRqSnlOamM0VWtwRmVFc3dTRmRFYW1WeEsyNU1TVWhP' +
+ 'TVVWdE5XbzJja0ZTV21sNGJYbFNVMnBvU1ZJd1MwOVJVRWRDVFZWc1pITmhlblJKU1VvM1R6Qm5Memd5Y1dvdmRr' +
+ 'ZEViQzh2TTNRMGRGUnhlR2xTYUV4UmJsUk1XRXBrWlVJck1rUm9hMlJWTmtsSlozZzJkMDQzUlRWT1kxVklNMUpq' +
+ 'YzJWcVkzRnFPSEExVTJveE9YWkNiVFpwTVVab2NVeEhlVzFvVFVaeWIxZFdWVWRQTTNoMFNVZzVNV1J6WjNrMFpV' +
+ 'WkxZMlpMVmt4WFN6TnZNakU1TUZFd1RHMHZVMmxMYlV4aVVrbzFRWFUwZVRGbGRVWktiVEpLVFRsbFFqZzBSbXR4' +
+ 'WVROcGRuSllWMVZsVm5SNVpUQkRVV1JMZG5OWk1rWnJZWHAyZUhSNGRuVnpURXA2VEZkWlNHczFOWHBqVWtGaFkw' +
+ 'UkJNbE5sUlhSQ1lsRm1SREZ4YzBOQmQwVkJRV0ZQUTBGWVdYZG5aMFo1VFVFMFIwRXhWV1JFZDBWQ0wzZFJSVUYz' +
+ 'U1VKb2FrRmtRbWRPVmtoVFZVVkdha0ZWUW1kbmNrSm5SVVpDVVdORVFWRlpTVXQzV1VKQ1VWVklRWGRKZDBWbldV' +
+ 'UldVakJVUVZGSUwwSkJaM2RDWjBWQ0wzZEpRa0ZFUVdSQ1owNVdTRkUwUlVablVWVktaVWxaUkhKS1dHdGFVWEUx' +
+ 'WkZKa2FIQkRSRE5zVDNwMVNrbDNTSGRaUkZaU01HcENRbWQzUm05QlZUVkxPSEpLYmtWaFN6Qm5ibWhUT1ZOYWFY' +
+ 'cDJPRWxyVkdOVU5IZGhRVmxKUzNkWlFrSlJWVWhCVVVWRldFUkNZVTFEV1VkRFEzTkhRVkZWUmtKNlFVSm9hSEJ2' +
+ 'WkVoU2QwOXBPSFppTWs1NlkwTTFkMkV5YTNWYU1qbDJXbms1Ym1SSVRubE5WRUYzUW1kbmNrSm5SVVpDVVdOM1FX' +
+ 'OVphMkZJVWpCalJHOTJURE5DY21GVE5XNWlNamx1VEROS2JHTkhPSFpaTWxaNVpFaE5kbG96VW5wamFrVjFXa2RX' +
+ 'ZVUxRVVVZEJNVlZrU0hkUmRFMURjM2RMWVVGdWIwTlhSMGt5YURCa1NFRTJUSGs1YW1OdGQzVmpSM1J3VEcxa2Rt' +
+ 'SXlZM1phTTFKNlkycEZkbG96VW5wamFrVjFXVE5LYzAxRk1FZEJNVlZrU1VGU1IwMUZVWGREUVZsSFdqUkZUVUZS' +
+ 'U1VKTlJHZEhRMmx6UjBGUlVVSXhibXREUWxGTmQwdHFRVzlDWjJkeVFtZEZSa0pSWTBOQlVsbGpZVWhTTUdOSVRU' +
+ 'Wk1lVGwzWVRKcmRWb3lPWFphZVRsNVdsaENkbU15YkRCaU0wbzFUSHBCVGtKbmEzRm9hMmxIT1hjd1FrRlJjMFpC' +
+ 'UVU5RFFXZEZRVWxXVkc5NU1qUnFkMWhWY2pCeVFWQmpPVEkwZG5WVFZtSkxVWFZaZHpOdVRHWnNUR1pNYURWQldW' +
+ 'ZEZaVlpzTDBSMU1UaFJRVmRWVFdSalNqWnZMM0ZHV21Kb1dHdENTREJRVG1OM09UZDBhR0ZtTWtKbGIwUlpXVGxE' +
+ 'YXk5aUsxVkhiSFZvZURBMmVtUTBSVUptTjBnNVVEZzBibTV5ZDNCU0t6UkhRa1JhU3l0WWFETkpNSFJ4U25reWNt' +
+ 'ZFBjVTVFWm14eU5VbE5VVGhhVkZkQk0zbHNkR0ZyZWxOQ1MxbzJXSEJHTUZCd2NYbERVblp3TDA1RFIzWXlTMWd5' +
+ 'VkhWUVEwcDJjMk53TVM5dE1uQldWSFI1UW1wWlVGSlJLMUYxUTFGSFFVcExhblJPTjFJMVJFWnlabFJ4VFZkMldX' +
+ 'ZFdiSEJEU2tKcmQyeDFOeXMzUzFrelkxUkpabnBGTjJOdFFVeHphMDFMVGt4MVJIb3JVbnBEWTNOWlZITldZVlUz' +
+ 'Vm5BemVFdzJNRTlaYUhGR2EzVkJUMDk0UkZvMmNFaFBhamtyVDBwdFdXZFFiVTlVTkZnekt6ZE1OVEZtV0VwNVVr' +
+ 'ZzVTMlpNVWxBMmJsUXpNVVExYm0xelIwRlBaMW95Tmk4NFZEbG9jMEpYTVhWdk9XcDFOV1phVEZwWVZsWlROVWd3' +
+ 'U0hsSlFrMUZTM2xIVFVsUWFFWlhjbXgwTDJoR1V6STRUakY2WVV0Sk1GcENSMFF6WjFsblJFeGlhVVJVT1daSFdI' +
+ 'TjBjR3NyUm0xak5HOXNWbXhYVUhwWVpUZ3hkbVJ2Ulc1R1luSTFUVEkzTWtoa1owcFhieXRYYUZRNVFsbE5NRXBw' +
+ 'SzNka1ZtMXVVbVptV0dkc2IwVnZiSFZVVG1OWGVtTTBNV1JHY0dkS2RUaG1Sak5NUnpCbmJESnBZbE5aYVVOcE9X' +
+ 'RTJhSFpWTUZSd2NHcEtlVWxYV0doclNsUmpUVXBzVUhKWGVERldlWFJGVlVkeVdESnNNRXBFZDFKcVZ5ODJOVFp5' +
+ 'TUV0V1FqQXllRWhTUzNadE1scExTVEF6Vkdkc1RFbHdiVlpEU3pOclFrdHJTMDV3UWs1clJuUTRjbWhoWm1ORFMw' +
+ 'OWlPVXA0THpsMGNFNUdiRkZVYkRkQ016bHlTbXhLVjJ0U01UZFJibHB4Vm5CMFJtVlFSazlTYjFwdFJucE5QU0lz' +
+ 'SWsxSlNVWlpha05EUWtWeFowRjNTVUpCWjBsUlpEY3dUbUpPY3pJclVuSnhTVkV2UlRoR2FsUkVWRUZPUW1kcmNX' +
+ 'aHJhVWM1ZHpCQ1FWRnpSa0ZFUWxoTlVYTjNRMUZaUkZaUlVVZEZkMHBEVWxSRldrMUNZMGRCTVZWRlEyaE5VVkl5' +
+ 'ZUhaWmJVWnpWVEpzYm1KcFFuVmthVEY2V1ZSRlVVMUJORWRCTVZWRlEzaE5TRlZ0T1haa1EwSkVVVlJGWWsxQ2Ew' +
+ 'ZEJNVlZGUVhoTlUxSXllSFpaYlVaelZUSnNibUpwUWxOaU1qa3dTVVZPUWsxQ05GaEVWRWwzVFVSWmVFOVVRWGRO' +
+ 'UkVFd1RXeHZXRVJVU1RSTlJFVjVUMFJCZDAxRVFUQk5iRzkzVW5wRlRFMUJhMGRCTVZWRlFtaE5RMVpXVFhoSmFr' +
+ 'Rm5RbWRPVmtKQmIxUkhWV1IyWWpKa2MxcFRRbFZqYmxaNlpFTkNWRnBZU2pKaFYwNXNZM2xDVFZSRlRYaEdSRUZU' +
+ 'UW1kT1ZrSkJUVlJETUdSVlZYbENVMkl5T1RCSlJrbDRUVWxKUTBscVFVNUNaMnR4YUd0cFJ6bDNNRUpCVVVWR1FV' +
+ 'RlBRMEZuT0VGTlNVbERRMmRMUTBGblJVRjBhRVZEYVhnM2FtOVlaV0pQT1hrdmJFUTJNMnhoWkVGUVMwZzVaM1pz' +
+ 'T1UxbllVTmpabUl5YWtndk56Wk9kVGhoYVRaWWJEWlBUVk12YTNJNWNrZzFlbTlSWkhObWJrWnNPVGQyZFdaTGFq' +
+ 'WmlkMU5wVmpadWNXeExjaXREVFc1NU5sTjRia2RRWWpFMWJDczRRWEJsTmpKcGJUbE5XbUZTZHpGT1JVUlFhbFJ5' +
+ 'UlZSdk9HZFpZa1YyY3k5QmJWRXpOVEZyUzFOVmFrSTJSekF3YWpCMVdVOUVVREJuYlVoMU9ERkpPRVV6UTNkdWNV' +
+ 'bHBjblUyZWpGcldqRnhLMUJ6UVdWM2JtcEllR2R6U0VFemVUWnRZbGQzV2tSeVdGbG1hVmxoVWxGTk9YTkliV3Rz' +
+ 'UTJsMFJETTRiVFZoWjBrdmNHSnZVRWRwVlZVck5rUlBiMmR5UmxwWlNuTjFRalpxUXpVeE1YQjZjbkF4V210cU5W' +
+ 'cFFZVXMwT1d3NFMwVnFPRU00VVUxQlRGaE1NekpvTjAweFlrdDNXVlZJSzBVMFJYcE9hM1JOWnpaVVR6aFZjRzEy' +
+ 'VFhKVmNITjVWWEYwUldvMVkzVklTMXBRWm0xbmFFTk9Oa296UTJsdmFqWlBSMkZMTDBkUU5VRm1iRFF2V0hSalpD' +
+ 'OXdNbWd2Y25Nek4wVlBaVnBXV0hSTU1HMDNPVmxDTUdWelYwTnlkVTlETjFoR2VGbHdWbkU1VDNNMmNFWk1TMk4z' +
+ 'V25CRVNXeFVhWEo0V2xWVVVVRnpObkY2YTIwd05uQTVPR2MzUWtGbEsyUkVjVFprYzI4ME9UbHBXVWcyVkV0WUx6' +
+ 'RlpOMFI2YTNabmRHUnBlbXByV0ZCa2MwUjBVVU4yT1ZWM0szZHdPVlUzUkdKSFMyOW5VR1ZOWVROTlpDdHdkbVY2' +
+ 'TjFjek5VVnBSWFZoS3l0MFoza3ZRa0pxUmtaR2VUTnNNMWRHY0U4NVMxZG5lamQ2Y0cwM1FXVkxTblE0VkRFeFpH' +
+ 'eGxRMlpsV0d0clZVRkxTVUZtTlhGdlNXSmhjSE5hVjNkd1ltdE9SbWhJWVhneWVFbFFSVVJuWm1jeFlYcFdXVGd3' +
+ 'V21OR2RXTjBURGRVYkV4dVRWRXZNR3hWVkdKcFUzY3hia2cyT1UxSE5ucFBNR0k1WmpaQ1VXUm5RVzFFTURaNVN6' +
+ 'VTJiVVJqV1VKYVZVTkJkMFZCUVdGUFEwRlVaM2RuWjBVd1RVRTBSMEV4VldSRWQwVkNMM2RSUlVGM1NVSm9ha0ZR' +
+ 'UW1kT1ZraFNUVUpCWmpoRlFsUkJSRUZSU0M5TlFqQkhRVEZWWkVSblVWZENRbFJyY25semJXTlNiM0pUUTJWR1RE' +
+ 'RktiVXhQTDNkcFVrNTRVR3BCWmtKblRsWklVMDFGUjBSQlYyZENVbWRsTWxsaFVsRXlXSGx2YkZGTU16QkZlbFJU' +
+ 'Ynk4dmVqbFRla0puUW1kbmNrSm5SVVpDVVdOQ1FWRlNWVTFHU1hkS1VWbEpTM2RaUWtKUlZVaE5RVWRIUjFkb01H' +
+ 'UklRVFpNZVRsMldUTk9kMHh1UW5KaFV6VnVZakk1Ymt3eVpIcGpha1YzUzFGWlNVdDNXVUpDVVZWSVRVRkxSMGhY' +
+ 'YURCa1NFRTJUSGs1ZDJFeWEzVmFNamwyV25rNWJtTXpTWGhNTW1SNlkycEZkVmt6U2pCTlJFbEhRVEZWWkVoM1VY' +
+ 'Sk5RMnQzU2paQmJHOURUMGRKVjJnd1pFaEJOa3g1T1dwamJYZDFZMGQwY0V4dFpIWmlNbU4yV2pOT2VVMVRPVzVq' +
+ 'TTBsNFRHMU9lV0pFUVRkQ1owNVdTRk5CUlU1RVFYbE5RV2RIUW0xbFFrUkJSVU5CVkVGSlFtZGFibWRSZDBKQlow' +
+ 'bDNSRkZaVEV0M1dVSkNRVWhYWlZGSlJrRjNTWGRFVVZsTVMzZFpRa0pCU0ZkbFVVbEdRWGROZDBSUldVcExiMXBK' +
+ 'YUhaalRrRlJSVXhDVVVGRVoyZEZRa0ZFVTJ0SWNrVnZiemxETUdSb1pXMU5XRzlvTm1SR1UxQnphbUprUWxwQ2FV' +
+ 'eG5PVTVTTTNRMVVDdFVORlo0Wm5FM2RuRm1UUzlpTlVFelVta3habmxLYlRsaWRtaGtSMkZLVVROaU1uUTJlVTFC' +
+ 'V1U0dmIyeFZZWHB6WVV3cmVYbEZiamxYY0hKTFFWTlBjMmhKUVhKQmIzbGFiQ3QwU21GdmVERXhPR1psYzNOdFdH' +
+ 'NHhhRWxXZHpReGIyVlJZVEYyTVhabk5FWjJOelI2VUd3MkwwRm9VM0ozT1ZVMWNFTmFSWFEwVjJrMGQxTjBlalpr' +
+ 'VkZvdlEweEJUbmc0VEZwb01VbzNVVXBXYWpKbWFFMTBabFJLY2psM05Ib3pNRm95TURsbVQxVXdhVTlOZVN0eFpI' +
+ 'VkNiWEIyZGxsMVVqZG9Xa3cyUkhWd2MzcG1ibmN3VTJ0bWRHaHpNVGhrUnpsYVMySTFPVlZvZG0xaFUwZGFVbFpp' +
+ 'VGxGd2MyY3pRbHBzZG1sa01HeEpTMDh5WkRGNGIzcGpiRTk2WjJwWVVGbHZka3BLU1hWc2RIcHJUWFV6TkhGUllq' +
+ 'bFRlaTk1YVd4eVlrTm5hamc5SWwxOS5leUp1YjI1alpTSTZJbTlWY0RrMlRUbE1ialpEWVN0alRGZzRaa3hqYTI1' +
+ 'bGFHMTVNMW8xTkZNNFEwOVVkbGc1Vm1zeEswazlJaXdpZEdsdFpYTjBZVzF3VFhNaU9qRTJNamMyTkRnNE1UUTFO' +
+ 'amdzSW1Gd2ExQmhZMnRoWjJWT1lXMWxJam9pWTI5dExtZHZiMmRzWlM1aGJtUnliMmxrTG1kdGN5SXNJbUZ3YTBS' +
+ 'cFoyVnpkRk5vWVRJMU5pSTZJbFY0ZFRWcFVYa3lObEZoY1ZoU2IwcG1NMHcwY0ZSQksyNU1jbGxTWmxkMFlYSjRh' +
+ 'WEJSYzA1Q1pXczlJaXdpWTNSelVISnZabWxzWlUxaGRHTm9JanAwY25WbExDSmhjR3REWlhKMGFXWnBZMkYwWlVS' +
+ 'cFoyVnpkRk5vWVRJMU5pSTZXeUk0VURGelZ6QkZVRXBqYzJ4M04xVjZVbk5wV0V3Mk5IY3JUelV3UldRclVrSkpR' +
+ 'M1JoZVRGbk1qUk5QU0pkTENKaVlYTnBZMGx1ZEdWbmNtbDBlU0k2ZEhKMVpTd2laWFpoYkhWaGRHbHZibFI1Y0dV' +
+ 'aU9pSkNRVk5KUXl4SVFWSkVWMEZTUlY5Q1FVTkxSVVFpZlEuT0ZIY2NSTGlXOFB5VGhxeXJ5X0J4SzlBeDNqODNn' +
+ 'OVdFT2ZKdU5SeUctWnFfRVdtdkU2RS1sYWNFQWJlRzFNZV9Ib1JkS2tkMktYbWpkMU5lOWx4ampuRUZWZFJwaUt5' +
+ 'T1F0bFMyR2RnQnZRWEVoWEM1WDlBdDA0WGFyQkctVHlpOUNhX2lTLXRiNV9rcXNqYmFjVWRqSTN4RUI5YVdQTHF5' +
+ 'M3lPX3JFM1JFTDZIVlU5bE9XQWtfbE5qdkozU3dXQkthNVZwVDZOclZuMEp1UkFuZ2tYVmRjS1JlaVpKbFdaNW9j' +
+ 'V1l4ajgxY2ZYX2xPR29FM3ozZEtheG44U0ZNNTlVLTVUQm5Gdl9NTzBFRVUwVXJpSDhmQlp6UmdGSHFoUlNvRGs2' +
+ 'UmF1aUh0a0JjZjhRVkJ4TURwVXdFd25qOWc0OUVLSkFwVWtqcjZxcFpxdXRfcFBBaGF1dGhEYXRhWMVJlg3liA6M' +
+ 'aHQ0Fw9kdmBbj-SuuaKGMseZXPO6gx2XY0UAAAAAuT_ZYfLmRi-xIoIAIkfeeABBAQsMmnEQ8OxpZxijXBMT4tya' +
+ 'mgkqC_3hr18_e8KeK8nG69ijcTaXNKX_CRmYiW0fegPE0N_3NVHEaj_kit7LPNOlAQIDJiABIVggxf5sshpkLLen' +
+ '92NUd9sRVM1fVR6FRFZY_P7fnCq3crgiWCALN83GhRoAD4faTpk1bp7bGclHRleO922RvPUpSnBb-w',
+ clientDataJSON:
+ 'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiQUhOWlE1WWFoZVpZOF9lYXdvM0VITHlXdjhCemlqaXFzQlVlNDZ2LVFTZyIsIm9yaWdpbiI6Imh0dHA6XC9cL2xvY2FsaG9zdDo0MjAwIiwiYW5kcm9pZFBhY2thZ2VOYW1lIjoiY29tLmFuZHJvaWQuY2hyb21lIn0',
+ },
+ type: 'public-key',
+ clientExtensionResults: {},
+ transports: [],
+};
diff --git a/packages/server/src/attestation/verifications/verifyAndroidSafetyNet.ts b/packages/server/src/attestation/verifications/verifyAndroidSafetyNet.ts
index 6c0a5c8..85eaba9 100644
--- a/packages/server/src/attestation/verifications/verifyAndroidSafetyNet.ts
+++ b/packages/server/src/attestation/verifications/verifyAndroidSafetyNet.ts
@@ -1,30 +1,29 @@
import base64url from 'base64url';
-import type { AttestationStatement } from '../../helpers/decodeAttestationObject';
+import type { AttestationFormatVerifierOpts } from '../verifyAttestationResponse';
import toHash from '../../helpers/toHash';
import verifySignature from '../../helpers/verifySignature';
import getCertificateInfo from '../../helpers/getCertificateInfo';
import validateCertificatePath from '../../helpers/validateCertificatePath';
-import convertX509CertToPEM from '../../helpers/convertX509CertToPEM';
-import MetadataService from '../../metadata/metadataService';
+import convertCertBufferToPEM from '../../helpers/convertCertBufferToPEM';
+import MetadataService from '../../services/metadataService';
import verifyAttestationWithMetadata from '../../metadata/verifyAttestationWithMetadata';
-type Options = {
- attStmt: AttestationStatement;
- clientDataHash: Buffer;
- authData: Buffer;
- aaguid: Buffer;
- verifyTimestampMS?: boolean;
-};
-
/**
* Verify an attestation response with fmt 'android-safetynet'
*/
export default async function verifyAttestationAndroidSafetyNet(
- options: Options,
+ options: AttestationFormatVerifierOpts,
): Promise<boolean> {
- const { attStmt, clientDataHash, authData, aaguid, verifyTimestampMS = true } = options;
+ const {
+ attStmt,
+ clientDataHash,
+ authData,
+ aaguid,
+ rootCertificates,
+ verifyTimestampMS = true,
+ } = options;
const { response, ver } = attStmt;
if (!ver) {
@@ -102,11 +101,9 @@ export default async function verifyAttestationAndroidSafetyNet(
throw new Error(`${err.message} (SafetyNet)`);
}
} else {
- // Validate certificate path using a fixed global root cert
- const path = HEADER.x5c.concat([GlobalSignRootCAR2]).map(convertX509CertToPEM);
-
try {
- await validateCertificatePath(path);
+ // Try validating the certificate path using the root certificates set via SettingsService
+ await validateCertificatePath(HEADER.x5c.map(convertCertBufferToPEM), rootCertificates);
} catch (err) {
throw new Error(`${err.message} (SafetyNet)`);
}
@@ -121,7 +118,7 @@ export default async function verifyAttestationAndroidSafetyNet(
const signatureBaseBuffer = Buffer.from(`${jwtParts[0]}.${jwtParts[1]}`);
const signatureBuffer = base64url.toBuffer(SIGNATURE);
- const leafCertPEM = convertX509CertToPEM(leafCertBuffer);
+ const leafCertPEM = convertCertBufferToPEM(leafCertBuffer);
const verified = verifySignature(signatureBuffer, signatureBaseBuffer, leafCertPEM);
/**
* END Verify Signature
@@ -130,28 +127,6 @@ export default async function verifyAttestationAndroidSafetyNet(
return verified;
}
-/**
- * This "GS Root R2" root certificate was downloaded from https://pki.goog/gsr2/GSR2.crt
- * on 08/10/2019 and then run through `base64url.encode()` to get this representation.
- *
- * The certificate is valid until Dec 15, 2021
- */
-const GlobalSignRootCAR2 =
- 'MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4GA1UEC' +
- 'xMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhc' +
- 'NMDYxMjE1MDgwMDAwWhcNMjExMjE1MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEGA' +
- '1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKb' +
- 'PJA6-Lm8omUVCxKs-IVSbC9N_hHD6ErPLv4dfxn-G07IwXNb9rfF73OX4YJYJkhD10FPe-3t-c4isUoh7SqbKSaZeqKeMW' +
- 'hG8eoLrvozps6yWJQeXSpkqBy-0Hne_ig-1AnwblrjFuTosvNYSuetZfeLQBoZfXklqtTleiDTsvHgMCJiEbKjNS7SgfQx' +
- '5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzdC9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ_gk' +
- 'wpRl4pazq-r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCBmTAOBgNVHQ8BAf8EBAMCAQY' +
- 'wDwYDVR0TAQH_BAUwAwEB_zAdBgNVHQ4EFgQUm-IHV2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0c' +
- 'DovL2NybC5nbG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG3lm0mi3f3BmGLjANBgk' +
- 'qhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4GsJ0_WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk' +
- '7mpM0sYmsL4h4hO291xNBrBVNpGP-DTKqttVCL1OmLNIG-6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavSot-3i9DAgBkcRcA' +
- 'tjOj4LaR0VknFBbVPFd5uRHg5h6h-u_N5GJG79G-dwfCMNYxdAfvDbbnvRG15RjF-Cv6pgsH_76tuIMRQyV-dTZsXjAzlA' +
- 'cmgQWpzU_qlULRuJQ_7TBj0_VLZjmmx6BEP3ojY-x1J96relc8geMJgEtslQIxq_H5COEBkEveegeGTLg';
-
type SafetyNetJWTHeader = {
alg: string;
x5c: string[];
diff --git a/packages/server/src/attestation/verifications/verifyApple.test.ts b/packages/server/src/attestation/verifications/verifyApple.test.ts
index 79ac6c6..6ba0a5e 100644
--- a/packages/server/src/attestation/verifications/verifyApple.test.ts
+++ b/packages/server/src/attestation/verifications/verifyApple.test.ts
@@ -1,6 +1,7 @@
-import verifyAttestationResponse from '../verifyAttestationResponse';
import base64url from 'base64url';
+import verifyAttestationResponse from '../verifyAttestationResponse';
+
test('should verify Apple attestation', async () => {
const expectedChallenge = 'h5xSyIRMx2IQPr1mQk6GD98XSQOBHgMHVpJIkMV9Nkc';
jest.spyOn(base64url, 'encode').mockReturnValueOnce(expectedChallenge);
diff --git a/packages/server/src/attestation/verifications/verifyApple.ts b/packages/server/src/attestation/verifications/verifyApple.ts
index 419db74..d0c3059 100644
--- a/packages/server/src/attestation/verifications/verifyApple.ts
+++ b/packages/server/src/attestation/verifications/verifyApple.ts
@@ -1,21 +1,17 @@
import { AsnParser } from '@peculiar/asn1-schema';
import { Certificate } from '@peculiar/asn1-x509';
-import type { AttestationStatement } from '../../helpers/decodeAttestationObject';
+import type { AttestationFormatVerifierOpts } from '../verifyAttestationResponse';
+
import validateCertificatePath from '../../helpers/validateCertificatePath';
-import convertX509CertToPEM from '../../helpers/convertX509CertToPEM';
+import convertCertBufferToPEM from '../../helpers/convertCertBufferToPEM';
import toHash from '../../helpers/toHash';
import convertCOSEtoPKCS from '../../helpers/convertCOSEtoPKCS';
-type Options = {
- attStmt: AttestationStatement;
- authData: Buffer;
- clientDataHash: Buffer;
- credentialPublicKey: Buffer;
-};
-
-export default async function verifyApple(options: Options): Promise<boolean> {
- const { attStmt, authData, clientDataHash, credentialPublicKey } = options;
+export default async function verifyApple(
+ options: AttestationFormatVerifierOpts,
+): Promise<boolean> {
+ const { attStmt, authData, clientDataHash, credentialPublicKey, rootCertificates } = options;
const { x5c } = attStmt;
if (!x5c) {
@@ -25,11 +21,8 @@ export default async function verifyApple(options: Options): Promise<boolean> {
/**
* Verify certificate path
*/
- const certPath = x5c.map(convertX509CertToPEM);
- certPath.push(AppleWebAuthnRootCertificate);
-
try {
- await validateCertificatePath(certPath);
+ await validateCertificatePath(x5c.map(convertCertBufferToPEM), rootCertificates);
} catch (err) {
throw new Error(`${err.message} (Apple)`);
}
@@ -77,25 +70,3 @@ export default async function verifyApple(options: Options): Promise<boolean> {
return true;
}
-
-/**
- * Apple WebAuthn Root CA PEM
- *
- * Downloaded from https://www.apple.com/certificateauthority/Apple_WebAuthn_Root_CA.pem
- *
- * Valid until 03/14/2045 @ 5:00 PM PST
- */
-const AppleWebAuthnRootCertificate = `-----BEGIN CERTIFICATE-----
-MIICEjCCAZmgAwIBAgIQaB0BbHo84wIlpQGUKEdXcTAKBggqhkjOPQQDAzBLMR8w
-HQYDVQQDDBZBcHBsZSBXZWJBdXRobiBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJ
-bmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMB4XDTIwMDMxODE4MjEzMloXDTQ1MDMx
-NTAwMDAwMFowSzEfMB0GA1UEAwwWQXBwbGUgV2ViQXV0aG4gUm9vdCBDQTETMBEG
-A1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTB2MBAGByqGSM49
-AgEGBSuBBAAiA2IABCJCQ2pTVhzjl4Wo6IhHtMSAzO2cv+H9DQKev3//fG59G11k
-xu9eI0/7o6V5uShBpe1u6l6mS19S1FEh6yGljnZAJ+2GNP1mi/YK2kSXIuTHjxA/
-pcoRf7XkOtO4o1qlcaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUJtdk
-2cV4wlpn0afeaxLQG2PxxtcwDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA
-MGQCMFrZ+9DsJ1PW9hfNdBywZDsWDbWFp28it1d/5w2RPkRX3Bbn/UbDTNLx7Jr3
-jAGGiQIwHFj+dJZYUJR786osByBelJYsVZd2GbHQu209b5RCmGQ21gpSAk9QZW4B
-1bWeT0vT
------END CERTIFICATE-----`;
diff --git a/packages/server/src/attestation/verifications/verifyFIDOU2F.ts b/packages/server/src/attestation/verifications/verifyFIDOU2F.ts
index 40367d6..a2bfd53 100644
--- a/packages/server/src/attestation/verifications/verifyFIDOU2F.ts
+++ b/packages/server/src/attestation/verifications/verifyFIDOU2F.ts
@@ -1,22 +1,16 @@
-import type { AttestationStatement } from '../../helpers/decodeAttestationObject';
+import type { AttestationFormatVerifierOpts } from '../verifyAttestationResponse';
import convertCOSEtoPKCS from '../../helpers/convertCOSEtoPKCS';
-import convertX509CertToPEM from '../../helpers/convertX509CertToPEM';
+import convertCertBufferToPEM from '../../helpers/convertCertBufferToPEM';
+import validateCertificatePath from '../../helpers/validateCertificatePath';
import verifySignature from '../../helpers/verifySignature';
-type Options = {
- attStmt: AttestationStatement;
- clientDataHash: Buffer;
- rpIdHash: Buffer;
- credentialID: Buffer;
- credentialPublicKey: Buffer;
- aaguid: Buffer;
-};
-
/**
* Verify an attestation response with fmt 'fido-u2f'
*/
-export default function verifyAttestationFIDOU2F(options: Options): boolean {
+export default async function verifyAttestationFIDOU2F(
+ options: AttestationFormatVerifierOpts,
+): Promise<boolean> {
const {
attStmt,
clientDataHash,
@@ -24,6 +18,7 @@ export default function verifyAttestationFIDOU2F(options: Options): boolean {
credentialID,
credentialPublicKey,
aaguid = '',
+ rootCertificates,
} = options;
const reservedByte = Buffer.from([0x00]);
@@ -53,7 +48,14 @@ export default function verifyAttestationFIDOU2F(options: Options): boolean {
throw new Error(`AAGUID "${aaguidToHex}" was not expected value`);
}
- const leafCertPEM = convertX509CertToPEM(x5c[0]);
+ try {
+ // Try validating the certificate path using the root certificates set via SettingsService
+ await validateCertificatePath(x5c.map(convertCertBufferToPEM), rootCertificates);
+ } catch (err) {
+ throw new Error(`${err.message} (FIDOU2F)`);
+ }
+
+ const leafCertPEM = convertCertBufferToPEM(x5c[0]);
return verifySignature(sig, signatureBase, leafCertPEM);
}
diff --git a/packages/server/src/attestation/verifications/verifyPacked.ts b/packages/server/src/attestation/verifications/verifyPacked.ts
index 3068bbb..dd876c2 100644
--- a/packages/server/src/attestation/verifications/verifyPacked.ts
+++ b/packages/server/src/attestation/verifications/verifyPacked.ts
@@ -1,7 +1,8 @@
import elliptic from 'elliptic';
import NodeRSA from 'node-rsa';
-import type { AttestationStatement } from '../../helpers/decodeAttestationObject';
+import type { AttestationFormatVerifierOpts } from '../verifyAttestationResponse';
+
import convertCOSEtoPKCS, {
COSEKEYS,
COSEALGHASH,
@@ -11,26 +12,22 @@ import convertCOSEtoPKCS, {
} from '../../helpers/convertCOSEtoPKCS';
import { FIDO_METADATA_ATTESTATION_TYPES } from '../../helpers/constants';
import toHash from '../../helpers/toHash';
-import convertX509CertToPEM from '../../helpers/convertX509CertToPEM';
+import convertCertBufferToPEM from '../../helpers/convertCertBufferToPEM';
+import validateCertificatePath from '../../helpers/validateCertificatePath';
import getCertificateInfo from '../../helpers/getCertificateInfo';
import verifySignature from '../../helpers/verifySignature';
import decodeCredentialPublicKey from '../../helpers/decodeCredentialPublicKey';
-import MetadataService from '../../metadata/metadataService';
+import MetadataService from '../../services/metadataService';
import verifyAttestationWithMetadata from '../../metadata/verifyAttestationWithMetadata';
-type Options = {
- attStmt: AttestationStatement;
- clientDataHash: Buffer;
- authData: Buffer;
- credentialPublicKey: Buffer;
- aaguid: Buffer;
-};
-
/**
* Verify an attestation response with fmt 'packed'
*/
-export default async function verifyAttestationPacked(options: Options): Promise<boolean> {
- const { attStmt, clientDataHash, authData, credentialPublicKey, aaguid } = options;
+export default async function verifyAttestationPacked(
+ options: AttestationFormatVerifierOpts,
+): Promise<boolean> {
+ const { attStmt, clientDataHash, authData, credentialPublicKey, aaguid, rootCertificates } =
+ options;
const { sig, x5c, alg } = attStmt;
@@ -48,7 +45,7 @@ export default async function verifyAttestationPacked(options: Options): Promise
const pkcsPublicKey = convertCOSEtoPKCS(credentialPublicKey);
if (x5c) {
- const leafCert = convertX509CertToPEM(x5c[0]);
+ const leafCert = convertCertBufferToPEM(x5c[0]);
const { subject, basicConstraintsCA, version, notBefore, notAfter } = getCertificateInfo(
x5c[0],
);
@@ -109,6 +106,13 @@ export default async function verifyAttestationPacked(options: Options): Promise
} catch (err) {
throw new Error(`${err.message} (Packed|Full)`);
}
+ } else {
+ try {
+ // Try validating the certificate path using the root certificates set via SettingsService
+ await validateCertificatePath(x5c.map(convertCertBufferToPEM), rootCertificates);
+ } catch (err) {
+ throw new Error(`${err.message} (Packed|Full)`);
+ }
}
verified = verifySignature(sig, signatureBase, leafCert);
diff --git a/packages/server/src/attestation/verifyAttestationResponse.test.ts b/packages/server/src/attestation/verifyAttestationResponse.test.ts
index 96e00db..edff91b 100644
--- a/packages/server/src/attestation/verifyAttestationResponse.test.ts
+++ b/packages/server/src/attestation/verifyAttestationResponse.test.ts
@@ -6,12 +6,19 @@ import * as decodeAttestationObject from '../helpers/decodeAttestationObject';
import * as decodeClientDataJSON from '../helpers/decodeClientDataJSON';
import * as parseAuthenticatorData from '../helpers/parseAuthenticatorData';
import * as decodeCredentialPublicKey from '../helpers/decodeCredentialPublicKey';
+import SettingsService from '../services/settingsService';
import * as verifyFIDOU2F from './verifications/verifyFIDOU2F';
import toHash from '../helpers/toHash';
import { AttestationCredentialJSON } 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({ attestationFormat: 'android-key', certificates: [] });
+
let mockDecodeAttestation: jest.SpyInstance;
let mockDecodeClientData: jest.SpyInstance;
let mockParseAuthData: jest.SpyInstance;
diff --git a/packages/server/src/attestation/verifyAttestationResponse.ts b/packages/server/src/attestation/verifyAttestationResponse.ts
index 0dc200f..726acec 100644
--- a/packages/server/src/attestation/verifyAttestationResponse.ts
+++ b/packages/server/src/attestation/verifyAttestationResponse.ts
@@ -4,13 +4,17 @@ import {
COSEAlgorithmIdentifier,
} from '@simplewebauthn/typescript-types';
-import decodeAttestationObject, { ATTESTATION_FORMAT } from '../helpers/decodeAttestationObject';
+import decodeAttestationObject, {
+ AttestationFormat,
+ AttestationStatement,
+} from '../helpers/decodeAttestationObject';
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 convertAAGUIDToString from '../helpers/convertAAGUIDToString';
+import settingsService from '../services/settingsService';
import { supportedCOSEAlgorithmIdentifiers } from './generateAttestationOptions';
import verifyFIDOU2F from './verifications/verifyFIDOU2F';
@@ -174,59 +178,37 @@ export default async function verifyAttestationResponse(
}
const clientDataHash = toHash(base64url.toBuffer(response.clientDataJSON));
+ const rootCertificates = settingsService.getRootCertificates({ attestationFormat: fmt });
+
+ // Prepare arguments to pass to the relevant verification method
+ const verifierOpts: AttestationFormatVerifierOpts = {
+ aaguid,
+ attStmt,
+ authData,
+ clientDataHash,
+ credentialID,
+ credentialPublicKey,
+ rootCertificates,
+ rpIdHash,
+ };
/**
* Verification can only be performed when attestation = 'direct'
*/
let verified = false;
- if (fmt === ATTESTATION_FORMAT.FIDO_U2F) {
- verified = verifyFIDOU2F({
- attStmt,
- clientDataHash,
- credentialID,
- credentialPublicKey,
- rpIdHash,
- aaguid,
- });
- } else if (fmt === ATTESTATION_FORMAT.PACKED) {
- verified = await verifyPacked({
- attStmt,
- authData,
- clientDataHash,
- credentialPublicKey,
- aaguid,
- });
- } else if (fmt === ATTESTATION_FORMAT.ANDROID_SAFETYNET) {
- verified = await verifyAndroidSafetynet({
- attStmt,
- authData,
- clientDataHash,
- aaguid,
- });
- } else if (fmt === ATTESTATION_FORMAT.ANDROID_KEY) {
- verified = await verifyAndroidKey({
- attStmt,
- authData,
- clientDataHash,
- credentialPublicKey,
- aaguid,
- });
- } else if (fmt === ATTESTATION_FORMAT.TPM) {
- verified = await verifyTPM({
- aaguid,
- attStmt,
- authData,
- credentialPublicKey,
- clientDataHash,
- });
- } else if (fmt === ATTESTATION_FORMAT.APPLE) {
- verified = await verifyApple({
- attStmt,
- authData,
- clientDataHash,
- credentialPublicKey,
- });
- } else if (fmt === ATTESTATION_FORMAT.NONE) {
+ if (fmt === 'fido-u2f') {
+ verified = await verifyFIDOU2F(verifierOpts);
+ } else if (fmt === 'packed') {
+ verified = await verifyPacked(verifierOpts);
+ } else if (fmt === 'android-safetynet') {
+ verified = await verifyAndroidSafetynet(verifierOpts);
+ } else if (fmt === 'android-key') {
+ verified = await verifyAndroidKey(verifierOpts);
+ } else if (fmt === 'tpm') {
+ verified = await verifyTPM(verifierOpts);
+ } else if (fmt === 'apple') {
+ verified = await verifyApple(verifierOpts);
+ } else if (fmt === 'none') {
if (Object.keys(attStmt).length > 0) {
throw new Error('None attestation had unexpected attestation statement');
}
@@ -275,7 +257,7 @@ export default async function verifyAttestationResponse(
export type VerifiedAttestation = {
verified: boolean;
attestationInfo?: {
- fmt: ATTESTATION_FORMAT;
+ fmt: AttestationFormat;
counter: number;
aaguid: string;
credentialPublicKey: Buffer;
@@ -285,3 +267,18 @@ export type VerifiedAttestation = {
attestationObject: Buffer;
};
};
+
+/**
+ * Values passed to all attestation format verifiers, from which they are free to use as they please
+ */
+export type AttestationFormatVerifierOpts = {
+ aaguid: Buffer;
+ attStmt: AttestationStatement;
+ authData: Buffer;
+ clientDataHash: Buffer;
+ credentialID: Buffer;
+ credentialPublicKey: Buffer;
+ rootCertificates: string[];
+ rpIdHash: Buffer;
+ verifyTimestampMS?: boolean;
+};
diff --git a/packages/server/src/helpers/convertX509CertToPEM.ts b/packages/server/src/helpers/convertCertBufferToPEM.ts
index 74fa157..e02a4c3 100644
--- a/packages/server/src/helpers/convertX509CertToPEM.ts
+++ b/packages/server/src/helpers/convertCertBufferToPEM.ts
@@ -2,9 +2,9 @@ import base64url from 'base64url';
import type { Base64URLString } from '@simplewebauthn/typescript-types';
/**
- * Convert X.509 certificate to an OpenSSL-compatible PEM text format.
+ * Convert buffer to an OpenSSL-compatible PEM text format.
*/
-export default function convertX509CertToPEM(certBuffer: Buffer | Base64URLString): string {
+export default function convertCertBufferToPEM(certBuffer: Buffer | Base64URLString): string {
let buffer: Buffer;
if (typeof certBuffer === 'string') {
buffer = base64url.toBuffer(certBuffer);
diff --git a/packages/server/src/helpers/decodeAttestationObject.ts b/packages/server/src/helpers/decodeAttestationObject.ts
index 8b69c90..3aa39d7 100644
--- a/packages/server/src/helpers/decodeAttestationObject.ts
+++ b/packages/server/src/helpers/decodeAttestationObject.ts
@@ -10,18 +10,17 @@ export default function decodeAttestationObject(attestationObject: Buffer): Atte
return toCBOR;
}
-export enum ATTESTATION_FORMAT {
- FIDO_U2F = 'fido-u2f',
- PACKED = 'packed',
- ANDROID_SAFETYNET = 'android-safetynet',
- ANDROID_KEY = 'android-key',
- TPM = 'tpm',
- APPLE = 'apple',
- NONE = 'none',
-}
+export type AttestationFormat =
+ | 'fido-u2f'
+ | 'packed'
+ | 'android-safetynet'
+ | 'android-key'
+ | 'tpm'
+ | 'apple'
+ | 'none';
export type AttestationObject = {
- fmt: ATTESTATION_FORMAT;
+ fmt: AttestationFormat;
attStmt: AttestationStatement;
authData: Buffer;
};
diff --git a/packages/server/src/helpers/validateCertificatePath.ts b/packages/server/src/helpers/validateCertificatePath.ts
index 92403e6..ae8a2fd 100644
--- a/packages/server/src/helpers/validateCertificatePath.ts
+++ b/packages/server/src/helpers/validateCertificatePath.ts
@@ -10,8 +10,44 @@ const { crypto } = KJUR;
/**
* Traverse an array of PEM certificates and ensure they form a proper chain
* @param certificates Typically the result of `x5c.map(convertASN1toPEM)`
+ * @param rootCertificates Possible root certificates to complete the path
*/
-export default async function validateCertificatePath(certificates: string[]): Promise<boolean> {
+export default async function validateCertificatePath(
+ certificates: string[],
+ rootCertificates: string[] = [],
+): Promise<boolean> {
+ if (rootCertificates.length === 0) {
+ // We have no root certs with which to create a full path, so skip path validation
+ // TODO: Is this going to be acceptable default behavior??
+ return true;
+ }
+
+ let invalidSubjectAndIssuerError = false;
+ for (const rootCert of rootCertificates) {
+ try {
+ const certsWithRoot = certificates.concat([rootCert]);
+ await _validatePath(certsWithRoot);
+ // If we successfully validated a path then there's no need to continue
+ invalidSubjectAndIssuerError = false;
+ break;
+ } catch (err) {
+ if (err instanceof InvalidSubjectAndIssuer) {
+ invalidSubjectAndIssuerError = true;
+ } else {
+ throw err;
+ }
+ }
+ }
+
+ // We tried multiple root certs and none of them worked
+ if (invalidSubjectAndIssuerError) {
+ throw new InvalidSubjectAndIssuer();
+ }
+
+ return true;
+}
+
+async function _validatePath(certificates: string[]): Promise<boolean> {
if (new Set(certificates).size !== certificates.length) {
throw new Error('Invalid certificate path: found duplicate certificates');
}
@@ -50,7 +86,7 @@ export default async function validateCertificatePath(certificates: string[]): P
}
if (subjectCert.getIssuerString() !== issuerCert.getSubjectString()) {
- throw new Error('Invalid certificate path: subject issuer did not match issuer subject');
+ throw new InvalidSubjectAndIssuer();
}
const subjectCertStruct = ASN1HEX.getTLVbyList(subjectCert.hex, 0, [0]);
@@ -68,3 +104,12 @@ export default async function validateCertificatePath(certificates: string[]): P
return true;
}
+
+// Custom errors to help pass on certain errors
+class InvalidSubjectAndIssuer extends Error {
+ constructor() {
+ const message = 'Subject issuer did not match issuer subject';
+ super(message);
+ this.name = 'InvalidSubjectAndIssuer';
+ }
+}
diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts
index 1a24c63..0e191b4 100644
--- a/packages/server/src/index.ts
+++ b/packages/server/src/index.ts
@@ -6,7 +6,8 @@ import generateAttestationOptions from './attestation/generateAttestationOptions
import verifyAttestationResponse from './attestation/verifyAttestationResponse';
import generateAssertionOptions from './assertion/generateAssertionOptions';
import verifyAssertionResponse from './assertion/verifyAssertionResponse';
-import MetadataService from './metadata/metadataService';
+import MetadataService from './services/metadataService';
+import SettingsService from './services/settingsService';
export {
generateAttestationOptions,
@@ -14,10 +15,12 @@ export {
generateAssertionOptions,
verifyAssertionResponse,
MetadataService,
+ SettingsService,
};
import type { GenerateAttestationOptionsOpts } from './attestation/generateAttestationOptions';
import type { GenerateAssertionOptionsOpts } from './assertion/generateAssertionOptions';
+import type { MetadataStatement } from './services/metadataService';
import type {
VerifiedAttestation,
VerifyAttestationResponseOpts,
@@ -30,6 +33,7 @@ import type {
export type {
GenerateAttestationOptionsOpts,
GenerateAssertionOptionsOpts,
+ MetadataStatement,
VerifyAttestationResponseOpts,
VerifyAssertionResponseOpts,
VerifiedAttestation,
diff --git a/packages/server/src/metadata/verifyAttestationWithMetadata.ts b/packages/server/src/metadata/verifyAttestationWithMetadata.ts
index 63ea1f6..5e0b01c 100644
--- a/packages/server/src/metadata/verifyAttestationWithMetadata.ts
+++ b/packages/server/src/metadata/verifyAttestationWithMetadata.ts
@@ -1,8 +1,8 @@
import { Base64URLString } from '@simplewebauthn/typescript-types';
-import { MetadataStatement } from './metadataService';
+import { MetadataStatement } from '../services/metadataService';
import { FIDO_METADATA_AUTH_ALG_TO_COSE } from '../helpers/constants';
-import convertX509CertToPEM from '../helpers/convertX509CertToPEM';
+import convertCertBufferToPEM from '../helpers/convertCertBufferToPEM';
import validateCertificatePath from '../helpers/validateCertificatePath';
export default async function verifyAttestationWithMetadata(
@@ -16,30 +16,12 @@ export default async function verifyAttestationWithMetadata(
throw new Error(`Attestation alg "${alg}" did not match metadata auth alg "${metaCOSE.alg}"`);
}
- // Make a copy of x5c so we don't modify the original
- const path = [...x5c].map(convertX509CertToPEM);
-
- // Try to validate the chain with each metadata root cert until we find one that works
- let foundValidPath = false;
- for (const rootCert of statement.attestationRootCertificates) {
- try {
- // Push the root cert to the cert path and try to validate it
- path.push(convertX509CertToPEM(rootCert));
- foundValidPath = await validateCertificatePath(path);
- } catch (err) {
- // Swallow the error for now
- foundValidPath = false;
- // Remove the root cert before we try again with another
- path.splice(path.length - 1, 1);
- }
-
- // Don't continue if we've validated a full path
- if (foundValidPath) {
- break;
- }
- }
-
- if (!foundValidPath) {
+ try {
+ await validateCertificatePath(
+ x5c.map(convertCertBufferToPEM),
+ statement.attestationRootCertificates.map(convertCertBufferToPEM),
+ );
+ } catch (err) {
throw new Error(`Could not validate certificate path with any metadata root certificates`);
}
diff --git a/packages/server/src/services/defaultRootCerts/android-key.ts b/packages/server/src/services/defaultRootCerts/android-key.ts
new file mode 100644
index 0000000..ec38a74
--- /dev/null
+++ b/packages/server/src/services/defaultRootCerts/android-key.ts
@@ -0,0 +1,86 @@
+/**
+ * Google Hardware Attestation Root 1
+ *
+ * Downloaded from https://developer.android.com/training/articles/security-key-attestation#root_certificate
+ * (first entry)
+ *
+ * Valid until 2026-05-24 @ 09:28 PST
+ *
+ * SHA256 Fingerprint
+ * C1:98:4A:3E:F4:5C:1E:2A:91:85:51:DE:10:60:3C:86:F7:05:1B:22:49:C4:89:1C:AE:32:30:EA:BD:0C:97:D5
+ */
+export const Google_Hardware_Attestation_Root_1 = `-----BEGIN CERTIFICATE-----
+MIIFYDCCA0igAwIBAgIJAOj6GWMU0voYMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
+BAUTEGY5MjAwOWU4NTNiNmIwNDUwHhcNMTYwNTI2MTYyODUyWhcNMjYwNTI0MTYy
+ODUyWjAbMRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MIICIjANBgkqhkiG9w0B
+AQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdS
+Sxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7
+tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggj
+nar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGq
+C4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQ
+oVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+O
+JtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/Eg
+sTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRi
+igHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+M
+RPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9E
+aDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5Um
+AGMCAwEAAaOBpjCBozAdBgNVHQ4EFgQUNmHhAHyIBQlRi0RsR/8aTMnqTxIwHwYD
+VR0jBBgwFoAUNmHhAHyIBQlRi0RsR/8aTMnqTxIwDwYDVR0TAQH/BAUwAwEB/zAO
+BgNVHQ8BAf8EBAMCAYYwQAYDVR0fBDkwNzA1oDOgMYYvaHR0cHM6Ly9hbmRyb2lk
+Lmdvb2dsZWFwaXMuY29tL2F0dGVzdGF0aW9uL2NybC8wDQYJKoZIhvcNAQELBQAD
+ggIBACDIw41L3KlXG0aMiS//cqrG+EShHUGo8HNsw30W1kJtjn6UBwRM6jnmiwfB
+Pb8VA91chb2vssAtX2zbTvqBJ9+LBPGCdw/E53Rbf86qhxKaiAHOjpvAy5Y3m00m
+qC0w/Zwvju1twb4vhLaJ5NkUJYsUS7rmJKHHBnETLi8GFqiEsqTWpG/6ibYCv7rY
+DBJDcR9W62BW9jfIoBQcxUCUJouMPH25lLNcDc1ssqvC2v7iUgI9LeoM1sNovqPm
+QUiG9rHli1vXxzCyaMTjwftkJLkf6724DFhuKug2jITV0QkXvaJWF4nUaHOTNA4u
+JU9WDvZLI1j83A+/xnAJUucIv/zGJ1AMH2boHqF8CY16LpsYgBt6tKxxWH00XcyD
+CdW2KlBCeqbQPcsFmWyWugxdcekhYsAWyoSf818NUsZdBWBaR/OukXrNLfkQ79Iy
+ZohZbvabO/X+MVT3rriAoKc8oE2Uws6DF+60PV7/WIPjNvXySdqspImSN78mflxD
+qwLqRBYkA3I75qppLGG9rp7UCdRjxMl8ZDBld+7yvHVgt1cVzJx9xnyGCC23Uaic
+MDSXYrB4I4WHXPGjxhZuCuPBLTdOLU8YRvMYdEvYebWHMpvwGCF6bAx3JBpIeOQ1
+wDB5y0USicV3YgYGmi+NZfhA4URSh77Yd6uuJOJENRaNVTzk
+-----END CERTIFICATE-----
+`;
+
+/**
+ * Google Hardware Attestation Root 2
+ *
+ * Downloaded from https://developer.android.com/training/articles/security-key-attestation#root_certificate
+ * (second entry)
+ *
+ * Valid until 2034-11-18 @ 12:37 PST
+ *
+ * SHA256 Fingerprint
+ * 1E:F1:A0:4B:8B:A5:8A:B9:45:89:AC:49:8C:89:82:A7:83:F2:4E:A7:30:7E:01:59:A0:C3:A7:3B:37:7D:87:CC
+ */
+export const Google_Hardware_Attestation_Root_2 = `-----BEGIN CERTIFICATE-----
+MIIFHDCCAwSgAwIBAgIJANUP8luj8tazMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNV
+BAUTEGY5MjAwOWU4NTNiNmIwNDUwHhcNMTkxMTIyMjAzNzU4WhcNMzQxMTE4MjAz
+NzU4WjAbMRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MIICIjANBgkqhkiG9w0B
+AQEFAAOCAg8AMIICCgKCAgEAr7bHgiuxpwHsK7Qui8xUFmOr75gvMsd/dTEDDJdS
+Sxtf6An7xyqpRR90PL2abxM1dEqlXnf2tqw1Ne4Xwl5jlRfdnJLmN0pTy/4lj4/7
+tv0Sk3iiKkypnEUtR6WfMgH0QZfKHM1+di+y9TFRtv6y//0rb+T+W8a9nsNL/ggj
+nar86461qO0rOs2cXjp3kOG1FEJ5MVmFmBGtnrKpa73XpXyTqRxB/M0n1n/W9nGq
+C4FSYa04T6N5RIZGBN2z2MT5IKGbFlbC8UrW0DxW7AYImQQcHtGl/m00QLVWutHQ
+oVJYnFPlXTcHYvASLu+RhhsbDmxMgJJ0mcDpvsC4PjvB+TxywElgS70vE0XmLD+O
+JtvsBslHZvPBKCOdT0MS+tgSOIfga+z1Z1g7+DVagf7quvmag8jfPioyKvxnK/Eg
+sTUVi2ghzq8wm27ud/mIM7AY2qEORR8Go3TVB4HzWQgpZrt3i5MIlCaY504LzSRi
+igHCzAPlHws+W0rB5N+er5/2pJKnfBSDiCiFAVtCLOZ7gLiMm0jhO2B6tUXHI/+M
+RPjy02i59lINMRRev56GKtcd9qO/0kUJWdZTdA2XoS82ixPvZtXQpUpuL12ab+9E
+aDK8Z4RHJYYfCT3Q5vNAXaiWQ+8PTWm2QgBR/bkwSWc+NpUFgNPN9PvQi8WEg5Um
+AGMCAwEAAaNjMGEwHQYDVR0OBBYEFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMB8GA1Ud
+IwQYMBaAFDZh4QB8iAUJUYtEbEf/GkzJ6k8SMA8GA1UdEwEB/wQFMAMBAf8wDgYD
+VR0PAQH/BAQDAgIEMA0GCSqGSIb3DQEBCwUAA4ICAQBOMaBc8oumXb2voc7XCWnu
+XKhBBK3e2KMGz39t7lA3XXRe2ZLLAkLM5y3J7tURkf5a1SutfdOyXAmeE6SRo83U
+h6WszodmMkxK5GM4JGrnt4pBisu5igXEydaW7qq2CdC6DOGjG+mEkN8/TA6p3cno
+L/sPyz6evdjLlSeJ8rFBH6xWyIZCbrcpYEJzXaUOEaxxXxgYz5/cTiVKN2M1G2ok
+QBUIYSY6bjEL4aUN5cfo7ogP3UvliEo3Eo0YgwuzR2v0KR6C1cZqZJSTnghIC/vA
+D32KdNQ+c3N+vl2OTsUVMC1GiWkngNx1OO1+kXW+YTnnTUOtOIswUP/Vqd5SYgAI
+mMAfY8U9/iIgkQj6T2W6FsScy94IN9fFhE1UtzmLoBIuUFsVXJMTz+Jucth+IqoW
+Fua9v1R93/k98p41pjtFX+H8DslVgfP097vju4KDlqN64xV1grw3ZLl4CiOe/A91
+oeLm2UHOq6wn3esB4r2EIQKb6jTVGu5sYCcdWpXr0AUVqcABPdgL+H7qJguBw09o
+jm6xNIrw2OocrDKsudk/okr/AwqEyPKw9WnMlQgLIKw1rODG2NvU9oR3GVGdMkUB
+ZutL8VuFkERQGt6vQ2OCw0sV47VMkuYbacK/xyZFiRcrPJPb41zgbQj9XAEyLKCH
+ex0SdDrx+tWUDqG8At2JHA==
+-----END CERTIFICATE-----
+`;
diff --git a/packages/server/src/services/defaultRootCerts/android-safetynet.ts b/packages/server/src/services/defaultRootCerts/android-safetynet.ts
new file mode 100644
index 0000000..5e42817
--- /dev/null
+++ b/packages/server/src/services/defaultRootCerts/android-safetynet.ts
@@ -0,0 +1,66 @@
+/**
+ * GlobalSign Root CA
+ *
+ * Downloaded from https://pki.goog/roots.pem
+ *
+ * Valid until 2028-01-28 @ 04:00 PST
+ *
+ * SHA256 Fingerprint
+ * EB:D4:10:40:E4:BB:3E:C7:42:C9:E3:81:D3:1E:F2:A4:1A:48:B6:68:5C:96:E7:CE:F3:C1:DF:6C:D4:33:1C:99
+ */
+export const GlobalSign_Root_CA = `-----BEGIN CERTIFICATE-----
+MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG
+A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv
+b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw
+MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i
+YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT
+aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ
+jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp
+xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp
+1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG
+snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ
+U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8
+9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E
+BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B
+AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz
+yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE
+38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP
+AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad
+DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME
+HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A==
+-----END CERTIFICATE-----
+`;
+
+/**
+ * GlobalSign R2
+ *
+ * Downloaded from https://pki.goog/repo/certs/gsr2.pem
+ *
+ * Valid until 2021-12-15 @ 00:00 PST
+ *
+ * SHA256 Fingerprint
+ * 69:E2:D0:6C:30:F3:66:16:61:65:E9:1D:68:D1:CE:E5:CC:47:58:4A:80:22:7E:76:66:60:86:C0:10:72:41:EB
+ */
+export const GlobalSign_R2 = `-----BEGIN CERTIFICATE-----
+MIIDvDCCAqSgAwIBAgINAgPk9GHsmdnVeWbKejANBgkqhkiG9w0BAQUFADBMMSAw
+HgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEGA1UEChMKR2xvYmFs
+U2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjAeFw0wNjEyMTUwODAwMDBaFw0yMTEy
+MTUwODAwMDBaMEwxIDAeBgNVBAsTF0dsb2JhbFNpZ24gUm9vdCBDQSAtIFIyMRMw
+EQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMIIBIjANBgkq
+hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAps8kDr4ubyiZRULEqz4hVJsL03+EcPoS
+s8u/h1/Gf4bTsjBc1v2t8Xvc5fhglgmSEPXQU977e35ziKxSiHtKpspJpl6op4xa
+Ebx6guu+jOmzrJYlB5dKmSoHL7Qed7+KD7UCfBuWuMW5Oiy81hK561l94tAGhl9e
+SWq1OV6INOy8eAwImIRsqM1LtKB9DHlN8LgtyyHK1WxbfeGgKYSh+dOUScskYpEg
+vN0L1dnM+eonCitzkcadG6zIy+jgoPQvkItN+7A2G/YZeoXgbfJhE4hcn+CTClGX
+ilrOr6vV96oJqmC93Nlf33KpYBNeAAHJSvo/pOoHAyECjoLKA8KbjwIDAQABo4Gc
+MIGZMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSb
+4gdXZxwewGoG3lm0mi3f3BmGLjAfBgNVHSMEGDAWgBSb4gdXZxwewGoG3lm0mi3f
+3BmGLjA2BgNVHR8ELzAtMCugKaAnhiVodHRwOi8vY3JsLmdsb2JhbHNpZ24ubmV0
+L3Jvb3QtcjIuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQANeX81Z1YqDIs4EaLjG0qP
+OxIzaJI/y4kiRj3a+y3KOx74clIkLuMgi/9/5iv/n+1LyhGU9g7174slbzJOPbSp
+p1eT19ST2mYbdgTLx/hm3tTLoHIY/w4ZbnQYwfnPwAG4RefnEFYPQJmpD+Wh8BJw
+Bgtm2drTale/T6NBwmwnEFunfaMfMX3g6IBrx7VKnxIkJh/3p190WveLKgl9n7i5
+SWce/4woPimEn9WfEQWRvp6wKhaCKFjuCMuulEZusoOUJ4LfJnXxcuQTgIrSnwI7
+KfSSjsd42w3lX1fbgJp7vPmLM6OBRvAXuYRKTFqMAWbb7OaGIEE+cbxY6PDepnva
+-----END CERTIFICATE-----
+`;
diff --git a/packages/server/src/services/defaultRootCerts/apple.ts b/packages/server/src/services/defaultRootCerts/apple.ts
new file mode 100644
index 0000000..b2644c2
--- /dev/null
+++ b/packages/server/src/services/defaultRootCerts/apple.ts
@@ -0,0 +1,25 @@
+/**
+ * Apple WebAuthn Root CA
+ *
+ * Downloaded from https://www.apple.com/certificateauthority/Apple_WebAuthn_Root_CA.pem
+ *
+ * Valid until 2045-03-14 @ 17:00 PST
+ *
+ * SHA256 Fingerprint
+ * 09:15:DD:5C:07:A2:8D:B5:49:D1:F6:77:BB:5A:75:D4:BF:BE:95:61:A7:73:42:43:27:76:2E:9E:02:F9:BB:29
+ */
+export const Apple_WebAuthn_Root_CA = `-----BEGIN CERTIFICATE-----
+MIICEjCCAZmgAwIBAgIQaB0BbHo84wIlpQGUKEdXcTAKBggqhkjOPQQDAzBLMR8w
+HQYDVQQDDBZBcHBsZSBXZWJBdXRobiBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJ
+bmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMB4XDTIwMDMxODE4MjEzMloXDTQ1MDMx
+NTAwMDAwMFowSzEfMB0GA1UEAwwWQXBwbGUgV2ViQXV0aG4gUm9vdCBDQTETMBEG
+A1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTB2MBAGByqGSM49
+AgEGBSuBBAAiA2IABCJCQ2pTVhzjl4Wo6IhHtMSAzO2cv+H9DQKev3//fG59G11k
+xu9eI0/7o6V5uShBpe1u6l6mS19S1FEh6yGljnZAJ+2GNP1mi/YK2kSXIuTHjxA/
+pcoRf7XkOtO4o1qlcaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUJtdk
+2cV4wlpn0afeaxLQG2PxxtcwDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA
+MGQCMFrZ+9DsJ1PW9hfNdBywZDsWDbWFp28it1d/5w2RPkRX3Bbn/UbDTNLx7Jr3
+jAGGiQIwHFj+dJZYUJR786osByBelJYsVZd2GbHQu209b5RCmGQ21gpSAk9QZW4B
+1bWeT0vT
+-----END CERTIFICATE-----
+`;
diff --git a/packages/server/src/metadata/metadataService.ts b/packages/server/src/services/metadataService.ts
index dc48f28..a9baf9e 100644
--- a/packages/server/src/metadata/metadataService.ts
+++ b/packages/server/src/services/metadataService.ts
@@ -6,12 +6,12 @@ import base64url from 'base64url';
import { FIDO_AUTHENTICATOR_STATUS } from '../helpers/constants';
import toHash from '../helpers/toHash';
import validateCertificatePath from '../helpers/validateCertificatePath';
-import convertX509CertToPEM from '../helpers/convertX509CertToPEM';
+import convertCertBufferToPEM from '../helpers/convertCertBufferToPEM';
import convertAAGUIDToString from '../helpers/convertAAGUIDToString';
// TODO: Re-enable this once we figure out logging
// import { log } from '../helpers/logging';
-import parseJWT from './parseJWT';
+import parseJWT from '../metadata/parseJWT';
// Cached WebAuthn metadata statements
type CachedAAGUID = {
@@ -224,7 +224,7 @@ class MetadataService {
throw new Error(`Latest TOC no. "${payload.no}" is not greater than previous ${no}`);
}
- let fullCertPath = header.x5c.map(convertX509CertToPEM);
+ let fullCertPath = header.x5c.map(convertCertBufferToPEM);
if (rootCertURL.length > 0) {
// Download FIDO the root certificate and append it to the TOC certs
const respFIDORootCert = await fetch(rootCertURL);
diff --git a/packages/server/src/services/settingsService.test.ts b/packages/server/src/services/settingsService.test.ts
new file mode 100644
index 0000000..65b6115
--- /dev/null
+++ b/packages/server/src/services/settingsService.test.ts
@@ -0,0 +1,47 @@
+import fs from 'fs';
+import path from 'path';
+
+import settingsService from './settingsService';
+
+import { GlobalSign_Root_CA } from './defaultRootCerts/android-safetynet';
+import { Apple_WebAuthn_Root_CA } from './defaultRootCerts/apple';
+
+function pemToBuffer(pem: string): Buffer {
+ const trimmed = pem
+ .replace('-----BEGIN CERTIFICATE-----', '')
+ .replace('-----END CERTIFICATE-----', '')
+ .replace('\n', '');
+ return Buffer.from(trimmed, 'base64');
+}
+
+describe('setRootCertificate/getRootCertificate', () => {
+ test('should accept cert as Buffer', () => {
+ const gsr1Buffer = pemToBuffer(GlobalSign_Root_CA);
+ settingsService.setRootCertificates({
+ attestationFormat: 'android-safetynet',
+ certificates: [gsr1Buffer],
+ });
+
+ const certs = settingsService.getRootCertificates({ attestationFormat: 'android-safetynet' });
+
+ expect(certs).toEqual([GlobalSign_Root_CA]);
+ });
+
+ test('should accept cert as PEM string', () => {
+ settingsService.setRootCertificates({
+ attestationFormat: 'apple',
+ certificates: [Apple_WebAuthn_Root_CA],
+ });
+
+ const certs = settingsService.getRootCertificates({ attestationFormat: 'apple' });
+
+ expect(certs).toEqual([Apple_WebAuthn_Root_CA]);
+ });
+
+ test('should return empty array when certificate is not set', () => {
+ const certs = settingsService.getRootCertificates({ attestationFormat: 'none' });
+
+ expect(Array.isArray(certs)).toEqual(true);
+ expect(certs.length).toEqual(0);
+ });
+});
diff --git a/packages/server/src/services/settingsService.ts b/packages/server/src/services/settingsService.ts
new file mode 100644
index 0000000..7f74223
--- /dev/null
+++ b/packages/server/src/services/settingsService.ts
@@ -0,0 +1,71 @@
+import { AttestationFormat } from '../helpers/decodeAttestationObject';
+import convertCertBufferToPEM from '../helpers/convertCertBufferToPEM';
+
+import { GlobalSign_Root_CA, GlobalSign_R2 } from './defaultRootCerts/android-safetynet';
+import {
+ Google_Hardware_Attestation_Root_1,
+ Google_Hardware_Attestation_Root_2,
+} from './defaultRootCerts/android-key';
+import { Apple_WebAuthn_Root_CA } from './defaultRootCerts/apple';
+
+class SettingsService {
+ // Certificates are stored as PEM-formatted strings
+ private pemCertificates: Map<AttestationFormat, string[]>;
+
+ constructor() {
+ this.pemCertificates = new Map();
+ }
+
+ /**
+ * Set potential root certificates for attestation formats that use them. Root certs will be tried
+ * one-by-one when validating a certificate path.
+ *
+ * Certificates can be specified as a raw `Buffer`, or as a PEM-formatted string. If a
+ * `Buffer` is passed in it will be converted to PEM format.
+ */
+ setRootCertificates(opts: {
+ attestationFormat: AttestationFormat;
+ certificates: (Buffer | string)[];
+ }): void {
+ const { attestationFormat, certificates } = opts;
+
+ const newCertificates: string[] = [];
+ for (const cert of certificates) {
+ if (cert instanceof Buffer) {
+ newCertificates.push(convertCertBufferToPEM(cert));
+ } else {
+ newCertificates.push(cert);
+ }
+ }
+
+ this.pemCertificates.set(attestationFormat, newCertificates);
+ }
+
+ /**
+ * Get any registered root certificates for the specified attestation format
+ */
+ getRootCertificates(opts: { attestationFormat: AttestationFormat }): string[] {
+ const { attestationFormat } = opts;
+ return this.pemCertificates.get(attestationFormat) ?? [];
+ }
+}
+
+const settingsService = new SettingsService();
+
+// Initialize default certificates
+settingsService.setRootCertificates({
+ attestationFormat: 'android-key',
+ certificates: [Google_Hardware_Attestation_Root_1, Google_Hardware_Attestation_Root_2],
+});
+
+settingsService.setRootCertificates({
+ attestationFormat: 'android-safetynet',
+ certificates: [GlobalSign_R2, GlobalSign_Root_CA],
+});
+
+settingsService.setRootCertificates({
+ attestationFormat: 'apple',
+ certificates: [Apple_WebAuthn_Root_CA],
+});
+
+export default settingsService;