diff options
Diffstat (limited to 'packages/server/src')
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; |