mirror of https://github.com/ghostfolio/ghostfolio
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
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';
|
|
}
|
|
}
|
|
|