diff --git a/deno.json b/deno.json index 8fcc01c2..e8f7dc6b 100644 --- a/deno.json +++ b/deno.json @@ -60,6 +60,7 @@ "@rollup/plugin-terser": "npm:@rollup/plugin-terser@^0.4.4", "@std/assert": "jsr:@std/assert@^1.0.7", "@std/path": "jsr:@std/path@^1.0.8", + "@std/semver": "jsr:@std/semver@^1.0.5", "@std/testing": "jsr:@std/testing@^1.0.4", "jsdom": "npm:jsdom@^25.0.1", "rollup": "npm:rollup@^4.27.3", diff --git a/deno.lock b/deno.lock index b5db7298..8144658e 100644 --- a/deno.lock +++ b/deno.lock @@ -26,6 +26,7 @@ "jsr:@std/path@^1.0.7": "1.0.8", "jsr:@std/path@^1.0.8": "1.0.8", "jsr:@std/path@~0.225.2": "0.225.2", + "jsr:@std/semver@^1.0.5": "1.0.5", "jsr:@std/testing@^1.0.4": "1.0.4", "jsr:@ts-morph/bootstrap@0.24": "0.24.0", "jsr:@ts-morph/common@0.24": "0.24.0", @@ -37,6 +38,7 @@ "npm:@peculiar/asn1-rsa@^2.3.8": "2.3.13", "npm:@peculiar/asn1-schema@^2.3.8": "2.3.13", "npm:@peculiar/asn1-x509@^2.3.8": "2.3.13", + "npm:@peculiar/x509@^1.13.0": "1.13.0", "npm:@rollup/plugin-babel@^6.0.4": "6.0.4_@babel+core@7.26.0_rollup@4.27.3", "npm:@rollup/plugin-node-resolve@^15.3.0": "15.3.0_rollup@4.27.3", "npm:@rollup/plugin-replace@^6.0.1": "6.0.1_rollup@4.27.3", @@ -155,6 +157,9 @@ "@std/path@1.0.8": { "integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be" }, + "@std/semver@1.0.5": { + "integrity": "529f79e83705714c105ad0ba55bec0f9da0f24d2f726b6cc1c15e505cc2c0624" + }, "@std/testing@1.0.4": { "integrity": "ca1368d720b183f572d40c469bb9faf09643ddd77b54f8b44d36ae6b94940576", "dependencies": [ @@ -992,45 +997,157 @@ "@peculiar/asn1-android@2.3.13": { "integrity": "sha512-0VTNazDGKrLS6a3BwTDZanqq6DR/I3SbvmDMuS8Be+OYpvM6x1SRDh9AGDsHVnaCOIztOspCPc6N1m+iUv1Xxw==", "dependencies": [ - "@peculiar/asn1-schema", - "asn1js", - "tslib" + "@peculiar/asn1-schema@2.3.13", + "asn1js@3.0.5", + "tslib@2.8.1" + ] + }, + "@peculiar/asn1-cms@2.4.0": { + "integrity": "sha512-TJvw5Tna/txvzzwnKUlCFd6zIz4R7qysHCaU6M2oe/MUT6EkvJDOzGGNY0hdjJYpuuHoqanQbIqEBhSLSWe1Tg==", + "dependencies": [ + "@peculiar/asn1-schema@2.4.0", + "@peculiar/asn1-x509@2.4.0", + "@peculiar/asn1-x509-attr", + "asn1js@3.0.6", + "tslib@2.8.1" + ] + }, + "@peculiar/asn1-csr@2.4.0": { + "integrity": "sha512-9yQz0hQ9ynGr/I1X1v64QQGfRMbviHXyqY07cy69UzXa8s4ayCKx/TncU6lDWcTKs7P/X/AEcuJcG7Xbw0cl1A==", + "dependencies": [ + "@peculiar/asn1-schema@2.4.0", + "@peculiar/asn1-x509@2.4.0", + "asn1js@3.0.6", + "tslib@2.8.1" ] }, "@peculiar/asn1-ecc@2.3.14": { "integrity": "sha512-zWPyI7QZto6rnLv6zPniTqbGaLh6zBpJyI46r1yS/bVHJXT2amdMHCRRnbV5yst2H8+ppXG6uXu/M6lKakiQ8w==", "dependencies": [ - "@peculiar/asn1-schema", - "@peculiar/asn1-x509", - "asn1js", - "tslib" + "@peculiar/asn1-schema@2.3.13", + "@peculiar/asn1-x509@2.3.13", + "asn1js@3.0.5", + "tslib@2.8.1" + ] + }, + "@peculiar/asn1-ecc@2.4.0": { + "integrity": "sha512-fJiYUBCJBDkjh347zZe5H81BdJ0+OGIg0X9z06v8xXUoql3MFeENUX0JsjCaVaU9A0L85PefLPGYkIoGpTnXLQ==", + "dependencies": [ + "@peculiar/asn1-schema@2.4.0", + "@peculiar/asn1-x509@2.4.0", + "asn1js@3.0.6", + "tslib@2.8.1" + ] + }, + "@peculiar/asn1-pfx@2.4.0": { + "integrity": "sha512-fhpeoJ6T4nCLWT5tt3Un+BbyM1lLFnGXcRC2Ioe5ra2I0yptdjw05j20rV8BlUVzPIvUYpatq6joMQKe3ibh0w==", + "dependencies": [ + "@peculiar/asn1-cms", + "@peculiar/asn1-pkcs8", + "@peculiar/asn1-rsa@2.4.0", + "@peculiar/asn1-schema@2.4.0", + "asn1js@3.0.6", + "tslib@2.8.1" + ] + }, + "@peculiar/asn1-pkcs8@2.4.0": { + "integrity": "sha512-4r2LtsAM0HWXLxetGTYKyBumky7W6C1EuiOctqhl7zFK5MHjiZ+9WOeaoeTPR1g3OEoeG7KEWIkaUOyRH4ojTw==", + "dependencies": [ + "@peculiar/asn1-schema@2.4.0", + "@peculiar/asn1-x509@2.4.0", + "asn1js@3.0.6", + "tslib@2.8.1" + ] + }, + "@peculiar/asn1-pkcs9@2.4.0": { + "integrity": "sha512-D7paqEVpu9wuWuClMN+vR5cqJWJITNPaMoa9R+FmkJ8ywF9UaS2JFI0RYclKILNoLdLg1N4eUCoJvM+ubsIIZQ==", + "dependencies": [ + "@peculiar/asn1-cms", + "@peculiar/asn1-pfx", + "@peculiar/asn1-pkcs8", + "@peculiar/asn1-schema@2.4.0", + "@peculiar/asn1-x509@2.4.0", + "@peculiar/asn1-x509-attr", + "asn1js@3.0.6", + "tslib@2.8.1" ] }, "@peculiar/asn1-rsa@2.3.13": { "integrity": "sha512-wBNQqCyRtmqvXkGkL4DR3WxZhHy8fDiYtOjTeCd7SFE5F6GBeafw3EJ94PX/V0OJJrjQ40SkRY2IZu3ZSyBqcg==", "dependencies": [ - "@peculiar/asn1-schema", - "@peculiar/asn1-x509", - "asn1js", - "tslib" + "@peculiar/asn1-schema@2.3.13", + "@peculiar/asn1-x509@2.3.13", + "asn1js@3.0.5", + "tslib@2.8.1" + ] + }, + "@peculiar/asn1-rsa@2.4.0": { + "integrity": "sha512-6PP75voaEnOSlWR9sD25iCQyLgFZHXbmxvUfnnDcfL6Zh5h2iHW38+bve4LfH7a60x7fkhZZNmiYqAlAff9Img==", + "dependencies": [ + "@peculiar/asn1-schema@2.4.0", + "@peculiar/asn1-x509@2.4.0", + "asn1js@3.0.6", + "tslib@2.8.1" ] }, "@peculiar/asn1-schema@2.3.13": { "integrity": "sha512-3Xq3a01WkHRZL8X04Zsfg//mGaA21xlL4tlVn4v2xGT0JStiztATRkMwa5b+f/HXmY2smsiLXYK46Gwgzvfg3g==", "dependencies": [ - "asn1js", - "pvtsutils", - "tslib" + "asn1js@3.0.5", + "pvtsutils@1.3.5", + "tslib@2.8.1" + ] + }, + "@peculiar/asn1-schema@2.4.0": { + "integrity": "sha512-umbembjIWOrPSOzEGG5vxFLkeM8kzIhLkgigtsOrfLKnuzxWxejAcUX+q/SoZCdemlODOcr5WiYa7+dIEzBXZQ==", + "dependencies": [ + "asn1js@3.0.6", + "pvtsutils@1.3.6", + "tslib@2.8.1" + ] + }, + "@peculiar/asn1-x509-attr@2.4.0": { + "integrity": "sha512-Tr5Zi+wcE2sfR0gKRvsPwXoA1U8CuDnwiFbxCS+5Z1Nck9zlHj86+4/EZhwucjKXwPEHk1ekhqb3iwISY/+E/w==", + "dependencies": [ + "@peculiar/asn1-schema@2.4.0", + "@peculiar/asn1-x509@2.4.0", + "asn1js@3.0.6", + "tslib@2.8.1" ] }, "@peculiar/asn1-x509@2.3.13": { "integrity": "sha512-PfeLQl2skXmxX2/AFFCVaWU8U6FKW1Db43mgBhShCOFS1bVxqtvusq1hVjfuEcuSQGedrLdCSvTgabluwN/M9A==", "dependencies": [ - "@peculiar/asn1-schema", - "asn1js", + "@peculiar/asn1-schema@2.3.13", + "asn1js@3.0.5", "ipaddr.js", - "pvtsutils", - "tslib" + "pvtsutils@1.3.5", + "tslib@2.8.1" + ] + }, + "@peculiar/asn1-x509@2.4.0": { + "integrity": "sha512-F7mIZY2Eao2TaoVqigGMLv+NDdpwuBKU1fucHPONfzaBS4JXXCNCmfO0Z3dsy7JzKGqtDcYC1mr9JjaZQZNiuw==", + "dependencies": [ + "@peculiar/asn1-schema@2.4.0", + "asn1js@3.0.6", + "pvtsutils@1.3.6", + "tslib@2.8.1" + ] + }, + "@peculiar/x509@1.13.0": { + "integrity": "sha512-r9BOb1GZ3gx58Pog7u9x70spnHlCQPFm7u/ZNlFv+uBsU7kTDY9QkUD+l+X0awopDuCK1fkH3nEIZeMDSG/jlw==", + "dependencies": [ + "@peculiar/asn1-cms", + "@peculiar/asn1-csr", + "@peculiar/asn1-ecc@2.4.0", + "@peculiar/asn1-pkcs9", + "@peculiar/asn1-rsa@2.4.0", + "@peculiar/asn1-schema@2.4.0", + "@peculiar/asn1-x509@2.4.0", + "pvtsutils@1.3.6", + "reflect-metadata", + "tslib@2.8.1", + "tsyringe" ] }, "@rollup/plugin-babel@6.0.4_@babel+core@7.26.0_rollup@4.27.3": { @@ -1165,9 +1282,17 @@ "asn1js@3.0.5": { "integrity": "sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==", "dependencies": [ - "pvtsutils", + "pvtsutils@1.3.5", "pvutils", - "tslib" + "tslib@2.8.1" + ] + }, + "asn1js@3.0.6": { + "integrity": "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==", + "dependencies": [ + "pvtsutils@1.3.6", + "pvutils", + "tslib@2.8.1" ] }, "asynckit@0.4.0": { @@ -1481,7 +1606,13 @@ "pvtsutils@1.3.5": { "integrity": "sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==", "dependencies": [ - "tslib" + "tslib@2.8.1" + ] + }, + "pvtsutils@1.3.6": { + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "dependencies": [ + "tslib@2.8.1" ] }, "pvutils@1.1.3": { @@ -1493,6 +1624,9 @@ "safe-buffer" ] }, + "reflect-metadata@0.2.2": { + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" + }, "regenerate-unicode-properties@10.2.0": { "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", "dependencies": [ @@ -1665,9 +1799,18 @@ "code-block-writer" ] }, + "tslib@1.14.1": { + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, "tslib@2.8.1": { "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, + "tsyringe@4.10.0": { + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", + "dependencies": [ + "tslib@1.14.1" + ] + }, "typescript@5.6.3": { "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==" }, @@ -1739,6 +1882,7 @@ "jsr:@deno/dnt@~0.41.3", "jsr:@std/assert@^1.0.7", "jsr:@std/path@^1.0.8", + "jsr:@std/semver@^1.0.5", "jsr:@std/testing@^1.0.4", "npm:@babel/preset-env@^7.26.0", "npm:@rollup/plugin-babel@^6.0.4", @@ -1760,7 +1904,8 @@ "npm:@peculiar/asn1-ecc@^2.3.8", "npm:@peculiar/asn1-rsa@^2.3.8", "npm:@peculiar/asn1-schema@^2.3.8", - "npm:@peculiar/asn1-x509@^2.3.8" + "npm:@peculiar/asn1-x509@^2.3.8", + "npm:@peculiar/x509@^1.13.0" ] } } diff --git a/packages/server/deno.json b/packages/server/deno.json index 7fc85619..810ef4f7 100644 --- a/packages/server/deno.json +++ b/packages/server/deno.json @@ -12,7 +12,7 @@ "test" ] }, - "test": "deno test -A src/", + "test": "deno test -A src/ --trace-leaks", "test:watch": "deno test -A --watch src/", "docs:serve": { "command": "deno run -A jsr:@std/http/file-server --host 127.0.0.1 docs/", @@ -36,6 +36,7 @@ "lineWidth": 100 }, "imports": { + "@peculiar/x509": "npm:@peculiar/x509@^1.13.0", "tiny-cbor": "npm:@levischuck/tiny-cbor@^0.2.2", "@hexagon/base64": "npm:@hexagon/base64@^1.1.27", "@levischuck/tiny-cbor": "npm:@levischuck/tiny-cbor@^0.2.2", diff --git a/packages/server/src/helpers/isCertRevoked.ts b/packages/server/src/helpers/isCertRevoked.ts index d32c6000..5e1090e5 100644 --- a/packages/server/src/helpers/isCertRevoked.ts +++ b/packages/server/src/helpers/isCertRevoked.ts @@ -1,16 +1,11 @@ -import { AsnParser } from '@peculiar/asn1-schema'; import { - AuthorityKeyIdentifier, - Certificate, - CertificateList, - CRLDistributionPoints, - id_ce_authorityKeyIdentifier, - id_ce_cRLDistributionPoints, - id_ce_subjectKeyIdentifier, - SubjectKeyIdentifier, -} from '@peculiar/asn1-x509'; - -import { isoUint8Array } from './iso/index.ts'; + AuthorityKeyIdentifierExtension, + CRLDistributionPointsExtension, + SubjectKeyIdentifierExtension, + type X509Certificate, + X509Crl, +} from '@peculiar/x509'; + import { fetch } from './fetch.ts'; /** @@ -30,64 +25,52 @@ const cacheRevokedCerts: { [certAuthorityKeyID: string]: CAAuthorityInfo } = {}; * * CRL certificate structure referenced from https://tools.ietf.org/html/rfc5280#page-117 */ -export async function isCertRevoked(cert: Certificate): Promise { - const { extensions } = cert.tbsCertificate; +export async function isCertRevoked(cert: X509Certificate): Promise { + const { extensions } = cert; if (!extensions) { return false; } - let extAuthorityKeyID: AuthorityKeyIdentifier | undefined; - let extSubjectKeyID: SubjectKeyIdentifier | undefined; - let extCRLDistributionPoints: CRLDistributionPoints | undefined; + let extAuthorityKeyID: AuthorityKeyIdentifierExtension | undefined; + let extSubjectKeyID: SubjectKeyIdentifierExtension | undefined; + let extCRLDistributionPoints: CRLDistributionPointsExtension | 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, - ); + if (ext instanceof AuthorityKeyIdentifierExtension) { + extAuthorityKeyID = ext; + } else if (ext instanceof SubjectKeyIdentifierExtension) { + extSubjectKeyID = ext; + } else if (ext instanceof CRLDistributionPointsExtension) { + extCRLDistributionPoints = ext; } }); // 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), - ); + if (extAuthorityKeyID && extAuthorityKeyID.keyId) { + keyIdentifier = extAuthorityKeyID.keyId; } 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)); + keyIdentifier = extSubjectKeyID.keyId; } - const certSerialHex = isoUint8Array.toHex( - new Uint8Array(cert.tbsCertificate.serialNumber), - ); - if (keyIdentifier) { const cached = cacheRevokedCerts[keyIdentifier]; if (cached) { const now = new Date(); // If there's a nextUpdate then make sure we're before it if (!cached.nextUpdate || cached.nextUpdate > now) { - return cached.revokedCerts.indexOf(certSerialHex) >= 0; + return cached.revokedCerts.indexOf(cert.serialNumber) >= 0; } } } - const crlURL = extCRLDistributionPoints?.[0].distributionPoint?.fullName?.[0] + const crlURL = extCRLDistributionPoints?.distributionPoints?.[0].distributionPoint?.fullName?.[0] .uniformResourceIdentifier; // If no URL is provided then we have nothing to check @@ -104,9 +87,9 @@ export async function isCertRevoked(cert: Certificate): Promise { return false; } - let data: CertificateList; + let data: X509Crl; try { - data = AsnParser.parse(certListBytes, CertificateList); + data = new X509Crl(certListBytes); } catch (_err) { // Something was malformed with the CRL, so pass return false; @@ -118,18 +101,16 @@ export async function isCertRevoked(cert: Certificate): Promise { }; // nextUpdate - if (data.tbsCertList.nextUpdate) { - newCached.nextUpdate = data.tbsCertList.nextUpdate.getTime(); + if (data.nextUpdate) { + newCached.nextUpdate = data.nextUpdate; } // revokedCertificates - const revokedCerts = data.tbsCertList.revokedCertificates; + const revokedCerts = data.entries; if (revokedCerts) { for (const cert of revokedCerts) { - const revokedHex = isoUint8Array.toHex( - new Uint8Array(cert.userCertificate), - ); + const revokedHex = cert.serialNumber; newCached.revokedCerts.push(revokedHex); } @@ -138,7 +119,7 @@ export async function isCertRevoked(cert: Certificate): Promise { cacheRevokedCerts[keyIdentifier] = newCached; } - return newCached.revokedCerts.indexOf(certSerialHex) >= 0; + return newCached.revokedCerts.indexOf(cert.serialNumber) >= 0; } return false; diff --git a/packages/server/src/helpers/validateCertificatePath.ts b/packages/server/src/helpers/validateCertificatePath.ts index f84fb400..9e9c9b6e 100644 --- a/packages/server/src/helpers/validateCertificatePath.ts +++ b/packages/server/src/helpers/validateCertificatePath.ts @@ -1,11 +1,7 @@ -import { AsnSerializer } from '@peculiar/asn1-schema'; -import type { Certificate } from '@peculiar/asn1-x509'; +import { X509Certificate } from '@peculiar/x509'; import { isCertRevoked } from './isCertRevoked.ts'; -import { verifySignature } from './verifySignature.ts'; -import { mapX509SignatureAlgToCOSEAlg } from './mapX509SignatureAlgToCOSEAlg.ts'; -import { type CertificateInfo, getCertificateInfo } from './getCertificateInfo.ts'; -import { convertPEMToBytes } from './convertPEMToBytes.ts'; +import { getWebCrypto } from './iso/isoCrypto/getWebCrypto.ts'; /** * Traverse an array of PEM certificates and ensure they form a proper chain @@ -21,24 +17,115 @@ export async function validateCertificatePath( return true; } + const WebCrypto = await getWebCrypto(); + + // Prepare to work with x5c certs + const x5cCertsParsed = x5cCertsPEM.map((certPEM) => new X509Certificate(certPEM)); + + // Check for any expired or temporally invalid certs in x5c + for (let i = 0; i < x5cCertsParsed.length; i++) { + const cert = x5cCertsParsed[i]; + const certPEM = x5cCertsPEM[i]; + + try { + await assertCertNotRevoked(cert); + } catch (_err) { + throw new Error(`Found revoked certificate in x5c:\n${certPEM}`); + } + + try { + assertCertIsWithinValidTimeWindow(cert.notBefore, cert.notAfter); + } catch (_err) { + throw new Error(`Found certificate out of validity period in x5c:\n${certPEM}`); + } + } + + // Prepare to work with trust anchor certs + const trustAnchorsParsed = trustAnchorsPEM.map((certPEM) => { + try { + return new X509Certificate(certPEM); + } catch (err) { + const _err = err as Error; + throw new Error(`Could not parse trust anchor certificate:\n${certPEM}`, { cause: _err }); + } + }); + + // Filter out any expired or temporally invalid trust anchors certs + const validTrustAnchors: X509Certificate[] = []; + for (let i = 0; i < trustAnchorsParsed.length; i++) { + const cert = trustAnchorsParsed[i]; + + try { + await assertCertNotRevoked(cert); + } catch (_err) { + // Continue processing the other certs + continue; + } + + try { + assertCertIsWithinValidTimeWindow(cert.notBefore, cert.notAfter); + } catch (_err) { + // Continue processing the other certs + continue; + } + + validTrustAnchors.push(cert); + } + + if (validTrustAnchors.length === 0) { + throw new Error('No specified trust anchor was valid for verifying x5c'); + } + + // Try to verify x5c with each trust anchor let invalidSubjectAndIssuerError = false; - let certificateNotYetValidOrExpiredErrorMessage = undefined; - for (const anchorPEM of trustAnchorsPEM) { + for (const anchor of trustAnchorsParsed) { try { - const certsWithTrustAnchor = x5cCertsPEM.concat([anchorPEM]); - await _validatePath(certsWithTrustAnchor); + const x5cWithTrustAnchor = x5cCertsParsed.concat([anchor]); + + if (new Set(x5cWithTrustAnchor).size !== x5cWithTrustAnchor.length) { + throw new Error('Invalid certificate path: found duplicate certificates'); + } + + // Check signatures, and notBefore and notAfter + for (let i = 0; i < x5cWithTrustAnchor.length - 1; i++) { + const subject = x5cWithTrustAnchor[i]; + const issuer = x5cWithTrustAnchor[i + 1]; + + // Leaf or intermediate cert, make sure the next cert in the chain signed it + const issuerSignedSubject = await subject.verify( + { publicKey: issuer.publicKey, signatureOnly: true }, + WebCrypto, + ); + + if (!issuerSignedSubject) { + throw new InvalidSubjectAndIssuer(); + } + + if (issuer.subject === issuer.issuer) { + // Root cert detected, make sure it signed itself + const issuerSignedIssuer = await issuer.verify( + { publicKey: issuer.publicKey, signatureOnly: true }, + WebCrypto, + ); + + if (!issuerSignedIssuer) { + throw new InvalidSubjectAndIssuer(); + } + + // Don't process anything else after a root cert + break; + } + } + // If we successfully validated a path then there's no need to continue. Reset any existing // errors that were thrown by earlier trust anchors invalidSubjectAndIssuerError = false; - certificateNotYetValidOrExpiredErrorMessage = undefined; break; } catch (err) { if (err instanceof InvalidSubjectAndIssuer) { invalidSubjectAndIssuerError = true; - } else if (err instanceof CertificateNotYetValidOrExpired) { - certificateNotYetValidOrExpiredErrorMessage = err.message; } else { - throw err; + throw new Error('Unexpected error while validating certificate path', { cause: err }); } } } @@ -46,52 +133,6 @@ export async function validateCertificatePath( // We tried multiple trust anchors and none of them worked if (invalidSubjectAndIssuerError) { throw new InvalidSubjectAndIssuer(); - } else if (certificateNotYetValidOrExpiredErrorMessage) { - throw new CertificateNotYetValidOrExpired( - certificateNotYetValidOrExpiredErrorMessage, - ); - } - - return true; -} - -/** - * @param x5cCerts X.509 `x5c` certs in PEM string format - * @param anchorCert X.509 trust anchor cert in PEM string format - */ -async function _validatePath(x5cCertsWithTrustAnchorPEM: string[]): Promise { - if (new Set(x5cCertsWithTrustAnchorPEM).size !== x5cCertsWithTrustAnchorPEM.length) { - throw new Error('Invalid certificate path: found duplicate certificates'); - } - - // Make sure no certs are revoked, and all are within their time validity window - for (const certificatePEM of x5cCertsWithTrustAnchorPEM) { - const certInfo = getCertificateInfo(convertPEMToBytes(certificatePEM)); - await assertCertNotRevoked(certInfo.parsedCertificate); - assertCertIsWithinValidTimeWindow(certInfo, certificatePEM); - } - - // Make sure each x5c cert is issued by the next certificate in the chain - for (let i = 0; i < (x5cCertsWithTrustAnchorPEM.length - 1); i += 1) { - const subjectPem = x5cCertsWithTrustAnchorPEM[i]; - const issuerPem = x5cCertsWithTrustAnchorPEM[i + 1]; - - const subjectInfo = getCertificateInfo(convertPEMToBytes(subjectPem)); - const issuerInfo = getCertificateInfo(convertPEMToBytes(issuerPem)); - - // Make sure subject issuer is issuer subject - if (subjectInfo.issuer.combined !== issuerInfo.subject.combined) { - throw new InvalidSubjectAndIssuer(); - } - - const issuerCertIsRootCert = issuerInfo.issuer.combined === issuerInfo.subject.combined; - - await assertSubjectIsSignedByIssuer(subjectInfo.parsedCertificate, issuerPem); - - // Perform one final check if the issuer cert is also a root certificate - if (issuerCertIsRootCert) { - await assertSubjectIsSignedByIssuer(issuerInfo.parsedCertificate, issuerPem); - } } return true; @@ -100,56 +141,22 @@ async function _validatePath(x5cCertsWithTrustAnchorPEM: string[]): Promise { +async function assertCertNotRevoked(certificate: X509Certificate): Promise { // Check for certificate revocation const subjectCertRevoked = await isCertRevoked(certificate); if (subjectCertRevoked) { - throw new Error(`Found revoked certificate in certificate path`); + throw new Error('Found revoked certificate in certificate path'); } } /** * Require the cert to be within its notBefore and notAfter time window - * - * @param certInfo Parsed cert information - * @param certPEM PEM-formatted certificate, for error reporting */ -function assertCertIsWithinValidTimeWindow(certInfo: CertificateInfo, certPEM: string): void { - const { notBefore, notAfter } = certInfo; - +function assertCertIsWithinValidTimeWindow(certNotBefore: Date, certNotAfter: Date): void { const now = new Date(Date.now()); - if (notBefore > now || notAfter < now) { - throw new CertificateNotYetValidOrExpired( - `Certificate is not yet valid or expired: ${certPEM}`, - ); - } -} - -/** - * Ensure that the subject cert has been signed by the next cert in the chain - */ -async function assertSubjectIsSignedByIssuer( - subjectCert: Certificate, - issuerPEM: string, -): Promise { - // Verify the subject certificate's signature with the issuer cert's public key - const data = AsnSerializer.serialize(subjectCert.tbsCertificate); - const signature = subjectCert.signatureValue; - const signatureAlgorithm = mapX509SignatureAlgToCOSEAlg( - subjectCert.signatureAlgorithm.algorithm, - ); - const issuerCertBytes = convertPEMToBytes(issuerPEM); - - const verified = await verifySignature({ - data: new Uint8Array(data), - signature: new Uint8Array(signature), - x509Certificate: issuerCertBytes, - hashAlgorithm: signatureAlgorithm, - }); - - if (!verified) { - throw new InvalidSubjectSignatureForIssuer(); + if (certNotBefore > now || certNotAfter < now) { + throw new Error('Certificate is not yet valid or expired'); } } @@ -161,18 +168,3 @@ class InvalidSubjectAndIssuer extends Error { this.name = 'InvalidSubjectAndIssuer'; } } - -class InvalidSubjectSignatureForIssuer extends Error { - constructor() { - const message = 'Subject signature was invalid for issuer'; - super(message); - this.name = 'InvalidSubjectSignatureForIssuer'; - } -} - -class CertificateNotYetValidOrExpired extends Error { - constructor(message: string) { - super(message); - this.name = 'CertificateNotYetValidOrExpired'; - } -} diff --git a/packages/server/src/registration/verifications/verifyAttestationAndroidKey.test.ts b/packages/server/src/registration/verifications/verifyAttestationAndroidKey.test.ts index 1eca7de5..edbbe24c 100644 --- a/packages/server/src/registration/verifications/verifyAttestationAndroidKey.test.ts +++ b/packages/server/src/registration/verifications/verifyAttestationAndroidKey.test.ts @@ -1,9 +1,13 @@ import { assertEquals } from '@std/assert'; import { FakeTime } from '@std/testing/time'; +import { lessThan, parse } from '@std/semver'; import { SettingsService } from '../../services/settingsService.ts'; import { verifyRegistrationResponse } from '../verifyRegistrationResponse.ts'; -import { Google_Hardware_Attestation_Root_2 } from '../../services/defaultRootCerts/android-key.ts'; +import { + Google_Hardware_Attestation_Root_1, + Google_Hardware_Attestation_Root_2, +} from '../../services/defaultRootCerts/android-key.ts'; /** * Clear out root certs for android-key since responses were captured from FIDO Conformance testing @@ -114,3 +118,65 @@ Deno.test('should verify Android Keystore response from a Pixel 8a in January 20 mockDate.restore(); }); + +Deno.test({ + name: 'should verify Android Keystore response from a Samsung Galaxy S9+ running Android 10', + /** + * Verifying a SHA256 hash with a P-384 public key, or vice-versa with SHA384 and P-256, + * isn't supported till Deno v2.2.0. In Deno v2.1, this test will error out with a + * "Not implemented" error so I'm ignoring this test in older Deno runtimes: + * + * https://github.com/denoland/deno/blob/v2.1/ext/crypto/00_crypto.js#L1318-L1326 + */ + ignore: lessThan(parse(Deno.version.deno), parse('2.2.0')), +}, async () => { + SettingsService.setRootCertificates({ + identifier: 'android-key', + certificates: [Google_Hardware_Attestation_Root_1], + }); + + /** + * Faking time to something that'll satisfy all of these ranges: + * + * { + * notBefore: 1970-01-01T00:00:00.000Z, + * notAfter: 2106-02-07T06:28:15.000Z + * } + * { + * notBefore: 2019-06-13T19:31:18.000Z, + * notAfter: 2029-06-10T19:31:18.000Z + * } + * { + * notBefore: 2019-06-13T19:25:28.000Z, + * notAfter: 2029-06-10T19:25:28.000Z + * } + * { + * notBefore: 2016-05-26T16:28:52.000Z, + * notAfter: 2026-05-24T16:28:52.000Z + * } + */ + const mockDate = new FakeTime(new Date('2025-07-15T10:00:00.000Z')); + + const verification = await verifyRegistrationResponse({ + response: { + id: 'AZNJEB2RcdcMJ0kZ1X1lyA6d7ENiKF5K945bpbZXxdVqoyjENSnHSZuxZz9sBMVyKAArpVBhwWr7WTutT_epNsk', + rawId: + 'AZNJEB2RcdcMJ0kZ1X1lyA6d7ENiKF5K945bpbZXxdVqoyjENSnHSZuxZz9sBMVyKAArpVBhwWr7WTutT_epNsk', + response: { + attestationObject: + 'o2NmbXRrYW5kcm9pZC1rZXlnYXR0U3RtdKNjYWxnJmNzaWdYRzBFAiBN6uMvi4Arrog6bM-EH_HHdcYowZb9AZ3OP8LF7BsOwQIhAIGMW51yybiu_p90i60qFilQ2NTBfNSKMxWSd-_ElLGGY3g1Y4RZAsAwggK8MIICYqADAgECAgEBMAoGCCqGSM49BAMCMCkxGTAXBgNVBAUTEDljZmFiZjY5ZWNjMzc0OWMxDDAKBgNVBAwMA1RFRTAgFw03MDAxMDEwMDAwMDBaGA8yMTA2MDIwNzA2MjgxNVowHzEdMBsGA1UEAwwUQW5kcm9pZCBLZXlzdG9yZSBLZXkwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARF-xUruvQrCRWHiRDV4Lsq4FjoEpLFf361IEQaaeBsGuz0zt29H0BKNbEIUpvWcuKBKwmcOvMH2wFAZ7tRHblHo4IBgTCCAX0wDgYDVR0PAQH_BAQDAgeAMIIBaQYKKwYBBAHWeQIBEQSCAVkwggFVAgEDCgEBAgEECgEBBCCtDPAKpMZ9hMbYOO1XIwN-v_gVMOTGAjDefrroBsj2-QQAMHe_hT0IAgYBl_krnvi_hUVnBGUwYzE9MBsEFmNvbS5nb29nbGUuYW5kcm9pZC5nc2YCAR4wHgQWY29tLmdvb2dsZS5hbmRyb2lkLmdtcwIEDwvKrjEiBCDw_WxbQQ8lyyXDtTNGyJcvrjD47nQR35EEgK1rLWDbgzCBqaEFMQMCAQKiAwIBA6MEAgIBAKUFMQMCAQSqAwIBAb-DeAMCAQO_g3kDAgEKv4U-AwIBAL-FQEwwSgQg2O2bmq25z_lUP96p1NX4bjoeGqNeSEFetzrqoDDefYEBAf8KAQAEIG_Q-U6jhMM6Kdz7OeX58NCiyMveuzh_N9gbNCMAB8_rv4VBBQIDAa2wv4VCBQIDAxV_v4VOBgIEATRlnb-FTwYCBAE0ZZ0wCgYIKoZIzj0EAwIDSAAwRQIgLy0SGjDM7BDO9xLfOjfHkYMiKMeY0CZ1SBs-lsAPSqcCIQDpxwWdHetnjLhMJrd6HGw88aI5-GZlO9_7mpNWu94r7lkCKDCCAiQwggGroAMCAQICCgNwFmEVJQaTJJAwCgYIKoZIzj0EAwIwKTEZMBcGA1UEBRMQMjg1ZjdmYTllZWIxNDAxNDEMMAoGA1UEDAwDVEVFMB4XDTE5MDYxMzE5MzExOFoXDTI5MDYxMDE5MzExOFowKTEZMBcGA1UEBRMQOWNmYWJmNjllY2MzNzQ5YzEMMAoGA1UEDAwDVEVFMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE0ZjVvvE-xQefNwWt2g0fjCzKcCGErvUUla1Sy0YRXGTUPW_xZg7OpoWB1XFfFlgM-1xih8UmDGpFF77KX2WHtqOBujCBtzAdBgNVHQ4EFgQUQHGUFV40EuDlHb86QZ6X74Rv1GswHwYDVR0jBBgwFoAUZsclWZX-WiRVCJ2uL-JyioxCRzowDwYDVR0TAQH_BAUwAwEB_zAOBgNVHQ8BAf8EBAMCAgQwVAYDVR0fBE0wSzBJoEegRYZDaHR0cHM6Ly9hbmRyb2lkLmdvb2dsZWFwaXMuY29tL2F0dGVzdGF0aW9uL2NybC8wMzcwMTY2MTE1MjUwNjkzMjQ5MDAKBggqhkjOPQQDAgNnADBkAjAay0bXweDTEiM5h3qEFZh0lvjfm7BrD6PdwSgHiMSoln9Lp1Y6dtdMifLSuqTSPp4CMASAp7BEH6DBT-B6S3MnpAz-pS_BPAZDgYr8rFH2tnlMM1WOEtjQIQ2KfodC4tJU81kD1TCCA9EwggG5oAMCAQICCgOIJmdgZYmWheIwDQYJKoZIhvcNAQELBQAwGzEZMBcGA1UEBRMQZjkyMDA5ZTg1M2I2YjA0NTAeFw0xOTA2MTMxOTI1MjhaFw0yOTA2MTAxOTI1MjhaMCkxGTAXBgNVBAUTEDI4NWY3ZmE5ZWViMTQwMTQxDDAKBgNVBAwMA1RFRTB2MBAGByqGSM49AgEGBSuBBAAiA2IABAUTSmkto8xjo3bsJ2VyoiU24xF1pA1wLmmqy6_rD60WMB4I3fU73p-NXVdQ720JSXel8O0-BH0kOQaGkQytYLXFnN7IcWfeQp1weEZpd8IbUPiN8gTUyl1Y0GCKSBL-kqOBtjCBszAdBgNVHQ4EFgQUZsclWZX-WiRVCJ2uL-JyioxCRzowHwYDVR0jBBgwFoAUNmHhAHyIBQlRi0RsR_8aTMnqTxIwDwYDVR0TAQH_BAUwAwEB_zAOBgNVHQ8BAf8EBAMCAgQwUAYDVR0fBEkwRzBFoEOgQYY_aHR0cHM6Ly9hbmRyb2lkLmdvb2dsZWFwaXMuY29tL2F0dGVzdGF0aW9uL2NybC9FOEZBMTk2MzE0RDJGQTE4MA0GCSqGSIb3DQEBCwUAA4ICAQBsKTstdjFUeQ1dVLRyx9ecE5qQaZV26Bos7boyz-R2HJv4iJ492aii9FLVwLei2c-aVgHuAKIfht3kP25-0crEoFKc0AiBzX3LS9a7P3V4tt8z-kBiKQkJtcbEw9r2HlTDviEa7GCRvLbFoORFyTZjQTR5tJQQEhYrsB5qo-vVweHZ_uQ_KR_Ag5DzNGPh_KFXwz-qVh720Ca99wixT4wMgGFIgIZxTIAz8c3kDYXqQ5j4jplksQbghTSN5lnKPeVjZpc_dga4r09bpm61z2ylNybUnBwUnkpRyzNVRlpZRpd0yq7royq_QRI-zoZd4nx--1_AqC3XshBfmSz9Dxxx8aNQ0QR3WJtLtya9ECxmyLh9LqNbCgoRSLi4g8sDLkIy9yaY7goL7XVdFZfTDKiwne-BsjD6Fgl7yFmCkndJMvJjVD0r4WaoFB-Tomx0eg7Lgdy2sJs5T_Yo-woGvn2qPGFsbm1oib4MgnsK2JtjH9VmfB2oUQ16sLWSVaqMHtSx1ZB2FxyB1auRs-sNeNxhAkLw4D-6R8j7Rug3sXoV4p14UL7KvjL8p2th7CImGvgUHRJ_EUhpEl6Rc8XLPeHi64Qw7POnha8oSaZFpHkSR2oGDKkHVcqUBz3KFizVh15du40fpq2TYtH4of_hJlUzEtH2Uidou3WyNtSjhNQq7VkFZDCCBWAwggNIoAMCAQICCQDo-hljFNL6GDANBgkqhkiG9w0BAQsFADAbMRkwFwYDVQQFExBmOTIwMDllODUzYjZiMDQ1MB4XDTE2MDUyNjE2Mjg1MloXDTI2MDUyNDE2Mjg1MlowGzEZMBcGA1UEBRMQZjkyMDA5ZTg1M2I2YjA0NTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK-2x4IrsacB7Cu0LovMVBZjq--YLzLHf3UxAwyXUksbX-gJ-8cqqUUfdDy9mm8TNXRKpV539rasNTXuF8JeY5UX3ZyS5jdKU8v-JY-P-7b9EpN4oipMqZxFLUelnzIB9EGXyhzNfnYvsvUxUbb-sv_9K2_k_lvGvZ7DS_4II52q_OuOtajtKzrNnF46d5DhtRRCeTFZhZgRrZ6yqWu916V8k6kcQfzNJ9Z_1vZxqguBUmGtOE-jeUSGRgTds9jE-SChmxZWwvFK1tA8VuwGCJkEHB7Rpf5tNEC1VrrR0KFSWJxT5V03B2LwEi7vkYYbGw5sTICSdJnA6b7AuD47wfk8csBJYEu9LxNF5iw_jibb7AbJR2bzwSgjnU9DEvrYEjiH4Gvs9WdYO_g1WoH-6rr5moPI3z4qMir8ZyvxILE1FYtoIc6vMJtu7nf5iDOwGNqhDkUfBqN01QeB81kIKWa7d4uTCJQmmOdOC80kYooBwswD5R8LPltKweTfnq-f9qSSp3wUg4gohQFbQizme4C4jJtI4TtgerVFxyP_jET48tNoufZSDTEUXr-ehirXHfajv9JFCVnWU3QNl6EvNosT72bV0KVKbi9dmm_vRGgyvGeERyWGHwk90ObzQF2olkPvD01ptkIAUf25MElnPjaVBYDTzfT70IvFhIOVJgBjAgMBAAGjgaYwgaMwHQYDVR0OBBYEFDZh4QB8iAUJUYtEbEf_GkzJ6k8SMB8GA1UdIwQYMBaAFDZh4QB8iAUJUYtEbEf_GkzJ6k8SMA8GA1UdEwEB_wQFMAMBAf8wDgYDVR0PAQH_BAQDAgGGMEAGA1UdHwQ5MDcwNaAzoDGGL2h0dHBzOi8vYW5kcm9pZC5nb29nbGVhcGlzLmNvbS9hdHRlc3RhdGlvbi9jcmwvMA0GCSqGSIb3DQEBCwUAA4ICAQAgyMONS9ypVxtGjIkv_3KqxvhEoR1BqPBzbMN9FtZCbY5-lAcETOo55osHwT2_FQPdXIW9r7LALV9s2076gSffiwTxgncPxOd0W3_OqocSmogBzo6bwMuWN5tNJqgtMP2cL47tbcG-L4S2ieTZFCWLFEu65iShxwZxEy4vBhaohLKk1qRv-om2Ar-62AwSQ3EfVutgVvY3yKAUHMVAlCaLjDx9uZSzXA3NbLKrwtr-4lICPS3qDNbDaL6j5kFIhvax5Ytb18cwsmjE48H7ZCS5H-u9uAxYbiroNoyE1dEJF72iVheJ1GhzkzQOLiVPVg72SyNY_NwPv8ZwCVLnCL_8xidQDB9m6B6hfAmNei6bGIAberSscVh9NF3MgwnVtipQQnqm0D3LBZlslroMXXHpIWLAFsqEn_NfDVLGXQVgWkfzrpF6zS35EO_SMmaIWW72mzv1_jFU9664gKCnPKBNlMLOgxfutD1e_1iD4zb18knarKSJkje_Jn5cQ6sC6kQWJANyO-aqaSxhva6e1AnUY8TJfGQwZXfu8rx1YLdXFcycfcZ8hggtt1GonDA0l2KweCOFh1zxo8YWbgrjwS03Ti1PGEbzGHRL2Hm1hzKb8BghemwMdyQaSHjkNcAwectFEonFd2IGBpovjWX4QOFEUoe-2HerriTiRDUWjVU85GhhdXRoRGF0YVjFT848GIB1YSy2AgqgP_bEoHGe74fBQecLAfr8oYF8zI5FAAAAALk_2WHy5kYvsSKCACJH3ngAQQGTSRAdkXHXDCdJGdV9ZcgOnexDYiheSveOW6W2V8XVaqMoxDUpx0mbsWc_bATFcigAK6VQYcFq-1k7rU_3qTbJpQECAyYgASFYIEX7FSu69CsJFYeJENXguyrgWOgSksV_frUgRBpp4GwaIlgg7PTO3b0fQEo1sQhSm9Zy4oErCZw68wfbAUBnu1EduUc', + clientDataJSON: + 'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoidkNMQVIzQUdoeGZQMFEtTDkyX0JzcHBraEFDYnkwSGp0TlpFQkZKMGdPayIsIm9yaWdpbiI6Imh0dHBzOi8vcG9ydGFsLmZ4b24uY29tIiwiY3Jvc3NPcmlnaW4iOmZhbHNlLCJvdGhlcl9rZXlzX2Nhbl9iZV9hZGRlZF9oZXJlIjoiZG8gbm90IGNvbXBhcmUgY2xpZW50RGF0YUpTT04gYWdhaW5zdCBhIHRlbXBsYXRlLiBTZWUgaHR0cHM6Ly9nb28uZ2wveWFiUGV4In0', + }, + type: 'public-key', + clientExtensionResults: { credProps: { rk: false } }, + }, + expectedChallenge: 'vCLAR3AGhxfP0Q-L92_BsppkhACby0HjtNZEBFJ0gOk', + expectedOrigin: 'https://portal.fxon.com', + expectedRPID: 'portal.fxon.com', + }); + + assertEquals(verification.verified, true); + + mockDate.restore(); +}); diff --git a/packages/server/src/registration/verifications/verifyAttestationAndroidKey.ts b/packages/server/src/registration/verifications/verifyAttestationAndroidKey.ts index 01b6e0c6..f9058dcf 100644 --- a/packages/server/src/registration/verifications/verifyAttestationAndroidKey.ts +++ b/packages/server/src/registration/verifications/verifyAttestationAndroidKey.ts @@ -132,7 +132,7 @@ export async function verifyAttestationAndroidKey( }); } catch (err) { const _err = err as Error; - throw new Error(`${_err.message} (Android Key)`); + throw new Error(`${_err.message} (Android Key)`, { cause: _err }); } } else { /** @@ -145,7 +145,7 @@ export async function verifyAttestationAndroidKey( await validateCertificatePath(x5cNoRootPEM, x5cRootPEM); } catch (err) { const _err = err as Error; - throw new Error(`${_err.message} (Android Key)`); + throw new Error(`${_err.message} (Android Key)`, { cause: _err }); } /** diff --git a/packages/server/src/registration/verifyRegistrationResponse.test.ts b/packages/server/src/registration/verifyRegistrationResponse.test.ts index 46af5db7..efb38230 100644 --- a/packages/server/src/registration/verifyRegistrationResponse.test.ts +++ b/packages/server/src/registration/verifyRegistrationResponse.test.ts @@ -1,5 +1,5 @@ import { assert, assertEquals, assertFalse, assertObjectMatch, assertRejects } from '@std/assert'; -import { returnsNext, stub } from '@std/testing/mock'; +import { resolvesNext, returnsNext, stub } from '@std/testing/mock'; import { FakeTime } from '@std/testing/time'; import { verifyRegistrationResponse } from './verifyRegistrationResponse.ts'; @@ -504,41 +504,28 @@ Deno.test('should throw error if unsupported alg is used', async () => { mockDecodePubKey.restore(); }); -Deno.test( - 'should not include authenticator info if not verified', - { - /** - * CI likes to intermittently fail this test with the following: - * - * > An async operation to verify data was started in this test, but never completed. This is - * > often caused by not awaiting the result of a `crypto.subtle.verify` call - * - * I suspect it's something to do with how `_verifySignatureInternals.stubThis` is being mocked. - * This will disable this warning on an otherwise passing test. - */ - sanitizeOps: false, - }, - async () => { - const mockVerifySignature = stub( - _verifySignatureInternals, - 'stubThis', - returnsNext([new Promise((resolve) => resolve(false))]), - ); +Deno.test.ignore('should not include authenticator info if not verified', async () => { + // NOTE: This test previously required specifying { sanitizeOps: false } to reliably pass. It's + // been removed but just in case I'm calling this flaky test out. + const mockVerifySignature = stub( + _verifySignatureInternals, + 'stubThis', + resolvesNext([false]), + ); - const verification = await verifyRegistrationResponse({ - response: attestationFIDOU2F, - expectedChallenge: attestationFIDOU2FChallenge, - expectedOrigin: 'https://dev.dontneeda.pw', - expectedRPID: 'dev.dontneeda.pw', - requireUserVerification: false, - }); + const verification = await verifyRegistrationResponse({ + response: attestationFIDOU2F, + expectedChallenge: attestationFIDOU2FChallenge, + expectedOrigin: 'https://dev.dontneeda.pw', + expectedRPID: 'dev.dontneeda.pw', + requireUserVerification: false, + }); - assertFalse(verification.verified); - assertEquals(verification.registrationInfo, undefined); + assertFalse(verification.verified); + assertEquals(verification.registrationInfo, undefined); - mockVerifySignature.restore(); - }, -); + mockVerifySignature.restore(); +}); Deno.test('should throw an error if user verification is required but user was not verified', async () => { const mockParseAuthData = stub( diff --git a/packages/server/src/services/defaultRootCerts/mds.ts b/packages/server/src/services/defaultRootCerts/mds.ts index 1a06db17..8f1d55ad 100644 --- a/packages/server/src/services/defaultRootCerts/mds.ts +++ b/packages/server/src/services/defaultRootCerts/mds.ts @@ -9,24 +9,24 @@ * CB:B5:22:D7:B7:F1:27:AD:6A:01:13:86:5B:DF:1C:D4:10:2E:7D:07:59:AF:63:5A:7C:F4:72:0D:C9:63:C5:3B */ export const GlobalSign_Root_CA_R3 = `-----BEGIN CERTIFICATE----- - MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G - A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp - Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 - MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG - A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI - hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 - RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT - gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm - KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd - QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ - XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw - DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o - LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU - RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp - jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK - 6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX - mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs - Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH - WD9f - -----END CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 +MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 +RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT +gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm +KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd +QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ +XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o +LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU +RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp +jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK +6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX +mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs +Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH +WD9f +-----END CERTIFICATE----- `; diff --git a/packages/server/src/services/metadataService.ts b/packages/server/src/services/metadataService.ts index 003f1943..7ae339d6 100644 --- a/packages/server/src/services/metadataService.ts +++ b/packages/server/src/services/metadataService.ts @@ -250,7 +250,8 @@ export class BaseMetadataService implements MetadataService { // From FIDO MDS docs: "ignore the file if the chain cannot be verified or if one of the // chain certificates is revoked" throw new Error( - `BLOB certificate path could not be validated: ${_error.message}`, + 'BLOB certificate path could not be validated', + { cause: _error }, ); }