diff options
author | Matthew Miller <matthew@millerti.me> | 2022-12-18 07:59:52 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-12-18 07:59:52 -0800 |
commit | f95c921d2578108ad3c7169d011a295665f8efea (patch) | |
tree | e177c21d1c6e830fd2ef5b02b976642486e84a4a /packages/server/src | |
parent | 6f363aa53a69cf8c1ea69664924c1e9f8e19dc4e (diff) | |
parent | d4778c4b604abfa1e8a59d3997a4afc26057f980 (diff) |
Merge pull request #311 from MasterKale/feat/faster-cert-chain-validation
feat/faster-cert-chain-validation
Diffstat (limited to 'packages/server/src')
17 files changed, 246 insertions, 181 deletions
diff --git a/packages/server/src/helpers/convertX509PublicKeyToCOSE.ts b/packages/server/src/helpers/convertX509PublicKeyToCOSE.ts index cd76146..5d6b2fe 100644 --- a/packages/server/src/helpers/convertX509PublicKeyToCOSE.ts +++ b/packages/server/src/helpers/convertX509PublicKeyToCOSE.ts @@ -1,6 +1,6 @@ import { AsnParser } from '@peculiar/asn1-schema'; import { Certificate } from '@peculiar/asn1-x509'; -import { ECParameters, id_ecPublicKey, id_secp256r1 } from '@peculiar/asn1-ecc'; +import { ECParameters, id_ecPublicKey, id_secp256r1, id_secp384r1 } from '@peculiar/asn1-ecc'; import { RSAPublicKey } from '@peculiar/asn1-rsa'; import { @@ -10,16 +10,16 @@ import { COSEKEYS, COSEPublicKeyEC2, COSEPublicKeyRSA, - COSEALG, } from './cose'; +import { mapX509SignatureAlgToCOSEAlg } from './mapX509SignatureAlgToCOSEAlg'; -export function convertX509PublicKeyToCOSE(leafCertificate: Uint8Array): COSEPublicKey { +export function convertX509PublicKeyToCOSE(x509Certificate: Uint8Array): COSEPublicKey { let cosePublicKey: COSEPublicKey = new Map(); /** - * Time to extract the public key from an X.509 leaf certificate + * Time to extract the public key from an X.509 certificate */ - const x509 = AsnParser.parse(leafCertificate, Certificate); + const x509 = AsnParser.parse(x509Certificate, Certificate); const { tbsCertificate } = x509; const { subjectPublicKeyInfo, signature: _tbsSignature } = tbsCertificate; @@ -32,7 +32,7 @@ export function convertX509PublicKeyToCOSE(leafCertificate: Uint8Array): COSEPub * EC2 Public Key */ if (!subjectPublicKeyInfo.algorithm.parameters) { - throw new Error('Leaf cert public key missing parameters (EC2)'); + throw new Error('Certificate public key was missing parameters (EC2)'); } const ecParameters = AsnParser.parse( @@ -41,12 +41,14 @@ export function convertX509PublicKeyToCOSE(leafCertificate: Uint8Array): COSEPub ); let crv = -999; - if (ecParameters.namedCurve === id_secp256r1) { + const { namedCurve } = ecParameters; + + if (namedCurve === id_secp256r1) { crv = COSECRV.P256; + } else if (namedCurve === id_secp384r1) { + crv = COSECRV.P384; } else { - throw new Error( - `Leaf cert public key contained unexpected namedCurve ${ecParameters.namedCurve} (EC2)`, - ); + throw new Error(`Certificate public key contained unexpected namedCurve ${namedCurve} (EC2)`); } const subjectPublicKey = new Uint8Array(subjectPublicKeyInfo.subjectPublicKey); @@ -65,7 +67,7 @@ export function convertX509PublicKeyToCOSE(leafCertificate: Uint8Array): COSEPub const coseEC2PubKey: COSEPublicKeyEC2 = new Map(); coseEC2PubKey.set(COSEKEYS.kty, COSEKTY.EC2); - coseEC2PubKey.set(COSEKEYS.alg, signatureAlgorithmToCOSEAlg(signatureAlgorithm)); + coseEC2PubKey.set(COSEKEYS.alg, mapX509SignatureAlgToCOSEAlg(signatureAlgorithm)); coseEC2PubKey.set(COSEKEYS.crv, crv); coseEC2PubKey.set(COSEKEYS.x, x); coseEC2PubKey.set(COSEKEYS.y, y); @@ -79,46 +81,16 @@ export function convertX509PublicKeyToCOSE(leafCertificate: Uint8Array): COSEPub const coseRSAPubKey: COSEPublicKeyRSA = new Map(); coseRSAPubKey.set(COSEKEYS.kty, COSEKTY.RSA); - coseRSAPubKey.set(COSEKEYS.alg, signatureAlgorithmToCOSEAlg(signatureAlgorithm)); + coseRSAPubKey.set(COSEKEYS.alg, mapX509SignatureAlgToCOSEAlg(signatureAlgorithm)); coseRSAPubKey.set(COSEKEYS.n, new Uint8Array(rsaPublicKey.modulus)); coseRSAPubKey.set(COSEKEYS.e, new Uint8Array(rsaPublicKey.publicExponent)); cosePublicKey = coseRSAPubKey; } else { - throw new Error(`Unexpected leaf cert public key algorithm ${publicKeyAlgorithmID}`); - } - - return cosePublicKey; -} - -/** - * Map X.509 signature algorithm OIDs to COSE algorithm IDs - * - * - EC2 OIDs: https://oidref.com/1.2.840.10045.4.3 - * - RSA OIDs: https://oidref.com/1.2.840.113549.1.1 - */ -function signatureAlgorithmToCOSEAlg(signatureAlgorithm: string): COSEALG { - let alg: COSEALG; - - if (signatureAlgorithm === '1.2.840.10045.4.3.2') { - alg = COSEALG.ES256; - } else if (signatureAlgorithm === '1.2.840.10045.4.3.3') { - alg = COSEALG.ES384; - } else if (signatureAlgorithm === '1.2.840.10045.4.3.4') { - alg = COSEALG.ES512; - } else if (signatureAlgorithm === '1.2.840.113549.1.1.11') { - alg = COSEALG.RS256; - } else if (signatureAlgorithm === '1.2.840.113549.1.1.12') { - alg = COSEALG.RS384; - } else if (signatureAlgorithm === '1.2.840.113549.1.1.13') { - alg = COSEALG.RS512; - } else if (signatureAlgorithm === '1.2.840.113549.1.1.5') { - alg = COSEALG.RS1; - } else { throw new Error( - `Leaf cert contained unexpected signature algorithm ${signatureAlgorithm} (EC2)`, + `Certificate public key contained unexpected algorithm ID ${publicKeyAlgorithmID}`, ); } - return alg; + return cosePublicKey; } diff --git a/packages/server/src/helpers/getCertificateInfo.ts b/packages/server/src/helpers/getCertificateInfo.ts index e503f70..7ec6eba 100644 --- a/packages/server/src/helpers/getCertificateInfo.ts +++ b/packages/server/src/helpers/getCertificateInfo.ts @@ -8,6 +8,7 @@ export type CertificateInfo = { basicConstraintsCA: boolean; notBefore: Date; notAfter: Date; + parsedCertificate: Certificate; }; type Issuer = { @@ -15,6 +16,7 @@ type Issuer = { O?: string; OU?: string; CN?: string; + combined: string; }; type Subject = { @@ -22,6 +24,7 @@ type Subject = { O?: string; OU?: string; CN?: string; + combined: string; }; const issuerSubjectIDKey: { [key: string]: 'C' | 'O' | 'OU' | 'CN' } = { @@ -37,26 +40,28 @@ const issuerSubjectIDKey: { [key: string]: 'C' | 'O' | 'OU' | 'CN' } = { * @param pemCertificate Result from call to `convertASN1toPEM(x5c[0])` */ export function getCertificateInfo(leafCertBuffer: Uint8Array): CertificateInfo { - const asnx509 = AsnParser.parse(leafCertBuffer, Certificate); - const parsedCert = asnx509.tbsCertificate; + const x509 = AsnParser.parse(leafCertBuffer, Certificate); + const parsedCert = x509.tbsCertificate; // Issuer - const issuer: Issuer = {}; + const issuer: Issuer = { combined: '' }; parsedCert.issuer.forEach(([iss]) => { const key = issuerSubjectIDKey[iss.type]; if (key) { issuer[key] = iss.value.toString(); } }); + issuer.combined = issuerSubjectToString(issuer); // Subject - const subject: Subject = {}; + const subject: Subject = { combined: '' }; parsedCert.subject.forEach(([iss]) => { const key = issuerSubjectIDKey[iss.type]; if (key) { subject[key] = iss.value.toString(); } }); + subject.combined = issuerSubjectToString(subject); let basicConstraintsCA = false; if (parsedCert.extensions) { @@ -76,5 +81,35 @@ export function getCertificateInfo(leafCertBuffer: Uint8Array): CertificateInfo basicConstraintsCA, notBefore: parsedCert.validity.notBefore.getTime(), notAfter: parsedCert.validity.notAfter.getTime(), + parsedCertificate: x509, }; } + +/** + * Stringify the parts of Issuer or Subject info for easier comparison of subject issuers with + * issuer subjects. + * + * The order might seem arbitrary, because it is. It should be enough that the two are stringified + * in the same order. + */ +function issuerSubjectToString(input: Issuer | Subject): string { + const parts: string[] = []; + + if (input.C) { + parts.push(input.C); + } + + if (input.O) { + parts.push(input.O); + } + + if (input.OU) { + parts.push(input.OU); + } + + if (input.CN) { + parts.push(input.CN); + } + + return parts.join(' : '); +} diff --git a/packages/server/src/helpers/isCertRevoked.ts b/packages/server/src/helpers/isCertRevoked.ts index 2d7f5d6..97f2216 100644 --- a/packages/server/src/helpers/isCertRevoked.ts +++ b/packages/server/src/helpers/isCertRevoked.ts @@ -1,9 +1,16 @@ -import { X509 } from 'jsrsasign'; import fetch from 'cross-fetch'; import { AsnParser } from '@peculiar/asn1-schema'; -import { CertificateList } from '@peculiar/asn1-x509'; +import { + CertificateList, + Certificate, + AuthorityKeyIdentifier, + id_ce_authorityKeyIdentifier, + SubjectKeyIdentifier, + id_ce_subjectKeyIdentifier, + id_ce_cRLDistributionPoints, + CRLDistributionPoints, +} from '@peculiar/asn1-x509'; -import { convertCertBufferToPEM } from './convertCertBufferToPEM'; import { isoUint8Array } from './iso'; /** @@ -23,31 +30,44 @@ const cacheRevokedCerts: { [certAuthorityKeyID: string]: CAAuthorityInfo } = {}; * * CRL certificate structure referenced from https://tools.ietf.org/html/rfc5280#page-117 */ -export async function isCertRevoked(cert: X509): Promise<boolean> { - const certSerialHex = cert.getSerialNumberHex(); +export async function isCertRevoked(cert: Certificate): Promise<boolean> { + const { extensions } = cert.tbsCertificate; - // Check to see if we've got cached info for the cert's CA - let keyIdentifier: jsrsasign.AuthorityKeyIdentifierResult | jsrsasign.ExtSubjectKeyIdentifier | undefined = undefined; - try { - keyIdentifier = cert.getExtAuthorityKeyIdentifier(); - } catch (err) { - // pass + if (!extensions) { + return false; } - /** - * We might be dealing with a self-signed root certificate. Check the - * Subject key Identifier extension next. - */ - if (!keyIdentifier) { - try { - keyIdentifier = cert.getExtSubjectKeyIdentifier(); - } catch (err) { - // pass + let extAuthorityKeyID: AuthorityKeyIdentifier | undefined; + let extSubjectKeyID: SubjectKeyIdentifier | undefined; + let extCRLDistributionPoints: CRLDistributionPoints | undefined; + + extensions.forEach(ext => { + if (ext.extnID === id_ce_authorityKeyIdentifier) { + extAuthorityKeyID = AsnParser.parse(ext.extnValue, AuthorityKeyIdentifier); + } else if (ext.extnID === id_ce_subjectKeyIdentifier) { + extSubjectKeyID = AsnParser.parse(ext.extnValue, SubjectKeyIdentifier); + } else if (ext.extnID === id_ce_cRLDistributionPoints) { + extCRLDistributionPoints = AsnParser.parse(ext.extnValue, CRLDistributionPoints); } + }); + + // Check to see if we've got cached info for the cert's CA + let keyIdentifier: string | undefined = undefined; + + if (extAuthorityKeyID && extAuthorityKeyID.keyIdentifier) { + keyIdentifier = isoUint8Array.toHex(new Uint8Array(extAuthorityKeyID.keyIdentifier.buffer)); + } else if (extSubjectKeyID) { + /** + * We might be dealing with a self-signed root certificate. Check the + * Subject key Identifier extension next. + */ + keyIdentifier = isoUint8Array.toHex(new Uint8Array(extSubjectKeyID.buffer)); } + const certSerialHex = isoUint8Array.toHex(new Uint8Array(cert.tbsCertificate.serialNumber)); + if (keyIdentifier) { - const cached = cacheRevokedCerts[keyIdentifier.kid.hex]; + const cached = cacheRevokedCerts[keyIdentifier]; if (cached) { const now = new Date(); // If there's a nextUpdate then make sure we're before it @@ -57,13 +77,8 @@ export async function isCertRevoked(cert: X509): Promise<boolean> { } } - let crlURL = undefined; - try { - crlURL = cert.getExtCRLDistributionPointsURI(); - } catch (err) { - // Cert probably didn't include any CDP URIs - return false; - } + const crlURL = + extCRLDistributionPoints?.[0].distributionPoint?.fullName?.[0].uniformResourceIdentifier; // If no URL is provided then we have nothing to check if (!crlURL) { @@ -71,17 +86,21 @@ export async function isCertRevoked(cert: X509): Promise<boolean> { } // Download and read the CRL - const crlCert = new X509(); + let certListBytes: ArrayBuffer; try { - const respCRL = await fetch(crlURL[0]); - const dataCRL = await respCRL.arrayBuffer(); - const dataPEM = convertCertBufferToPEM(new Uint8Array(dataCRL)); - crlCert.readCertPEM(dataPEM); + const respCRL = await fetch(crlURL); + certListBytes = await respCRL.arrayBuffer(); } catch (err) { return false; } - const data = AsnParser.parse(isoUint8Array.fromHex(crlCert.hex), CertificateList); + let data: CertificateList; + try { + data = AsnParser.parse(certListBytes, CertificateList); + } catch (err) { + // Something was malformed with the CRL, so pass + return false; + } const newCached: CAAuthorityInfo = { revokedCerts: [], @@ -104,7 +123,7 @@ export async function isCertRevoked(cert: X509): Promise<boolean> { // Cache the results if (keyIdentifier) { - cacheRevokedCerts[keyIdentifier.kid.hex] = newCached; + cacheRevokedCerts[keyIdentifier] = newCached; } return newCached.revokedCerts.indexOf(certSerialHex) >= 0; diff --git a/packages/server/src/helpers/iso/isoCrypto/mapCoseAlgToWebCryptoAlg.ts b/packages/server/src/helpers/iso/isoCrypto/mapCoseAlgToWebCryptoAlg.ts index 3394b90..277bc9e 100644 --- a/packages/server/src/helpers/iso/isoCrypto/mapCoseAlgToWebCryptoAlg.ts +++ b/packages/server/src/helpers/iso/isoCrypto/mapCoseAlgToWebCryptoAlg.ts @@ -15,5 +15,5 @@ export function mapCoseAlgToWebCryptoAlg(alg: COSEALG): SubtleCryptoAlg { return 'SHA-512'; } - throw new Error(`Unexpected COSE alg value of ${alg}`); + throw new Error(`Could not map COSE alg value of ${alg} to a WebCrypto alg`); } diff --git a/packages/server/src/helpers/iso/isoCrypto/mapCoseAlgToWebCryptoKeyAlgName.ts b/packages/server/src/helpers/iso/isoCrypto/mapCoseAlgToWebCryptoKeyAlgName.ts index 8be875c..a33c219 100644 --- a/packages/server/src/helpers/iso/isoCrypto/mapCoseAlgToWebCryptoKeyAlgName.ts +++ b/packages/server/src/helpers/iso/isoCrypto/mapCoseAlgToWebCryptoKeyAlgName.ts @@ -9,11 +9,11 @@ export function mapCoseAlgToWebCryptoKeyAlgName(alg: COSEALG): SubtleCryptoKeyAl return 'Ed25519'; } else if ([COSEALG.ES256, COSEALG.ES384, COSEALG.ES512, COSEALG.ES256K].indexOf(alg) >= 0) { return 'ECDSA'; - } else if ([COSEALG.RS256, COSEALG.RS384, COSEALG.RS512].indexOf(alg) >= 0) { + } else if ([COSEALG.RS256, COSEALG.RS384, COSEALG.RS512, COSEALG.RS1].indexOf(alg) >= 0) { return 'RSASSA-PKCS1-v1_5'; } else if ([COSEALG.PS256, COSEALG.PS384, COSEALG.PS512].indexOf(alg) >= 0) { return 'RSA-PSS'; } - throw new Error(`Unexpected COSE alg value of ${alg}`); + throw new Error(`Could not map COSE alg value of ${alg} to a WebCrypto key alg name`); } diff --git a/packages/server/src/helpers/mapX509SignatureAlgToCOSEAlg.ts b/packages/server/src/helpers/mapX509SignatureAlgToCOSEAlg.ts new file mode 100644 index 0000000..026b5d0 --- /dev/null +++ b/packages/server/src/helpers/mapX509SignatureAlgToCOSEAlg.ts @@ -0,0 +1,33 @@ +import { COSEALG } from './cose'; + +/** + * Map X.509 signature algorithm OIDs to COSE algorithm IDs + * + * - EC2 OIDs: https://oidref.com/1.2.840.10045.4.3 + * - RSA OIDs: https://oidref.com/1.2.840.113549.1.1 + */ +export function mapX509SignatureAlgToCOSEAlg(signatureAlgorithm: string): COSEALG { + let alg: COSEALG; + + if (signatureAlgorithm === '1.2.840.10045.4.3.2') { + alg = COSEALG.ES256; + } else if (signatureAlgorithm === '1.2.840.10045.4.3.3') { + alg = COSEALG.ES384; + } else if (signatureAlgorithm === '1.2.840.10045.4.3.4') { + alg = COSEALG.ES512; + } else if (signatureAlgorithm === '1.2.840.113549.1.1.11') { + alg = COSEALG.RS256; + } else if (signatureAlgorithm === '1.2.840.113549.1.1.12') { + alg = COSEALG.RS384; + } else if (signatureAlgorithm === '1.2.840.113549.1.1.13') { + alg = COSEALG.RS512; + } else if (signatureAlgorithm === '1.2.840.113549.1.1.5') { + alg = COSEALG.RS1; + } else { + throw new Error( + `Unable to map X.509 signature algorithm ${signatureAlgorithm} to a COSE algorithm`, + ); + } + + return alg; +} diff --git a/packages/server/src/helpers/validateCertificatePath.ts b/packages/server/src/helpers/validateCertificatePath.ts index 661c9e7..65e4870 100644 --- a/packages/server/src/helpers/validateCertificatePath.ts +++ b/packages/server/src/helpers/validateCertificatePath.ts @@ -1,11 +1,11 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -// `ASN1HEX` exists in the lib but not in its typings -// @ts-ignore 2305 -import { KJUR, X509, ASN1HEX, zulutodate } from 'jsrsasign'; +import { AsnSerializer } from '@peculiar/asn1-schema'; import { isCertRevoked } from './isCertRevoked'; - -const { crypto } = KJUR; +import { isoBase64URL } from './iso'; +import { verifySignature } from './verifySignature'; +import { mapX509SignatureAlgToCOSEAlg } from './mapX509SignatureAlgToCOSEAlg'; +import { getCertificateInfo } from './getCertificateInfo'; /** * Traverse an array of PEM certificates and ensure they form a proper chain @@ -63,9 +63,6 @@ async function _validatePath(certificates: string[]): Promise<boolean> { for (let i = 0; i < certificates.length; i += 1) { const subjectPem = certificates[i]; - const subjectCert = new X509(); - subjectCert.readCertPEM(subjectPem); - const isLeafCert = i === 0; const isRootCert = i + 1 >= certificates.length; @@ -76,19 +73,20 @@ async function _validatePath(certificates: string[]): Promise<boolean> { issuerPem = certificates[i + 1]; } - const issuerCert = new X509(); - issuerCert.readCertPEM(issuerPem); + const subjectInfo = getCertificateInfo(pemToBytes(subjectPem)); + const issuerInfo = getCertificateInfo(pemToBytes(issuerPem)); + + const x509Subject = subjectInfo.parsedCertificate; // Check for certificate revocation - const subjectCertRevoked = await isCertRevoked(subjectCert); + const subjectCertRevoked = await isCertRevoked(x509Subject); if (subjectCertRevoked) { throw new Error(`Found revoked certificate in certificate path`); } // Check that intermediate certificate is within its valid time window - const notBefore = zulutodate(issuerCert.getNotBefore()); - const notAfter = zulutodate(issuerCert.getNotAfter()); + const { notBefore, notAfter } = issuerInfo; const now = new Date(Date.now()); if (notBefore > now || notAfter < now) { @@ -107,20 +105,26 @@ async function _validatePath(certificates: string[]): Promise<boolean> { } } - if (subjectCert.getIssuerString() !== issuerCert.getSubjectString()) { + if (subjectInfo.issuer.combined !== issuerInfo.subject.combined) { throw new InvalidSubjectAndIssuer(); } - const subjectCertStruct = ASN1HEX.getTLVbyList(subjectCert.hex, 0, [0]); - const alg = subjectCert.getSignatureAlgorithmField(); - const signatureHex = subjectCert.getSignatureValueHex(); - - const Signature = new crypto.Signature({ alg }); - Signature.init(issuerPem); - // TODO: `updateHex()` takes approximately two seconds per execution, can we improve this? - Signature.updateHex(subjectCertStruct ?? ''); - - if (!Signature.verify(signatureHex)) { + // Verify the subject certificate's signature with the issuer cert's public key + const data = AsnSerializer.serialize(x509Subject.tbsCertificate); + const signature = x509Subject.signatureValue; + const signatureAlgorithm = mapX509SignatureAlgToCOSEAlg( + x509Subject.signatureAlgorithm.algorithm, + ); + const issuerCertBytes = pemToBytes(issuerPem); + + const verified = await verifySignature({ + data: new Uint8Array(data), + signature: new Uint8Array(signature), + x509Certificate: issuerCertBytes, + hashAlgorithm: signatureAlgorithm, + }); + + if (!verified) { throw new Error('Invalid certificate path: invalid signature'); } } @@ -143,3 +147,15 @@ class CertificateNotYetValidOrExpired extends Error { this.name = 'CertificateNotYetValidOrExpired'; } } + +/** + * Take a certificate in PEM format and convert it to bytes + */ +function pemToBytes(pem: string): Uint8Array { + const certBase64 = pem + .replace('-----BEGIN CERTIFICATE-----', '') + .replace('-----END CERTIFICATE-----', '') + .replace(/\n/g, ''); + + return isoBase64URL.toBuffer(certBase64, 'base64'); +} diff --git a/packages/server/src/helpers/verifySignature.ts b/packages/server/src/helpers/verifySignature.ts index ff4e73b..00ada70 100644 --- a/packages/server/src/helpers/verifySignature.ts +++ b/packages/server/src/helpers/verifySignature.ts @@ -10,16 +10,16 @@ export async function verifySignature(opts: { signature: Uint8Array; data: Uint8Array; credentialPublicKey?: Uint8Array; - leafCertificate?: Uint8Array; - attestationHashAlgorithm?: COSEALG; + x509Certificate?: Uint8Array; + hashAlgorithm?: COSEALG; }): Promise<boolean> { - const { signature, data, credentialPublicKey, leafCertificate, attestationHashAlgorithm } = opts; + const { signature, data, credentialPublicKey, x509Certificate, hashAlgorithm } = opts; - if (!leafCertificate && !credentialPublicKey) { + if (!x509Certificate && !credentialPublicKey) { throw new Error('Must declare either "leafCert" or "credentialPublicKey"'); } - if (leafCertificate && credentialPublicKey) { + if (x509Certificate && credentialPublicKey) { throw new Error('Must not declare both "leafCert" and "credentialPublicKey"'); } @@ -27,14 +27,14 @@ export async function verifySignature(opts: { if (credentialPublicKey) { cosePublicKey = decodeCredentialPublicKey(credentialPublicKey); - } else if (leafCertificate) { - cosePublicKey = convertX509PublicKeyToCOSE(leafCertificate); + } else if (x509Certificate) { + cosePublicKey = convertX509PublicKeyToCOSE(x509Certificate); } return isoCrypto.verify({ cosePublicKey, signature, data, - shaHashOverride: attestationHashAlgorithm, + shaHashOverride: hashAlgorithm, }); } diff --git a/packages/server/src/metadata/verifyAttestationWithMetadata.test.ts b/packages/server/src/metadata/verifyAttestationWithMetadata.test.ts index f0a1bdb..f2d2afd 100644 --- a/packages/server/src/metadata/verifyAttestationWithMetadata.test.ts +++ b/packages/server/src/metadata/verifyAttestationWithMetadata.test.ts @@ -175,51 +175,53 @@ test('should verify idmelon attestation with updated root certificate', async () * new root certificate, which is included below and will eventually get rolled out via FIDO MDS. */ const metadataStatement: MetadataStatement = { - "legalHeader": "Submission of this statement and retrieval and use of this statement indicates acceptance of the appropriate agreement located at https://fidoalliance.org/metadata/metadata-legal-terms/.", - "aaguid": "820d89ed-d65a-409e-85cb-f73f0578f82a", - "description": "Vancosys iOS Authenticator", - "authenticatorVersion": 2, - "protocolFamily": "fido2", - "schema": 3, - "upv": [{ "major": 1, "minor": 0 }], - "authenticationAlgorithms": ["secp256r1_ecdsa_sha256_raw"], - "publicKeyAlgAndEncodings": ["cose"], - "attestationTypes": ["basic_full"], - "userVerificationDetails": [ + legalHeader: + 'Submission of this statement and retrieval and use of this statement indicates acceptance of the appropriate agreement located at https://fidoalliance.org/metadata/metadata-legal-terms/.', + aaguid: '820d89ed-d65a-409e-85cb-f73f0578f82a', + description: 'Vancosys iOS Authenticator', + authenticatorVersion: 2, + protocolFamily: 'fido2', + schema: 3, + upv: [{ major: 1, minor: 0 }], + authenticationAlgorithms: ['secp256r1_ecdsa_sha256_raw'], + publicKeyAlgAndEncodings: ['cose'], + attestationTypes: ['basic_full'], + userVerificationDetails: [ [ - { "userVerificationMethod": "faceprint_internal" }, - { "userVerificationMethod": "voiceprint_internal" }, - { "userVerificationMethod": "passcode_internal" }, - { "userVerificationMethod": "eyeprint_internal" }, - { "userVerificationMethod": "handprint_internal" }, - { "userVerificationMethod": "fingerprint_internal" }, - { "userVerificationMethod": "pattern_internal" }, - { "userVerificationMethod": "location_internal" }, - { "userVerificationMethod": "presence_internal" }, - ] + { userVerificationMethod: 'faceprint_internal' }, + { userVerificationMethod: 'voiceprint_internal' }, + { userVerificationMethod: 'passcode_internal' }, + { userVerificationMethod: 'eyeprint_internal' }, + { userVerificationMethod: 'handprint_internal' }, + { userVerificationMethod: 'fingerprint_internal' }, + { userVerificationMethod: 'pattern_internal' }, + { userVerificationMethod: 'location_internal' }, + { userVerificationMethod: 'presence_internal' }, + ], ], - "keyProtection": ["hardware", "secure_element"], - "matcherProtection": ["on_chip"], - "cryptoStrength": 128, - "attachmentHint": ["external"], - "tcDisplay": [], - "attestationRootCertificates": [ - "MIIByzCCAXGgAwIBAgIJANmMNK6jVpuuMAoGCCqGSM49BAMCMEExJDAiBgNVBAoMG1ZhbmNvc3lzIERhdGEgU2VjdXJpdHkgSW5jLjEZMBcGA1UEAwwQVmFuY29zeXMgUm9vdCBDQTAgFw0yMjEyMTQxODQxMDlaGA8yMDcyMTIwMTE4NDEwOVowQTEkMCIGA1UECgwbVmFuY29zeXMgRGF0YSBTZWN1cml0eSBJbmMuMRkwFwYDVQQDDBBWYW5jb3N5cyBSb290IENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEalYgEopnKScAm+d9f1XpGB3zbkZCD3hZEKuxTclpBYlj4ypNRg0gMSa7geBgd6nck50YaVhdy75uIc2wbWX8t6NQME4wHQYDVR0OBBYEFOxyf0cDs8Yl+VnWSZ1uYJAKkFeVMB8GA1UdIwQYMBaAFOxyf0cDs8Yl+VnWSZ1uYJAKkFeVMAwGA1UdEwQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIhAO2XuiRDXxy/UkWhsuZQYNUXeOj08AeTWADAqXvcA30hAiBi2cdGd61PNwHDTYjXPenPcD8S0rFTDncNWfs3E/WDXA==" + keyProtection: ['hardware', 'secure_element'], + matcherProtection: ['on_chip'], + cryptoStrength: 128, + attachmentHint: ['external'], + tcDisplay: [], + attestationRootCertificates: [ + 'MIIByzCCAXGgAwIBAgIJANmMNK6jVpuuMAoGCCqGSM49BAMCMEExJDAiBgNVBAoMG1ZhbmNvc3lzIERhdGEgU2VjdXJpdHkgSW5jLjEZMBcGA1UEAwwQVmFuY29zeXMgUm9vdCBDQTAgFw0yMjEyMTQxODQxMDlaGA8yMDcyMTIwMTE4NDEwOVowQTEkMCIGA1UECgwbVmFuY29zeXMgRGF0YSBTZWN1cml0eSBJbmMuMRkwFwYDVQQDDBBWYW5jb3N5cyBSb290IENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEalYgEopnKScAm+d9f1XpGB3zbkZCD3hZEKuxTclpBYlj4ypNRg0gMSa7geBgd6nck50YaVhdy75uIc2wbWX8t6NQME4wHQYDVR0OBBYEFOxyf0cDs8Yl+VnWSZ1uYJAKkFeVMB8GA1UdIwQYMBaAFOxyf0cDs8Yl+VnWSZ1uYJAKkFeVMAwGA1UdEwQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIhAO2XuiRDXxy/UkWhsuZQYNUXeOj08AeTWADAqXvcA30hAiBi2cdGd61PNwHDTYjXPenPcD8S0rFTDncNWfs3E/WDXA==', ], - "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAM1BMVEUtmc3y+fyWzOZis9rK5fI6n9B8v+Cw2ezl8vlHptNVrNbX7Paj0ulvud293++JxuP///89HRvpAAAAEXRSTlP/////////////////////ACWtmWIAAABsSURBVHgBxdPBCoAwDIPh/yDise//tIIQCZo6RNGdtuWDstFSg/UOgMiADQBJ6J4iCwS4BgzBuEQHCoFa+mdM+qijsDMVhBfdoRFaAL4nAe6AeghODYPnsaNyLuAqg5AHwO9AYu5BmqEPhncFmecvM5KKQHMAAAAASUVORK5CYII=", - "authenticatorGetInfo": { - "versions": ["FIDO_2_0"], - "extensions": ["hmac-secret"], - "aaguid": "820d89edd65a409e85cbf73f0578f82a", - "options": { "plat": false, "rk": true, "up": true, "uv": true }, - "maxMsgSize": 2048 - } + icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAM1BMVEUtmc3y+fyWzOZis9rK5fI6n9B8v+Cw2ezl8vlHptNVrNbX7Paj0ulvud293++JxuP///89HRvpAAAAEXRSTlP/////////////////////ACWtmWIAAABsSURBVHgBxdPBCoAwDIPh/yDise//tIIQCZo6RNGdtuWDstFSg/UOgMiADQBJ6J4iCwS4BgzBuEQHCoFa+mdM+qijsDMVhBfdoRFaAL4nAe6AeghODYPnsaNyLuAqg5AHwO9AYu5BmqEPhncFmecvM5KKQHMAAAAASUVORK5CYII=', + authenticatorGetInfo: { + versions: ['FIDO_2_0'], + extensions: ['hmac-secret'], + aaguid: '820d89edd65a409e85cbf73f0578f82a', + options: { plat: false, rk: true, up: true, uv: true }, + maxMsgSize: 2048, + }, }; const x5c = [ - 'MIIB6TCCAY+gAwIBAgIJAJz56pzvu76hMAoGCCqGSM49BAMCMEExJDAiBgNVBAoMG1ZhbmNvc3lzIERhdGEgU2VjdXJpdHkgSW5jLjEZMBcGA1UEAwwQVmFuY29zeXMgUm9vdCBDQTAgFw0xODEyMjIxNzQzMjhaGA8yMDY4MTIwOTE3NDMyOFowfDELMAkGA1UEBhMCQ0ExJDAiBgNVBAoMG1ZhbmNvc3lzIERhdGEgU2VjdXJpdHkgSW5jLjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEjMCEGA1UEAwwaVmFuY29zeXMgaU9TIEF1dGhlbnRpY2F0b3IwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATZVzbtmSJbzsoN6mfdD+t1LV52zB+LWN0UusmV9+sRmfNB49adqPQ+h0JOlEwfL4zkbMmuDr6JhBRKJ5/c0SkeozMwMTAMBgNVHRMBAf8EAjAAMCEGCysGAQQBguUcAQEEBBIEEIINie3WWkCehcv3PwV4+CowCgYIKoZIzj0EAwIDSAAwRQIgV7++U2fQyy6Qido7fDhsi5Grrt76LTgZ5XJlA9UKEVECIQDJO0YHevdU77VlZ+Of58oKMjWD3SkzC1SWSlhl3nezHQ==' + 'MIIB6TCCAY+gAwIBAgIJAJz56pzvu76hMAoGCCqGSM49BAMCMEExJDAiBgNVBAoMG1ZhbmNvc3lzIERhdGEgU2VjdXJpdHkgSW5jLjEZMBcGA1UEAwwQVmFuY29zeXMgUm9vdCBDQTAgFw0xODEyMjIxNzQzMjhaGA8yMDY4MTIwOTE3NDMyOFowfDELMAkGA1UEBhMCQ0ExJDAiBgNVBAoMG1ZhbmNvc3lzIERhdGEgU2VjdXJpdHkgSW5jLjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEjMCEGA1UEAwwaVmFuY29zeXMgaU9TIEF1dGhlbnRpY2F0b3IwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATZVzbtmSJbzsoN6mfdD+t1LV52zB+LWN0UusmV9+sRmfNB49adqPQ+h0JOlEwfL4zkbMmuDr6JhBRKJ5/c0SkeozMwMTAMBgNVHRMBAf8EAjAAMCEGCysGAQQBguUcAQEEBBIEEIINie3WWkCehcv3PwV4+CowCgYIKoZIzj0EAwIDSAAwRQIgV7++U2fQyy6Qido7fDhsi5Grrt76LTgZ5XJlA9UKEVECIQDJO0YHevdU77VlZ+Of58oKMjWD3SkzC1SWSlhl3nezHQ==', ]; - const credentialPublicKey = 'pQECAyYgASFYINuJbeLdkZwgKtUw2VSopICTTO5PKdj95GXJ7JCsQi7iIlggxygEp0_P0oMXhfw2BjtL0M7-yIpnk5uSHc0oNkXfdJw'; + const credentialPublicKey = + 'pQECAyYgASFYINuJbeLdkZwgKtUw2VSopICTTO5PKdj95GXJ7JCsQi7iIlggxygEp0_P0oMXhfw2BjtL0M7-yIpnk5uSHc0oNkXfdJw'; const verified = await verifyAttestationWithMetadata({ statement: metadataStatement, @@ -228,4 +230,4 @@ test('should verify idmelon attestation with updated root certificate', async () }); expect(verified).toEqual(true); -}) +}); diff --git a/packages/server/src/metadata/verifyAttestationWithMetadata.ts b/packages/server/src/metadata/verifyAttestationWithMetadata.ts index 9b4c471..44f693c 100644 --- a/packages/server/src/metadata/verifyAttestationWithMetadata.ts +++ b/packages/server/src/metadata/verifyAttestationWithMetadata.ts @@ -4,14 +4,7 @@ import type { MetadataStatement, AlgSign } from '../metadata/mdsTypes'; import { convertCertBufferToPEM } from '../helpers/convertCertBufferToPEM'; import { validateCertificatePath } from '../helpers/validateCertificatePath'; import { decodeCredentialPublicKey } from '../helpers/decodeCredentialPublicKey'; -import { - COSEALG, - COSECRV, - COSEKEYS, - COSEKTY, - COSEPublicKeyEC2, - isCOSEPublicKeyEC2, -} from '../helpers/cose'; +import { COSEALG, COSECRV, COSEKEYS, COSEKTY, isCOSEPublicKeyEC2 } from '../helpers/cose'; /** * Match properties of the authenticator's attestation statement against expected values as diff --git a/packages/server/src/registration/verifications/tpm/verifyAttestationTPM.ts b/packages/server/src/registration/verifications/tpm/verifyAttestationTPM.ts index c665be3..95c7952 100644 --- a/packages/server/src/registration/verifications/tpm/verifyAttestationTPM.ts +++ b/packages/server/src/registration/verifications/tpm/verifyAttestationTPM.ts @@ -14,7 +14,6 @@ import { decodeCredentialPublicKey } from '../../../helpers/decodeCredentialPubl import { COSEKEYS, isCOSEAlg, - COSEKTY, isCOSEPublicKeyRSA, isCOSEPublicKeyEC2, COSEALG, @@ -215,7 +214,7 @@ export async function verifyAttestationTPM( } // Check that Subject sequence is empty. - if (Object.keys(subject).length > 0) { + if (subject.combined.length > 0) { throw new Error('Certificate subject was not empty (TPM)'); } @@ -316,8 +315,8 @@ export async function verifyAttestationTPM( return verifySignature({ signature: sig, data: certInfo, - leafCertificate: x5c[0], - attestationHashAlgorithm: alg, + x509Certificate: x5c[0], + hashAlgorithm: alg, }); } diff --git a/packages/server/src/registration/verifications/verifyAttestationAndroidKey.ts b/packages/server/src/registration/verifications/verifyAttestationAndroidKey.ts index 1f3eb83..0128c09 100644 --- a/packages/server/src/registration/verifications/verifyAttestationAndroidKey.ts +++ b/packages/server/src/registration/verifications/verifyAttestationAndroidKey.ts @@ -111,7 +111,7 @@ export async function verifyAttestationAndroidKey( return verifySignature({ signature: sig, data: signatureBase, - leafCertificate: x5c[0], - attestationHashAlgorithm: alg, + x509Certificate: x5c[0], + hashAlgorithm: alg, }); } diff --git a/packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.ts b/packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.ts index d47dd70..40fcca2 100644 --- a/packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.ts +++ b/packages/server/src/registration/verifications/verifyAttestationAndroidSafetyNet.ts @@ -129,7 +129,7 @@ export async function verifyAttestationAndroidSafetyNet( const verified = await verifySignature({ signature: signatureBuffer, data: signatureBaseBuffer, - leafCertificate: leafCertBuffer, + x509Certificate: leafCertBuffer, }); /** * END Verify Signature diff --git a/packages/server/src/registration/verifications/verifyAttestationApple.test.ts b/packages/server/src/registration/verifications/verifyAttestationApple.test.ts index 1459df1..ef47e8e 100644 --- a/packages/server/src/registration/verifications/verifyAttestationApple.test.ts +++ b/packages/server/src/registration/verifications/verifyAttestationApple.test.ts @@ -1,7 +1,3 @@ -// TODO: This test can take upwards of 7 seconds to complete locally, more in CI...need to figure -// out why -jest.setTimeout(30000); - import { verifyRegistrationResponse } from '../verifyRegistrationResponse'; test('should verify Apple attestation', async () => { diff --git a/packages/server/src/registration/verifications/verifyAttestationFIDOU2F.ts b/packages/server/src/registration/verifications/verifyAttestationFIDOU2F.ts index e271e48..2674502 100644 --- a/packages/server/src/registration/verifications/verifyAttestationFIDOU2F.ts +++ b/packages/server/src/registration/verifications/verifyAttestationFIDOU2F.ts @@ -62,7 +62,7 @@ export async function verifyAttestationFIDOU2F( return verifySignature({ signature: sig, data: signatureBase, - leafCertificate: x5c[0], - attestationHashAlgorithm: COSEALG.ES256, + x509Certificate: x5c[0], + hashAlgorithm: COSEALG.ES256, }); } diff --git a/packages/server/src/registration/verifications/verifyAttestationPacked.ts b/packages/server/src/registration/verifications/verifyAttestationPacked.ts index a57bf13..2780764 100644 --- a/packages/server/src/registration/verifications/verifyAttestationPacked.ts +++ b/packages/server/src/registration/verifications/verifyAttestationPacked.ts @@ -115,14 +115,14 @@ export async function verifyAttestationPacked( verified = await verifySignature({ signature: sig, data: signatureBase, - leafCertificate: x5c[0], + x509Certificate: x5c[0], }); } else { verified = await verifySignature({ signature: sig, data: signatureBase, credentialPublicKey, - attestationHashAlgorithm: alg, + hashAlgorithm: alg, }); } diff --git a/packages/server/src/registration/verifyRegistrationResponse.test.ts b/packages/server/src/registration/verifyRegistrationResponse.test.ts index aa0011e..0a97af5 100644 --- a/packages/server/src/registration/verifyRegistrationResponse.test.ts +++ b/packages/server/src/registration/verifyRegistrationResponse.test.ts @@ -90,7 +90,7 @@ test('should verify Packed (EC2) attestation', async () => { expect(verification.registrationInfo?.credentialID).toEqual( isoBase64URL.toBuffer( 'AYThY1csINY4JrbHyGmqTl1nL_F1zjAF3hSAIngz8kAcjugmAMNVvxZRwqpEH-bNHHAIv291OX5ko9eDf_5mu3U' + - 'B2BvsScr2K-ppM4owOpGsqwg5tZglqqmxIm1Q', + 'B2BvsScr2K-ppM4owOpGsqwg5tZglqqmxIm1Q', ), ); }); |