node-opcua-pki
Version:
PKI management for node-opcua
1 lines • 414 kB
Source Map (JSON)
{"version":3,"sources":["../../lib/misc/applicationurn.ts","../../lib/misc/hostname.ts","../../lib/toolbox/config.ts","../../lib/toolbox/debug.ts","../../lib/toolbox/common2.ts","../../lib/toolbox/display.ts","../../lib/toolbox/without_openssl/create_certificate_signing_request.ts","../../lib/toolbox/common.ts","../../lib/toolbox/without_openssl/create_self_signed_certificate.ts","../../lib/toolbox/without_openssl/index.ts","../../lib/pki/templates/simple_config_template.cnf.ts","../../lib/pki/certificate_manager.ts","../../lib/toolbox/index.ts","../../lib/toolbox/with_openssl/_env.ts","../../lib/misc/subject.ts","../../lib/toolbox/with_openssl/install_prerequisite.ts","../../lib/toolbox/with_openssl/execute_openssl.ts","../../lib/toolbox/with_openssl/toolbox.ts","../../lib/toolbox/with_openssl/create_certificate_signing_request.ts","../../lib/toolbox/with_openssl/index.ts","../../lib/pki/toolbox_pfx.ts","../../lib/ca/templates/ca_config_template.cnf.ts","../../lib/ca/certificate_authority.ts","../../lib/ca/crypto_create_CA.ts","../../bin/pki.ts"],"sourcesContent":["// ---------------------------------------------------------------------------------------------------------------------\n// node-opcua-pki\n// ---------------------------------------------------------------------------------------------------------------------\n// Copyright (c) 2014-2026 - Etienne Rossignon - etienne.rossignon (at) gadz.org\n// Copyright (c) 2022-2026 - Sterfive.com\n// ---------------------------------------------------------------------------------------------------------------------\n//\n// This project is licensed under the terms of the MIT license.\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated\n// documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the\n// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to\n// permit persons to whom the Software is furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the\n// Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE\n// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\n// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n// ---------------------------------------------------------------------------------------------------------------------\nimport assert from \"node:assert\";\nimport { createHash } from \"node:crypto\";\n\nexport function makeApplicationUrn(hostname: string, suffix: string): string {\n // beware : Openssl doesn't support urn with length greater than 64 !!\n // sometimes hostname length could be too long ...\n // application urn length must not exceed 64 car. to comply with openssl\n // see cryptoCA\n let hostnameHash = hostname;\n if (hostnameHash.length + 7 + suffix.length >= 64) {\n // we need to reduce the applicationUrn side => let's take\n // a portion of the hostname hash.\n hostnameHash = createHash(\"md5\").update(hostname).digest(\"hex\").substring(0, 16);\n }\n\n const applicationUrn = `urn:${hostnameHash}:${suffix}`;\n assert(applicationUrn.length <= 64);\n return applicationUrn;\n}\n","/**\n * @module node-opcua-hostname\n */\nimport dns from \"node:dns\";\nimport os from \"node:os\";\nimport { promisify } from \"node:util\";\n\nfunction trim(str: string, length?: number): string {\n if (!length) {\n return str;\n }\n return str.substring(0, Math.min(str.length, length));\n}\n\nfunction fqdn(callback: (err: Error | null, fqdn?: string) => void) {\n const uqdn = os.hostname();\n\n dns.lookup(uqdn, { hints: dns.ADDRCONFIG }, (err1: Error | null, ip: string) => {\n if (err1) {\n return callback(err1);\n }\n\n dns.lookupService(ip, 0, (err2: Error | null, _fqdn: string) => {\n if (err2) {\n return callback(err2);\n }\n _fqdn = _fqdn.replace(\".localdomain\", \"\");\n callback(null, _fqdn);\n });\n });\n}\n\nlet _fullyQualifiedDomainNameInCache: string | undefined;\n\n/**\n * extract FullyQualifiedDomainName of this computer\n */\nexport async function extractFullyQualifiedDomainName(): Promise<string> {\n if (_fullyQualifiedDomainNameInCache) {\n return _fullyQualifiedDomainNameInCache;\n }\n if (process.platform === \"win32\") {\n // http://serverfault.com/a/73643/251863\n const env = process.env;\n _fullyQualifiedDomainNameInCache =\n env.COMPUTERNAME + (env.USERDNSDOMAIN && env.USERDNSDOMAIN?.length > 0 ? `.${env.USERDNSDOMAIN as string}` : \"\");\n } else {\n try {\n _fullyQualifiedDomainNameInCache = await promisify(fqdn)();\n if (_fullyQualifiedDomainNameInCache === \"localhost\") {\n throw new Error(\"localhost not expected\");\n }\n if (/sethostname/.test(_fullyQualifiedDomainNameInCache as string)) {\n throw new Error(\"Detecting fqdn on windows !!!\");\n }\n } catch (_err) {\n // fall back to old method\n _fullyQualifiedDomainNameInCache = os.hostname();\n }\n }\n return _fullyQualifiedDomainNameInCache as string;\n}\n\nexport async function prepareFQDN() {\n _fullyQualifiedDomainNameInCache = await extractFullyQualifiedDomainName();\n}\n\nexport function getFullyQualifiedDomainName(optional_max_length?: number) {\n if (!_fullyQualifiedDomainNameInCache) {\n throw new Error(\"FullyQualifiedDomainName computation is not completed yet\");\n }\n return _fullyQualifiedDomainNameInCache ? trim(_fullyQualifiedDomainNameInCache, optional_max_length) : \"%FQDN%\";\n}\n\nexport function getHostname() {\n return os.hostname();\n}\n\nexport function resolveFullyQualifiedDomainName(str: string): string {\n if (!_fullyQualifiedDomainNameInCache) {\n throw new Error(\"FullyQualifiedDomainName computation is not completed yet\");\n }\n str = str.replace(\"%FQDN%\", _fullyQualifiedDomainNameInCache);\n str = str.replace(\"{FQDN}\", _fullyQualifiedDomainNameInCache);\n str = str.replace(\"{hostname}\", getHostname());\n return str;\n}\n// note : under windows ... echo %COMPUTERNAME%.%USERDNSDOMAIN%\nprepareFQDN();\n","// ---------------------------------------------------------------------------------------------------------------------\n// node-opcua-pki\n// ---------------------------------------------------------------------------------------------------------------------\n// Copyright (c) 2014-2026 - Etienne Rossignon - etienne.rossignon (at) gadz.org\n// Copyright (c) 2022-2026 - Sterfive.com\n// ---------------------------------------------------------------------------------------------------------------------\n//\n// This project is licensed under the terms of the MIT license.\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated\n// documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the\n// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to\n// permit persons to whom the Software is furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the\n// Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE\n// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\n// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n// ---------------------------------------------------------------------------------------------------------------------\nexport const g_config = {\n opensslVersion: \"unset\",\n silent: process.env.VERBOSE ? !process.env.VERBOSE : true,\n force: false\n};\n","// ---------------------------------------------------------------------------------------------------------------------\n// node-opcua-pki\n// ---------------------------------------------------------------------------------------------------------------------\n// Copyright (c) 2014-2026 - Etienne Rossignon - etienne.rossignon (at) gadz.org\n// Copyright (c) 2022-2026 - Sterfive.com\n// ---------------------------------------------------------------------------------------------------------------------\n//\n// This project is licensed under the terms of the MIT license.\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated\n// documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the\n// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to\n// permit persons to whom the Software is furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the\n// Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE\n// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\n// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n// ---------------------------------------------------------------------------------------------------------------------\nexport const doDebug = process.env.NODEOPCUAPKIDEBUG || false;\nexport const displayError = true;\nexport const displayDebug = !!process.env.NODEOPCUAPKIDEBUG || false;\n// tslint:disable-next-line:no-empty\nexport function debugLog(...args: unknown[]) {\n // istanbul ignore next\n if (displayDebug) {\n console.log.apply(null, args);\n }\n}\nexport function warningLog(...args: unknown[]) {\n console.log.apply(null, args);\n}\n","// ---------------------------------------------------------------------------------------------------------------------\n// node-opcua-pki\n// ---------------------------------------------------------------------------------------------------------------------\n// Copyright (c) 2014-2026 - Etienne Rossignon - etienne.rossignon (at) gadz.org\n// Copyright (c) 2022-2026 - Sterfive.com\n// ---------------------------------------------------------------------------------------------------------------------\n//\n// This project is licensed under the terms of the MIT license.\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated\n// documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the\n// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to\n// permit persons to whom the Software is furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the\n// Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE\n// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\n// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n// ---------------------------------------------------------------------------------------------------------------------\n\nimport assert from \"node:assert\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport chalk from \"chalk\";\n\nimport { g_config } from \"./config\";\n\nimport { debugLog, warningLog } from \"./debug\";\n\nexport function certificateFileExist(certificateFile: string): boolean {\n // istanbul ignore next\n if (fs.existsSync(certificateFile) && !g_config.force) {\n warningLog(\n chalk.yellow(\" certificate \") + chalk.cyan(certificateFile) + chalk.yellow(\" already exists => do not overwrite\")\n );\n return false;\n }\n return true;\n}\n\nexport function mkdirRecursiveSync(folder: string): void {\n if (!fs.existsSync(folder)) {\n // istanbul ignore next\n debugLog(chalk.white(\" .. constructing \"), folder);\n fs.mkdirSync(folder, { recursive: true });\n }\n}\n\nexport function makePath(folderName: string, filename?: string): string {\n let s: string;\n if (filename) {\n s = path.join(path.normalize(folderName), filename);\n } else {\n assert(folderName);\n s = folderName;\n }\n s = s.replace(/\\\\/g, \"/\");\n return s;\n}\n","// ---------------------------------------------------------------------------------------------------------------------\n// node-opcua-pki\n// ---------------------------------------------------------------------------------------------------------------------\n// Copyright (c) 2014-2026 - Etienne Rossignon - etienne.rossignon (at) gadz.org\n// Copyright (c) 2022-2026 - Sterfive.com\n// ---------------------------------------------------------------------------------------------------------------------\n//\n// This project is licensed under the terms of the MIT license.\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated\n// documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the\n// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to\n// permit persons to whom the Software is furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the\n// Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE\n// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\n// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n// ---------------------------------------------------------------------------------------------------------------------\nimport chalk from \"chalk\";\nimport { g_config } from \"./config\";\nimport { warningLog } from \"./debug\";\n\n// istanbul ignore next\nexport function displayChapter(str: string) {\n const l = \" \";\n warningLog(`${chalk.bgWhite(l)} `);\n str = ` ${str}${l}`.substring(0, l.length);\n warningLog(chalk.bgWhite.cyan(str));\n warningLog(`${chalk.bgWhite(l)} `);\n}\n\nexport function displayTitle(str: string) {\n // istanbul ignore next\n if (!g_config.silent) {\n warningLog(\"\");\n warningLog(chalk.yellowBright(str));\n warningLog(chalk.yellow(new Array(str.length + 1).join(\"=\")), \"\\n\");\n }\n}\n\nexport function displaySubtitle(str: string) {\n // istanbul ignore next\n if (!g_config.silent) {\n warningLog(\"\");\n warningLog(` ${chalk.yellowBright(str)}`);\n warningLog(` ${chalk.white(new Array(str.length + 1).join(\"-\"))}`, \"\\n\");\n }\n}\nexport function display(str: string) {\n // istanbul ignore next\n if (!g_config.silent) {\n warningLog(` ${str}`);\n }\n}\n","// ---------------------------------------------------------------------------------------------------------------------\n// node-opcua-pki\n// ---------------------------------------------------------------------------------------------------------------------\n// Copyright (c) 2022-2026 Sterfive.com\n// ---------------------------------------------------------------------------------------------------------------------\n//\n// This project is licensed under the terms of the MIT license.\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated\n// documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the\n// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to\n// permit persons to whom the Software is furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the\n// Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE\n// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\n// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n// ---------------------------------------------------------------------------------------------------------------------\nimport assert from \"node:assert\";\nimport fs from \"node:fs\";\nimport { createCertificateSigningRequest, pemToPrivateKey, Subject } from \"node-opcua-crypto\";\nimport type { CreateCertificateSigningRequestWithConfigOptions } from \"../common\";\nimport { display, displaySubtitle } from \"../display\";\n\n/**\n * create a certificate signing request\n */\nexport async function createCertificateSigningRequestAsync(\n certificateSigningRequestFilename: string,\n params: CreateCertificateSigningRequestWithConfigOptions\n): Promise<void> {\n assert(params);\n assert(params.rootDir);\n assert(params.configFile);\n assert(params.privateKey);\n assert(typeof params.privateKey === \"string\");\n assert(fs.existsSync(params.privateKey), `Private key must exist${params.privateKey}`);\n\n // assert(fs.existsSync(params.configFile), \"config file must exist \" + params.configFile);\n assert(fs.existsSync(params.rootDir), \"RootDir key must exist\");\n assert(typeof certificateSigningRequestFilename === \"string\");\n\n const subject = params.subject ? new Subject(params.subject).toString() : undefined;\n displaySubtitle(\"- Creating a Certificate Signing Request with subtile\");\n\n const privateKeyPem = await fs.promises.readFile(params.privateKey, \"utf-8\");\n const privateKey = await pemToPrivateKey(privateKeyPem);\n\n const { csr } = await createCertificateSigningRequest({\n privateKey,\n dns: params.dns,\n ip: params.ip,\n subject,\n applicationUri: params.applicationUri,\n purpose: params.purpose\n });\n await fs.promises.writeFile(certificateSigningRequestFilename, csr, \"utf-8\");\n\n display(`- privateKey ${params.privateKey}`);\n display(`- certificateSigningRequestFilename ${certificateSigningRequestFilename}`);\n\n // to verify that the CSR is correct:\n // openssl req -in ./tmp/without_openssl.csr -noout -verify\n}\n","// ---------------------------------------------------------------------------------------------------------------------\n// node-opcua-pki\n// ---------------------------------------------------------------------------------------------------------------------\n// Copyright (c) 2014-2026 - Etienne Rossignon - etienne.rossignon (at) gadz.org\n// Copyright (c) 2022-2026 - Sterfive.com\n// ---------------------------------------------------------------------------------------------------------------------\n//\n// This project is licensed under the terms of the MIT license.\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated\n// documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the\n// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to\n// permit persons to whom the Software is furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the\n// Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE\n// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\n// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n// ---------------------------------------------------------------------------------------------------------------------\nimport assert from \"node:assert\";\n\n/** RSA key size in bits. */\nexport type KeySize = 1024 | 2048 | 3072 | 4096;\n/** Hex-encoded SHA-1 certificate thumbprint. */\nexport type Thumbprint = string;\n/** A filesystem path to a file. */\nexport type Filename = string;\n/** Status of a certificate in the trust store. */\nexport type CertificateStatus = \"unknown\" | \"trusted\" | \"rejected\";\n\nimport type { CertificatePurpose } from \"node-opcua-crypto\";\nimport type { SubjectOptions } from \"../misc/subject\";\n\n/**\n * @deprecated Use {@link KeySize} instead.\n */\nexport type KeyLength = 1024 | 2048 | 3072 | 4096;\n\nexport function quote(str?: string): string {\n return `\"${str || \"\"}\"`;\n}\n\n/**\n * Subject Alternative Name (SAN) parameters for certificate\n * generation.\n */\nexport interface ProcessAltNamesParam {\n /** DNS host names to include in the SAN extension. */\n dns?: string[];\n /** IP addresses to include in the SAN extension. */\n ip?: string[];\n /** OPC UA application URI for the SAN extension. */\n applicationUri?: string;\n}\n\n/**\n * Options for creating a Certificate Signing Request (CSR).\n */\nexport interface CreateCertificateSigningRequestOptions extends ProcessAltNamesParam {\n /** X.500 subject for the certificate. */\n subject?: SubjectOptions | string;\n}\n\n/**\n * Extended CSR options that include filesystem paths and\n * certificate purpose — used internally by the OpenSSL toolbox.\n */\nexport interface CreateCertificateSigningRequestWithConfigOptions extends CreateCertificateSigningRequestOptions {\n /** Root directory of the PKI store. */\n rootDir: Filename;\n /** Path to the OpenSSL configuration file. */\n configFile: Filename;\n /** Path to the private key file. */\n privateKey: Filename;\n /** Intended purpose of the certificate. */\n purpose: CertificatePurpose;\n}\n\n/**\n * Validity period parameters for certificate generation.\n */\nexport interface StartDateEndDateParam {\n /** Certificate \"Not Before\" date. Defaults to now. */\n startDate?: Date;\n /** Certificate \"Not After\" date (computed from validity). */\n endDate?: Date;\n /** Number of days the certificate is valid. @defaultValue 365 */\n validity?: number;\n /**\n * Certificate validity in milliseconds.\n *\n * When provided, takes precedence over {@link validity} and enables\n * sub-day validity (X.509 supports second precision per RFC 5280\n * §4.1.2.5; OpenSSL is invoked with `-startdate`/`-enddate` already).\n *\n * Typical use is short-lived certificates for demos or for renewal\n * cycle testing. Existing day-based callers are unaffected.\n */\n validityMs?: number;\n}\n\n/**\n * Parameters for creating a self-signed certificate.\n */\nexport interface CreateSelfSignCertificateParam extends ProcessAltNamesParam, StartDateEndDateParam {\n /** X.500 subject for the certificate. */\n subject?: SubjectOptions | string;\n}\n\n/**\n * Extended self-signed certificate options that include\n * filesystem paths and purpose — used internally.\n */\nexport interface CreateSelfSignCertificateWithConfigParam extends CreateSelfSignCertificateParam {\n /** Root directory of the PKI store. */\n rootDir: Filename;\n /** Path to the OpenSSL configuration file. */\n configFile: Filename;\n /** Path to the private key file. */\n privateKey: Filename;\n /** Intended purpose of the certificate. */\n purpose: CertificatePurpose;\n}\n\n/**\n * General-purpose parameters passed to CA operations such as\n * {@link CertificateAuthority.signCertificateRequest} and\n * {@link CertificateAuthority.revokeCertificate}.\n */\nexport interface Params extends ProcessAltNamesParam, StartDateEndDateParam {\n /** X.500 subject for the certificate. */\n subject?: SubjectOptions | string;\n\n /** Path to the private key file. */\n privateKey?: string;\n /** Path to the OpenSSL configuration file. */\n configFile?: string;\n /** Root directory of the PKI store. */\n rootDir?: string;\n\n /** Output filename for the generated certificate. */\n outputFile?: string;\n /** CRL revocation reason (e.g. `\"keyCompromise\"`). */\n reason?: string;\n}\n\nexport function adjustDate(params: StartDateEndDateParam) {\n assert(params instanceof Object);\n params.startDate = params.startDate || new Date();\n assert(params.startDate instanceof Date);\n\n // Precedence: validityMs > validity (days) > default 365 days.\n // When validityMs is set, compute endDate via millisecond arithmetic\n // (sub-day capable). Otherwise preserve the legacy calendar-day\n // arithmetic via setDate() so day-based callers remain\n // bit-for-bit identical (including DST boundary handling).\n if (params.validityMs !== undefined) {\n if (params.validityMs <= 0) {\n throw new RangeError(`validityMs must be > 0 (got ${params.validityMs})`);\n }\n params.endDate = new Date(params.startDate.getTime() + params.validityMs);\n // Keep params.validity in sync as the ceil-rounded day count, so\n // downstream code paths that read params.validity (e.g. logging,\n // OpenSSL command echo) still see a sensible value.\n params.validity = Math.ceil(params.validityMs / 86_400_000);\n } else {\n params.validity = params.validity || 365; // one year\n params.endDate = new Date(params.startDate.getTime());\n params.endDate.setDate(params.startDate.getDate() + params.validity);\n }\n\n // params.endDate = x509Date(endDate);\n // params.startDate = x509Date(startDate);\n\n assert(params.endDate instanceof Date);\n assert(params.startDate instanceof Date);\n\n // // istanbul ignore next\n // if (!g_config.silent) {\n // warningLog(\" start Date \", params.startDate.toUTCString(), x509Date(params.startDate));\n // warningLog(\" end Date \", params.endDate.toUTCString(), x509Date(params.endDate));\n // }\n}\n\nexport function adjustApplicationUri(params: Params) {\n const applicationUri = params.applicationUri || \"\";\n if (applicationUri.length > 200) {\n throw new Error(`Openssl doesn't support urn with length greater than 200${applicationUri}`);\n }\n}\n","// ---------------------------------------------------------------------------------------------------------------------\n// node-opcua-pki\n// ---------------------------------------------------------------------------------------------------------------------\n// Copyright (c) 2022-2026 Sterfive.com\n// ---------------------------------------------------------------------------------------------------------------------\n//\n// This project is licensed under the terms of the MIT license.\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated\n// documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the\n// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to\n// permit persons to whom the Software is furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the\n// Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE\n// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\n// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n// ---------------------------------------------------------------------------------------------------------------------\nimport assert from \"node:assert\";\nimport fs from \"node:fs\";\n\nimport {\n CertificatePurpose,\n createSelfSignedCertificate as createSelfSignedCertificate1,\n pemToPrivateKey,\n Subject\n} from \"node-opcua-crypto\";\nimport { adjustDate, type CreateSelfSignCertificateWithConfigParam } from \"../common\";\nimport { displayTitle } from \"../display\";\n\nexport async function createSelfSignedCertificateAsync(\n certificate: string,\n params: CreateSelfSignCertificateWithConfigParam\n): Promise<void> {\n params.purpose = params.purpose || CertificatePurpose.ForApplication;\n assert(params.purpose, \"Please provide a Certificate Purpose\");\n /**\n * note: due to a limitation of openssl ,\n * it is not possible to control the startDate of the certificate validity\n * to achieve this the certificateAuthority tool shall be used.\n */\n assert(fs.existsSync(params.configFile));\n assert(fs.existsSync(params.rootDir));\n assert(fs.existsSync(params.privateKey));\n if (!params.subject) {\n throw Error(\"Missing subject\");\n }\n\n assert(typeof params.applicationUri === \"string\");\n assert(Array.isArray(params.dns));\n\n // xx no key size in self-signed assert(params.keySize == 2048 || params.keySize == 4096);\n\n // processAltNames(params);\n adjustDate(params);\n assert(Object.prototype.hasOwnProperty.call(params, \"validity\"));\n\n let subject: Subject | string = new Subject(params.subject);\n subject = subject.toString();\n\n // xx const certificateRequestFilename = certificate + \".csr\";\n const purpose = params.purpose;\n\n displayTitle(\"Generate a certificate request\");\n\n const privateKeyPem = await fs.promises.readFile(params.privateKey, \"utf-8\");\n const privateKey = await pemToPrivateKey(privateKeyPem);\n\n const { cert } = await createSelfSignedCertificate1({\n privateKey,\n notBefore: params.startDate,\n notAfter: params.endDate,\n validity: params.validity,\n dns: params.dns,\n ip: params.ip,\n subject,\n applicationUri: params.applicationUri,\n purpose\n });\n await fs.promises.writeFile(certificate, cert, \"utf-8\");\n}\n\nexport async function createSelfSignedCertificate(\n certificate: string,\n params: CreateSelfSignCertificateWithConfigParam\n): Promise<void> {\n await createSelfSignedCertificateAsync(certificate, params);\n}\n","// ---------------------------------------------------------------------------------------------------------------------\n// node-opcua-pki\n// ---------------------------------------------------------------------------------------------------------------------\n// Copyright (c) 2014-2026 - Etienne Rossignon - etienne.rossignon (at) gadz.org\n// Copyright (c) 2022-2026 - Sterfive.com\n// ---------------------------------------------------------------------------------------------------------------------\n//\n// This project is licensed under the terms of the MIT license.\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated\n// documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the\n// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to\n// permit persons to whom the Software is furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the\n// Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE\n// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\n// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR\n// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./create_certificate_signing_request\";\nexport * from \"./create_self_signed_certificate\";\n","const config =\n \"##################################################################################################\\n\" +\n \"## SIMPLE OPENSSL CONFIG FILE FOR SELF-SIGNED CERTIFICATE GENERATION\\n\" +\n \"################################################################################################################\\n\" +\n \"\\n\" +\n \"distinguished_name = req_distinguished_name\\n\" +\n \"default_md = sha1\\n\" +\n \"\\n\" +\n \"default_md = sha256 # The default digest algorithm\\n\" +\n \"\\n\" +\n \"[ v3_ca ]\\n\" +\n \"subjectKeyIdentifier = hash\\n\" +\n \"authorityKeyIdentifier = keyid:always,issuer:always\\n\" +\n \"\\n\" +\n \"# authorityKeyIdentifier = keyid\\n\" +\n \"basicConstraints = CA:TRUE\\n\" +\n \"keyUsage = critical, cRLSign, keyCertSign\\n\" +\n 'nsComment = \"Self-signed Certificate for CA generated by Node-OPCUA Certificate utility\"\\n' +\n \"#nsCertType = sslCA, emailCA\\n\" +\n \"#subjectAltName = email:copy\\n\" +\n \"#issuerAltName = issuer:copy\\n\" +\n \"#obj = DER:02:03\\n\" +\n \"# crlDistributionPoints = @crl_info\\n\" +\n \"# [ crl_info ]\\n\" +\n \"# URI.0 = http://localhost:8900/crl.pem\\n\" +\n \"subjectAltName = $ENV::ALTNAME\\n\" +\n \"\\n\" +\n \"[ req ]\\n\" +\n \"days = 390\\n\" +\n \"req_extensions = v3_req\\n\" +\n \"x509_extensions = v3_ca\\n\" +\n \"\\n\" +\n \"[v3_req]\\n\" +\n \"basicConstraints = CA:false\\n\" +\n \"keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment\\n\" +\n \"subjectAltName = $ENV::ALTNAME\\n\" +\n \"\\n\" +\n \"[ v3_ca_signed]\\n\" +\n \"subjectKeyIdentifier = hash\\n\" +\n \"authorityKeyIdentifier = keyid,issuer\\n\" +\n \"basicConstraints = critical, CA:FALSE\\n\" +\n \"keyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment\\n\" +\n \"extendedKeyUsage = clientAuth,serverAuth \\n\" +\n 'nsComment = \"certificate generated by Node-OPCUA Certificate utility and signed by a CA\"\\n' +\n \"subjectAltName = $ENV::ALTNAME\\n\" +\n \"[ v3_selfsigned]\\n\" +\n \"subjectKeyIdentifier = hash\\n\" +\n \"authorityKeyIdentifier = keyid,issuer\\n\" +\n \"basicConstraints = critical, CA:FALSE\\n\" +\n \"keyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment\\n\" +\n \"extendedKeyUsage = clientAuth,serverAuth \\n\" +\n 'nsComment = \"Self-signed certificate generated by Node-OPCUA Certificate utility\"\\n' +\n \"subjectAltName = $ENV::ALTNAME\\n\" +\n \"[ req_distinguished_name ]\\n\" +\n \"countryName = Country Name (2 letter code)\\n\" +\n \"countryName_default = FR\\n\" +\n \"countryName_min = 2\\n\" +\n \"countryName_max = 2\\n\" +\n \"# stateOrProvinceName = State or Province Name (full name)\\n\" +\n \"# stateOrProvinceName_default = Ile de France\\n\" +\n \"# localityName = Locality Name (city, district)\\n\" +\n \"# localityName_default = Paris\\n\" +\n \"organizationName = Organization Name (company)\\n\" +\n \"organizationName_default = NodeOPCUA\\n\" +\n \"# organizationalUnitName = Organizational Unit Name (department, division)\\n\" +\n \"# organizationalUnitName_default = R&D\\n\" +\n \"commonName = Common Name (hostname, FQDN, IP, or your name)\\n\" +\n \"commonName_max = 256\\n\" +\n \"commonName_default = NodeOPCUA\\n\" +\n \"# emailAddress = Email Address\\n\" +\n \"# emailAddress_max = 40\\n\" +\n \"# emailAddress_default = node-opcua (at) node-opcua (dot) com\\n\" +\n \"subjectAltName = $ENV::ALTNAME\";\n\nexport default config;\n","// ---------------------------------------------------------------------------------------------------------------------\n// node-opcua-pki — CertificateManager\n// ---------------------------------------------------------------------------------------------------------------------\n// Copyright (c) 2014-2026 - Etienne Rossignon - etienne.rossignon (at) gadz.org\n// Copyright (c) 2022-2026 - Sterfive.com\n// ---------------------------------------------------------------------------------------------------------------------\n// This project is licensed under the terms of the MIT license.\n// ---------------------------------------------------------------------------------------------------------------------\n\nimport { EventEmitter } from \"node:events\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { drainPendingLocks, withLock } from \"@ster5/global-mutex\";\nimport chalk from \"chalk\";\nimport chokidar, { type FSWatcher as ChokidarFSWatcher } from \"chokidar\";\nimport {\n type Certificate,\n type CertificateInternals,\n type CertificateRevocationList,\n type CertificateRevocationListInfo,\n type DER,\n exploreCertificate,\n exploreCertificateInfo,\n exploreCertificateRevocationList,\n generatePrivateKeyFile,\n makeSHA1Thumbprint,\n readCertificateChain,\n readCertificateChainAsync,\n readCertificateRevocationList,\n split_der,\n toPem,\n verifyCertificateSignature\n} from \"node-opcua-crypto\";\n\nimport type { SubjectOptions } from \"../misc/subject\";\nimport type {\n CertificateStatus,\n CreateSelfSignCertificateParam,\n CreateSelfSignCertificateWithConfigParam,\n Filename,\n KeySize,\n Thumbprint\n} from \"../toolbox/common\";\nimport { makePath, mkdirRecursiveSync } from \"../toolbox/common2\";\nimport { debugLog, warningLog } from \"../toolbox/debug\";\nimport { createCertificateSigningRequestAsync, createSelfSignedCertificate } from \"../toolbox/without_openssl\";\n\nimport _simple_config_template from \"./templates/simple_config_template.cnf\";\n\n/**\n *\n * a minimalist config file for openssl that allows\n * self-signed certificate to be generated.\n *\n */\nconst configurationFileSimpleTemplate: string = _simple_config_template;\nconst fsWriteFile = fs.promises.writeFile;\n\ninterface Entry {\n certificate: Certificate;\n filename: string;\n /** Lazily cached result of `exploreCertificate(certificate)`. */\n info?: CertificateInternals;\n}\n\n/** Return the cached `info` or compute and cache it. */\nfunction getOrComputeInfo(entry: Entry): CertificateInternals {\n if (!entry.info) {\n entry.info = exploreCertificate(entry.certificate);\n }\n return entry.info;\n}\n\ninterface CRLEntry {\n crlInfo: CertificateRevocationListInfo;\n filename: string;\n}\ninterface CRLData {\n serialNumbers: { [key: string]: Date };\n crls: CRLEntry[];\n}\ninterface Thumbs {\n trusted: Map<string, Entry>;\n rejected: Map<string, Entry>;\n issuers: {\n certs: Map<string, Entry>;\n };\n /** key = subjectFingerPrint of issuer certificate */\n crl: Map<string, CRLData>;\n /** key = subjectFingerPrint of issuer certificate */\n issuersCrl: Map<string, CRLData>;\n}\n\n/**\n * Identifies which PKI sub-store a certificate event originated from.\n */\nexport type CertificateStore = \"trusted\" | \"rejected\" | \"issuersCerts\";\n\n/**\n * Identifies which PKI sub-store a CRL event originated from.\n */\nexport type CrlStore = \"crl\" | \"issuersCrl\";\n\n/**\n * Events emitted by {@link CertificateManager} when the\n * file-system watchers detect certificate or CRL changes.\n */\nexport interface CertificateManagerEvents {\n /** A certificate file was added to a store. */\n certificateAdded: (event: { store: CertificateStore; certificate: Certificate; fingerprint: string; filename: string }) => void;\n /** A certificate file was removed from a store. */\n certificateRemoved: (event: { store: CertificateStore; fingerprint: string; filename: string }) => void;\n /** A certificate file was modified in a store. */\n certificateChange: (event: {\n store: CertificateStore;\n certificate: Certificate;\n fingerprint: string;\n filename: string;\n }) => void;\n /** A CRL file was added. */\n crlAdded: (event: { store: CrlStore; filename: string }) => void;\n /** A CRL file was removed. */\n crlRemoved: (event: { store: CrlStore; filename: string }) => void;\n}\n\n/**\n * Options controlling certificate validation in\n * {@link CertificateManager.addTrustedCertificateFromChain}.\n *\n * By default all checks are **strict** (secure). Set individual\n * flags to `true` only in test/development environments.\n */\nexport interface AddCertificateValidationOptions {\n /**\n * Accept certificates whose validity period has expired\n * or is not yet active.\n * @defaultValue false\n */\n acceptExpiredCertificate?: boolean;\n\n /**\n * Accept certificates that have been revoked by their\n * issuer's CRL. When `false` (the default), a revoked\n * certificate is rejected with `BadCertificateRevoked`.\n * @defaultValue false\n */\n acceptRevokedCertificate?: boolean;\n\n /**\n * Do not fail when a CRL is missing for an issuer in the\n * chain. When `false` (the default), a missing CRL causes\n * `BadCertificateRevocationUnknown`.\n * @defaultValue false\n */\n ignoreMissingRevocationList?: boolean;\n\n /**\n * Maximum depth of the certificate chain (leaf + issuers).\n * The leaf certificate counts as depth 1.\n * @defaultValue 5\n */\n maxChainLength?: number;\n}\n\n/**\n * Options for creating a {@link CertificateManager}.\n */\nexport interface CertificateManagerOptions {\n /**\n * RSA key size for generated private keys.\n * @defaultValue 2048\n */\n keySize?: KeySize;\n /** Filesystem path where the PKI directory structure is stored. */\n location: string;\n\n /**\n * Validation options applied by\n * {@link CertificateManager.addTrustedCertificateFromChain}.\n *\n * Defaults are secure — all checks enabled.\n */\n addCertificateValidationOptions?: AddCertificateValidationOptions;\n\n /**\n * When `true`, the CertificateManager will **not** start\n * chokidar file-system watchers on the PKI folders.\n *\n * The initial file-system scan still runs so the in-memory\n * indexes are populated, but live change detection is\n * disabled. This is useful in test / CI environments where\n * many CertificateManager instances are created in parallel\n * and the accumulated `fs.watch` handles exhaust the libuv\n * thread-pool, causing event-loop starvation.\n *\n * @defaultValue false\n */\n disableFileWatchers?: boolean;\n}\n\n/**\n * Parameters for {@link createSelfSignedCertificate}.\n * All fields from {@link CreateSelfSignCertificateParam} are required.\n */\nexport interface CreateSelfSignCertificateParam1 extends CreateSelfSignCertificateParam {\n /**\n * Output path for the certificate.\n * @defaultValue `\"own/certs/self_signed_certificate.pem\"`\n */\n outputFile?: Filename;\n /** X.500 subject for the certificate. */\n subject: SubjectOptions | string;\n /** OPC UA application URI for the SAN extension. */\n applicationUri: string;\n /** DNS host names to include in the SAN extension. */\n dns: string[];\n /** Certificate \"Not Before\" date. */\n startDate: Date;\n /** Number of days the certificate is valid. */\n validity: number;\n}\n\n/**\n * Options to fine-tune certificate verification behaviour.\n * Passed to {@link CertificateManager.verifyCertificate}.\n *\n * Without any options, `verifyCertificate` is **strict**: only\n * certificates that are explicitly present in the trusted store\n * will return {@link VerificationStatus.Good}. Unknown or\n * rejected certificates return\n * {@link VerificationStatus.BadCertificateUntrusted} even when\n * their issuer chain is valid.\n *\n * Set {@link acceptCertificateWithValidIssuerChain} to `true`\n * to accept certificates whose issuer chain validates against\n * a trusted CA — even if the leaf certificate itself is not\n * in the trusted store.\n */\nexport interface VerifyCertificateOptions {\n /** Accept certificates whose \"Not After\" date has passed. */\n acceptOutdatedCertificate?: boolean;\n /** Accept issuer certificates whose \"Not After\" date has passed. */\n acceptOutDatedIssuerCertificate?: boolean;\n /** Do not fail when a CRL is missing for an issuer. */\n ignoreMissingRevocationList?: boolean;\n /** Accept certificates whose \"Not Before\" date is in the future. */\n acceptPendingCertificate?: boolean;\n /**\n * Accept a certificate that is not in the trusted store when\n * its issuer (CA) certificate is trusted, the signature is\n * valid, and the certificate does not appear in the CRL.\n *\n * When `false` (the default), only certificates explicitly\n * placed in the trusted store are accepted — this is the\n * same behaviour as {@link CertificateManager.isCertificateTrusted}.\n *\n * @defaultValue false\n */\n acceptCertificateWithValidIssuerChain?: boolean;\n}\n\n/**\n * OPC UA certificate verification status codes.\n *\n * These mirror the OPC UA `StatusCode` values for certificate\n * validation results.\n */\nexport enum VerificationStatus {\n /** The certificate provided as a parameter is not valid. */\n BadCertificateInvalid = \"BadCertificateInvalid\",\n /** An error occurred verifying security. */\n BadSecurityChecksFailed = \"BadSecurityChecksFailed\",\n /** The certificate does not meet the requirements of the security policy. */\n BadCertificatePolicyCheckFailed = \"BadCertificatePolicyCheckFailed\",\n /** The certificate has expired or is not yet valid. */\n BadCertificateTimeInvalid = \"BadCertificateTimeInvalid\",\n /** An issuer certificate has expired or is not yet valid. */\n BadCertificateIssuerTimeInvalid = \"BadCertificateIssuerTimeInvalid\",\n /** The HostName used to connect to a server does not match a HostName in the certificate. */\n BadCertificateHostNameInvalid = \"BadCertificateHostNameInvalid\",\n /** The URI specified in the ApplicationDescription does not match the URI in the certificate. */\n BadCertificateUriInvalid = \"BadCertificateUriInvalid\",\n /** The certificate may not be used for the requested operation. */\n BadCertificateUseNotAllowed = \"BadCertificateUseNotAllowed\",\n /** The issuer certificate may not be used for the requested operation. */\n BadCertificateIssuerUseNotAllowed = \"BadCertificateIssuerUseNotAllowed\",\n /** The certificate is not trusted.