diff options
Diffstat (limited to 'packages/server/src')
4 files changed, 135 insertions, 0 deletions
diff --git a/packages/server/src/attestation/verifications/verifyApple.test.ts b/packages/server/src/attestation/verifications/verifyApple.test.ts new file mode 100644 index 0000000..8490520 --- /dev/null +++ b/packages/server/src/attestation/verifications/verifyApple.test.ts @@ -0,0 +1,25 @@ +import verifyAttestationResponse from '../verifyAttestationResponse'; +import base64url from 'base64url'; + +test('should verify Apple attestation', async () => { + const expectedChallenge = 'h5xSyIRMx2IQPr1mQk6GD98XSQOBHgMHVpJIkMV9Nkc'; + jest.spyOn(base64url, 'encode').mockReturnValueOnce(expectedChallenge); + const verification = await verifyAttestationResponse({ + credential: { + id: 'J4lAqPXhefDrUD7oh5LQMbBH5TE', + rawId: 'J4lAqPXhefDrUD7oh5LQMbBH5TE', + response: { + attestationObject: + 'o2NmbXRlYXBwbGVnYXR0U3RtdKJjYWxnJmN4NWOCWQJHMIICQzCCAcmgAwIBAgIGAXSFZw11MAoGCCqGSM49BAMCMEgxHDAaBgNVBAMME0FwcGxlIFdlYkF1dGhuIENBIDExEzARBgNVBAoMCkFwcGxlIEluYy4xEzARBgNVBAgMCkNhbGlmb3JuaWEwHhcNMjAwOTEzMDI0OTE3WhcNMjAwOTE0MDI1OTE3WjCBkTFJMEcGA1UEAwxAMzI3ZWI1ODhmMTU3ZDZiYjY0NTRmOTdmNWU1NmM4NmY0NGI1MDdjODgxOGZmMjMwYmQwZjYyNWJkYjY1YmNiNjEaMBgGA1UECwwRQUFBIENlcnRpZmljYXRpb24xEzARBgNVBAoMCkFwcGxlIEluYy4xEzARBgNVBAgMCkNhbGlmb3JuaWEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARiAlQ11YPbcpjmwM93iOefyu00h8-4BALNKnBDB5I9n17wD5wNqP0hYua340eB75Z1L_V6I7R4qraq7763zj9mo1UwUzAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB_wQEAwIE8DAzBgkqhkiG92NkCAIEJjAkoSIEIPuwR1EQvcCtYCRahnJWisqz6YYLEAXH16p0WXbLfY6tMAoGCCqGSM49BAMCA2gAMGUCMDpEvt_ifVr8uu1rnLykezfrHBXwLL-D6DO73l_sX_DLRwXDmqTiPSx0WHiB554m5AIxAIAXIId3WdSC2B2zYFm4ZsJP_jAgjTL1GguZ-Ae78AN2AcjKblEabOdkbKr0aL_M9FkCODCCAjQwggG6oAMCAQICEFYlU5XHp_tA6-Io2CYIU7YwCgYIKoZIzj0EAwMwSzEfMB0GA1UEAwwWQXBwbGUgV2ViQXV0aG4gUm9vdCBDQTETMBEGA1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwKQ2FsaWZvcm5pYTAeFw0yMDAzMTgxODM4MDFaFw0zMDAzMTMwMDAwMDBaMEgxHDAaBgNVBAMME0FwcGxlIFdlYkF1dGhuIENBIDExEzARBgNVBAoMCkFwcGxlIEluYy4xEzARBgNVBAgMCkNhbGlmb3JuaWEwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASDLocvJhSRgQIlufX81rtjeLX1Xz_LBFvHNZk0df1UkETfm_4ZIRdlxpod2gULONRQg0AaQ0-yTREtVsPhz7_LmJH-wGlggb75bLx3yI3dr0alruHdUVta-quTvpwLJpGjZjBkMBIGA1UdEwEB_wQIMAYBAf8CAQAwHwYDVR0jBBgwFoAUJtdk2cV4wlpn0afeaxLQG2PxxtcwHQYDVR0OBBYEFOuugsT_oaxbUdTPJGEFAL5jvXeIMA4GA1UdDwEB_wQEAwIBBjAKBggqhkjOPQQDAwNoADBlAjEA3YsaNIGl-tnbtOdle4QeFEwnt1uHakGGwrFHV1Azcifv5VRFfvZIlQxjLlxIPnDBAjAsimBE3CAfz-Wbw00pMMFIeFHZYO1qdfHrSsq-OM0luJfQyAW-8Mf3iwelccboDgdoYXV0aERhdGFYmD3cRxDpwIiyKduonVYyILs59yKa_0ZbCmVrGvuaivigRQAAAAAAAAAAAAAAAAAAAAAAAAAAABQniUCo9eF58OtQPuiHktAxsEflMaUBAgMmIAEhWCBiAlQ11YPbcpjmwM93iOefyu00h8-4BALNKnBDB5I9nyJYIF7wD5wNqP0hYua340eB75Z1L_V6I7R4qraq7763zj9m', + clientDataJSON: + 'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiaDV4U3lJUk14MklRUHIxbVFrNkdEOThYU1FPQkhnTUhWcEpJa01WOU5rYyIsIm9yaWdpbiI6Imh0dHBzOi8vZGV2LmRvbnRuZWVkYS5wdyJ9', + }, + type: 'public-key', + }, + expectedChallenge, + expectedOrigin: 'https://dev.dontneeda.pw', + expectedRPID: 'dev.dontneeda.pw', + }); + + expect(verification.verified).toEqual(true); +}); diff --git a/packages/server/src/attestation/verifications/verifyApple.ts b/packages/server/src/attestation/verifications/verifyApple.ts new file mode 100644 index 0000000..89ad540 --- /dev/null +++ b/packages/server/src/attestation/verifications/verifyApple.ts @@ -0,0 +1,101 @@ +import { AsnParser } from '@peculiar/asn1-schema'; +import { Certificate } from '@peculiar/asn1-x509'; + +import type { AttestationStatement } from '../../helpers/decodeAttestationObject'; +import validateCertificatePath from '../../helpers/validateCertificatePath'; +import convertX509CertToPEM from '../../helpers/convertX509CertToPEM'; +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; + const { x5c } = attStmt; + + if (!x5c) { + throw new Error('No attestation certificate provided in attestation statement (Apple)'); + } + + /** + * Verify certificate path + */ + const certPath = x5c.map(convertX509CertToPEM); + certPath.push(AppleWebAuthnRootCertificate); + + try { + await validateCertificatePath(certPath); + } catch (err) { + throw new Error(`${err.message} (Apple)`); + } + + /** + * Compare nonce in certificate extension to computed nonce + */ + const parsedCredCert = AsnParser.parse(x5c[0], Certificate); + const { extensions, subjectPublicKeyInfo } = parsedCredCert.tbsCertificate; + + if (!extensions) { + throw new Error('credCert missing extensions (Apple)'); + } + + const extCertNonce = extensions.find(ext => ext.extnID === '1.2.840.113635.100.8.2'); + + if (!extCertNonce) { + throw new Error('credCert missing "1.2.840.113635.100.8.2" extension (Apple)'); + } + + const nonceToHash = Buffer.concat([authData, clientDataHash]); + const nonce = toHash(nonceToHash, 'SHA256'); + /** + * Ignore the first six ASN.1 structure bytes that define the nonce as an OCTET STRING. Should + * trim off <Buffer 30 24 a1 22 04 20> + * + * TODO: Try and get @peculiar (GitHub) to add a schema for "1.2.840.113635.100.8.2" when we + * find out where it's defined (doesn't seem to be publicly documented at the moment...) + */ + const extNonce = Buffer.from(extCertNonce.extnValue).slice(6); + + if (!nonce.equals(extNonce)) { + throw new Error(`credCert nonce was not expected value (Apple)`); + } + + /** + * Verify credential public key matches the Subject Public Key of credCert + */ + const credPubKeyPKCS = convertCOSEtoPKCS(credentialPublicKey); + const credCertSubjectPublicKey = Buffer.from(subjectPublicKeyInfo.subjectPublicKey); + + if (!credPubKeyPKCS.equals(credCertSubjectPublicKey)) { + throw new Error('Credential public key does not equal credCert public key (Apple)'); + } + + 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/verifyAttestationResponse.ts b/packages/server/src/attestation/verifyAttestationResponse.ts index c6531a7..8acd028 100644 --- a/packages/server/src/attestation/verifyAttestationResponse.ts +++ b/packages/server/src/attestation/verifyAttestationResponse.ts @@ -14,6 +14,7 @@ import verifyPacked from './verifications/verifyPacked'; import verifyAndroidSafetynet from './verifications/verifyAndroidSafetyNet'; import verifyTPM from './verifications/tpm/verifyTPM'; import verifyAndroidKey from './verifications/verifyAndroidKey'; +import verifyApple from './verifications/verifyApple'; type Options = { credential: AttestationCredentialJSON; @@ -193,6 +194,13 @@ export default async function verifyAttestationResponse( credentialPublicKey, clientDataHash, }); + } else if (fmt === ATTESTATION_FORMATS.APPLE) { + verified = await verifyApple({ + attStmt, + authData, + clientDataHash, + credentialPublicKey, + }); } else if (fmt === ATTESTATION_FORMATS.NONE) { if (Object.keys(attStmt).length > 0) { throw new Error('None attestation had unexpected attestation statement'); diff --git a/packages/server/src/helpers/decodeAttestationObject.ts b/packages/server/src/helpers/decodeAttestationObject.ts index a692d6f..09d95fb 100644 --- a/packages/server/src/helpers/decodeAttestationObject.ts +++ b/packages/server/src/helpers/decodeAttestationObject.ts @@ -20,6 +20,7 @@ export enum ATTESTATION_FORMATS { ANDROID_SAFETYNET = 'android-safetynet', ANDROID_KEY = 'android-key', TPM = 'tpm', + APPLE = 'apple', NONE = 'none', } |