"use strict";
/**
 * Shared utilities for certificate 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.bufferToPem = bufferToPem;
exports.extractCertificateChain = extractCertificateChain;
exports.extractCRLDistributionPoints = extractCRLDistributionPoints;
exports.extractRevocationUrls = extractRevocationUrls;
exports.extractOCSPUrls = extractOCSPUrls;
exports.pemToBuffer = pemToBuffer;
exports.createCacheKey = createCacheKey;
exports.createCRLCacheKey = createCRLCacheKey;
exports.createRevokedCertificateId = createRevokedCertificateId;
exports.extractSerialNumber = extractSerialNumber;
exports.extractIssuerKeyId = extractIssuerKeyId;
exports.getCertificateCacheTable = getCertificateCacheTable;
const node_crypto_1 = require("node:crypto");
const pkijs = __importStar(require("pkijs"));
const asn1js = __importStar(require("asn1js"));
const logger_ts_1 = require("../../utility/logging/logger.js");
const databases_ts_1 = require("../../resources/databases.js");
const logger = (0, logger_ts_1.loggerWithTag)('cert-verification-utils');
/**
 * Convert a buffer to PEM format
 * @param buffer - Certificate data as buffer
 * @param type - Certificate type (e.g., 'CERTIFICATE')
 * @returns PEM formatted string
 */
function bufferToPem(buffer, type) {
    const base64 = buffer.toString('base64');
    const lines = [`-----BEGIN ${type}-----`];
    // Split into 64-char lines
    for (let i = 0; i < base64.length; i += 64) {
        lines.push(base64.substring(i, i + 64));
    }
    lines.push(`-----END ${type}-----`);
    return lines.join('\n');
}
/**
 * Extract certificate chain from peer certificate object
 * @param peerCertificate - Peer certificate object from TLS connection
 * @returns Certificate chain with issuer relationships
 */
function extractCertificateChain(peerCertificate) {
    const chain = [];
    let current = peerCertificate;
    while (current?.raw) {
        const entry = { cert: current.raw };
        // Get issuer if available and different from self
        if (current.issuerCertificate && current.issuerCertificate !== current && current.issuerCertificate.raw) {
            entry.issuer = current.issuerCertificate.raw;
        }
        chain.push(entry);
        // Move to next in chain
        if (current.issuerCertificate && current.issuerCertificate !== current) {
            current = current.issuerCertificate;
        }
        else {
            break;
        }
    }
    return chain;
}
/**
 * Extract CRL Distribution Points from a certificate using PKI.js
 * @param certPem - Certificate in PEM format
 * @returns Array of CRL distribution point URLs
 */
function extractCRLDistributionPoints(certPem) {
    try {
        // Parse the certificate using PKI.js
        const certBuffer = pemToBuffer(certPem);
        const cert = pkijs.Certificate.fromBER(certBuffer);
        // Look for CRL Distribution Points extension (OID: 2.5.29.31)
        const crlDistExt = cert.extensions?.find((ext) => ext.extnID === '2.5.29.31');
        if (!crlDistExt) {
            logger.debug?.('Certificate has no CRL Distribution Points extension');
            return [];
        }
        // Parse the extension value
        const asn1 = asn1js.fromBER(crlDistExt.extnValue.valueBlock.valueHexView);
        if (asn1.offset === -1) {
            throw new Error('Failed to parse ASN.1 structure in CRL Distribution Points extension');
        }
        const crlDistPoints = new pkijs.CRLDistributionPoints({
            schema: asn1.result,
        });
        const distributionPoints = [];
        // Extract distribution point URLs
        for (const distPoint of crlDistPoints.distributionPoints) {
            if (distPoint.distributionPoint && Array.isArray(distPoint.distributionPoint)) {
                // Handle the actual PKI.js structure where distributionPoint is an array
                for (const dp of distPoint.distributionPoint) {
                    if (dp.type === 6 && typeof dp.value === 'string') {
                        // uniformResourceIdentifier
                        const url = dp.value;
                        if (url.startsWith('http://') || url.startsWith('https://')) {
                            distributionPoints.push(url);
                        }
                    }
                }
            }
        }
        logger.debug?.(`Found ${distributionPoints.length} CRL distribution points: ${distributionPoints}`);
        return distributionPoints;
    }
    catch (error) {
        // Parsing failures are treated as "no CRL URLs available"
        // Rationale: The certificate was already validated by Node.js TLS (signature, trust chain)
        // If PKI.js can't parse it, it's likely a library incompatibility or unsupported extension format
        // Not a security issue since TLS already validated the cert - we just can't extract revocation URLs
        // The higher-level fail-open/fail-closed configuration determines final behavior when no URLs found
        logger.warn?.(`Failed to extract CRL distribution points: ${error}`);
        return [];
    }
}
/**
 * Extract both CRL and OCSP URLs from a certificate in a single parse operation
 * @param certPem - Certificate in PEM format
 * @returns Object containing arrays of CRL and OCSP URLs
 */
function extractRevocationUrls(certPem) {
    try {
        // Parse the certificate using PKI.js (single parse for both URL types)
        const certBuffer = pemToBuffer(certPem);
        const cert = pkijs.Certificate.fromBER(certBuffer);
        const crlUrls = [];
        const ocspUrls = [];
        // Single pass through extensions to extract both CRL and OCSP URLs
        for (const ext of cert.extensions || []) {
            if (ext.extnID === '2.5.29.31') {
                // CRL Distribution Points extension
                try {
                    const asn1 = asn1js.fromBER(ext.extnValue.valueBlock.valueHexView);
                    if (asn1.offset !== -1) {
                        const crlDistPoints = new pkijs.CRLDistributionPoints({
                            schema: asn1.result,
                        });
                        // Extract CRL distribution point URLs
                        for (const distPoint of crlDistPoints.distributionPoints) {
                            if (distPoint.distributionPoint && Array.isArray(distPoint.distributionPoint)) {
                                // Handle the actual PKI.js structure where distributionPoint is an array
                                for (const dp of distPoint.distributionPoint) {
                                    if (dp.type === 6 && typeof dp.value === 'string') {
                                        // uniformResourceIdentifier
                                        const url = dp.value;
                                        if (url.startsWith('http://') || url.startsWith('https://')) {
                                            crlUrls.push(url);
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
                catch (crlError) {
                    logger.warn?.(`Failed to parse CRL Distribution Points extension: ${crlError}`);
                }
            }
            else if (ext.extnID === '1.3.6.1.5.5.7.1.1') {
                // Authority Information Access extension
                try {
                    const asn1 = asn1js.fromBER(ext.extnValue.valueBlock.valueHexView);
                    if (asn1.offset !== -1 && asn1.result instanceof asn1js.Sequence) {
                        for (const accessDesc of asn1.result.valueBlock.value) {
                            if (accessDesc instanceof asn1js.Sequence && accessDesc.valueBlock.value.length >= 2) {
                                const accessMethod = accessDesc.valueBlock.value[0];
                                const accessLocation = accessDesc.valueBlock.value[1];
                                // Check if accessMethod is OCSP (OID 1.3.6.1.5.5.7.48.1)
                                if (accessMethod instanceof asn1js.ObjectIdentifier &&
                                    accessMethod.valueBlock.toString() === '1.3.6.1.5.5.7.48.1') {
                                    // Check if accessLocation is a URI (context tag 6)
                                    if (accessLocation.idBlock.tagNumber === 6) {
                                        const url = String.fromCharCode(...Array.from(accessLocation.valueBlock.valueHexView));
                                        if (url.startsWith('http://') || url.startsWith('https://')) {
                                            ocspUrls.push(url);
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
                catch (ocspError) {
                    logger.warn?.(`Failed to parse Authority Information Access extension: ${ocspError}`);
                }
            }
        }
        logger.debug?.(`Found ${crlUrls.length} CRL distribution points and ${ocspUrls.length} OCSP responder URLs`);
        return { crlUrls, ocspUrls };
    }
    catch (error) {
        // Parsing failures are treated as "no revocation URLs available"
        // Rationale: The certificate was already validated by Node.js TLS (signature, trust chain)
        // If PKI.js can't parse it, it's likely a library incompatibility or unsupported extension format
        // Not a security issue since TLS already validated the cert - we just can't extract revocation URLs
        // The higher-level fail-open/fail-closed configuration determines final behavior when no URLs found
        logger.warn?.(`Failed to extract revocation URLs: ${error}`);
        return { crlUrls: [], ocspUrls: [] };
    }
}
/**
 * Extract OCSP responder URLs from a certificate
 * @param certPem - Certificate in PEM format
 * @returns Array of OCSP responder URLs
 */
function extractOCSPUrls(certPem) {
    try {
        // Parse the certificate using PKI.js
        const certBuffer = pemToBuffer(certPem);
        const cert = pkijs.Certificate.fromBER(certBuffer);
        // Look for Authority Information Access extension (OID: 1.3.6.1.5.5.7.1.1)
        const aiaExt = cert.extensions?.find((ext) => ext.extnID === '1.3.6.1.5.5.7.1.1');
        if (!aiaExt) {
            logger.debug?.('Certificate has no Authority Information Access extension');
            return [];
        }
        // Parse the extension value using asn1js
        const asn1 = asn1js.fromBER(aiaExt.extnValue.valueBlock.valueHexView);
        if (asn1.offset === -1) {
            throw new Error('Failed to parse ASN.1 structure in Authority Information Access extension');
        }
        const ocspUrls = [];
        // The AIA extension contains a sequence of AccessDescription entries
        // Each AccessDescription has: accessMethod (OID) and accessLocation (GeneralName)
        if (asn1.result instanceof asn1js.Sequence) {
            for (const accessDesc of asn1.result.valueBlock.value) {
                if (accessDesc instanceof asn1js.Sequence && accessDesc.valueBlock.value.length >= 2) {
                    const accessMethod = accessDesc.valueBlock.value[0];
                    const accessLocation = accessDesc.valueBlock.value[1];
                    // Check if accessMethod is OCSP (OID 1.3.6.1.5.5.7.48.1)
                    if (accessMethod instanceof asn1js.ObjectIdentifier &&
                        accessMethod.valueBlock.toString() === '1.3.6.1.5.5.7.48.1') {
                        // Check if accessLocation is a URI (context tag 6)
                        if (accessLocation.idBlock.tagNumber === 6) {
                            const url = String.fromCharCode(...Array.from(accessLocation.valueBlock.valueHexView));
                            if (url.startsWith('http://') || url.startsWith('https://')) {
                                ocspUrls.push(url);
                            }
                        }
                    }
                }
            }
        }
        logger.debug?.(`Found ${ocspUrls.length} OCSP responder URLs: ${ocspUrls}`);
        return ocspUrls;
    }
    catch (error) {
        logger.error?.(`Failed to extract OCSP URLs: ${error}`);
        return [];
    }
}
/**
 * Convert PEM string to buffer for PKI.js parsing
 * @param pem - PEM formatted certificate
 * @returns Buffer containing certificate data
 */
function pemToBuffer(pem) {
    // Remove PEM headers and whitespace
    const base64 = pem
        .replace(/-----BEGIN [^-]+-----/g, '')
        .replace(/-----END [^-]+-----/g, '')
        .replace(/\s/g, '');
    // Convert to buffer
    const binaryString = atob(base64);
    const buffer = new ArrayBuffer(binaryString.length);
    const view = new Uint8Array(buffer);
    for (let i = 0; i < binaryString.length; i++) {
        view[i] = binaryString.charCodeAt(i);
    }
    return buffer;
}
/**
 * Create a cache key for certificate verification
 * @param certPem - Certificate in PEM format
 * @param issuerPem - Issuer certificate in PEM format
 * @param method - Verification method (ocsp, crl)
 * @param additionalData - Additional data to include in hash
 * @returns Cache key string
 */
function createCacheKey(certPem, issuerPem, method, additionalData) {
    const cacheData = {
        certPem,
        issuerPem,
        method,
        ...additionalData,
    };
    const cacheKeyHash = (0, node_crypto_1.createHash)('sha256').update(JSON.stringify(cacheData)).digest('hex');
    return `${method}:${cacheKeyHash}`;
}
/**
 * Create a cache key for CRL storage
 * @param distributionPoint - CRL distribution point URL
 * @returns Cache key string
 */
function createCRLCacheKey(distributionPoint) {
    const hash = (0, node_crypto_1.createHash)('sha256').update(distributionPoint).digest('hex');
    return `crl:${hash}`;
}
/**
 * Create a composite ID for revoked certificate lookup
 * @param issuerKeyId - Issuer key identifier or DN hash
 * @param serialNumber - Certificate serial number
 * @returns Composite ID string
 */
function createRevokedCertificateId(issuerKeyId, serialNumber) {
    return `${issuerKeyId}:${serialNumber}`;
}
/**
 * Extract serial number from a certificate
 * @param certPem - Certificate in PEM format
 * @returns Certificate serial number as string
 */
function extractSerialNumber(certPem) {
    try {
        const certBuffer = pemToBuffer(certPem);
        const cert = pkijs.Certificate.fromBER(certBuffer);
        // Convert serial number to string
        const serialNumber = cert.serialNumber.valueBlock.valueHexView;
        return Array.from(serialNumber)
            .map((b) => b.toString(16).padStart(2, '0'))
            .join('');
    }
    catch (error) {
        logger.error?.(`Failed to extract serial number: ${error}`);
        throw new Error(`Failed to extract certificate serial number: ${error.message}`);
    }
}
/**
 * Extract issuer key identifier from a certificate
 * @param certPem - Certificate in PEM format
 * @returns Issuer key identifier as hex string, or hash of issuer DN if not available
 */
function extractIssuerKeyId(certPem) {
    try {
        const certBuffer = pemToBuffer(certPem);
        const cert = pkijs.Certificate.fromBER(certBuffer);
        // Look for Authority Key Identifier extension (OID: 2.5.29.35)
        const akiExt = cert.extensions?.find((ext) => ext.extnID === '2.5.29.35');
        if (akiExt) {
            try {
                // Parse the extension value manually since parsedValue may be undefined for Ed25519 certs
                const asn1 = asn1js.fromBER(akiExt.extnValue.valueBlock.valueHexView);
                if (asn1.offset !== -1) {
                    const aki = new pkijs.AuthorityKeyIdentifier({
                        schema: asn1.result,
                    });
                    if (aki.keyIdentifier) {
                        const keyId = aki.keyIdentifier.valueBlock.valueHexView;
                        return Array.from(keyId)
                            .map((b) => b.toString(16).padStart(2, '0'))
                            .join('');
                    }
                }
            }
            catch (parseError) {
                logger.debug?.(`Failed to parse Authority Key Identifier: ${parseError}, falling back to hash`);
            }
        }
        // Fall back to hash of issuer DN
        const issuerDN = cert.issuer.typesAndValues.map((tv) => `${tv.type}=${tv.value.valueBlock.value}`).join(',');
        return (0, node_crypto_1.createHash)('sha256').update(issuerDN).digest('hex');
    }
    catch (error) {
        logger.error?.(`Failed to extract issuer key ID: ${error}`);
        throw new Error(`Failed to extract issuer key ID: ${error.message}`);
    }
}
/**
 * Get shared certificate verification cache table
 * @returns Harper table instance for certificate verification cache
 */
// Cache the certificate cache table instance to avoid recreating it
let certificateCacheTable = null;
function getCertificateCacheTable() {
    if (!certificateCacheTable) {
        certificateCacheTable = (0, databases_ts_1.table)({
            table: 'hdb_certificate_cache',
            database: 'system',
            attributes: [
                {
                    name: 'certificate_id',
                    isPrimaryKey: true,
                },
                {
                    name: 'status', // 'good', 'revoked', 'unknown'
                },
                {
                    name: 'reason',
                },
                {
                    name: 'checked_at',
                },
                {
                    name: 'expiresAt',
                    expiresAt: true,
                    indexed: true,
                },
                {
                    name: 'method', // 'ocsp' or 'crl'
                },
            ],
        });
    }
    return certificateCacheTable;
}
//# sourceMappingURL=verificationUtils.js.map