"use strict";
/**
 * CRL (Certificate Revocation List) verification
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.CRLSignatureVerificationError = void 0;
exports.verifyCRL = verifyCRL;
exports.performCRLCheck = performCRLCheck;
const pkijs = __importStar(require("pkijs"));
const logger_ts_1 = require("../../utility/logging/logger.js");
const databases_ts_1 = require("../../resources/databases.js");
const Resource_ts_1 = require("../../resources/Resource.js");
const verificationUtils_ts_1 = require("./verificationUtils.js");
const verificationConfig_ts_1 = require("./verificationConfig.js");
/**
 * Custom error for CRL signature verification failures
 * This distinguishes security failures (invalid signatures) from operational failures (network, timeout)
 */
class CRLSignatureVerificationError extends Error {
    constructor(message) {
        super(message);
        this.name = 'CRLSignatureVerificationError';
    }
}
exports.CRLSignatureVerificationError = CRLSignatureVerificationError;
const certificateVerificationSource_ts_1 = require("./certificateVerificationSource.js");
const logger = (0, logger_ts_1.loggerWithTag)('crl-verification');
// Lazy-load the certificate verification cache table with CRL source configured
let certCacheTable;
function getCertificateCacheTable() {
    if (!certCacheTable) {
        certCacheTable = (0, verificationUtils_ts_1.getCertificateCacheTable)();
        // Configure the caching source using the shared CertificateVerificationSource class
        certCacheTable.sourcedFrom(certificateVerificationSource_ts_1.CertificateVerificationSource);
    }
    return certCacheTable;
}
/**
 * CRL fetching and validation source
 */
class CertificateRevocationListSource extends Resource_ts_1.Resource {
    async get(id) {
        const context = this.getContext();
        const requestContext = context?.requestContext;
        if (!requestContext?.distributionPoint || !requestContext?.issuerPem) {
            throw new Error(`No CRL data provided for cache key: ${id}`);
        }
        const { distributionPoint, issuerPem: issuerPemStr, config } = requestContext;
        try {
            const result = await downloadAndParseCRL(distributionPoint, issuerPemStr, config.timeout);
            // Set expiration - use the CRL's nextUpdate time or configured TTL, whichever is sooner
            const crlExpiry = result.next_update;
            const configExpiry = Date.now() + config.cacheTtl;
            const expiresAt = Math.min(crlExpiry, configExpiry);
            return {
                ...result,
                expiresAt,
            };
        }
        catch (error) {
            logger.error?.(`CRL fetch error for: ${distributionPoint} - ${error}`);
            // Check failure mode
            if (config.failureMode === 'fail-closed') {
                // Cache the error for faster recovery
                const expiresAt = Date.now() + verificationConfig_ts_1.ERROR_CACHE_TTL;
                return {
                    crl_id: id,
                    distribution_point: distributionPoint,
                    issuer_dn: 'unknown',
                    crl_blob: Buffer.alloc(0),
                    this_update: Date.now(),
                    next_update: expiresAt,
                    signature_valid: false,
                    expiresAt,
                };
            }
            // Fail open - return null to not cache
            logger.warn?.('CRL fetch failed, not caching (fail-open mode)');
            return null;
        }
    }
}
// Lazy-load Harper tables
let crlCacheTable;
let revokedCertificateTable;
function getCRLCacheTable() {
    if (!crlCacheTable) {
        crlCacheTable = (0, databases_ts_1.table)({
            table: 'hdb_crl_cache',
            database: 'system',
            attributes: [
                {
                    name: 'distribution_point',
                    isPrimaryKey: true,
                },
                {
                    name: 'issuer_dn',
                },
                {
                    name: 'crl_blob',
                },
                {
                    name: 'this_update',
                },
                {
                    name: 'next_update',
                },
                {
                    name: 'signature_valid',
                },
                {
                    name: 'expiresAt',
                    expiresAt: true,
                    indexed: true,
                },
            ],
        });
        // Configure the caching source
        crlCacheTable.sourcedFrom(CertificateRevocationListSource);
    }
    return crlCacheTable;
}
function getRevokedCertificateTable() {
    if (!revokedCertificateTable) {
        revokedCertificateTable = (0, databases_ts_1.table)({
            table: 'hdb_revoked_certificates',
            database: 'system',
            attributes: [
                {
                    name: 'composite_id',
                    isPrimaryKey: true,
                },
                {
                    name: 'serial_number',
                    indexed: true,
                },
                {
                    name: 'issuer_key_id',
                    indexed: true,
                },
                {
                    name: 'revocation_date',
                },
                {
                    name: 'revocation_reason',
                },
                {
                    name: 'crl_source',
                    indexed: true, // Links to CRL cache
                },
                {
                    name: 'crl_next_update',
                },
                {
                    name: 'expiresAt',
                    expiresAt: true,
                    indexed: true,
                },
            ],
        });
    }
    return revokedCertificateTable;
}
/**
 * Verify CRL status of a client certificate
 * @param certPem - Client certificate as Buffer (DER format)
 * @param issuerPem - Issuer (CA) certificate as Buffer (DER format)
 * @param config - CRL configuration
 * @param crlUrls - Optional pre-extracted CRL distribution point URLs (avoids re-parsing)
 * @returns Promise resolving to verification result
 */
async function verifyCRL(certPem, issuerPem, config, crlUrls) {
    // Check if CRL verification is disabled
    if (config?.enabled === false) {
        return { valid: true, status: 'disabled', method: 'disabled' };
    }
    try {
        // Convert DER buffers to PEM strings for certificate parsing libraries
        // PKI.js and other certificate utilities expect PEM format for extension extraction
        const certPemStr = (0, verificationUtils_ts_1.bufferToPem)(certPem, 'CERTIFICATE');
        const issuerPemStr = (0, verificationUtils_ts_1.bufferToPem)(issuerPem, 'CERTIFICATE');
        // Extract CRL distribution points from the certificate (if not already provided)
        const distributionPoints = crlUrls ?? (0, verificationUtils_ts_1.extractCRLDistributionPoints)(certPemStr);
        if (distributionPoints.length === 0) {
            return { valid: true, status: 'no-crl-distribution-points', method: 'crl' };
        }
        // Create a cache key that includes all verification parameters
        const cacheKey = (0, verificationUtils_ts_1.createCacheKey)(certPemStr, issuerPemStr, 'crl');
        // Pass certificate data as context - Harper will make it available as requestContext in the source
        const cacheEntry = await getCertificateCacheTable().get(cacheKey, {
            certPem: certPemStr,
            issuerPem: issuerPemStr,
            distributionPoint: distributionPoints[0], // Use first distribution point for CRL fetch
            config: { crl: config ?? {} },
        });
        if (!cacheEntry) {
            // This should not happen if the source is configured correctly but handle it gracefully
            logger.error?.('Cache fetch returned null - this indicates a source configuration issue');
            if (config.failureMode === 'fail-closed') {
                return { valid: false, status: 'error', error: 'Cache fetch failed', method: 'crl' };
            }
            logger.warn?.('CRL cache fetch failed, allowing connection (fail-open mode)');
            return { valid: true, status: 'error-allowed', method: 'crl' };
        }
        const cached = cacheEntry;
        const wasLoadedFromSource = cacheEntry.wasLoadedFromSource?.();
        logger.trace?.(`CRL ${wasLoadedFromSource ? 'source fetch' : 'cache hit'} for certificate`);
        return {
            valid: cached.status === 'good',
            status: cached.status,
            cached: !wasLoadedFromSource,
            method: cached.method || 'crl',
        };
    }
    catch (error) {
        logger.error?.(`CRL verification error: ${error}`);
        // Check failure mode
        if (config.failureMode === 'fail-closed') {
            return { valid: false, status: 'error', error: error.message, method: 'crl' };
        }
        // Fail open - allow connection on CRL errors
        logger.warn?.('CRL check failed, allowing connection (fail-open mode)');
        return { valid: true, status: 'error-allowed', method: 'crl' };
    }
}
/**
 * Perform the actual CRL check by looking up the certificate in the revoked certificates table
 * @param certPem - Certificate in PEM format
 * @param issuerPem - Issuer certificate in PEM format
 * @param config - CRL configuration
 * @param crlUrls - Optional pre-extracted CRL distribution point URLs (avoids re-parsing)
 * @returns CRL check result
 */
async function performCRLCheck(certPem, issuerPem, config, crlUrls) {
    // Extract CRL distribution points from the certificate (if not already provided)
    const distributionPoints = crlUrls ?? (0, verificationUtils_ts_1.extractCRLDistributionPoints)(certPem);
    if (distributionPoints.length === 0) {
        return { status: 'good' };
    }
    // Extract certificate identifiers for lookup
    const serialNumber = (0, verificationUtils_ts_1.extractSerialNumber)(certPem);
    const issuerKeyId = (0, verificationUtils_ts_1.extractIssuerKeyId)(issuerPem);
    const compositeId = (0, verificationUtils_ts_1.createRevokedCertificateId)(issuerKeyId, serialNumber);
    try {
        // Get the revoked certificates table
        const revokedTable = getRevokedCertificateTable();
        // Look up the certificate in the revoked list
        const revokedEntry = await revokedTable.get(compositeId);
        if (revokedEntry) {
            // Check if CRL data is still valid (within grace period if expired)
            const now = Date.now();
            const entry = revokedEntry;
            if (entry.crl_next_update > now) {
                // CRL is still valid
                return {
                    status: 'revoked',
                    reason: entry.revocation_reason || 'unspecified',
                    source: entry.crl_source,
                };
            }
            else if (entry.crl_next_update + config.gracePeriod > now) {
                // CRL is expired but within grace period
                logger.warn?.('Using expired CRL data within grace period');
                return {
                    status: 'revoked',
                    reason: entry.revocation_reason || 'unspecified',
                    source: entry.crl_source,
                };
            }
            else {
                // CRL is too old, treat as unknown
                logger.warn?.('CRL data is too old, treating as unknown');
                return {
                    status: 'unknown',
                    reason: 'crl-expired',
                };
            }
        }
        // Certificate not found in revocation list - check if we have current CRL data
        // This requires checking if CRLs for the distribution points are up to date
        const crlStatus = await checkCRLFreshness(distributionPoints, issuerPem, config);
        if (crlStatus.upToDate) {
            // CRL was just fetched/refreshed — re-check the revoked table since it may have
            // been populated by processRevokedCertificates during the download above
            const revokedEntryFresh = await revokedTable.get(compositeId);
            if (revokedEntryFresh) {
                const entry = revokedEntryFresh;
                const now = Date.now();
                if (entry.crl_next_update > now || entry.crl_next_update + config.gracePeriod > now) {
                    return {
                        status: 'revoked',
                        reason: entry.revocation_reason || 'unspecified',
                        source: entry.crl_source,
                    };
                }
            }
            // Certificate is not in the fresh CRL — it's good
            return {
                status: 'good',
                source: crlStatus.source,
            };
        }
        else {
            // CRL data is stale or missing
            logger.warn?.('CRL data is stale or missing, treating as unknown');
            return {
                status: 'unknown',
                reason: crlStatus.reason || 'crl-unavailable',
            };
        }
    }
    catch (error) {
        logger.error?.(`CRL lookup error: ${error}`);
        return {
            status: 'unknown',
            reason: error.message,
        };
    }
}
/**
 * Check if CRL data is fresh for the given distribution points, and fetch if needed
 * @param distributionPoints - Array of CRL distribution point URLs
 * @param issuerPem - Issuer certificate for CRL signature verification
 * @param config - CRL configuration
 * @returns Status of CRL freshness
 */
async function checkCRLFreshness(distributionPoints, issuerPem, config) {
    const now = Date.now();
    // Check each distribution point
    for (const distributionPoint of distributionPoints) {
        try {
            // First, check if we have a cached CRL that's still valid
            const crlTable = getCRLCacheTable();
            let crlData = null;
            let cachedCRL = null;
            try {
                const cached = await crlTable.get(distributionPoint);
                cachedCRL = cached;
                if (cachedCRL && cachedCRL.next_update > now) {
                    crlData = cachedCRL;
                }
                else if (cachedCRL && cachedCRL.next_update + config.gracePeriod > now) {
                    crlData = cachedCRL;
                }
            }
            catch {
                // Failed to check cache, continue
            }
            // If no valid cached CRL, download and parse fresh
            if (!crlData) {
                crlData = await downloadAndParseCRL(distributionPoint, issuerPem, config.timeout);
            }
            // Check if CRL is current
            const crlExpiry = crlData.next_update;
            if (crlExpiry > now) {
                // Store in cache for future use (only if we downloaded it fresh)
                if (!cachedCRL) {
                    try {
                        await crlTable.put(distributionPoint, crlData);
                    }
                    catch {
                        // Failed to cache, but continue anyway
                    }
                }
                return { upToDate: true, source: distributionPoint };
            }
            else if (crlExpiry + config.gracePeriod > now) {
                return { upToDate: true, source: distributionPoint };
            }
            else {
                return { upToDate: false, reason: 'crl-expired' };
            }
        }
        catch (error) {
            // Signature verification failures are security failures, not operational failures
            // Rethrow them so they don't get swallowed as "unknown" status
            if (error instanceof CRLSignatureVerificationError) {
                throw error;
            }
            // Operational failures (network, timeout, parse errors) - continue to next distribution point
        }
    }
    return { upToDate: false, reason: 'no-current-crl-data' };
}
/**
 * Download and parse a CRL from a distribution point
 * @param distributionPoint - CRL URL
 * @param issuerPemStr - Issuer certificate for signature verification
 * @param timeout - Download timeout in milliseconds
 * @returns Parsed CRL entry for caching
 */
async function downloadAndParseCRL(distributionPoint, issuerPemStr, timeout) {
    // Download the CRL
    // Note: Using fetch here since CRL downloads are cached and infrequent
    // (typically one per CA), so this is not a hot path
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeout);
    try {
        const response = await fetch(distributionPoint, {
            signal: controller.signal,
            headers: {
                'User-Agent': verificationConfig_ts_1.CRL_USER_AGENT,
            },
        });
        clearTimeout(timeoutId);
        if (!response.ok) {
            throw new Error(`CRL download failed: ${response.status}`);
        }
        const crlBuffer = Buffer.from(await response.arrayBuffer());
        // Convert PEM to DER format if needed (PKI.js expects DER)
        let crlDerBuffer;
        const crlText = crlBuffer.toString('utf8');
        if (crlText.includes('-----BEGIN X509 CRL-----')) {
            crlDerBuffer = Buffer.from((0, verificationUtils_ts_1.pemToBuffer)(crlText));
        }
        else {
            crlDerBuffer = crlBuffer;
        }
        // Parse and validate the CRL
        const crl = pkijs.CertificateRevocationList.fromBER(crlDerBuffer);
        // Verify CRL signature
        const issuerCert = pkijs.Certificate.fromBER((0, verificationUtils_ts_1.pemToBuffer)(issuerPemStr));
        const signatureValid = await crl.verify({ issuerCertificate: issuerCert });
        if (!signatureValid) {
            // Invalid signature is a security failure - always reject regardless of fail-open/fail-closed mode
            // Fail-open mode is for operational failures (network issues, timeouts), not security validation failures
            const msg = `CRL signature verification failed for: ${distributionPoint}`;
            logger.error?.(msg);
            throw new CRLSignatureVerificationError(msg);
        }
        // Extract timing information
        const thisUpdate = crl.thisUpdate.value.getTime();
        const nextUpdate = crl.nextUpdate?.value.getTime() ?? thisUpdate + verificationConfig_ts_1.CRL_DEFAULT_VALIDITY_PERIOD;
        // Extract issuer DN
        const issuerDN = issuerCert.issuer.typesAndValues.map((tv) => `${tv.type}=${tv.value.valueBlock.value}`).join(',');
        const cacheEntry = {
            distribution_point: distributionPoint,
            issuer_dn: issuerDN,
            crl_blob: crlBuffer,
            this_update: thisUpdate,
            next_update: nextUpdate,
            signature_valid: signatureValid,
            expiresAt: nextUpdate,
        };
        // Process revoked certificates before returning so the revoked table is populated
        // before any subsequent lookup in performCRLCheck
        await processRevokedCertificates(crl, issuerPemStr, distributionPoint, nextUpdate);
        return cacheEntry;
    }
    finally {
        clearTimeout(timeoutId);
    }
}
/**
 * Process revoked certificates from CRL and store them in the revoked certificates table
 * @param crl - Parsed CRL object
 * @param issuerPemStr - Issuer certificate PEM
 * @param distributionPoint - CRL distribution point URL
 * @param nextUpdate - When this CRL expires
 */
async function processRevokedCertificates(crl, issuerPemStr, distributionPoint, nextUpdate) {
    const revokedTable = getRevokedCertificateTable();
    const issuerKeyId = (0, verificationUtils_ts_1.extractIssuerKeyId)(issuerPemStr);
    const cacheKey = distributionPoint;
    // Clear existing entries for this CRL to ensure data consistency
    // This prevents stale revocation data when certificates are removed from updated CRLs
    try {
        await clearExistingCRLEntries(revokedTable, cacheKey);
    }
    catch (error) {
        logger.warn?.(`Failed to clear existing CRL entries: ${error}`);
        // Continue with processing - partial cleanup is better than no update
    }
    // Add new revoked certificates
    if (crl.revokedCertificates) {
        for (const revokedCert of crl.revokedCertificates) {
            try {
                // Extract serial number using PKI.js - same method as extractSerialNumber() function
                // This gives us the clean serial number without ASN.1 encoding
                const serialHex = revokedCert.userCertificate.valueBlock.valueHexView;
                if (!serialHex) {
                    logger.warn?.('Could not extract serial number from revoked certificate');
                    continue;
                }
                const serialNumber = Array.from(serialHex)
                    .map((b) => b.toString(16).padStart(2, '0'))
                    .join('');
                const compositeId = (0, verificationUtils_ts_1.createRevokedCertificateId)(issuerKeyId, serialNumber);
                // Extract revocation date
                const revocationDate = revokedCert.revocationDate.value.getTime();
                // For now, skip complex extension parsing and just use default reason
                const revocationReason = 'unspecified';
                const entry = {
                    composite_id: compositeId,
                    serial_number: serialNumber,
                    issuer_key_id: issuerKeyId,
                    revocation_date: revocationDate,
                    revocation_reason: revocationReason,
                    crl_source: cacheKey,
                    crl_next_update: nextUpdate,
                    expiresAt: nextUpdate,
                };
                await revokedTable.put(entry.composite_id, entry);
            }
            catch (error) {
                logger.warn?.(`Failed to process revoked certificate: ${error}`);
                // Continue with other certificates
            }
        }
    }
}
/**
 * Clear existing revoked certificate entries for a specific CRL source
 * This ensures data consistency when CRLs are updated and certificates are removed
 * @param revokedTable - Harper table for revoked certificates
 * @param crlSource - CRL cache key to identify entries to remove
 */
async function clearExistingCRLEntries(revokedTable, crlSource) {
    // We need to find all entries with the matching crl_source and delete them individually
    try {
        // Use Harper's search capabilities to find entries by crl_source
        // Note: This assumes the crl_source field is indexed (which it is)
        const existingEntries = revokedTable.search([
            {
                attribute: 'crl_source',
                value: crlSource,
            },
        ]);
        for await (const entry of existingEntries) {
            try {
                await revokedTable.delete(entry.composite_id);
            }
            catch (deleteError) {
                logger.warn?.(`Failed to delete revoked certificate entry: ${deleteError}`);
                // Continue with other entries
            }
        }
    }
    catch (searchError) {
        logger.error?.(`Failed to search for existing CRL entries: ${searchError}`);
        throw searchError;
    }
}
//# sourceMappingURL=crlVerification.js.map