"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.validateCertificatePath = validateCertificatePath; const x509_1 = require("@peculiar/x509"); const isCertRevoked_js_1 = require("./isCertRevoked.js"); const getWebCrypto_js_1 = require("./iso/isoCrypto/getWebCrypto.js"); /** * Traverse an array of PEM certificates and ensure they form a proper chain * @param x5cCertsPEM Typically the result of `x5c.map(convertASN1toPEM)` * @param trustAnchorsPEM PEM-formatted certs that an attestation statement x5c may chain back to */ async function validateCertificatePath(x5cCertsPEM, trustAnchorsPEM = []) { if (trustAnchorsPEM.length === 0) { // We have no trust anchors to chain back to, so skip path validation return true; } const WebCrypto = await (0, getWebCrypto_js_1.getWebCrypto)(); // Prepare to work with x5c certs const x5cCertsParsed = x5cCertsPEM.map((certPEM) => new x509_1.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 x509_1.X509Certificate(certPEM); } catch (err) { const _err = err; 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 = []; 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; for (const anchor of trustAnchorsParsed) { try { 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; break; } catch (err) { if (err instanceof InvalidSubjectAndIssuer) { invalidSubjectAndIssuerError = true; } else { throw new Error('Unexpected error while validating certificate path', { cause: err }); } } } // We tried multiple trust anchors and none of them worked if (invalidSubjectAndIssuerError) { throw new InvalidSubjectAndIssuer(); } return true; } /** * Check if the certificate is revoked or not. If it is, raise an error */ async function assertCertNotRevoked(certificate) { // Check for certificate revocation const subjectCertRevoked = await (0, isCertRevoked_js_1.isCertRevoked)(certificate); if (subjectCertRevoked) { throw new Error('Found revoked certificate in certificate path'); } } /** * Require the cert to be within its notBefore and notAfter time window */ function assertCertIsWithinValidTimeWindow(certNotBefore, certNotAfter) { const now = new Date(Date.now()); if (certNotBefore > now || certNotAfter < now) { throw new Error('Certificate is not yet valid or expired'); } } // Custom errors to help pass on certain errors class InvalidSubjectAndIssuer extends Error { constructor() { const message = 'Subject issuer did not match issuer subject'; super(message); this.name = 'InvalidSubjectAndIssuer'; } }