"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CertificateRevokedError = exports.CRL_VALIDATOR_INTERNAL = void 0;
exports.isCrlValidationEnabled = isCrlValidationEnabled;
exports.corkSocketAndValidateCrl = corkSocketAndValidateCrl;
exports.validateCrl = validateCrl;
const asn1_js_rfc5280_1 = __importDefault(require("asn1.js-rfc5280"));
const crypto_1 = __importDefault(require("crypto"));
const logger_1 = __importDefault(require("../../logger"));
const certificate_utils_1 = require("./certificate_utils");
const crl_fetcher_1 = require("./crl_fetcher");
const crl_signature_verifier_1 = require("./crl_signature_verifier");
const errors_1 = require("../../errors");
// Allows to mock/spy internal calls in tests
exports.CRL_VALIDATOR_INTERNAL = {
    validateCrl: (...args) => validateCrl(...args),
};
class CertificateRevokedError extends Error {
    constructor(message) {
        super(message);
        this.name = 'CertificateRevokedError';
    }
}
exports.CertificateRevokedError = CertificateRevokedError;
function isCrlValidationEnabled(config) {
    return config.checkMode !== 'DISABLED';
}
function corkSocketAndValidateCrl(socket, config) {
    socket.once('secureConnect', async () => {
        const certChain = socket.getPeerCertificate(true);
        try {
            await exports.CRL_VALIDATOR_INTERNAL.validateCrl(certChain, config);
        }
        catch (error) {
            if (!(error instanceof CertificateRevokedError) && config.checkMode === 'ADVISORY') {
                (0, logger_1.default)().warn('Failed to check CRL revocation, but checkMode=ADVISORY. Allowing connection. Error: %j', error);
            }
            else {
                // NOTE: Wrap error into CrlError to prevent retries
                socket.destroy((0, errors_1.createCrlError)(error));
            }
        }
        socket.uncork();
    });
    socket.cork();
}
function* iterateCertChain(cert) {
    let current = cert;
    while (current) {
        if (current === current.issuerCertificate) {
            break; // Root is self-signed, ignoring
        }
        yield current;
        current = current.issuerCertificate;
    }
}
// NOTE:
// Sticking with asn1.js-rfc5280 + custom signature validation, because popular libraries have issues:
// - jsrsasign: has outdated crypto library with CEV issues
// - pkijs: takes 4 seconds to parse 9Mb CRL
// - @peculiar/x509: takes 2.5 seconds to parse 9Mb CRL
async function validateCrl(certChain, config) {
    for (const certificate of iterateCertChain(certChain)) {
        const decodedCertificate = asn1_js_rfc5280_1.default.Certificate.decode(certificate.raw, 'der');
        const name = (0, certificate_utils_1.getCertificateDebugName)(certificate);
        const logDebug = (msg) => (0, logger_1.default)().debug(`validateCrl[${name}]: ${msg}`);
        logDebug('starting validation');
        if ((0, certificate_utils_1.isShortLivedCertificate)(decodedCertificate)) {
            logDebug('certificate is short-lived, skipping');
            continue;
        }
        logDebug('getting CRL distribution points');
        const crlUrls = (0, certificate_utils_1.getCertificateCrlUrls)(name, decodedCertificate);
        if (!crlUrls) {
            if (config.allowCertificatesWithoutCrlURL) {
                logDebug('certificate has no CRL distribution points, skipping');
                continue;
            }
            throw new Error(`Certificate ${name} does not have CRL http URL. This could be disabled with allowCertificatesWithoutCrlURL`);
        }
        const decodedIssuerCertificate = asn1_js_rfc5280_1.default.Certificate.decode(certificate.issuerCertificate.raw, 'der');
        const issuerSubject = JSON.stringify(decodedIssuerCertificate.tbsCertificate.subject);
        const issuerPublicKey = crypto_1.default
            .createPublicKey({
            key: certificate.issuerCertificate.pubkey,
            format: 'der',
            type: 'spki',
        })
            .export({ format: 'pem', type: 'spki' });
        for (const crlUrl of crlUrls) {
            logDebug(`fetching ${crlUrl}`);
            const crl = await (0, crl_fetcher_1.getCrl)(crlUrl, {
                inMemoryCache: config.inMemoryCache,
                onDiskCache: config.onDiskCache,
            });
            logDebug(`validating ${crlUrl} signature`);
            if (!(0, crl_signature_verifier_1.isCrlSignatureValid)(crl, issuerPublicKey)) {
                throw new Error(`CRL ${crlUrl} signature is invalid. Expected signature by ${(0, certificate_utils_1.getCertificateDebugName)(certificate.issuerCertificate)}`);
            }
            logDebug(`validating ${crlUrl} issuingDistributionPoint extension`);
            if (!(0, certificate_utils_1.isIssuingDistributionPointExtensionValid)(crl, crlUrl)) {
                throw new Error(`CRL ${crlUrl} issuingDistributionPoint extension is invalid`);
            }
            logDebug(`validating ${crlUrl} issuer`);
            const crlIssuer = JSON.stringify(crl.tbsCertList.issuer);
            if (issuerSubject !== crlIssuer) {
                throw new Error(`CRL ${crlUrl} issuer is invalid. Expected ${issuerSubject} but got ${crlIssuer}`);
            }
            logDebug(`validating ${crlUrl} nextUpdate`);
            if (crl.tbsCertList.nextUpdate.value < Date.now()) {
                throw new Error(`CRL ${crlUrl} nextUpdate is expired`);
            }
            logDebug(`checking if certificate is revoked in ${crlUrl}`);
            if ((0, certificate_utils_1.isCertificateRevoked)(decodedCertificate, crl)) {
                throw new CertificateRevokedError(`Certificate ${name} is revoked in ${crlUrl}`);
            }
        }
    }
    return true;
}
//# sourceMappingURL=index.js.map