diff options
Diffstat (limited to 'packages/server/src')
5 files changed, 110 insertions, 53 deletions
diff --git a/packages/server/src/attestation/verifications/tpm/verifyTPM.ts b/packages/server/src/attestation/verifications/tpm/verifyTPM.ts index fd9ff9d..fc549ff 100644 --- a/packages/server/src/attestation/verifications/tpm/verifyTPM.ts +++ b/packages/server/src/attestation/verifications/tpm/verifyTPM.ts @@ -177,8 +177,7 @@ export default async function verifyTPM(options: Options): Promise<boolean> { } // Pick a leaf AIK certificate of the x5c array and parse it. - const leafCertPEM = convertX509CertToPEM(x5c[0]); - const leafCertInfo = getCertificateInfo(leafCertPEM); + const leafCertInfo = getCertificateInfo(x5c[0]); const { basicConstraintsCA, version, subject, notAfter, notBefore } = leafCertInfo; if (basicConstraintsCA) { @@ -186,7 +185,7 @@ export default async function verifyTPM(options: Options): Promise<boolean> { } // Check that certificate is of version 3 (value must be set to 2). - if (version !== 3) { + if (version !== 2) { throw new Error('Certificate version was not `3` (ASN.1 value of 2) (TPM)'); } @@ -275,6 +274,7 @@ export default async function verifyTPM(options: Options): Promise<boolean> { // 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]); return verifySignature(sig, certInfo, leafCertPEM, hashAlg); } diff --git a/packages/server/src/attestation/verifications/verifyAndroidSafetyNet.ts b/packages/server/src/attestation/verifications/verifyAndroidSafetyNet.ts index 4ce7f36..6c0a5c8 100644 --- a/packages/server/src/attestation/verifications/verifyAndroidSafetyNet.ts +++ b/packages/server/src/attestation/verifications/verifyAndroidSafetyNet.ts @@ -81,8 +81,8 @@ export default async function verifyAttestationAndroidSafetyNet( /** * START Verify Header */ - const leafCert = convertX509CertToPEM(HEADER.x5c[0]); - const leafCertInfo = getCertificateInfo(leafCert); + const leafCertBuffer = base64url.toBuffer(HEADER.x5c[0]); + const leafCertInfo = getCertificateInfo(leafCertBuffer); const { subject } = leafCertInfo; @@ -121,7 +121,8 @@ export default async function verifyAttestationAndroidSafetyNet( const signatureBaseBuffer = Buffer.from(`${jwtParts[0]}.${jwtParts[1]}`); const signatureBuffer = base64url.toBuffer(SIGNATURE); - const verified = verifySignature(signatureBuffer, signatureBaseBuffer, leafCert); + const leafCertPEM = convertX509CertToPEM(leafCertBuffer); + const verified = verifySignature(signatureBuffer, signatureBaseBuffer, leafCertPEM); /** * END Verify Signature */ diff --git a/packages/server/src/attestation/verifications/verifyPacked.test.ts b/packages/server/src/attestation/verifications/verifyPacked.test.ts new file mode 100644 index 0000000..3de7633 --- /dev/null +++ b/packages/server/src/attestation/verifications/verifyPacked.test.ts @@ -0,0 +1,50 @@ +import verifyAttestationResponse from '../verifyAttestationResponse'; + +test('should verify Packed response from Chrome virtual authenticator', async () => { + /** + * This unit test will ensure future compatibility with Chrome virtual authenticators. + * + * Context: + * + * Chrome's WebAuthn dev tool enables developers to use "virtual" software authenticators in place + * of typical authenticator hardware. The reason this test exists is to ensure SimpleWebAuthn can + * handle leaf certs, such as the ones in these virtual authenticators, that specify the byte + * sequence "\x30\x03\x01\x01\x00" for the cert's Basic Constraints extension. + * + * As of March 2021 the jsrsasign@^10.0.5 library has a hardcoded check for "30030101ff", but + * not "3003010100" (notice the difference between "ff" and "00"), indicating whether or not this + * is a certificate authority certificate: + * + * https://github.com/kjur/jsrsasign/blob/482e651f2bb380dad3da4bbf0ae220fe3021d407/src/x509-1.1.js#L660 + * + * Physical hardware authenticators have been observed to specify "3000" for this constraint; + * this value evaluates to `!!undefined` => `false`, satisfying the Packed attestation + * verification's requirement that, "the Basic Constraints extension MUST have the CA component + * set to false."" + * + * https://w3c.github.io/webauthn/#sctn-packed-attestation-cert-requirements + * + * SimpleWebAuthn will have to implement its own workaround until this issue is resolved in + * jsrsasign. + */ + const verification = await verifyAttestationResponse({ + credential: { + id: '5Hwc78jGjXrzOS8Mke9KhFZEtX54iYD-UEBKgvMXM64', + rawId: '5Hwc78jGjXrzOS8Mke9KhFZEtX54iYD-UEBKgvMXM64', + response: { + attestationObject: + 'o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEcwRQIhANUrPJzUYX7JGbo4yN_qsQ_2c7xw6br2U1y_OxNcFd1cAiAo6f7LtQ67viVKxs7TLo9nj6nxgxqwEaOpzQhGtdXbqGN4NWOBWQHgMIIB3DCCAYCgAwIBAgIBATANBgkqhkiG9w0BAQsFADBgMQswCQYDVQQGEwJVUzERMA8GA1UECgwIQ2hyb21pdW0xIjAgBgNVBAsMGUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xGjAYBgNVBAMMEUJhdGNoIENlcnRpZmljYXRlMB4XDTE3MDcxNDAyNDAwMFoXDTQxMDMyNjAzNDIzNFowYDELMAkGA1UEBhMCVVMxETAPBgNVBAoMCENocm9taXVtMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMRowGAYDVQQDDBFCYXRjaCBDZXJ0aWZpY2F0ZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABI1hfmXJUI5kvMVnOsgqZ5naPBRGaCwljEY__99Y39L6Pmw3i1PXlcSk3_tBme3Xhi8jq68CA7S4kRugVpmU4QGjKDAmMBMGCysGAQQBguUcAgEBBAQDAgUgMA8GA1UdEwEB_wQFMAMBAQAwDQYJKoZIhvcNAQELBQADRwAwRAIgK8W82BY7-iHUcd5mSfWX4R-uGdOk49XKTkV3L6ilUPQCIEs68ZEr_yAjG39UwNexAVLBfbxkDdkLZlMtBvUsV27PaGF1dGhEYXRhWKQ93EcQ6cCIsinbqJ1WMiC7Ofcimv9GWwplaxr7mor4oEUAAAABAQIDBAUGBwgBAgMEBQYHCAAg5Hwc78jGjXrzOS8Mke9KhFZEtX54iYD-UEBKgvMXM66lAQIDJiABIVgghBdEOBTvUm-jPaYY0wvvO_HzCupmyS7YQzagxtn1T5IiWCDwJ5XQ_SzKoiV64TXfdsTrnxFoNljUCzJOJhwrDyhkRA', + clientDataJSON: + 'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiOUdJczBRUUJuYTE2eWN3NHN0U25BcWgyQWI2QWlIN1NTMF9YbTR5SjF6ayIsIm9yaWdpbiI6Imh0dHBzOi8vZGV2LmRvbnRuZWVkYS5wdyIsImNyb3NzT3JpZ2luIjpmYWxzZX0', + }, + type: 'public-key', + clientExtensionResults: {}, + transports: ['usb'], + }, + expectedChallenge: '9GIs0QQBna16ycw4stSnAqh2Ab6AiH7SS0_Xm4yJ1zk', + expectedOrigin: 'https://dev.dontneeda.pw', + expectedRPID: 'dev.dontneeda.pw', + }); + + expect(verification.verified).toEqual(true); +}); diff --git a/packages/server/src/attestation/verifications/verifyPacked.ts b/packages/server/src/attestation/verifications/verifyPacked.ts index f16aa50..3068bbb 100644 --- a/packages/server/src/attestation/verifications/verifyPacked.ts +++ b/packages/server/src/attestation/verifications/verifyPacked.ts @@ -50,7 +50,7 @@ export default async function verifyAttestationPacked(options: Options): Promise if (x5c) { const leafCert = convertX509CertToPEM(x5c[0]); const { subject, basicConstraintsCA, version, notBefore, notAfter } = getCertificateInfo( - leafCert, + x5c[0], ); const { OU, CN, O, C } = subject; @@ -75,7 +75,7 @@ export default async function verifyAttestationPacked(options: Options): Promise throw new Error('Certificate basic constraints CA was not `false` (Packed|Full)'); } - if (version !== 3) { + if (version !== 2) { throw new Error('Certificate version was not `3` (ASN.1 value of 2) (Packed|Full)'); } diff --git a/packages/server/src/helpers/getCertificateInfo.ts b/packages/server/src/helpers/getCertificateInfo.ts index d9efd3e..021ad77 100644 --- a/packages/server/src/helpers/getCertificateInfo.ts +++ b/packages/server/src/helpers/getCertificateInfo.ts @@ -1,74 +1,80 @@ -import { X509, zulutodate } from 'jsrsasign'; +import { AsnParser } from '@peculiar/asn1-schema'; +import { Certificate, BasicConstraints, id_ce_basicConstraints } from '@peculiar/asn1-x509'; export type CertificateInfo = { - issuer: { [key: string]: string }; - subject: { [key: string]: string }; + issuer: Issuer; + subject: Subject; version: number; basicConstraintsCA: boolean; notBefore: Date; notAfter: Date; }; -type ExtInfo = { - critical: boolean; - oid: string; - vidx: number; +type Issuer = { + C?: string; + O?: string; + OU?: string; + CN?: string; }; -interface x5cCertificate extends jsrsasign.X509 { - version: number; - foffset: number; - aExtInfo: ExtInfo[]; -} +type Subject = { + C?: string; + O?: string; + OU?: string; + CN?: string; +}; + +const issuerSubjectIDKey: { [key: string]: 'C' | 'O' | 'OU' | 'CN' } = { + '2.5.4.6': 'C', + '2.5.4.10': 'O', + '2.5.4.11': 'OU', + '2.5.4.3': 'CN', +}; /** * Extract PEM certificate info * * @param pemCertificate Result from call to `convertASN1toPEM(x5c[0])` */ -export default function getCertificateInfo(pemCertificate: string): CertificateInfo { - const subjectCert = new X509(); - subjectCert.readCertPEM(pemCertificate); - - // Break apart the Issuer - const issuerString = subjectCert.getIssuerString(); - const issuerParts = issuerString.slice(1).split('/'); +export default function getCertificateInfo(leafCertBuffer: Buffer): CertificateInfo { + const asnx509 = AsnParser.parse(leafCertBuffer, Certificate); + const parsedCert = asnx509.tbsCertificate; - const issuer: { [key: string]: string } = {}; - issuerParts.forEach(field => { - const [key, val] = field.split('='); - issuer[key] = val; - }); - - // Break apart the Subject - let subjectRaw = '/'; - try { - subjectRaw = subjectCert.getSubjectString(); - } catch (err) { - // Don't throw on an error that indicates an empty subject - if (err !== 'malformed RDN') { - throw err; + // Issuer + const issuer: Issuer = {}; + parsedCert.issuer.forEach(([iss]) => { + const key = issuerSubjectIDKey[iss.type]; + if (key) { + issuer[key] = iss.value.toString(); } - } - const subjectParts = subjectRaw.slice(1).split('/'); + }); - const subject: { [key: string]: string } = {}; - subjectParts.forEach(field => { - if (field) { - const [key, val] = field.split('='); - subject[key] = val; + // Subject + const subject: Subject = {}; + parsedCert.subject.forEach(([iss]) => { + const key = issuerSubjectIDKey[iss.type]; + if (key) { + subject[key] = iss.value.toString(); } }); - const { version } = subjectCert as x5cCertificate; - const basicConstraintsCA = !!subjectCert.getExtBasicConstraints()?.cA; + let basicConstraintsCA = false; + if (parsedCert.extensions) { + // console.log(parsedCert.extensions); + for (const ext of parsedCert.extensions) { + if (ext.extnID === id_ce_basicConstraints) { + const basicConstraints = AsnParser.parse(ext.extnValue, BasicConstraints); + basicConstraintsCA = basicConstraints.cA; + } + } + } return { issuer, subject, - version, + version: parsedCert.version, basicConstraintsCA, - notBefore: zulutodate(subjectCert.getNotBefore()), - notAfter: zulutodate(subjectCert.getNotAfter()), + notBefore: parsedCert.validity.notBefore.getTime(), + notAfter: parsedCert.validity.notAfter.getTime(), }; } |