"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.signCertificate = signCertificate;
exports.createCsr = createCsr;
exports.getReplicationCert = getReplicationCert;
exports.getReplicationCertAuth = getReplicationCertAuth;
const joi_1 = __importDefault(require("joi"));
const node_forge_1 = __importDefault(require("node-forge"));
const promises_1 = require("node:fs/promises");
const node_path_1 = require("node:path");
const node_crypto_1 = require("node:crypto");
const validationWrapper_js_1 = require("../core/validation/validationWrapper.js");
const hdbError_js_1 = require("../core/utility/errors/hdbError.js");
const keys_js_1 = require("../core/security/keys.js");
const environmentManager_js_1 = __importDefault(require("../core/utility/environment/environmentManager.js"));
const hdbTerms_ts_1 = require("../core/utility/hdbTerms.js");
const harper_logger_js_1 = __importDefault(require("../core/utility/logging/harper_logger.js"));
const nodeName_ts_1 = require("../core/server/nodeName.js");
const Server_ts_1 = require("../core/server/Server.js");
const replicator_ts_1 = require("../replication/replicator.js");
const { forComponent } = harper_logger_js_1.default;
const logger = forComponent('certificate').conditional;
const pki = node_forge_1.default.pki;
const CERT_VALIDITY_DAYS = 3650;
const fileExists = async (path) => (0, promises_1.access)(path, promises_1.constants.F_OK)
    .then(() => true)
    .catch(() => false);
async function signCertificate(req) {
    const response = {};
    const hdbKeysDir = (0, node_path_1.join)(environmentManager_js_1.default.getHdbBasePath(), hdbTerms_ts_1.LICENSE_KEY_DIR_NAME);
    if (req.csr) {
        let private_key;
        let cert_auth;
        const certificateTable = (0, keys_js_1.getCertTable)();
        const privateKeys = (0, keys_js_1.getPrivateKeys)();
        // Search hdbCertificate for a non-HDB CA that also has a local private key
        for await (const cert of certificateTable.search([])) {
            if (cert.is_authority && !cert.details.issuer.includes('HarperDB-Certificate-Authority')) {
                if (privateKeys.has(cert.private_key_name)) {
                    private_key = privateKeys.get(cert.private_key_name);
                    cert_auth = cert;
                    break;
                }
                else if (cert.private_key_name && (await fileExists((0, node_path_1.join)(hdbKeysDir, cert.private_key_name)))) {
                    private_key = await (0, promises_1.readFile)((0, node_path_1.join)(hdbKeysDir, cert.private_key_name));
                    cert_auth = cert;
                    break;
                }
            }
        }
        // If the search above did not find a CA look for a CA with a matching private key
        if (!private_key) {
            const certAndKey = await (0, keys_js_1.getCertAuthority)();
            cert_auth = certAndKey.ca;
            private_key = certAndKey.private_key;
        }
        private_key = pki.privateKeyFromPem(private_key);
        response.signingCA = cert_auth.certificate;
        const caAppCert = pki.certificateFromPem(cert_auth.certificate);
        logger.info?.('Signing CSR with cert named', cert_auth.name);
        const csr = pki.certificationRequestFromPem(req.csr);
        try {
            csr.verify();
        }
        catch (err) {
            logger.error?.(err);
            return new Error(`Error verifying CSR: ` + err.message);
        }
        const cert = node_forge_1.default.pki.createCertificate();
        cert.serialNumber = (0, keys_js_1.generateSerialNumber)();
        cert.validity.notBefore = new Date();
        const notAfter = new Date();
        cert.validity.notAfter = notAfter;
        cert.validity.notAfter.setDate(notAfter.getDate() + CERT_VALIDITY_DAYS);
        logger.info?.('sign cert setting validity:', cert.validity);
        // subject from CSR
        logger.info?.('sign cert setting subject from CSR:', csr.subject.attributes);
        cert.setSubject(csr.subject.attributes);
        // issuer from CA
        logger.info?.('sign cert setting issuer:', caAppCert.subject.attributes);
        cert.setIssuer(caAppCert.subject.attributes);
        const extensions = csr.getAttribute({ name: 'extensionRequest' }).extensions;
        logger.info?.('sign cert adding extensions from CSR:', extensions);
        cert.setExtensions(extensions);
        cert.publicKey = csr.publicKey;
        cert.sign(private_key, node_forge_1.default.md.sha256.create());
        response.certificate = pki.certificateToPem(cert);
    }
    else {
        logger.info?.('Sign cert did not receive a CSR from:', req.url, 'only the CA will be returned');
    }
    return response;
}
async function createCsr() {
    // The CSR should always use the self-signed HarperDB cert (RSA), even when
    // the active replication cert is a non-RSA cert (e.g. Let's Encrypt EC).
    const certificateTable = (0, keys_js_1.getCertTable)();
    const privateKeys = (0, keys_js_1.getPrivateKeys)();
    const hdbKeysDir = (0, node_path_1.join)(environmentManager_js_1.default.getHdbBasePath(), hdbTerms_ts_1.LICENSE_KEY_DIR_NAME);
    let opsCert, opsPrivateKey, certName, privateKeyName;
    for await (const cert of certificateTable.search([])) {
        if (cert.is_self_signed && (cert.details?.issuer?.includes('Harper-Certificate-Authority') || cert.details?.issuer?.includes('HarperDB-Certificate-Authority'))) {
            // privateKeys Map is populated from config-referenced paths only, so a Harper CA key
            // on disk but not referenced in config won't appear there — fall back to reading it.
            let key = privateKeys.get(cert.private_key_name);
            if (!key && cert.private_key_name && (await fileExists((0, node_path_1.join)(hdbKeysDir, cert.private_key_name)))) {
                key = await (0, promises_1.readFile)((0, node_path_1.join)(hdbKeysDir, cert.private_key_name));
            }
            if (key) {
                opsCert = pki.certificateFromPem(cert.certificate);
                opsPrivateKey = pki.privateKeyFromPem(key);
                certName = cert.name;
                privateKeyName = cert.private_key_name;
                break;
            }
        }
    }
    if (!opsCert || !opsPrivateKey) {
        // Fall back to the replication cert (original behavior)
        const rep = await getReplicationCert();
        opsCert = pki.certificateFromPem(rep.options.cert);
        opsPrivateKey = pki.privateKeyFromPem(rep.options.key);
        certName = rep.name;
        privateKeyName = rep.options.key_file;
    }
    logger.info?.('Creating CSR with cert named:', certName);
    const csr = pki.createCertificationRequest();
    csr.publicKey = opsCert.publicKey;
    const subject = [
        {
            name: 'commonName',
            value: (0, keys_js_1.getCommonName)(),
        },
        ...keys_js_1.CERT_ATTRIBUTES,
    ];
    logger.info?.('Creating CSR with subject', subject);
    csr.setSubject(subject);
    const attributes = [
        {
            name: 'unstructuredName',
            value: 'HarperDB, Inc.',
        },
        {
            name: 'extensionRequest',
            extensions: (0, keys_js_1.certExtensions)(),
        },
    ];
    logger.info?.('Creating CSR with attributes', attributes);
    csr.setAttributes(attributes);
    csr.sign(opsPrivateKey);
    return { pem: node_forge_1.default.pki.certificationRequestToPem(csr), privateKeyName };
}
async function getReplicationCert() {
    const SNICallback = (0, keys_js_1.createTLSSelector)('replication');
    const secureTarget = {
        secureContexts: null,
        setSecureContext: () => { },
    };
    await SNICallback.initialize(secureTarget);
    const cert = secureTarget.secureContexts.get((0, nodeName_ts_1.getThisNodeName)());
    if (!cert)
        return;
    const certParsed = new node_crypto_1.X509Certificate(cert.options.cert);
    cert.cert_parsed = certParsed;
    cert.issuer = certParsed.issuer;
    return cert;
}
async function getReplicationCertAuth() {
    (0, keys_js_1.getCertTable)();
    const certPem = (await getReplicationCert()).options.cert;
    const repCert = new node_crypto_1.X509Certificate(certPem);
    const caName = repCert.issuer.match(/CN=(.*)/)?.[1];
    return (0, keys_js_1.getCertTable)().get(caName);
}
/**
 * Adds or updates a certificate in the hdbCertificate table.
 *
 * If `private_key` is provided, it will be written to disk (as `<name>.pem`) rather than
 * stored in the table. If no `private_key` is provided, existing stored keys are searched
 * for one that matches the certificate. Non-CA certificates require a matching private key
 * to be either provided or already stored.
 *
 * If `name` is omitted, the primary hostname (CN) is extracted from the certificate itself.
 *
 * @param req.name - Primary key for the hdbCertificate record. Falls back to the certificate's CN if omitted.
 * @param req.certificate - PEM-encoded certificate string to add or update.
 * @param req.is_authority - Whether this certificate is a Certificate Authority (CA).
 *   CA certs do not require an associated private key, but can have one.
 * @param req.private_key - Optional PEM-encoded private key. Written to disk and referenced
 *   by name in the table. If omitted, existing keys are checked for a match.
 * @param req.hosts - Optional list of hostnames this certificate is valid for.
 * @param req.uses - Optional list of use cases this certificate is assigned to.
 * @param req.ciphers - Optional cipher suite string associated with this certificate.
 * @throws {ClientError} If the certificate is not a CA and no matching private key is found.
 * @throws {ClientError} If `name` is omitted and the CN cannot be extracted from the certificate.
 * @returns A replication response with a `message` confirming the certificate name added.
 */
async function addCertificate(req) {
    const validation = (0, validationWrapper_js_1.validateBySchema)(req, joi_1.default.object({
        name: joi_1.default.string().optional(),
        certificate: joi_1.default.string().required(),
        is_authority: joi_1.default.boolean().required(),
        private_key: joi_1.default.string(),
        hosts: joi_1.default.array(),
        uses: joi_1.default.array(),
    }));
    if (validation)
        throw new hdbError_js_1.ClientError(validation.message);
    const { name, certificate, private_key, is_authority } = req;
    const x509Cert = new node_crypto_1.X509Certificate(certificate);
    // Track whether we found a matching key among existing keys, and which one.
    let matchingKeyFound = false;
    let existingPrivateKeyName;
    const privateKeys = (0, keys_js_1.getPrivateKeys)();
    if (private_key) {
        // A key was provided — check if we already have it stored so we don't duplicate it.
        for (const [keyName, key] of privateKeys) {
            if (private_key === key) {
                matchingKeyFound = true;
                existingPrivateKeyName = keyName;
                break;
            }
        }
    }
    else {
        // No key provided — search existing keys to see if one matches this cert.
        for (const [keyName, key] of privateKeys) {
            if (x509Cert.checkPrivateKey((0, node_crypto_1.createPrivateKey)(key))) {
                matchingKeyFound = true;
                existingPrivateKeyName = keyName;
                break;
            }
        }
    }
    // CA certs don't require a private key, but non-CA certs must have one either
    // provided directly or already stored.
    if (!is_authority && !private_key && !matchingKeyFound)
        throw new hdbError_js_1.ClientError('A suitable private key was not found for this certificate');
    // If no name was provided, fall back to extracting the CN from the cert itself.
    let certCn;
    if (!name) {
        try {
            certCn = (0, keys_js_1.getPrimaryHostName)(x509Cert);
        }
        catch (err) {
            logger.error?.(err);
        }
        if (certCn == null)
            throw new hdbError_js_1.ClientError('Error extracting certificate host name, please provide a name parameter');
    }
    const saniName = sanitizeName(name ?? certCn);
    // Only write the key to disk if it's new (not already stored).
    if (private_key && !matchingKeyFound) {
        await (0, promises_1.writeFile)((0, node_path_1.join)(environmentManager_js_1.default.getHdbBasePath(), hdbTerms_ts_1.LICENSE_KEY_DIR_NAME, saniName + '.pem'), private_key);
        privateKeys.set(saniName, private_key);
    }
    const record = {
        name: name ?? certCn,
        certificate,
        is_authority,
        hosts: req.hosts,
        uses: req.uses,
    };
    // Attach private_key_name for non-CA certs, and for CA certs that have an associated key.
    if (!is_authority || (is_authority && existingPrivateKeyName) || (is_authority && private_key)) {
        record.private_key_name = existingPrivateKeyName ?? saniName + '.pem';
    }
    if (req.ciphers)
        record.ciphers = req.ciphers;
    await (0, keys_js_1.setCertTable)(record);
    const response = await (0, replicator_ts_1.replicateOperation)(req);
    response.message = 'Successfully added certificate: ' + saniName;
    return response;
}
/**
 * Removes a certificate from the hdbCertificate table.
 *
 * If the certificate has an associated private key file, it will be deleted from disk —
 * but only if no other certificates reference the same key.
 *
 * @param req.name - Name of the certificate to remove. Must match an existing record.
 * @throws {ClientError} If no certificate with the given name is found.
 * @returns A replication response with a `message` confirming the certificate name removed.
 */
async function removeCertificate(req) {
    const validation = (0, validationWrapper_js_1.validateBySchema)(req, joi_1.default.object({
        name: joi_1.default.string().required(),
    }));
    if (validation)
        throw new hdbError_js_1.ClientError(validation.message);
    const { name } = req;
    const certificateTable = (0, keys_js_1.getCertTable)();
    const certRecord = await certificateTable.get(name);
    if (!certRecord)
        throw new hdbError_js_1.ClientError(`${name} not found`);
    const { private_key_name } = certRecord;
    if (private_key_name) {
        const matchingKeys = Array.from(await certificateTable.search([{ attribute: 'private_key_name', value: private_key_name }]));
        // Only delete the key file if this is the only cert referencing it.
        if (matchingKeys.length === 1 && matchingKeys[0].name === name) {
            try {
                logger.info?.('Removing private key named', private_key_name);
                await (0, promises_1.unlink)((0, node_path_1.join)(environmentManager_js_1.default.getHdbBasePath(), hdbTerms_ts_1.LICENSE_KEY_DIR_NAME, private_key_name));
            }
            catch (err) {
                logger.error?.('Failed to remove private key file', private_key_name, err);
            }
        }
    }
    await certificateTable.delete(name);
    const response = await (0, replicator_ts_1.replicateOperation)(req);
    response.message = `Successfully removed ${name}`;
    return response;
}
/**
 * List all the records in hdbCertificate table
 * @returns {Promise<*[]>}
 */
async function listCertificates() {
    const certificateTable = (0, keys_js_1.getCertTable)();
    let response = [];
    for await (const cert of certificateTable.search([])) {
        response.push(cert);
    }
    return response;
}
/**
 * Used to sanitize a cert common name or the 'name' param used in cert ops
 * @param cn
 * @returns {*}
 */
function sanitizeName(cn) {
    return cn.replace(/[^a-z0-9.]/gi, '-');
}
// These will register the operations for the operations API. For now the method and schema are ignored,
// they are there for when build the REST interface for operations API
Server_ts_1.server.registerOperation?.({
    name: 'add_certificate',
    execute: addCertificate,
    httpMethod: 'PUT',
    parametersSchema: [{ name: 'hostname', in: 'path', schema: { type: 'string' } }],
});
Server_ts_1.server.registerOperation?.({
    name: 'remove_certificate',
    execute: removeCertificate,
    httpMethod: 'DELETE',
    parametersSchema: [{ name: 'hostname', in: 'path', schema: { type: 'string' } }],
});
Server_ts_1.server.registerOperation?.({
    name: 'list_certificates',
    execute: listCertificates,
    httpMethod: 'GET',
    parametersSchema: [{ name: 'hostname', in: 'path', schema: { type: 'string' } }],
});
Server_ts_1.server.registerOperation?.({
    name: 'create_csr',
    execute: createCsr,
    httpMethod: 'POST',
    parametersSchema: [{ name: 'hostname', in: 'path', schema: { type: 'string' } }],
});
Server_ts_1.server.registerOperation?.({
    name: 'sign_certificate',
    execute: signCertificate,
    httpMethod: 'POST',
    parametersSchema: [{ name: 'hostname', in: 'path', schema: { type: 'string' } }],
});
//# sourceMappingURL=certificate.js.map