You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

143 lines
5.8 KiB

"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';
}
}