UNPKG

node-opcua-pki

Version:
1,286 lines (1,256 loc) 147 kB
// packages/node-opcua-pki/lib/ca/certificate_authority.ts import assert7 from "assert"; import fs7 from "fs"; import os3 from "os"; import path5 from "path"; import chalk5 from "chalk"; import { CertificatePurpose, certificateMatchesPrivateKey, convertPEMtoDER, exploreCertificate, exploreCertificateSigningRequest, generatePrivateKeyFile, readCertificatePEM, readCertificateSigningRequest, readPrivateKey, Subject as Subject2, toPem } from "node-opcua-crypto"; // packages/node-opcua-pki/lib/pki/toolbox_pfx.ts import assert4 from "assert"; import fs4 from "fs"; // packages/node-opcua-pki/lib/toolbox/common.ts import assert from "assert"; function quote(str) { return `"${str || ""}"`; } function adjustDate(params) { assert(params instanceof Object); params.startDate = params.startDate || /* @__PURE__ */ new Date(); assert(params.startDate instanceof Date); if (params.validityMs !== void 0) { if (params.validityMs <= 0) { throw new RangeError(`validityMs must be > 0 (got ${params.validityMs})`); } params.endDate = new Date(params.startDate.getTime() + params.validityMs); params.validity = Math.ceil(params.validityMs / 864e5); } else { params.validity = params.validity || 365; params.endDate = new Date(params.startDate.getTime()); params.endDate.setDate(params.startDate.getDate() + params.validity); } assert(params.endDate instanceof Date); assert(params.startDate instanceof Date); } function adjustApplicationUri(params) { const applicationUri = params.applicationUri || ""; if (applicationUri.length > 200) { throw new Error(`Openssl doesn't support urn with length greater than 200${applicationUri}`); } } // packages/node-opcua-pki/lib/toolbox/common2.ts import assert2 from "assert"; import fs from "fs"; import path from "path"; import chalk from "chalk"; // packages/node-opcua-pki/lib/toolbox/config.ts var g_config = { opensslVersion: "unset", silent: process.env.VERBOSE ? !process.env.VERBOSE : true, force: false }; // packages/node-opcua-pki/lib/toolbox/debug.ts var doDebug = process.env.NODEOPCUAPKIDEBUG || false; var displayError = true; var displayDebug = !!process.env.NODEOPCUAPKIDEBUG || false; function debugLog(...args) { if (displayDebug) { console.log.apply(null, args); } } function warningLog(...args) { console.log.apply(null, args); } // packages/node-opcua-pki/lib/toolbox/common2.ts function certificateFileExist(certificateFile) { if (fs.existsSync(certificateFile) && !g_config.force) { warningLog( chalk.yellow(" certificate ") + chalk.cyan(certificateFile) + chalk.yellow(" already exists => do not overwrite") ); return false; } return true; } function mkdirRecursiveSync(folder) { if (!fs.existsSync(folder)) { debugLog(chalk.white(" .. constructing "), folder); fs.mkdirSync(folder, { recursive: true }); } } function makePath(folderName, filename) { let s; if (filename) { s = path.join(path.normalize(folderName), filename); } else { assert2(folderName); s = folderName; } s = s.replace(/\\/g, "/"); return s; } // packages/node-opcua-pki/lib/toolbox/with_openssl/execute_openssl.ts import assert3 from "assert"; import child_process2 from "child_process"; import fs3 from "fs"; import os2 from "os"; import byline2 from "byline"; import chalk3 from "chalk"; // packages/node-opcua-pki/lib/toolbox/with_openssl/_env.ts var exportedEnvVars = {}; function setEnv(varName, value) { if (!g_config.silent) { warningLog(` set ${varName}=${value}`); } exportedEnvVars[varName] = value; if (["OPENSSL_CONF"].indexOf(varName) >= 0) { process.env[varName] = value; } if (["RANDFILE"].indexOf(varName) >= 0) { process.env[varName] = value; } } function hasEnv(varName) { return Object.prototype.hasOwnProperty.call(exportedEnvVars, varName); } function getEnv(varName) { return exportedEnvVars[varName]; } function unsetEnv(varName) { delete exportedEnvVars[varName]; } function getEnvironmentVarNames() { return Object.keys(exportedEnvVars).map((varName) => { return { key: varName, pattern: `\\$ENV\\:\\:${varName}` }; }); } function processAltNames(params) { params.dns = params.dns || []; params.ip = params.ip || []; let subjectAltName = []; subjectAltName.push(`URI:${params.applicationUri}`); subjectAltName = [].concat( subjectAltName, params.dns.map((d) => `DNS:${d}`) ); subjectAltName = [].concat( subjectAltName, params.ip.map((d) => `IP:${d}`) ); const subjectAltNameString = subjectAltName.join(", "); setEnv("ALTNAME", subjectAltNameString); } // packages/node-opcua-pki/lib/toolbox/with_openssl/install_prerequisite.ts import child_process from "child_process"; import fs2 from "fs"; import os from "os"; import path2 from "path"; import url from "url"; import byline from "byline"; import chalk2 from "chalk"; import ProgressBar from "progress"; import wget from "wget-improved-2"; import yauzl from "yauzl"; var doDebug2 = process.env.NODEOPCUAPKIDEBUG || false; function makeOptions() { const proxy = process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy || void 0; if (proxy) { const a = new url.URL(proxy); const auth = a.username ? `${a.username}:${a.password}` : void 0; const options = { proxy: { port: a.port ? parseInt(a.port, 10) : 80, protocol: a.protocol.replace(":", ""), host: a.hostname ?? "", proxyAuth: auth } }; warningLog(chalk2.green("- using proxy "), proxy); warningLog(options); return options; } return {}; } async function execute(cmd, cwd) { let output = ""; const options = { cwd, windowsHide: true }; return await new Promise((resolve, reject) => { const child = child_process.exec( cmd, options, (err) => { const exitCode = err === null ? 0 : err.code || 1; if (err) reject(err); else { resolve({ exitCode, output }); } } ); const stream1 = byline(child.stdout); stream1.on("data", (line) => { output += `${line} `; if (doDebug2) { process.stdout.write(` stdout ${chalk2.yellow(line)} `); } }); }); } function quote2(str) { return `"${str.replace(/\\/g, "/")}"`; } function is_expected_openssl_version(strVersion) { return !!strVersion.match(/OpenSSL 1|3/); } async function getopensslExecPath() { let result1; try { result1 = await execute("which openssl"); } catch (err) { warningLog("warning: ", err.message); throw new Error("Cannot find openssl"); } const exitCode = result1?.exitCode; const output = result1?.output; if (exitCode !== 0) { warningLog(chalk2.yellow(" it seems that ") + chalk2.cyan("openssl") + chalk2.yellow(" is not installed on your computer ")); warningLog(chalk2.yellow("Please install it before running this programs")); throw new Error("Cannot find openssl"); } const opensslExecPath = output.replace(/\n\r/g, "").trim(); return opensslExecPath; } async function check_system_openssl_version() { const opensslExecPath = await getopensslExecPath(); const q_opensslExecPath = quote2(opensslExecPath); if (doDebug2) { warningLog(` OpenSSL found in : ${chalk2.yellow(opensslExecPath)}`); } const result = await execute(`${q_opensslExecPath} version`); const exitCode = result?.exitCode; const output = result?.output; const version = output.trim(); const versionOK = exitCode === 0 && is_expected_openssl_version(version); if (!versionOK) { let message = chalk2.whiteBright("Warning !!!!!!!!!!!! ") + "\nyour version of openssl is " + version + ". It doesn't match the expected version"; if (process.platform === "darwin") { message += chalk2.cyan("\nplease refer to :") + chalk2.yellow(" https://github.com/node-opcua/node-opcua/wiki/installing-node-opcua-or-node-red-on-MacOS"); } console.log(message); } return output; } async function install_and_check_win32_openssl_version() { const downloadFolder = path2.join(os.tmpdir(), "."); function get_openssl_folder_win32() { if (process.env.LOCALAPPDATA) { const userProgramFolder = path2.join(process.env.LOCALAPPDATA, "Programs"); if (fs2.existsSync(userProgramFolder)) { return path2.join(userProgramFolder, "openssl"); } } return path2.join(process.cwd(), "openssl"); } function get_openssl_exec_path_win32() { const opensslFolder2 = get_openssl_folder_win32(); return path2.join(opensslFolder2, "openssl.exe"); } async function check_openssl_win32() { const opensslExecPath2 = get_openssl_exec_path_win32(); const exists = fs2.existsSync(opensslExecPath2); if (!exists) { warningLog("checking presence of ", opensslExecPath2); warningLog(chalk2.red(" cannot find file ") + opensslExecPath2); return { opensslOk: false, version: `cannot find file ${opensslExecPath2}` }; } else { const q_openssl_exe_path = quote2(opensslExecPath2); const cwd = "."; const { exitCode, output } = await execute(`${q_openssl_exe_path} version`, cwd); const version = output.trim(); if (doDebug2) { warningLog(" Version = ", version); } return { opensslOk: exitCode === 0 && is_expected_openssl_version(version), version }; } } function win32or64() { if (process.env.PROCESSOR_ARCHITECTURE === "x86" && process.env.PROCESSOR_ARCHITEW6432) { return 64; } if (process.env.PROCESSOR_ARCHITECTURE === "AMD64") { return 64; } if (process.env.CURRENT_CPU === "x64") { return 64; } return 32; } async function download_openssl() { const url2 = win32or64() === 64 ? "https://github.com/node-opcua/node-opcua-pki/releases/download/2.14.2/openssl-1.0.2u-x64_86-win64.zip" : "https://github.com/node-opcua/node-opcua-pki/releases/download/2.14.2/openssl-1.0.2u-i386-win32.zip"; const outputFilename = path2.join(downloadFolder, path2.basename(url2)); warningLog(`downloading ${chalk2.yellow(url2)} to ${outputFilename}`); if (fs2.existsSync(outputFilename)) { return { downloadedFile: outputFilename }; } const options = makeOptions(); const bar = new ProgressBar(chalk2.cyan("[:bar]") + chalk2.cyan(" :percent ") + chalk2.white(":etas"), { complete: "=", incomplete: " ", total: 100, width: 100 }); return await new Promise((resolve, reject) => { const download = wget.download(url2, outputFilename, options); download.on("error", (err) => { warningLog(err); setImmediate(() => { reject(err); }); }); download.on("end", (output) => { if (doDebug2) { warningLog(output); } resolve({ downloadedFile: outputFilename }); }); download.on("progress", (progress) => { bar.update(progress); }); }); } async function unzip_openssl(zipFilename) { const opensslFolder2 = get_openssl_folder_win32(); const zipFile = await new Promise((resolve, reject) => { yauzl.open(zipFilename, { lazyEntries: true }, (err, zipfile) => { if (err) { reject(err); } else { if (!zipfile) { reject(new Error("zipfile is null")); } else { resolve(zipfile); } } }); }); zipFile.readEntry(); await new Promise((resolve, reject) => { zipFile.on("end", (err) => { setImmediate(() => { if (doDebug2) { warningLog("unzip done"); } if (err) { reject(err); } else { resolve(); } }); }); zipFile.on("entry", (entry) => { zipFile.openReadStream(entry, (err, readStream) => { if (err) { return reject(err); } const file = path2.join(opensslFolder2, entry.fileName); if (doDebug2) { warningLog(" unzipping :", file); } const writeStream = fs2.createWriteStream(file, "binary"); readStream?.pipe(writeStream); writeStream.on("close", () => { zipFile.readEntry(); }); }); }); }); } const opensslFolder = get_openssl_folder_win32(); const opensslExecPath = get_openssl_exec_path_win32(); if (!fs2.existsSync(opensslFolder)) { if (doDebug2) { warningLog("creating openssl_folder", opensslFolder); } fs2.mkdirSync(opensslFolder); } const { opensslOk, version: _version } = await check_openssl_win32(); if (!opensslOk) { warningLog(chalk2.yellow("openssl seems to be missing and need to be installed")); const { downloadedFile } = await download_openssl(); if (doDebug2) { warningLog("deflating ", chalk2.yellow(downloadedFile)); } await unzip_openssl(downloadedFile); const opensslExists = !!fs2.existsSync(opensslExecPath); if (doDebug2) { warningLog("verifying ", opensslExists, opensslExists ? chalk2.green("OK ") : chalk2.red(" Error"), opensslExecPath); } const _opensslExecPath2 = await check_openssl_win32(); return opensslExecPath; } else { if (doDebug2) { warningLog(chalk2.green("openssl is already installed and have the expected version.")); } return opensslExecPath; } } async function install_prerequisite() { if (process.platform !== "win32") { return await check_system_openssl_version(); } else { return await install_and_check_win32_openssl_version(); } } async function get_openssl_exec_path() { if (process.platform === "win32") { const opensslExecPath = await install_prerequisite(); if (!fs2.existsSync(opensslExecPath)) { throw new Error(`internal error cannot find ${opensslExecPath}`); } return opensslExecPath; } else { return "openssl"; } } // packages/node-opcua-pki/lib/toolbox/with_openssl/execute_openssl.ts var opensslPath; var n = makePath; async function execute2(cmd, options) { const from = new Error(); options.cwd = options.cwd || process.cwd(); if (!g_config.silent) { warningLog(chalk3.cyan(" CWD "), options.cwd); } const outputs = []; return await new Promise((resolve, reject) => { const child = child_process2.exec( cmd, { cwd: options.cwd, windowsHide: true }, (err) => { if (err) { if (!options.hideErrorMessage) { const fence = "###########################################"; console.error(chalk3.bgWhiteBright.redBright(`${fence} OPENSSL ERROR ${fence}`)); console.error(chalk3.bgWhiteBright.redBright(`CWD = ${options.cwd}`)); console.error(chalk3.bgWhiteBright.redBright(err.message)); console.error(chalk3.bgWhiteBright.redBright(`${fence} OPENSSL ERROR ${fence}`)); console.error(from.stack); } reject(new Error(err.message)); return; } resolve(outputs.join("")); } ); if (child.stdout) { const stream2 = byline2(child.stdout); stream2.on("data", (line) => { outputs.push(`${line} `); }); if (!g_config.silent) { stream2.on("data", (line) => { line = line.toString(); if (doDebug) { process.stdout.write(`${chalk3.white(" stdout ") + chalk3.whiteBright(line)} `); } }); } } if (!g_config.silent) { if (child.stderr) { const stream1 = byline2(child.stderr); stream1.on("data", (line) => { line = line.toString(); if (displayError) { process.stdout.write(`${chalk3.white(" stderr ") + chalk3.red(line)} `); } }); } } }); } async function find_openssl() { return await get_openssl_exec_path(); } async function ensure_openssl_installed() { if (!opensslPath) { opensslPath = await find_openssl(); const outputs = await execute_openssl("version", { cwd: "." }); g_config.opensslVersion = outputs.trim(); if (doDebug) { warningLog("OpenSSL version : ", g_config.opensslVersion); } } } async function execute_openssl_no_failure(cmd, options) { options = options || {}; options.hideErrorMessage = true; try { return await execute_openssl(cmd, options); } catch (err) { debugLog(" (ignored error = ERROR : )", err.message); } } function getTempFolder() { return os2.tmpdir(); } async function execute_openssl(cmd, options) { debugLog("execute_openssl", cmd, options); const empty_config_file = n(getTempFolder(), "empty_config.cnf"); if (!fs3.existsSync(empty_config_file)) { await fs3.promises.writeFile(empty_config_file, "# empty config file"); } options = options || {}; options.openssl_conf = options.openssl_conf || empty_config_file; assert3(options.openssl_conf); setEnv("OPENSSL_CONF", options.openssl_conf); if (!g_config.silent) { warningLog(chalk3.cyan(" OPENSSL_CONF"), process.env.OPENSSL_CONF); warningLog(chalk3.cyan(" RANDFILE "), process.env.RANDFILE); warningLog(chalk3.cyan(" CMD openssl "), chalk3.cyanBright(cmd)); } await ensure_openssl_installed(); return await execute2(`${quote(opensslPath)} ${cmd}`, options); } // packages/node-opcua-pki/lib/pki/toolbox_pfx.ts var q = quote; var n2 = makePath; async function createPFX(options) { const { certificateFile, privateKeyFile, outputFile, passphrase = "", caCertificateFiles } = options; assert4(fs4.existsSync(certificateFile), `Certificate file does not exist: ${certificateFile}`); assert4(fs4.existsSync(privateKeyFile), `Private key file does not exist: ${privateKeyFile}`); let cmd = `pkcs12 -export`; cmd += ` -in ${q(n2(certificateFile))}`; cmd += ` -inkey ${q(n2(privateKeyFile))}`; if (caCertificateFiles) { for (const caFile of caCertificateFiles) { assert4(fs4.existsSync(caFile), `CA certificate file does not exist: ${caFile}`); cmd += ` -certfile ${q(n2(caFile))}`; } } cmd += ` -out ${q(n2(outputFile))}`; cmd += ` -passout pass:${passphrase}`; await execute_openssl(cmd, {}); } async function extractCertificateFromPFX(options) { const { pfxFile, passphrase = "" } = options; assert4(fs4.existsSync(pfxFile), `PFX file does not exist: ${pfxFile}`); const cmd = `pkcs12 -in ${q(n2(pfxFile))} -clcerts -nokeys -nodes -passin pass:${passphrase}`; return await execute_openssl(cmd, {}); } async function extractPrivateKeyFromPFX(options) { const { pfxFile, passphrase = "" } = options; assert4(fs4.existsSync(pfxFile), `PFX file does not exist: ${pfxFile}`); const cmd = `pkcs12 -in ${q(n2(pfxFile))} -nocerts -nodes -passin pass:${passphrase}`; return await execute_openssl(cmd, {}); } async function extractCACertificatesFromPFX(options) { const { pfxFile, passphrase = "" } = options; assert4(fs4.existsSync(pfxFile), `PFX file does not exist: ${pfxFile}`); const cmd = `pkcs12 -in ${q(n2(pfxFile))} -cacerts -nokeys -nodes -passin pass:${passphrase}`; return await execute_openssl(cmd, {}); } async function extractAllFromPFX(options) { const [certificate, privateKey, caCertificates] = await Promise.all([ extractCertificateFromPFX(options), extractPrivateKeyFromPFX(options), extractCACertificatesFromPFX(options) ]); return { certificate, privateKey, caCertificates }; } async function convertPFXtoPEM(pfxFile, pemFile, passphrase = "") { assert4(fs4.existsSync(pfxFile), `PFX file does not exist: ${pfxFile}`); const cmd = `pkcs12 -in ${q(n2(pfxFile))} -out ${q(n2(pemFile))} -nodes -passin pass:${passphrase}`; await execute_openssl(cmd, {}); } async function dumpPFX(pfxFile, passphrase = "") { assert4(fs4.existsSync(pfxFile), `PFX file does not exist: ${pfxFile}`); const cmd = `pkcs12 -in ${q(n2(pfxFile))} -info -nodes -passin pass:${passphrase}`; return await execute_openssl(cmd, {}); } // packages/node-opcua-pki/lib/toolbox/display.ts import chalk4 from "chalk"; function displayTitle(str) { if (!g_config.silent) { warningLog(""); warningLog(chalk4.yellowBright(str)); warningLog(chalk4.yellow(new Array(str.length + 1).join("=")), "\n"); } } function displaySubtitle(str) { if (!g_config.silent) { warningLog(""); warningLog(` ${chalk4.yellowBright(str)}`); warningLog(` ${chalk4.white(new Array(str.length + 1).join("-"))}`, "\n"); } } function display(str) { if (!g_config.silent) { warningLog(` ${str}`); } } // packages/node-opcua-pki/lib/toolbox/with_openssl/index.ts import exp from "constants"; // packages/node-opcua-pki/lib/toolbox/with_openssl/create_certificate_signing_request.ts import assert6 from "assert"; import fs6 from "fs"; import path4 from "path"; // packages/node-opcua-pki/lib/misc/subject.ts import { Subject } from "node-opcua-crypto"; // packages/node-opcua-pki/lib/toolbox/with_openssl/toolbox.ts import assert5 from "assert"; import fs5 from "fs"; import path3 from "path"; function openssl_require2DigitYearInDate() { if (!g_config.opensslVersion) { throw new Error( "openssl_require2DigitYearInDate : openssl version is not known: please call ensure_openssl_installed()" ); } return g_config.opensslVersion.match(/OpenSSL 0\.9/); } g_config.opensslVersion = ""; var _counter = 0; function stripConditionalBlocks(template) { return template.replace(/\{\{#([A-Z_][A-Z0-9_]*)\}\}([\s\S]*?)\{\{\/\1\}\}\r?\n?/g, (_match, key, content) => { const keep = hasEnv(key) && getEnv(key) !== ""; return keep ? content : ""; }); } function generateStaticConfig(configPath, options) { const prePath = options?.cwd || ""; const originalFilename = !path3.isAbsolute(configPath) ? path3.join(prePath, configPath) : configPath; let staticConfig = fs5.readFileSync(originalFilename, { encoding: "utf8" }); staticConfig = stripConditionalBlocks(staticConfig); for (const envVar of getEnvironmentVarNames()) { staticConfig = staticConfig.replace(new RegExp(envVar.pattern, "gi"), getEnv(envVar.key)); } const staticConfigPath = `${configPath}.${process.pid}-${_counter++}.tmp`; const temporaryConfigPath = !path3.isAbsolute(configPath) ? path3.join(prePath, staticConfigPath) : staticConfigPath; fs5.writeFileSync(temporaryConfigPath, staticConfig); if (options?.cwd) { return path3.relative(options.cwd, temporaryConfigPath); } else { return temporaryConfigPath; } } function x509Date(date) { date = date || /* @__PURE__ */ new Date(); const Y = date.getUTCFullYear(); const M = date.getUTCMonth() + 1; const D = date.getUTCDate(); const h = date.getUTCHours(); const m = date.getUTCMinutes(); const s = date.getUTCSeconds(); function w(s2, l) { return `${s2}`.padStart(l, "0"); } if (openssl_require2DigitYearInDate()) { return `${w(Y, 2) + w(M, 2) + w(D, 2) + w(h, 2) + w(m, 2) + w(s, 2)}Z`; } else { return `${w(Y, 4) + w(M, 2) + w(D, 2) + w(h, 2) + w(m, 2) + w(s, 2)}Z`; } } // packages/node-opcua-pki/lib/toolbox/with_openssl/create_certificate_signing_request.ts var q2 = quote; var n3 = makePath; async function createCertificateSigningRequestWithOpenSSL(certificateSigningRequestFilename, params) { assert6(params); assert6(params.rootDir); assert6(params.configFile); assert6(params.privateKey); assert6(typeof params.privateKey === "string"); assert6(fs6.existsSync(params.configFile), `config file must exist ${params.configFile}`); assert6(fs6.existsSync(params.privateKey), `Private key must exist${params.privateKey}`); assert6(fs6.existsSync(params.rootDir), "RootDir key must exist"); assert6(typeof certificateSigningRequestFilename === "string"); processAltNames(params); const configFile = generateStaticConfig(params.configFile, { cwd: params.rootDir }); const options = { cwd: params.rootDir, openssl_conf: path4.relative(params.rootDir, configFile) }; const configOption = ` -config ${q2(n3(configFile))}`; const subject = params.subject ? new Subject(params.subject).toString() : void 0; const subjectOptions = subject ? ` -subj "${subject}"` : ""; displaySubtitle("- Creating a Certificate Signing Request with openssl"); await execute_openssl( "req -new -sha256 -batch -text " + configOption + " -key " + q2(n3(params.privateKey)) + subjectOptions + " -out " + q2(n3(certificateSigningRequestFilename)), options ); } // packages/node-opcua-pki/lib/pki/templates/simple_config_template.cnf.ts var config = '##################################################################################################\n## SIMPLE OPENSSL CONFIG FILE FOR SELF-SIGNED CERTIFICATE GENERATION\n################################################################################################################\n\ndistinguished_name = req_distinguished_name\ndefault_md = sha1\n\ndefault_md = sha256 # The default digest algorithm\n\n[ v3_ca ]\nsubjectKeyIdentifier = hash\nauthorityKeyIdentifier = keyid:always,issuer:always\n\n# authorityKeyIdentifier = keyid\nbasicConstraints = CA:TRUE\nkeyUsage = critical, cRLSign, keyCertSign\nnsComment = "Self-signed Certificate for CA generated by Node-OPCUA Certificate utility"\n#nsCertType = sslCA, emailCA\n#subjectAltName = email:copy\n#issuerAltName = issuer:copy\n#obj = DER:02:03\n# crlDistributionPoints = @crl_info\n# [ crl_info ]\n# URI.0 = http://localhost:8900/crl.pem\nsubjectAltName = $ENV::ALTNAME\n\n[ req ]\ndays = 390\nreq_extensions = v3_req\nx509_extensions = v3_ca\n\n[v3_req]\nbasicConstraints = CA:false\nkeyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment\nsubjectAltName = $ENV::ALTNAME\n\n[ v3_ca_signed]\nsubjectKeyIdentifier = hash\nauthorityKeyIdentifier = keyid,issuer\nbasicConstraints = critical, CA:FALSE\nkeyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment\nextendedKeyUsage = clientAuth,serverAuth \nnsComment = "certificate generated by Node-OPCUA Certificate utility and signed by a CA"\nsubjectAltName = $ENV::ALTNAME\n[ v3_selfsigned]\nsubjectKeyIdentifier = hash\nauthorityKeyIdentifier = keyid,issuer\nbasicConstraints = critical, CA:FALSE\nkeyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment\nextendedKeyUsage = clientAuth,serverAuth \nnsComment = "Self-signed certificate generated by Node-OPCUA Certificate utility"\nsubjectAltName = $ENV::ALTNAME\n[ req_distinguished_name ]\ncountryName = Country Name (2 letter code)\ncountryName_default = FR\ncountryName_min = 2\ncountryName_max = 2\n# stateOrProvinceName = State or Province Name (full name)\n# stateOrProvinceName_default = Ile de France\n# localityName = Locality Name (city, district)\n# localityName_default = Paris\norganizationName = Organization Name (company)\norganizationName_default = NodeOPCUA\n# organizationalUnitName = Organizational Unit Name (department, division)\n# organizationalUnitName_default = R&D\ncommonName = Common Name (hostname, FQDN, IP, or your name)\ncommonName_max = 256\ncommonName_default = NodeOPCUA\n# emailAddress = Email Address\n# emailAddress_max = 40\n# emailAddress_default = node-opcua (at) node-opcua (dot) com\nsubjectAltName = $ENV::ALTNAME'; var simple_config_template_cnf_default = config; // packages/node-opcua-pki/lib/ca/templates/ca_config_template.cnf.ts var config2 = `#.........DO NOT MODIFY BY HAND ......................... [ ca ] default_ca = CA_default [ CA_default ] dir = %%ROOT_FOLDER%% # the main CA folder certs = $dir/certs # where to store certificates new_certs_dir = $dir/certs # database = $dir/index.txt # the certificate database serial = $dir/serial # the serial number counter certificate = $dir/public/cacert.pem # The root CA certificate private_key = $dir/private/cakey.pem # the CA private key x509_extensions = usr_cert # default_days = 3650 # default validity : 10 years # default_md = sha1 default_md = sha256 # The default digest algorithm preserve = no policy = policy_match # randfile = $dir/random.rnd # default_startdate = YYMMDDHHMMSSZ # default_enddate = YYMMDDHHMMSSZ crl_dir = $dir/crl crl_extensions = crl_ext crl = $dir/revocation_list.crl # the Revocation list crlnumber = $dir/crlnumber # CRL number file default_crl_days = 30 default_crl_hours = 24 #msie_hack [ policy_match ] countryName = optional stateOrProvinceName = optional localityName = optional organizationName = optional organizationalUnitName = optional commonName = optional emailAddress = optional [ req ] default_bits = 4096 # Size of keys default_keyfile = key.pem # name of generated keys distinguished_name = req_distinguished_name attributes = req_attributes x509_extensions = v3_ca #input_password #output_password string_mask = nombstr # permitted characters req_extensions = v3_req [ req_distinguished_name ] #0 countryName = Country Name (2 letter code) # countryName_default = FR # countryName_min = 2 # countryName_max = 2 # stateOrProvinceName = State or Province Name (full name) # stateOrProvinceName_default = Ile de France # localityName = Locality Name (city, district) # localityName_default = Paris organizationName = Organization Name (company) organizationName_default = NodeOPCUA # organizationalUnitName = Organizational Unit Name (department, division) # organizationalUnitName_default = R&D commonName = Common Name (hostname, FQDN, IP, or your name) commonName_max = 256 commonName_default = NodeOPCUA # emailAddress = Email Address # emailAddress_max = 40 # emailAddress_default = node-opcua (at) node-opcua (dot) com [ req_attributes ] #challengePassword = A challenge password #challengePassword_min = 4 #challengePassword_max = 20 #unstructuredName = An optional company name [ usr_cert ] basicConstraints = critical, CA:FALSE subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer:always #authorityKeyIdentifier = keyid subjectAltName = $ENV::ALTNAME # issuerAltName = issuer:copy nsComment = ''OpenSSL Generated Certificate'' #nsCertType = client, email, objsign for ''everything including object signing'' #nsCaRevocationUrl = http://www.domain.dom/ca-crl.pem #nsBaseUrl = #nsRenewalUrl = #nsCaPolicyUrl = #nsSslServerName = keyUsage = critical, digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment, keyAgreement extendedKeyUsage = critical,serverAuth ,clientAuth {{#CDP_URL}}crlDistributionPoints = URI:$ENV::CDP_URL {{/CDP_URL}}{{#AIA_VALUE}}authorityInfoAccess = $ENV::AIA_VALUE {{/AIA_VALUE}} [ v3_req ] basicConstraints = critical, CA:FALSE keyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment, keyAgreement extendedKeyUsage = critical,serverAuth ,clientAuth subjectAltName = $ENV::ALTNAME nsComment = "CA Generated by Node-OPCUA Certificate utility using openssl" [ v3_ca_req ] subjectKeyIdentifier = hash basicConstraints = CA:TRUE keyUsage = critical, cRLSign, keyCertSign subjectAltName = $ENV::ALTNAME nsComment = "CA CSR generated by Node-OPCUA Certificate utility using openssl" [ v3_ca ] subjectKeyIdentifier = hash authorityKeyIdentifier = keyid:always,issuer:always basicConstraints = CA:TRUE keyUsage = critical, cRLSign, keyCertSign subjectAltName = $ENV::ALTNAME nsComment = "CA Certificate generated by Node-OPCUA Certificate utility using openssl" #nsCertType = sslCA, emailCA #issuerAltName = issuer:copy #obj = DER:02:03 {{#CDP_URL}}crlDistributionPoints = URI:$ENV::CDP_URL {{/CDP_URL}}{{#AIA_VALUE}}authorityInfoAccess = $ENV::AIA_VALUE {{/AIA_VALUE}}[ v3_selfsigned] basicConstraints = critical, CA:FALSE keyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment, keyAgreement extendedKeyUsage = critical,serverAuth ,clientAuth nsComment = "Self-signed certificate, generated by NodeOPCUA" subjectAltName = $ENV::ALTNAME [ crl_ext ] #issuerAltName = issuer:copy authorityKeyIdentifier = keyid:always,issuer:always #authorityInfoAccess = @issuer_info`; var ca_config_template_cnf_default = config2; // packages/node-opcua-pki/lib/ca/certificate_authority.ts var defaultSubject = "/C=FR/ST=IDF/L=Paris/O=Local NODE-OPCUA Certificate Authority/CN=NodeOPCUA-CA"; var configurationFileTemplate = ca_config_template_cnf_default; var configurationFileSimpleTemplate = simple_config_template_cnf_default; var config3 = { certificateDir: "INVALID", forceCA: false, pkiDir: "INVALID" }; var n4 = makePath; var q3 = quote; function octetStringToIpAddress(a) { return parseInt(a.substring(0, 2), 16).toString() + "." + parseInt(a.substring(2, 4), 16).toString() + "." + parseInt(a.substring(4, 6), 16).toString() + "." + parseInt(a.substring(6, 8), 16).toString(); } assert7(octetStringToIpAddress("c07b9179") === "192.123.145.121"); async function construct_CertificateAuthority(certificateAuthority) { const subject = certificateAuthority.subject; const caRootDir = path5.resolve(certificateAuthority.rootDir); async function make_folders() { mkdirRecursiveSync(caRootDir); mkdirRecursiveSync(path5.join(caRootDir, "private")); mkdirRecursiveSync(path5.join(caRootDir, "public")); mkdirRecursiveSync(path5.join(caRootDir, "certs")); mkdirRecursiveSync(path5.join(caRootDir, "crl")); mkdirRecursiveSync(path5.join(caRootDir, "conf")); } await make_folders(); async function construct_default_files() { const serial = path5.join(caRootDir, "serial"); if (!fs7.existsSync(serial)) { await fs7.promises.writeFile(serial, "1000"); } const crlNumber = path5.join(caRootDir, "crlnumber"); if (!fs7.existsSync(crlNumber)) { await fs7.promises.writeFile(crlNumber, "1000"); } const indexFile = path5.join(caRootDir, "index.txt"); if (!fs7.existsSync(indexFile)) { await fs7.promises.writeFile(indexFile, ""); } } await construct_default_files(); const caKeyExists = fs7.existsSync(path5.join(caRootDir, "private/cakey.pem")); const caCertExists = fs7.existsSync(path5.join(caRootDir, "public/cacert.pem")); if (caKeyExists && caCertExists && !config3.forceCA) { debugLog("CA private key and certificate already exist ... skipping"); return; } if (caKeyExists && !caCertExists) { debugLog("CA private key exists but cacert.pem is missing \u2014 rebuilding CA"); fs7.unlinkSync(path5.join(caRootDir, "private/cakey.pem")); const staleCsr = path5.join(caRootDir, "private/cakey.csr"); if (fs7.existsSync(staleCsr)) { fs7.unlinkSync(staleCsr); } } displayTitle("Create Certificate Authority (CA)"); const indexFileAttr = path5.join(caRootDir, "index.txt.attr"); if (!fs7.existsSync(indexFileAttr)) { await fs7.promises.writeFile(indexFileAttr, "unique_subject = no"); } const caConfigFile = certificateAuthority.configFile; if (1) { let data = configurationFileTemplate; data = makePath(data.replace(/%%ROOT_FOLDER%%/, caRootDir)); await fs7.promises.writeFile(caConfigFile, data); } const subjectOpt = ` -subj "${subject.toString()}" `; const caCommonName = subject.commonName || "NodeOPCUA-CA"; setEnv("ALTNAME", `URI:urn:${caCommonName}`); certificateAuthority._wireRevocationEnvVars(); const options = { cwd: caRootDir }; const configFile = generateStaticConfig("conf/caconfig.cnf", options); const configOption = ` -config ${q3(n4(configFile))}`; const keySize = certificateAuthority.keySize; const privateKeyFilename = path5.join(caRootDir, "private/cakey.pem"); const csrFilename = path5.join(caRootDir, "private/cakey.csr"); displayTitle(`Generate the CA private Key - ${keySize}`); await generatePrivateKeyFile(privateKeyFilename, keySize); displayTitle("Generate a certificate request for the CA key"); await execute_openssl( "req -new -sha256 -text -extensions v3_ca_req" + configOption + " -key " + q3(n4(privateKeyFilename)) + " -out " + q3(n4(csrFilename)) + " " + subjectOpt, options ); const issuerCA = certificateAuthority._issuerCA; if (issuerCA) { displayTitle("Generate CA Certificate (signed by issuer CA)"); const issuerCert = path5.resolve(issuerCA.caCertificate); const issuerKey = path5.resolve(issuerCA.rootDir, "private/cakey.pem"); const issuerSerial = path5.resolve(issuerCA.rootDir, "serial"); await execute_openssl( " x509 -sha256 -req -days 3650 -text -extensions v3_ca -extfile " + q3(n4(configFile)) + " -in private/cakey.csr -CA " + q3(n4(issuerCert)) + " -CAkey " + q3(n4(issuerKey)) + " -CAserial " + q3(n4(issuerSerial)) + " -out public/cacert.pem", options ); } else { displayTitle("Generate CA Certificate (self-signed)"); await execute_openssl( " x509 -sha256 -req -days 3650 -text -extensions v3_ca -extfile " + q3(n4(configFile)) + " -in private/cakey.csr -signkey " + q3(n4(privateKeyFilename)) + " -out public/cacert.pem", options ); } displaySubtitle("generate initial CRL (Certificate Revocation List)"); await regenerateCrl(certificateAuthority.revocationList, configOption, options); displayTitle("Create Certificate Authority (CA) ---> DONE"); } async function regenerateCrl(revocationList, configOption, options) { displaySubtitle("regenerate CRL (Certificate Revocation List)"); await execute_openssl(`ca -gencrl ${configOption} -out crl/revocation_list.crl`, options); await execute_openssl("crl -in crl/revocation_list.crl -out crl/revocation_list.der -outform der", options); displaySubtitle("Display (Certificate Revocation List)"); await execute_openssl(`crl -in ${q3(n4(revocationList))} -text -noout`, options); } function parseOpenSSLDate(dateStr) { const raw = dateStr?.split(",")[0] ?? ""; if (raw.length < 12) return ""; const yy = parseInt(raw.substring(0, 2), 10); const year = yy >= 70 ? 1900 + yy : 2e3 + yy; const month = raw.substring(2, 4); const day = raw.substring(4, 6); const hour = raw.substring(6, 8); const min = raw.substring(8, 10); const sec = raw.substring(10, 12); return `${year}-${month}-${day}T${hour}:${min}:${sec}Z`; } function validateRevocationUrl(url2, fieldName) { if (url2 === void 0) { return void 0; } if (url2 === "") { throw new Error(`${fieldName} must not be empty \u2014 pass undefined to disable the extension`); } let parsed; try { parsed = new URL(url2); } catch { throw new Error(`${fieldName} is not a valid URL: ${url2}`); } if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { throw new Error(`${fieldName} must use http: or https: (got ${parsed.protocol} in ${url2})`); } if (!parsed.pathname || parsed.pathname === "/") { throw new Error(`${fieldName} must include a path component (got ${url2})`); } const isLoopback = parsed.hostname === "localhost" || parsed.hostname === "::1" || parsed.hostname.startsWith("127."); if (isLoopback) { console.warn( `[node-opcua-pki] ${fieldName} points at loopback (${url2}) \u2014 certificates issued with this URL will be unreachable from any other host.` ); } return url2; } var CertificateAuthority = class { /** RSA key size used when generating the CA private key. */ keySize; /** Root filesystem path of the CA directory structure. */ location; /** X.500 subject of the CA certificate. */ subject; /** @internal Parent CA (undefined for root CAs). */ _issuerCA; /** @internal Configured CDP / AIA URLs (US-202). */ _crlDistributionUrl; _ocspResponderUrl; _caIssuersUrl; constructor(options) { assert7(Object.prototype.hasOwnProperty.call(options, "location")); assert7(Object.prototype.hasOwnProperty.call(options, "keySize")); this.location = options.location; this.keySize = options.keySize || 2048; this.subject = new Subject2(options.subject || defaultSubject); this._issuerCA = options.issuerCA; if (options.crlDistributionUrl !== void 0) { this.setCrlDistributionUrl(options.crlDistributionUrl); } if (options.ocspResponderUrl !== void 0) { this.setOcspResponderUrl(options.ocspResponderUrl); } if (options.caIssuersUrl !== void 0) { this.setCaIssuersUrl(options.caIssuersUrl); } } /** * Public URL where the CRL produced by this CA is reachable, or * `undefined` if no CDP extension should be emitted on issued certs. */ get crlDistributionUrl() { return this._crlDistributionUrl; } /** * Public URL of the OCSP responder, or `undefined` if no AIA OCSP * leg should be emitted on issued certs. */ get ocspResponderUrl() { return this._ocspResponderUrl; } /** * Public URL where the issuer's certificate can be fetched, or * `undefined` if no AIA caIssuers leg should be emitted. */ get caIssuersUrl() { return this._caIssuersUrl; } /** * Configure the URL embedded as `crlDistributionPoints` in every * subsequently-issued certificate. Pass `undefined` to disable * the extension entirely. Validated synchronously — throws on * empty string, non-http(s) protocol, missing path. Warns (does * not throw) when the URL points at loopback. * * @see US-202 */ setCrlDistributionUrl(url2) { this._crlDistributionUrl = validateRevocationUrl(url2, "crlDistributionUrl"); } /** * Configure the OCSP responder URL embedded as the `OCSP` leg of * the `authorityInfoAccess` extension on every subsequently-issued * certificate. Pass `undefined` to disable. * * @see US-202 */ setOcspResponderUrl(url2) { this._ocspResponderUrl = validateRevocationUrl(url2, "ocspResponderUrl"); } /** * Configure the caIssuers URL embedded as the `caIssuers` leg of * the `authorityInfoAccess` extension on every subsequently-issued * certificate. Pass `undefined` to disable. * * @see US-202 */ setCaIssuersUrl(url2) { this._caIssuersUrl = validateRevocationUrl(url2, "caIssuersUrl"); } /** * @internal * Populate the OpenSSL config substitution env vars (`CDP_URL` and * `AIA_VALUE`) from the configured URLs, or unset them so the * matching `{{#KEY}}...{{/KEY}}` blocks in the templates are * stripped. MUST be called before every `generateStaticConfig` * invocation that signs a certificate. */ _wireRevocationEnvVars() { unsetEnv("CDP_URL"); unsetEnv("AIA_VALUE"); if (this._crlDistributionUrl) { setEnv("CDP_URL", this._crlDistributionUrl); } const aiaLegs = []; if (this._ocspResponderUrl) { aiaLegs.push(`OCSP;URI:${this._ocspResponderUrl}`); } if (this._caIssuersUrl) { aiaLegs.push(`caIssuers;URI:${this._caIssuersUrl}`); } if (aiaLegs.length > 0) { setEnv("AIA_VALUE", aiaLegs.join(",")); } } /** Absolute path to the CA root directory (alias for {@link location}). */ get rootDir() { return this.location; } /** Path to the OpenSSL configuration file (`conf/caconfig.cnf`). */ get configFile() { return path5.normalize(path5.join(this.rootDir, "./conf/caconfig.cnf")); } /** Path to the CA certificate in PEM format (`public/cacert.pem`). */ get caCertificate() { return makePath(this.rootDir, "./public/cacert.pem"); } /** * Path to the issuer certificate chain (`public/issuer_chain.pem`). * * This file is created by {@link installCACertificate} when the * provided cert file contains additional issuer certificates * (e.g. intermediate + root). It is appended to signed certs * by {@link constructCertificateChain} to produce a full chain * per OPC UA Part 6 §6.2.6. */ get issuerCertificateChain() { return makePath(this.rootDir, "./public/issuer_chain.pem"); } /** * Path to the current Certificate Revocation List in DER format. * (`crl/revocation_list.der`) */ get revocationListDER() { return makePath(this.rootDir, "./crl/revocation_list.der"); } /** * Path to the current Certificate Revocation List in PEM format. * (`crl/revocation_list.crl`) */ get revocationList() { return makePath(this.rootDir, "./crl/revocation_list.crl"); } /** * Path to the concatenated CA certificate + CRL file. * Used by OpenSSL for CRL-based verification. */ get caCertificateWithCrl() { return makePath(this.rootDir, "./public/cacertificate_with_crl.pem"); } // --------------------------------------------------------------- // Buffer-based accessors (US-059) // --------------------------------------------------------------- /** * Return the CA certificate as a DER-encoded buffer. * * @throws if the CA certificate file does not exist * (call {@link initialize} first). */ getCACertificateDER() { const pem = readCertificatePEM(this.caCertificate); return convertPEMtoDER(pem); } /** * Return the CA certificate as a PEM-encoded string. * * @throws if the CA certificate file does not exist * (call {@link initialize} first). */ getCACertificatePEM() { const raw = readCertificatePEM(this.caCertificate); const beginMarker = "-----BEGIN CERTIFICATE-----"; const idx = raw.indexOf(beginMarker); if (idx > 0) { return raw.substring(idx); } return raw; } /** * Return the current Certificate Revocation List as a * DER-encoded buffer. * * Returns an empty buffer if no CRL has been generated yet. */ getCRLDER() { const crlPath = this.revocationListDER; if (!fs7.existsSync(crlPath)) { return Buffer.alloc(0); } return fs7.readFileSync(crlPath); } /** * Return the current Certificate Revocation List as a * PEM-encoded string. * * Returns an empty string if no CRL has been generated yet. */ getCRLPEM() { const crlPath = this.revocationList; if (!fs7.existsSync(crlPath)) { return ""; } const raw = fs7.readFileSync(crlPath, "utf-8"); const beginMarker = "-----BEGIN X509 CRL-----"; const idx = raw.indexOf(beginMarker); if (idx > 0) { return raw.substring(idx); } return raw; } // --------------------------------------------------------------- // Certificate database API (US-057) // --------------------------------------------------------------- /** * Return a list of all issued certificates recorded in the * OpenSSL `index.txt` database. * * Each entry includes the serial number, subject, status, * expiry date, and (for revoked certs) the revocation date. */ getIssuedCertificates() { return this._parseIndexTxt(); } /** * Return the total number of certificates recorded in * `index.txt`. */ getIssuedCertificateCount() { return this._parseIndexTxt().length; } /** * Return the status of a certificate by its serial number. * * @param serial - hex-encoded serial number (e.g. `"1000"`) * @returns `"valid"`, `"revoked"`, `"expired"`, or * `undefined` if not found */ getCertificateStatus(serial) { const upper = serial.toUpperCase(); const record = this._parseIndexTxt().find((r) => r.serial.toUpperCase() === upper); return record?.status; } /** * Read a specific issued certificate by serial number and * return its content as a DER-encoded buffer. * * OpenSSL stores signed certificates in the `certs/` * directory using the naming convention `<SERIAL>.pem`. * * @param serial - hex-encoded serial number (e.g. `"1000"`) * @returns the DER buffer, or `undefined` if not found */ getCertificateBySerial(serial) { const upper = serial.toUpperCase(); const certFile = path5.join(this.rootDir, "certs", `${upper}.pem`); if (!fs7.existsSync(certFile)) { return void 0; } const pem = readCertificatePEM(certFile); return convertPEMtoDER(pem); } /** * Path to the OpenSSL certificate database file. */ get indexFile() { return path5.join(this.rootDir, "index.txt"); } /** * Parse the OpenSSL `index.txt` certificate database. * * Each line has tab-separated fields: