node-opcua-pki 6.20.0 → 6.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/pki.mjs +1667 -965
- package/dist/bin/pki.mjs.map +1 -1
- package/dist/chunk-2HTNH73K.mjs +1913 -0
- package/dist/chunk-2HTNH73K.mjs.map +1 -0
- package/dist/core-C6faZClF.d.mts +1032 -0
- package/dist/core-C6faZClF.d.ts +1032 -0
- package/dist/core.d.mts +2 -0
- package/dist/core.d.ts +2 -0
- package/dist/core.js +1902 -0
- package/dist/core.js.map +1 -0
- package/dist/core.mjs +21 -0
- package/dist/core.mjs.map +1 -0
- package/dist/index.d.mts +91 -832
- package/dist/index.d.ts +91 -832
- package/dist/index.js +1812 -1112
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +787 -1925
- package/dist/index.mjs.map +1 -1
- package/package.json +14 -3
package/dist/bin/pki.mjs
CHANGED
|
@@ -519,7 +519,31 @@ var init_certificate_manager = __esm({
|
|
|
519
519
|
// even if the consumer forgets to call dispose().
|
|
520
520
|
static #activeInstances = /* @__PURE__ */ new Set();
|
|
521
521
|
static #cleanupInstalled = false;
|
|
522
|
-
static #
|
|
522
|
+
static #exitHandler;
|
|
523
|
+
/**
|
|
524
|
+
* Install a best-effort `exit` hook that closes any watcher
|
|
525
|
+
* still open when the process terminates.
|
|
526
|
+
*
|
|
527
|
+
* **This library never terminates the host process.** No
|
|
528
|
+
* SIGINT/SIGTERM handler is installed: deciding how (and
|
|
529
|
+
* whether) to shut down on a signal is the application's
|
|
530
|
+
* responsibility, and a listener registered here would both
|
|
531
|
+
* pre-empt the application's own graceful shutdown and
|
|
532
|
+
* silently suppress Node's default signal behaviour.
|
|
533
|
+
*
|
|
534
|
+
* Nothing here is load-bearing for process exit. The native
|
|
535
|
+
* `fs.watch` handles are `unref()`'d when the watchers are
|
|
536
|
+
* created (see `#readCertificates`), so an undisposed
|
|
537
|
+
* CertificateManager never keeps the event loop alive. This
|
|
538
|
+
* hook is only tidiness on the way out.
|
|
539
|
+
*
|
|
540
|
+
* `exit` rather than `beforeExit`: `beforeExit` fires when
|
|
541
|
+
* the loop merely drains and the loop can subsequently be
|
|
542
|
+
* resurrected, which would leave a still-in-use instance
|
|
543
|
+
* marked Disposed. `exit` is terminal, synchronous-only and
|
|
544
|
+
* cannot alter the exit code.
|
|
545
|
+
*/
|
|
546
|
+
static #installExitCleanup() {
|
|
523
547
|
if (_CertificateManager.#cleanupInstalled) return;
|
|
524
548
|
_CertificateManager.#cleanupInstalled = true;
|
|
525
549
|
const closeDanglingWatchers = () => {
|
|
@@ -535,13 +559,22 @@ var init_certificate_manager = __esm({
|
|
|
535
559
|
}
|
|
536
560
|
_CertificateManager.#activeInstances.clear();
|
|
537
561
|
};
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
562
|
+
_CertificateManager.#exitHandler = closeDanglingWatchers;
|
|
563
|
+
process.on("exit", closeDanglingWatchers);
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Remove the `exit` hook once the last instance is disposed,
|
|
567
|
+
* so a library that is initialized and disposed repeatedly
|
|
568
|
+
* does not accumulate process listeners. A later
|
|
569
|
+
* `initialize()` re-arms it.
|
|
570
|
+
*/
|
|
571
|
+
static #uninstallExitCleanupIfIdle() {
|
|
572
|
+
if (_CertificateManager.#activeInstances.size > 0) return;
|
|
573
|
+
if (_CertificateManager.#exitHandler) {
|
|
574
|
+
process.removeListener("exit", _CertificateManager.#exitHandler);
|
|
575
|
+
_CertificateManager.#exitHandler = void 0;
|
|
544
576
|
}
|
|
577
|
+
_CertificateManager.#cleanupInstalled = false;
|
|
545
578
|
}
|
|
546
579
|
/**
|
|
547
580
|
* Dispose **all** active CertificateManager instances,
|
|
@@ -966,23 +999,19 @@ var init_certificate_manager = __esm({
|
|
|
966
999
|
return "BadCertificateUntrusted" /* BadCertificateUntrusted */;
|
|
967
1000
|
}
|
|
968
1001
|
}
|
|
969
|
-
const
|
|
970
|
-
debugLog("chain[1] info=", _c2);
|
|
971
|
-
const certificateInfo = exploreCertificateInfo(chain[0]);
|
|
1002
|
+
const { validity } = exploreCertificate(chain[0]).tbsCertificate;
|
|
972
1003
|
const now = /* @__PURE__ */ new Date();
|
|
973
1004
|
let isTimeInvalid = false;
|
|
974
|
-
if (
|
|
1005
|
+
if (validity.notBefore.getTime() > now.getTime()) {
|
|
975
1006
|
debugLog(
|
|
976
|
-
`${chalk3.red("certificate is invalid : certificate is not active yet !")} not before date =${
|
|
1007
|
+
`${chalk3.red("certificate is invalid : certificate is not active yet !")} not before date =${validity.notBefore}`
|
|
977
1008
|
);
|
|
978
1009
|
if (!options.acceptPendingCertificate) {
|
|
979
1010
|
isTimeInvalid = true;
|
|
980
1011
|
}
|
|
981
1012
|
}
|
|
982
|
-
if (
|
|
983
|
-
debugLog(
|
|
984
|
-
`${chalk3.red("certificate is invalid : certificate has expired !")} not after date =${certificateInfo.notAfter}`
|
|
985
|
-
);
|
|
1013
|
+
if (validity.notAfter.getTime() <= now.getTime()) {
|
|
1014
|
+
debugLog(`${chalk3.red("certificate is invalid : certificate has expired !")} not after date =${validity.notAfter}`);
|
|
986
1015
|
if (!options.acceptOutdatedCertificate) {
|
|
987
1016
|
isTimeInvalid = true;
|
|
988
1017
|
}
|
|
@@ -1021,7 +1050,7 @@ var init_certificate_manager = __esm({
|
|
|
1021
1050
|
const chain = coerceCertificateChain(certificate);
|
|
1022
1051
|
for (const element of chain) {
|
|
1023
1052
|
try {
|
|
1024
|
-
|
|
1053
|
+
exploreCertificate(element);
|
|
1025
1054
|
} catch (_err) {
|
|
1026
1055
|
return "BadCertificateInvalid" /* BadCertificateInvalid */;
|
|
1027
1056
|
}
|
|
@@ -1074,7 +1103,7 @@ var init_certificate_manager = __esm({
|
|
|
1074
1103
|
this.#initializingPromise = void 0;
|
|
1075
1104
|
this.state = 2 /* Initialized */;
|
|
1076
1105
|
_CertificateManager.#activeInstances.add(this);
|
|
1077
|
-
_CertificateManager.#
|
|
1106
|
+
_CertificateManager.#installExitCleanup();
|
|
1078
1107
|
}
|
|
1079
1108
|
async #initialize() {
|
|
1080
1109
|
const pkiDir = this.#location;
|
|
@@ -1161,6 +1190,7 @@ var init_certificate_manager = __esm({
|
|
|
1161
1190
|
this.#cachedPrivateKey = void 0;
|
|
1162
1191
|
this.#privateKeyPromise = void 0;
|
|
1163
1192
|
_CertificateManager.#activeInstances.delete(this);
|
|
1193
|
+
_CertificateManager.#uninstallExitCleanupIfIdle();
|
|
1164
1194
|
}
|
|
1165
1195
|
}
|
|
1166
1196
|
/**
|
|
@@ -1509,17 +1539,12 @@ var init_certificate_manager = __esm({
|
|
|
1509
1539
|
return "BadSecurityChecksFailed" /* BadSecurityChecksFailed */;
|
|
1510
1540
|
}
|
|
1511
1541
|
if (!opts.acceptExpiredCertificate) {
|
|
1512
|
-
|
|
1513
|
-
try {
|
|
1514
|
-
certDetails = exploreCertificateInfo(currentCert);
|
|
1515
|
-
} catch (_err) {
|
|
1516
|
-
return "BadCertificateInvalid" /* BadCertificateInvalid */;
|
|
1517
|
-
}
|
|
1542
|
+
const { validity } = currentInfo.tbsCertificate;
|
|
1518
1543
|
const now = /* @__PURE__ */ new Date();
|
|
1519
|
-
if (
|
|
1544
|
+
if (validity.notBefore.getTime() > now.getTime()) {
|
|
1520
1545
|
return "BadCertificateTimeInvalid" /* BadCertificateTimeInvalid */;
|
|
1521
1546
|
}
|
|
1522
|
-
if (
|
|
1547
|
+
if (validity.notAfter.getTime() <= now.getTime()) {
|
|
1523
1548
|
return depth === 1 ? "BadCertificateTimeInvalid" /* BadCertificateTimeInvalid */ : "BadCertificateIssuerTimeInvalid" /* BadCertificateIssuerTimeInvalid */;
|
|
1524
1549
|
}
|
|
1525
1550
|
}
|
|
@@ -1869,6 +1894,7 @@ var init_certificate_manager = __esm({
|
|
|
1869
1894
|
const chokidarOptions = {
|
|
1870
1895
|
usePolling,
|
|
1871
1896
|
...usePolling ? { interval: pollingInterval } : {},
|
|
1897
|
+
depth: 0,
|
|
1872
1898
|
persistent: false
|
|
1873
1899
|
};
|
|
1874
1900
|
const allCapturedHandles = [];
|
|
@@ -2145,21 +2171,17 @@ function getEnvironmentVarNames() {
|
|
|
2145
2171
|
return { key: varName, pattern: `\\$ENV\\:\\:${varName}` };
|
|
2146
2172
|
});
|
|
2147
2173
|
}
|
|
2174
|
+
function buildSubjectAltNameString(params) {
|
|
2175
|
+
return [
|
|
2176
|
+
`URI:${params.applicationUri}`,
|
|
2177
|
+
...(params.dns ?? []).map((d) => `DNS:${d}`),
|
|
2178
|
+
...(params.ip ?? []).map((d) => `IP:${d}`)
|
|
2179
|
+
].join(", ");
|
|
2180
|
+
}
|
|
2148
2181
|
function processAltNames(params) {
|
|
2149
2182
|
params.dns = params.dns || [];
|
|
2150
2183
|
params.ip = params.ip || [];
|
|
2151
|
-
|
|
2152
|
-
subjectAltName.push(`URI:${params.applicationUri}`);
|
|
2153
|
-
subjectAltName = [].concat(
|
|
2154
|
-
subjectAltName,
|
|
2155
|
-
params.dns.map((d) => `DNS:${d}`)
|
|
2156
|
-
);
|
|
2157
|
-
subjectAltName = [].concat(
|
|
2158
|
-
subjectAltName,
|
|
2159
|
-
params.ip.map((d) => `IP:${d}`)
|
|
2160
|
-
);
|
|
2161
|
-
const subjectAltNameString = subjectAltName.join(", ");
|
|
2162
|
-
setEnv("ALTNAME", subjectAltNameString);
|
|
2184
|
+
setEnv("ALTNAME", buildSubjectAltNameString(params));
|
|
2163
2185
|
}
|
|
2164
2186
|
var SAFE_ENV_PASSTHROUGH, exportedEnvVars;
|
|
2165
2187
|
var init_env = __esm({
|
|
@@ -2511,9 +2533,6 @@ import chalk5 from "chalk";
|
|
|
2511
2533
|
function passinArg(passphrase = "") {
|
|
2512
2534
|
return { args: ["-passin", `env:${PASSIN_ENV_VAR}`], env: { [PASSIN_ENV_VAR]: passphrase } };
|
|
2513
2535
|
}
|
|
2514
|
-
function passoutArg(passphrase = "") {
|
|
2515
|
-
return { args: ["-passout", `env:${PASSOUT_ENV_VAR}`], env: { [PASSOUT_ENV_VAR]: passphrase } };
|
|
2516
|
-
}
|
|
2517
2536
|
function renderForDisplay(file, args) {
|
|
2518
2537
|
return [file, ...args].map((a) => a === "" || /[\s"'`$\\]/.test(a) ? JSON.stringify(a) : a).join(" ");
|
|
2519
2538
|
}
|
|
@@ -2534,7 +2553,7 @@ async function execute2(file, args, options) {
|
|
|
2534
2553
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2535
2554
|
});
|
|
2536
2555
|
const fail = (message) => {
|
|
2537
|
-
if (!options.hideErrorMessage) {
|
|
2556
|
+
if (!options.hideErrorMessage && !g_config.silent) {
|
|
2538
2557
|
const fence = "###########################################";
|
|
2539
2558
|
console.error(chalk5.bgWhiteBright.redBright(`${fence} OPENSSL ERROR ${fence}`));
|
|
2540
2559
|
console.error(chalk5.bgWhiteBright.redBright(`CWD = ${options.cwd}`));
|
|
@@ -2628,7 +2647,7 @@ async function execute_openssl(args, options) {
|
|
|
2628
2647
|
await ensure_openssl_installed();
|
|
2629
2648
|
return await execute2(opensslPath, args, options);
|
|
2630
2649
|
}
|
|
2631
|
-
var opensslPath, n, PASSIN_ENV_VAR
|
|
2650
|
+
var opensslPath, n, PASSIN_ENV_VAR;
|
|
2632
2651
|
var init_execute_openssl = __esm({
|
|
2633
2652
|
"packages/node-opcua-pki/lib/toolbox/with_openssl/execute_openssl.ts"() {
|
|
2634
2653
|
"use strict";
|
|
@@ -2640,7 +2659,6 @@ var init_execute_openssl = __esm({
|
|
|
2640
2659
|
init_install_prerequisite();
|
|
2641
2660
|
n = makePath;
|
|
2642
2661
|
PASSIN_ENV_VAR = "NODE_OPCUA_PKI_OPENSSL_PASSIN";
|
|
2643
|
-
PASSOUT_ENV_VAR = "NODE_OPCUA_PKI_OPENSSL_PASSOUT";
|
|
2644
2662
|
}
|
|
2645
2663
|
});
|
|
2646
2664
|
|
|
@@ -2656,19 +2674,27 @@ function openssl_require2DigitYearInDate() {
|
|
|
2656
2674
|
}
|
|
2657
2675
|
return g_config.opensslVersion.match(/OpenSSL 0\.9/);
|
|
2658
2676
|
}
|
|
2659
|
-
function stripConditionalBlocks(template) {
|
|
2677
|
+
function stripConditionalBlocks(template, envOverrides) {
|
|
2660
2678
|
return template.replace(/\{\{#([A-Z_][A-Z0-9_]*)\}\}([\s\S]*?)\{\{\/\1\}\}\r?\n?/g, (_match, key, content) => {
|
|
2661
|
-
const keep = hasEnv(key) && getEnv(key) !== "";
|
|
2679
|
+
const keep = envOverrides && Object.prototype.hasOwnProperty.call(envOverrides, key) ? envOverrides[key] !== "" : hasEnv(key) && getEnv(key) !== "";
|
|
2662
2680
|
return keep ? content : "";
|
|
2663
2681
|
});
|
|
2664
2682
|
}
|
|
2665
|
-
function generateStaticConfig(configPath, options) {
|
|
2683
|
+
function generateStaticConfig(configPath, options, envOverrides) {
|
|
2666
2684
|
const prePath = options?.cwd || "";
|
|
2667
2685
|
const originalFilename = !path4.isAbsolute(configPath) ? path4.join(prePath, configPath) : configPath;
|
|
2668
2686
|
let staticConfig = fs7.readFileSync(originalFilename, { encoding: "utf8" });
|
|
2669
|
-
staticConfig = stripConditionalBlocks(staticConfig);
|
|
2687
|
+
staticConfig = stripConditionalBlocks(staticConfig, envOverrides);
|
|
2670
2688
|
for (const envVar of getEnvironmentVarNames()) {
|
|
2671
|
-
|
|
2689
|
+
if (envOverrides && Object.prototype.hasOwnProperty.call(envOverrides, envVar.key)) {
|
|
2690
|
+
continue;
|
|
2691
|
+
}
|
|
2692
|
+
staticConfig = staticConfig.replace(new RegExp(envVar.pattern, "gi"), () => getEnv(envVar.key));
|
|
2693
|
+
}
|
|
2694
|
+
if (envOverrides) {
|
|
2695
|
+
for (const [key, value] of Object.entries(envOverrides)) {
|
|
2696
|
+
staticConfig = staticConfig.replace(new RegExp(`\\$ENV\\:\\:${key}`, "gi"), () => value);
|
|
2697
|
+
}
|
|
2672
2698
|
}
|
|
2673
2699
|
const staticConfigPath = `${configPath}.${process.pid}-${_counter++}.tmp`;
|
|
2674
2700
|
const temporaryConfigPath = !path4.isAbsolute(configPath) ? path4.join(prePath, staticConfigPath) : staticConfigPath;
|
|
@@ -2679,6 +2705,9 @@ function generateStaticConfig(configPath, options) {
|
|
|
2679
2705
|
return temporaryConfigPath;
|
|
2680
2706
|
}
|
|
2681
2707
|
}
|
|
2708
|
+
async function cleanupStaticConfig(configFile, options) {
|
|
2709
|
+
await fs7.promises.rm(path4.resolve(options?.cwd ?? "", configFile), { force: true });
|
|
2710
|
+
}
|
|
2682
2711
|
async function getPublicKeyFromPrivateKey(privateKeyFilename, publicKeyFilename, passphrase) {
|
|
2683
2712
|
assert7(fs7.existsSync(privateKeyFilename));
|
|
2684
2713
|
const passin = passinArg(passphrase);
|
|
@@ -2747,26 +2776,30 @@ async function createCertificateSigningRequestWithOpenSSL(certificateSigningRequ
|
|
|
2747
2776
|
assert8(typeof certificateSigningRequestFilename === "string");
|
|
2748
2777
|
processAltNames(params);
|
|
2749
2778
|
const configFile = generateStaticConfig(params.configFile, { cwd: params.rootDir });
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2779
|
+
try {
|
|
2780
|
+
const options = { cwd: params.rootDir, openssl_conf: path5.relative(params.rootDir, configFile) };
|
|
2781
|
+
const subject = params.subject ? new Subject3(params.subject).toString() : void 0;
|
|
2782
|
+
displaySubtitle("- Creating a Certificate Signing Request with openssl");
|
|
2783
|
+
await execute_openssl(
|
|
2784
|
+
[
|
|
2785
|
+
"req",
|
|
2786
|
+
"-new",
|
|
2787
|
+
"-sha256",
|
|
2788
|
+
"-batch",
|
|
2789
|
+
"-text",
|
|
2790
|
+
"-config",
|
|
2791
|
+
n3(configFile),
|
|
2792
|
+
"-key",
|
|
2793
|
+
n3(params.privateKey),
|
|
2794
|
+
...subject ? ["-subj", subject] : [],
|
|
2795
|
+
"-out",
|
|
2796
|
+
n3(certificateSigningRequestFilename)
|
|
2797
|
+
],
|
|
2798
|
+
options
|
|
2799
|
+
);
|
|
2800
|
+
} finally {
|
|
2801
|
+
await cleanupStaticConfig(configFile, { cwd: params.rootDir });
|
|
2802
|
+
}
|
|
2770
2803
|
}
|
|
2771
2804
|
var n3;
|
|
2772
2805
|
var init_create_certificate_signing_request2 = __esm({
|
|
@@ -2797,363 +2830,10 @@ var init_with_openssl = __esm({
|
|
|
2797
2830
|
}
|
|
2798
2831
|
});
|
|
2799
2832
|
|
|
2800
|
-
// packages/node-opcua-pki/lib/
|
|
2801
|
-
import assert9 from "assert";
|
|
2833
|
+
// packages/node-opcua-pki/lib/ca/core/ca_database.ts
|
|
2802
2834
|
import fs9 from "fs";
|
|
2803
|
-
async function createPFX(options) {
|
|
2804
|
-
const { certificateFile, privateKeyFile, privateKeyPassphrase, outputFile, passphrase = "", caCertificateFiles } = options;
|
|
2805
|
-
assert9(fs9.existsSync(certificateFile), `Certificate file does not exist: ${certificateFile}`);
|
|
2806
|
-
assert9(fs9.existsSync(privateKeyFile), `Private key file does not exist: ${privateKeyFile}`);
|
|
2807
|
-
const args = ["pkcs12", "-export", "-in", n4(certificateFile), "-inkey", n4(privateKeyFile)];
|
|
2808
|
-
if (caCertificateFiles) {
|
|
2809
|
-
for (const caFile of caCertificateFiles) {
|
|
2810
|
-
assert9(fs9.existsSync(caFile), `CA certificate file does not exist: ${caFile}`);
|
|
2811
|
-
args.push("-certfile", n4(caFile));
|
|
2812
|
-
}
|
|
2813
|
-
}
|
|
2814
|
-
const passin = passinArg(privateKeyPassphrase);
|
|
2815
|
-
const passout = passoutArg(passphrase);
|
|
2816
|
-
args.push("-out", n4(outputFile), ...passin.args, ...passout.args);
|
|
2817
|
-
await execute_openssl(args, { env: { ...passin.env, ...passout.env } });
|
|
2818
|
-
}
|
|
2819
|
-
var n4;
|
|
2820
|
-
var init_toolbox_pfx = __esm({
|
|
2821
|
-
"packages/node-opcua-pki/lib/pki/toolbox_pfx.ts"() {
|
|
2822
|
-
"use strict";
|
|
2823
|
-
init_esm_shims();
|
|
2824
|
-
init_common2();
|
|
2825
|
-
init_execute_openssl();
|
|
2826
|
-
n4 = makePath;
|
|
2827
|
-
}
|
|
2828
|
-
});
|
|
2829
|
-
|
|
2830
|
-
// packages/node-opcua-pki/lib/ca/templates/ca_config_template.cnf.ts
|
|
2831
|
-
var config2, ca_config_template_cnf_default;
|
|
2832
|
-
var init_ca_config_template_cnf = __esm({
|
|
2833
|
-
"packages/node-opcua-pki/lib/ca/templates/ca_config_template.cnf.ts"() {
|
|
2834
|
-
"use strict";
|
|
2835
|
-
init_esm_shims();
|
|
2836
|
-
config2 = `#.........DO NOT MODIFY BY HAND .........................
|
|
2837
|
-
[ ca ]
|
|
2838
|
-
default_ca = CA_default
|
|
2839
|
-
[ CA_default ]
|
|
2840
|
-
dir = "%%ROOT_FOLDER%%" # the main CA folder (quoted: see renderCaConfig)
|
|
2841
|
-
certs = $dir/certs # where to store certificates
|
|
2842
|
-
new_certs_dir = $dir/certs #
|
|
2843
|
-
database = $dir/index.txt # the certificate database
|
|
2844
|
-
serial = $dir/serial # the serial number counter
|
|
2845
|
-
certificate = $dir/public/cacert.pem # The root CA certificate
|
|
2846
|
-
private_key = $dir/private/cakey.pem # the CA private key
|
|
2847
|
-
x509_extensions = usr_cert #
|
|
2848
|
-
default_days = 3650 # default validity : 10 years
|
|
2849
|
-
|
|
2850
|
-
# default_md = sha1
|
|
2851
|
-
|
|
2852
|
-
default_md = sha256 # The default digest algorithm
|
|
2853
|
-
|
|
2854
|
-
preserve = no
|
|
2855
|
-
policy = policy_match
|
|
2856
|
-
# randfile = $dir/random.rnd
|
|
2857
|
-
# default_startdate = YYMMDDHHMMSSZ
|
|
2858
|
-
# default_enddate = YYMMDDHHMMSSZ
|
|
2859
|
-
crl_dir = $dir/crl
|
|
2860
|
-
crl_extensions = crl_ext
|
|
2861
|
-
crl = $dir/revocation_list.crl # the Revocation list
|
|
2862
|
-
crlnumber = $dir/crlnumber # CRL number file
|
|
2863
|
-
default_crl_days = 30
|
|
2864
|
-
default_crl_hours = 24
|
|
2865
|
-
#msie_hack
|
|
2866
|
-
|
|
2867
|
-
[ policy_match ]
|
|
2868
|
-
countryName = optional
|
|
2869
|
-
stateOrProvinceName = optional
|
|
2870
|
-
localityName = optional
|
|
2871
|
-
organizationName = optional
|
|
2872
|
-
organizationalUnitName = optional
|
|
2873
|
-
commonName = optional
|
|
2874
|
-
emailAddress = optional
|
|
2875
|
-
|
|
2876
|
-
[ req ]
|
|
2877
|
-
default_bits = 4096 # Size of keys
|
|
2878
|
-
default_keyfile = key.pem # name of generated keys
|
|
2879
|
-
distinguished_name = req_distinguished_name
|
|
2880
|
-
attributes = req_attributes
|
|
2881
|
-
x509_extensions = v3_ca
|
|
2882
|
-
#input_password
|
|
2883
|
-
#output_password
|
|
2884
|
-
string_mask = nombstr # permitted characters
|
|
2885
|
-
req_extensions = v3_req
|
|
2886
|
-
|
|
2887
|
-
[ req_distinguished_name ]
|
|
2888
|
-
|
|
2889
|
-
#0 countryName = Country Name (2 letter code)
|
|
2890
|
-
# countryName_default = FR
|
|
2891
|
-
# countryName_min = 2
|
|
2892
|
-
# countryName_max = 2
|
|
2893
|
-
# stateOrProvinceName = State or Province Name (full name)
|
|
2894
|
-
# stateOrProvinceName_default = Ile de France
|
|
2895
|
-
# localityName = Locality Name (city, district)
|
|
2896
|
-
# localityName_default = Paris
|
|
2897
|
-
organizationName = Organization Name (company)
|
|
2898
|
-
organizationName_default = NodeOPCUA
|
|
2899
|
-
# organizationalUnitName = Organizational Unit Name (department, division)
|
|
2900
|
-
# organizationalUnitName_default = R&D
|
|
2901
|
-
commonName = Common Name (hostname, FQDN, IP, or your name)
|
|
2902
|
-
commonName_max = 256
|
|
2903
|
-
commonName_default = NodeOPCUA
|
|
2904
|
-
# emailAddress = Email Address
|
|
2905
|
-
# emailAddress_max = 40
|
|
2906
|
-
# emailAddress_default = node-opcua (at) node-opcua (dot) com
|
|
2907
|
-
|
|
2908
|
-
[ req_attributes ]
|
|
2909
|
-
#challengePassword = A challenge password
|
|
2910
|
-
#challengePassword_min = 4
|
|
2911
|
-
#challengePassword_max = 20
|
|
2912
|
-
#unstructuredName = An optional company name
|
|
2913
|
-
[ usr_cert ]
|
|
2914
|
-
basicConstraints = critical, CA:FALSE
|
|
2915
|
-
subjectKeyIdentifier = hash
|
|
2916
|
-
authorityKeyIdentifier = keyid,issuer:always
|
|
2917
|
-
#authorityKeyIdentifier = keyid
|
|
2918
|
-
subjectAltName = $ENV::ALTNAME
|
|
2919
|
-
# issuerAltName = issuer:copy
|
|
2920
|
-
nsComment = ''OpenSSL Generated Certificate''
|
|
2921
|
-
#nsCertType = client, email, objsign for ''everything including object signing''
|
|
2922
|
-
#nsCaRevocationUrl = http://www.domain.dom/ca-crl.pem
|
|
2923
|
-
#nsBaseUrl =
|
|
2924
|
-
#nsRenewalUrl =
|
|
2925
|
-
#nsCaPolicyUrl =
|
|
2926
|
-
#nsSslServerName =
|
|
2927
|
-
keyUsage = critical, digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment, keyAgreement
|
|
2928
|
-
extendedKeyUsage = critical,serverAuth ,clientAuth
|
|
2929
|
-
{{#CDP_URL}}crlDistributionPoints = URI:$ENV::CDP_URL
|
|
2930
|
-
{{/CDP_URL}}{{#AIA_VALUE}}authorityInfoAccess = $ENV::AIA_VALUE
|
|
2931
|
-
{{/AIA_VALUE}}
|
|
2932
|
-
[ v3_req ]
|
|
2933
|
-
basicConstraints = critical, CA:FALSE
|
|
2934
|
-
keyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment, keyAgreement
|
|
2935
|
-
extendedKeyUsage = critical,serverAuth ,clientAuth
|
|
2936
|
-
subjectAltName = $ENV::ALTNAME
|
|
2937
|
-
nsComment = "CA Generated by Node-OPCUA Certificate utility using openssl"
|
|
2938
|
-
[ v3_ca_req ]
|
|
2939
|
-
subjectKeyIdentifier = hash
|
|
2940
|
-
basicConstraints = CA:TRUE
|
|
2941
|
-
keyUsage = critical, cRLSign, keyCertSign
|
|
2942
|
-
subjectAltName = $ENV::ALTNAME
|
|
2943
|
-
nsComment = "CA CSR generated by Node-OPCUA Certificate utility using openssl"
|
|
2944
|
-
[ v3_ca ]
|
|
2945
|
-
subjectKeyIdentifier = hash
|
|
2946
|
-
authorityKeyIdentifier = keyid:always,issuer:always
|
|
2947
|
-
basicConstraints = CA:TRUE
|
|
2948
|
-
keyUsage = critical, cRLSign, keyCertSign
|
|
2949
|
-
subjectAltName = $ENV::ALTNAME
|
|
2950
|
-
nsComment = "CA Certificate generated by Node-OPCUA Certificate utility using openssl"
|
|
2951
|
-
#nsCertType = sslCA, emailCA
|
|
2952
|
-
#issuerAltName = issuer:copy
|
|
2953
|
-
#obj = DER:02:03
|
|
2954
|
-
{{#CDP_URL}}crlDistributionPoints = URI:$ENV::CDP_URL
|
|
2955
|
-
{{/CDP_URL}}{{#AIA_VALUE}}authorityInfoAccess = $ENV::AIA_VALUE
|
|
2956
|
-
{{/AIA_VALUE}}[ v3_selfsigned]
|
|
2957
|
-
basicConstraints = critical, CA:FALSE
|
|
2958
|
-
keyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment, keyAgreement
|
|
2959
|
-
extendedKeyUsage = critical,serverAuth ,clientAuth
|
|
2960
|
-
nsComment = "Self-signed certificate, generated by NodeOPCUA"
|
|
2961
|
-
subjectAltName = $ENV::ALTNAME
|
|
2962
|
-
|
|
2963
|
-
[ crl_ext ]
|
|
2964
|
-
#issuerAltName = issuer:copy
|
|
2965
|
-
authorityKeyIdentifier = keyid:always,issuer:always
|
|
2966
|
-
#authorityInfoAccess = @issuer_info`;
|
|
2967
|
-
ca_config_template_cnf_default = config2;
|
|
2968
|
-
}
|
|
2969
|
-
});
|
|
2970
|
-
|
|
2971
|
-
// packages/node-opcua-pki/lib/ca/certificate_authority.ts
|
|
2972
|
-
import assert10 from "assert";
|
|
2973
|
-
import fs10 from "fs";
|
|
2974
|
-
import os4 from "os";
|
|
2975
2835
|
import path6 from "path";
|
|
2976
|
-
import
|
|
2977
|
-
import {
|
|
2978
|
-
CertificatePurpose as CertificatePurpose2,
|
|
2979
|
-
certificateMatchesPrivateKey,
|
|
2980
|
-
convertPEMtoDER,
|
|
2981
|
-
exploreCertificate as exploreCertificate2,
|
|
2982
|
-
exploreCertificateSigningRequest,
|
|
2983
|
-
generatePrivateKeyFile as generatePrivateKeyFile2,
|
|
2984
|
-
readCertificatePEM,
|
|
2985
|
-
readCertificateSigningRequest,
|
|
2986
|
-
readPrivateKey as readPrivateKey2,
|
|
2987
|
-
Subject as Subject4,
|
|
2988
|
-
toPem as toPem2,
|
|
2989
|
-
writePrivateKeyFile as writePrivateKeyFile2
|
|
2990
|
-
} from "node-opcua-crypto";
|
|
2991
|
-
function escapeOpensslConfDoubleQuoted(value) {
|
|
2992
|
-
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
2993
|
-
}
|
|
2994
|
-
function renderCaConfig(caRootDir) {
|
|
2995
|
-
return configurationFileTemplate.replace(/%%ROOT_FOLDER%%/, escapeOpensslConfDoubleQuoted(makePath(caRootDir)));
|
|
2996
|
-
}
|
|
2997
|
-
function octetStringToIpAddress(a) {
|
|
2998
|
-
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();
|
|
2999
|
-
}
|
|
3000
|
-
async function construct_CertificateAuthority(certificateAuthority) {
|
|
3001
|
-
const subject = certificateAuthority.subject;
|
|
3002
|
-
const caRootDir = path6.resolve(certificateAuthority.rootDir);
|
|
3003
|
-
async function make_folders() {
|
|
3004
|
-
mkdirRecursiveSync(caRootDir);
|
|
3005
|
-
ensurePrivateDirectory(path6.join(caRootDir, "private"));
|
|
3006
|
-
mkdirRecursiveSync(path6.join(caRootDir, "public"));
|
|
3007
|
-
mkdirRecursiveSync(path6.join(caRootDir, "certs"));
|
|
3008
|
-
mkdirRecursiveSync(path6.join(caRootDir, "crl"));
|
|
3009
|
-
mkdirRecursiveSync(path6.join(caRootDir, "conf"));
|
|
3010
|
-
}
|
|
3011
|
-
await make_folders();
|
|
3012
|
-
async function construct_default_files() {
|
|
3013
|
-
const serial = path6.join(caRootDir, "serial");
|
|
3014
|
-
if (!fs10.existsSync(serial)) {
|
|
3015
|
-
await fs10.promises.writeFile(serial, "1000");
|
|
3016
|
-
}
|
|
3017
|
-
const crlNumber = path6.join(caRootDir, "crlnumber");
|
|
3018
|
-
if (!fs10.existsSync(crlNumber)) {
|
|
3019
|
-
await fs10.promises.writeFile(crlNumber, "1000");
|
|
3020
|
-
}
|
|
3021
|
-
const indexFile = path6.join(caRootDir, "index.txt");
|
|
3022
|
-
if (!fs10.existsSync(indexFile)) {
|
|
3023
|
-
await fs10.promises.writeFile(indexFile, "");
|
|
3024
|
-
}
|
|
3025
|
-
}
|
|
3026
|
-
await construct_default_files();
|
|
3027
|
-
const caKeyExists = fs10.existsSync(path6.join(caRootDir, "private/cakey.pem"));
|
|
3028
|
-
const caCertExists = fs10.existsSync(path6.join(caRootDir, "public/cacert.pem"));
|
|
3029
|
-
if (caKeyExists && caCertExists && !config3.forceCA) {
|
|
3030
|
-
restrictPrivateFilePermissions(path6.join(caRootDir, "private/cakey.pem"), 384);
|
|
3031
|
-
await certificateAuthority._ensurePrivateKeyProtection();
|
|
3032
|
-
debugLog("CA private key and certificate already exist ... skipping");
|
|
3033
|
-
return;
|
|
3034
|
-
}
|
|
3035
|
-
if (caKeyExists && !caCertExists) {
|
|
3036
|
-
debugLog("CA private key exists but cacert.pem is missing \u2014 rebuilding CA");
|
|
3037
|
-
fs10.unlinkSync(path6.join(caRootDir, "private/cakey.pem"));
|
|
3038
|
-
const staleCsr = path6.join(caRootDir, "private/cakey.csr");
|
|
3039
|
-
if (fs10.existsSync(staleCsr)) {
|
|
3040
|
-
fs10.unlinkSync(staleCsr);
|
|
3041
|
-
}
|
|
3042
|
-
}
|
|
3043
|
-
displayTitle("Create Certificate Authority (CA)");
|
|
3044
|
-
const indexFileAttr = path6.join(caRootDir, "index.txt.attr");
|
|
3045
|
-
if (!fs10.existsSync(indexFileAttr)) {
|
|
3046
|
-
await fs10.promises.writeFile(indexFileAttr, "unique_subject = no");
|
|
3047
|
-
}
|
|
3048
|
-
const caConfigFile = certificateAuthority.configFile;
|
|
3049
|
-
if (1) {
|
|
3050
|
-
await fs10.promises.writeFile(caConfigFile, renderCaConfig(caRootDir));
|
|
3051
|
-
}
|
|
3052
|
-
const subjectOpt = ["-subj", subject.toString()];
|
|
3053
|
-
const caCommonName = subject.commonName || "NodeOPCUA-CA";
|
|
3054
|
-
setEnv("ALTNAME", `URI:urn:${caCommonName}`);
|
|
3055
|
-
certificateAuthority._wireRevocationEnvVars();
|
|
3056
|
-
const options = { cwd: caRootDir };
|
|
3057
|
-
const configFile = generateStaticConfig("conf/caconfig.cnf", options);
|
|
3058
|
-
const configOption = ["-config", n5(configFile)];
|
|
3059
|
-
const keySize = certificateAuthority.keySize;
|
|
3060
|
-
const privateKeyFilename = path6.join(caRootDir, "private/cakey.pem");
|
|
3061
|
-
const csrFilename = path6.join(caRootDir, "private/cakey.csr");
|
|
3062
|
-
displayTitle(`Generate the CA private Key - ${keySize}`);
|
|
3063
|
-
const passin = await certificateAuthority._opensslPassin();
|
|
3064
|
-
await generatePrivateKeyFile2(privateKeyFilename, keySize, { passphrase: await certificateAuthority._privateKeyPassphrase() });
|
|
3065
|
-
restrictPrivateFilePermissions(privateKeyFilename, 384);
|
|
3066
|
-
displayTitle("Generate a certificate request for the CA key");
|
|
3067
|
-
await execute_openssl(
|
|
3068
|
-
[
|
|
3069
|
-
"req",
|
|
3070
|
-
"-new",
|
|
3071
|
-
"-sha256",
|
|
3072
|
-
"-text",
|
|
3073
|
-
"-extensions",
|
|
3074
|
-
"v3_ca_req",
|
|
3075
|
-
...configOption,
|
|
3076
|
-
"-key",
|
|
3077
|
-
n5(privateKeyFilename),
|
|
3078
|
-
"-out",
|
|
3079
|
-
n5(csrFilename),
|
|
3080
|
-
...subjectOpt,
|
|
3081
|
-
...passin.args
|
|
3082
|
-
],
|
|
3083
|
-
{ ...options, env: passin.env }
|
|
3084
|
-
);
|
|
3085
|
-
const issuerCA = certificateAuthority._issuerCA;
|
|
3086
|
-
if (issuerCA) {
|
|
3087
|
-
displayTitle("Generate CA Certificate (signed by issuer CA)");
|
|
3088
|
-
const issuerCert = path6.resolve(issuerCA.caCertificate);
|
|
3089
|
-
const issuerKey = path6.resolve(issuerCA.rootDir, "private/cakey.pem");
|
|
3090
|
-
const issuerSerial = path6.resolve(issuerCA.rootDir, "serial");
|
|
3091
|
-
const issuerPassin = await issuerCA._opensslPassin();
|
|
3092
|
-
await execute_openssl(
|
|
3093
|
-
[
|
|
3094
|
-
"x509",
|
|
3095
|
-
"-sha256",
|
|
3096
|
-
"-req",
|
|
3097
|
-
"-days",
|
|
3098
|
-
"3650",
|
|
3099
|
-
"-text",
|
|
3100
|
-
"-extensions",
|
|
3101
|
-
"v3_ca",
|
|
3102
|
-
"-extfile",
|
|
3103
|
-
n5(configFile),
|
|
3104
|
-
"-in",
|
|
3105
|
-
"private/cakey.csr",
|
|
3106
|
-
"-CA",
|
|
3107
|
-
n5(issuerCert),
|
|
3108
|
-
"-CAkey",
|
|
3109
|
-
n5(issuerKey),
|
|
3110
|
-
"-CAserial",
|
|
3111
|
-
n5(issuerSerial),
|
|
3112
|
-
"-out",
|
|
3113
|
-
"public/cacert.pem",
|
|
3114
|
-
...issuerPassin.args
|
|
3115
|
-
],
|
|
3116
|
-
{ ...options, env: issuerPassin.env }
|
|
3117
|
-
);
|
|
3118
|
-
} else {
|
|
3119
|
-
displayTitle("Generate CA Certificate (self-signed)");
|
|
3120
|
-
await execute_openssl(
|
|
3121
|
-
[
|
|
3122
|
-
"x509",
|
|
3123
|
-
"-sha256",
|
|
3124
|
-
"-req",
|
|
3125
|
-
"-days",
|
|
3126
|
-
"3650",
|
|
3127
|
-
"-text",
|
|
3128
|
-
"-extensions",
|
|
3129
|
-
"v3_ca",
|
|
3130
|
-
"-extfile",
|
|
3131
|
-
n5(configFile),
|
|
3132
|
-
"-in",
|
|
3133
|
-
"private/cakey.csr",
|
|
3134
|
-
"-signkey",
|
|
3135
|
-
n5(privateKeyFilename),
|
|
3136
|
-
"-out",
|
|
3137
|
-
"public/cacert.pem",
|
|
3138
|
-
...passin.args
|
|
3139
|
-
],
|
|
3140
|
-
{ ...options, env: passin.env }
|
|
3141
|
-
);
|
|
3142
|
-
}
|
|
3143
|
-
displaySubtitle("generate initial CRL (Certificate Revocation List)");
|
|
3144
|
-
await regenerateCrl(certificateAuthority.revocationList, configOption, options, passin);
|
|
3145
|
-
displayTitle("Create Certificate Authority (CA) ---> DONE");
|
|
3146
|
-
}
|
|
3147
|
-
async function regenerateCrl(revocationList, configOption, options, passin) {
|
|
3148
|
-
displaySubtitle("regenerate CRL (Certificate Revocation List)");
|
|
3149
|
-
await execute_openssl(["ca", "-gencrl", ...configOption, "-out", "crl/revocation_list.crl", ...passin.args], {
|
|
3150
|
-
...options,
|
|
3151
|
-
env: passin.env
|
|
3152
|
-
});
|
|
3153
|
-
await execute_openssl(["crl", "-in", "crl/revocation_list.crl", "-out", "crl/revocation_list.der", "-outform", "der"], options);
|
|
3154
|
-
displaySubtitle("Display (Certificate Revocation List)");
|
|
3155
|
-
await execute_openssl(["crl", "-in", n5(revocationList), "-text", "-noout"], options);
|
|
3156
|
-
}
|
|
2836
|
+
import { convertPEMtoDER, readCertificatePEM } from "node-opcua-crypto";
|
|
3157
2837
|
function parseOpenSSLDate(dateStr) {
|
|
3158
2838
|
const raw = dateStr?.split(",")[0] ?? "";
|
|
3159
2839
|
if (raw.length < 12) return "";
|
|
@@ -3166,24 +2846,1032 @@ function parseOpenSSLDate(dateStr) {
|
|
|
3166
2846
|
const sec = raw.substring(10, 12);
|
|
3167
2847
|
return `${year}-${month}-${day}T${hour}:${min}:${sec}Z`;
|
|
3168
2848
|
}
|
|
3169
|
-
function
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
2849
|
+
function formatOpenSSLDate(date) {
|
|
2850
|
+
const yy = String(date.getUTCFullYear() % 100).padStart(2, "0");
|
|
2851
|
+
const mm = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
2852
|
+
const dd = String(date.getUTCDate()).padStart(2, "0");
|
|
2853
|
+
const hh = String(date.getUTCHours()).padStart(2, "0");
|
|
2854
|
+
const mi = String(date.getUTCMinutes()).padStart(2, "0");
|
|
2855
|
+
const ss = String(date.getUTCSeconds()).padStart(2, "0");
|
|
2856
|
+
return `${yy}${mm}${dd}${hh}${mi}${ss}Z`;
|
|
2857
|
+
}
|
|
2858
|
+
function parseRevocationReason(dateStr) {
|
|
2859
|
+
const parts = dateStr?.split(",");
|
|
2860
|
+
return parts && parts.length > 1 ? parts[1] : void 0;
|
|
2861
|
+
}
|
|
2862
|
+
function evenLengthHex(value) {
|
|
2863
|
+
const hex = value.toString(16).toUpperCase();
|
|
2864
|
+
return hex.length % 2 === 0 ? hex : `0${hex}`;
|
|
2865
|
+
}
|
|
2866
|
+
function escapeIndexField(value) {
|
|
2867
|
+
return value.replace(/[\x00-\x1f\x7f\\]/g, (c) => `\\x${c.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`);
|
|
2868
|
+
}
|
|
2869
|
+
function assertHexSerial(serial) {
|
|
2870
|
+
if (!/^[0-9A-Fa-f]+$/.test(serial)) {
|
|
2871
|
+
throw new Error(`Invalid certificate serial number: ${JSON.stringify(serial)}`);
|
|
3184
2872
|
}
|
|
3185
|
-
|
|
3186
|
-
|
|
2873
|
+
return serial.toUpperCase();
|
|
2874
|
+
}
|
|
2875
|
+
var CaDatabase;
|
|
2876
|
+
var init_ca_database = __esm({
|
|
2877
|
+
"packages/node-opcua-pki/lib/ca/core/ca_database.ts"() {
|
|
2878
|
+
"use strict";
|
|
2879
|
+
init_esm_shims();
|
|
2880
|
+
CaDatabase = class {
|
|
2881
|
+
#rootDir;
|
|
2882
|
+
constructor(rootDir) {
|
|
2883
|
+
this.#rootDir = rootDir;
|
|
2884
|
+
}
|
|
2885
|
+
/** Path to the OpenSSL certificate database file (`index.txt`). */
|
|
2886
|
+
get indexFile() {
|
|
2887
|
+
return path6.join(this.#rootDir, "index.txt");
|
|
2888
|
+
}
|
|
2889
|
+
/**
|
|
2890
|
+
* Parse the OpenSSL `index.txt` certificate database.
|
|
2891
|
+
*
|
|
2892
|
+
* Each line has tab-separated fields:
|
|
2893
|
+
* ```
|
|
2894
|
+
* status expiry [revocationDate] serial unknown subject
|
|
2895
|
+
* ```
|
|
2896
|
+
*
|
|
2897
|
+
* - status: `V` (valid), `R` (revoked), `E` (expired)
|
|
2898
|
+
* - expiry: `YYMMDDHHmmssZ`
|
|
2899
|
+
* - revocationDate: present only for revoked certs
|
|
2900
|
+
* - serial: hex string
|
|
2901
|
+
* - unknown: always `"unknown"`
|
|
2902
|
+
* - subject: X.500 slash-delimited string
|
|
2903
|
+
*/
|
|
2904
|
+
readIndex() {
|
|
2905
|
+
const indexPath = this.indexFile;
|
|
2906
|
+
if (!fs9.existsSync(indexPath)) {
|
|
2907
|
+
return [];
|
|
2908
|
+
}
|
|
2909
|
+
const content = fs9.readFileSync(indexPath, "utf-8");
|
|
2910
|
+
const lines = content.split("\n").filter((l) => l.trim().length > 0);
|
|
2911
|
+
const records = [];
|
|
2912
|
+
for (const line of lines) {
|
|
2913
|
+
const fields = line.split(" ");
|
|
2914
|
+
if (fields.length < 4) continue;
|
|
2915
|
+
const statusChar = fields[0];
|
|
2916
|
+
const expiryStr = fields[1];
|
|
2917
|
+
let serial;
|
|
2918
|
+
let subject;
|
|
2919
|
+
let revocationDate;
|
|
2920
|
+
if (statusChar === "R") {
|
|
2921
|
+
revocationDate = fields[2];
|
|
2922
|
+
serial = fields[3];
|
|
2923
|
+
subject = fields.length >= 6 ? fields[5] : "";
|
|
2924
|
+
} else {
|
|
2925
|
+
serial = fields[3];
|
|
2926
|
+
subject = fields.length >= 6 ? fields[5] : "";
|
|
2927
|
+
}
|
|
2928
|
+
let status;
|
|
2929
|
+
switch (statusChar) {
|
|
2930
|
+
case "V":
|
|
2931
|
+
status = "valid";
|
|
2932
|
+
break;
|
|
2933
|
+
case "R":
|
|
2934
|
+
status = "revoked";
|
|
2935
|
+
break;
|
|
2936
|
+
case "E":
|
|
2937
|
+
status = "expired";
|
|
2938
|
+
break;
|
|
2939
|
+
default:
|
|
2940
|
+
continue;
|
|
2941
|
+
}
|
|
2942
|
+
records.push({
|
|
2943
|
+
serial,
|
|
2944
|
+
status,
|
|
2945
|
+
subject,
|
|
2946
|
+
expiryDate: parseOpenSSLDate(expiryStr),
|
|
2947
|
+
revocationDate: revocationDate ? parseOpenSSLDate(revocationDate) : void 0,
|
|
2948
|
+
reason: revocationDate ? parseRevocationReason(revocationDate) : void 0
|
|
2949
|
+
});
|
|
2950
|
+
}
|
|
2951
|
+
return records;
|
|
2952
|
+
}
|
|
2953
|
+
/** Look up one record by serial number (case-insensitive). */
|
|
2954
|
+
findBySerial(serial) {
|
|
2955
|
+
const upper = serial.toUpperCase();
|
|
2956
|
+
return this.readIndex().find((r) => r.serial.toUpperCase() === upper);
|
|
2957
|
+
}
|
|
2958
|
+
/**
|
|
2959
|
+
* Read a specific issued certificate by serial number.
|
|
2960
|
+
*
|
|
2961
|
+
* OpenSSL stores signed certificates in the `certs/` directory using
|
|
2962
|
+
* the naming convention `<SERIAL>.pem`.
|
|
2963
|
+
*
|
|
2964
|
+
* @param serial - hex-encoded serial number (e.g. `"1000"`)
|
|
2965
|
+
* @returns the DER buffer, or `undefined` if not found
|
|
2966
|
+
*/
|
|
2967
|
+
getCertificateBySerial(serial) {
|
|
2968
|
+
const upper = serial.toUpperCase();
|
|
2969
|
+
const certFile = path6.join(this.#rootDir, "certs", `${upper}.pem`);
|
|
2970
|
+
if (!fs9.existsSync(certFile)) {
|
|
2971
|
+
return void 0;
|
|
2972
|
+
}
|
|
2973
|
+
const pem = readCertificatePEM(certFile);
|
|
2974
|
+
return convertPEMtoDER(pem);
|
|
2975
|
+
}
|
|
2976
|
+
/**
|
|
2977
|
+
* Read-increment-write a hex counter file (`serial`/`crlnumber`),
|
|
2978
|
+
* returning the value to hand out — matching `openssl ca`'s own
|
|
2979
|
+
* behavior: the file holds the *next* value to assign, and is bumped
|
|
2980
|
+
* to the following one immediately after being read. A `.old` backup
|
|
2981
|
+
* of the pre-bump content is kept, as `openssl` itself does.
|
|
2982
|
+
*/
|
|
2983
|
+
#nextHexCounter(fileName) {
|
|
2984
|
+
const counterFile = path6.join(this.#rootDir, fileName);
|
|
2985
|
+
const current = BigInt(`0x${fs9.readFileSync(counterFile, "utf-8").trim()}`);
|
|
2986
|
+
fs9.copyFileSync(counterFile, `${counterFile}.old`);
|
|
2987
|
+
fs9.writeFileSync(counterFile, evenLengthHex(current + 1n));
|
|
2988
|
+
return evenLengthHex(current);
|
|
2989
|
+
}
|
|
2990
|
+
/** Hand out the next certificate serial number, bumping the `serial` file. */
|
|
2991
|
+
nextSerial() {
|
|
2992
|
+
return this.#nextHexCounter("serial");
|
|
2993
|
+
}
|
|
2994
|
+
/** Hand out the next CRL number, bumping the `crlnumber` file. */
|
|
2995
|
+
nextCrlNumber() {
|
|
2996
|
+
return this.#nextHexCounter("crlnumber");
|
|
2997
|
+
}
|
|
2998
|
+
/** Append a newly-issued certificate's `V` (valid) row to `index.txt`. */
|
|
2999
|
+
appendIssued(record) {
|
|
3000
|
+
const serial = assertHexSerial(record.serial);
|
|
3001
|
+
const subject = escapeIndexField(record.subject);
|
|
3002
|
+
const line = `V ${formatOpenSSLDate(record.expiryDate)} ${serial} unknown ${subject}
|
|
3003
|
+
`;
|
|
3004
|
+
fs9.appendFileSync(this.indexFile, line);
|
|
3005
|
+
}
|
|
3006
|
+
/**
|
|
3007
|
+
* Rewrite a certificate's `index.txt` row from `V` to `R` (revoked).
|
|
3008
|
+
* Throws if the serial is not found, or is already revoked — the same
|
|
3009
|
+
* "already revoked" case `openssl ca -revoke` itself rejects.
|
|
3010
|
+
*/
|
|
3011
|
+
markRevoked(serial, revocationDate, reason) {
|
|
3012
|
+
const upper = assertHexSerial(serial);
|
|
3013
|
+
if (!/^[A-Za-z]+$/.test(reason)) {
|
|
3014
|
+
throw new Error(`Invalid CRL reason: ${JSON.stringify(reason)}`);
|
|
3015
|
+
}
|
|
3016
|
+
const lines = fs9.readFileSync(this.indexFile, "utf-8").split("\n").filter((l) => l.trim().length > 0);
|
|
3017
|
+
let found = false;
|
|
3018
|
+
const rewritten = lines.map((line) => {
|
|
3019
|
+
const fields = line.split(" ");
|
|
3020
|
+
if (fields.length < 4 || fields[3]?.toUpperCase() !== upper) {
|
|
3021
|
+
return line;
|
|
3022
|
+
}
|
|
3023
|
+
if (fields[0] === "R") {
|
|
3024
|
+
throw new Error(`Certificate ${upper} is already revoked`);
|
|
3025
|
+
}
|
|
3026
|
+
found = true;
|
|
3027
|
+
const subject = fields.length >= 6 ? fields[5] : "";
|
|
3028
|
+
return `R ${fields[1]} ${formatOpenSSLDate(revocationDate)},${reason} ${upper} unknown ${subject}`;
|
|
3029
|
+
});
|
|
3030
|
+
if (!found) {
|
|
3031
|
+
throw new Error(`Certificate ${upper} not found in the certificate database`);
|
|
3032
|
+
}
|
|
3033
|
+
fs9.writeFileSync(this.indexFile, `${rewritten.join("\n")}
|
|
3034
|
+
`);
|
|
3035
|
+
}
|
|
3036
|
+
/** Store a signed certificate's PEM under `certs/<SERIAL>.pem`, as `openssl ca` does. */
|
|
3037
|
+
storeCertificate(serial, pem) {
|
|
3038
|
+
const certFile = path6.join(this.#rootDir, "certs", `${assertHexSerial(serial)}.pem`);
|
|
3039
|
+
fs9.writeFileSync(certFile, pem);
|
|
3040
|
+
}
|
|
3041
|
+
};
|
|
3042
|
+
}
|
|
3043
|
+
});
|
|
3044
|
+
|
|
3045
|
+
// packages/node-opcua-pki/lib/ca/backends/native_ca_backend.ts
|
|
3046
|
+
import fs10 from "fs";
|
|
3047
|
+
import path7 from "path";
|
|
3048
|
+
import {
|
|
3049
|
+
CertificatePurpose as CertificatePurpose2,
|
|
3050
|
+
createCertificateFromCsr,
|
|
3051
|
+
createCertificateSigningRequest as createCertificateSigningRequest2,
|
|
3052
|
+
createCrl,
|
|
3053
|
+
isCaSigner,
|
|
3054
|
+
privateKeyToCryptoKey,
|
|
3055
|
+
readPrivateKey as readPrivateKey2,
|
|
3056
|
+
Subject as Subject4,
|
|
3057
|
+
x509
|
|
3058
|
+
} from "node-opcua-crypto";
|
|
3059
|
+
function signingAlgorithmOf(key) {
|
|
3060
|
+
return isCaSigner(key) ? key.algorithm : RSA_SHA256;
|
|
3061
|
+
}
|
|
3062
|
+
function daysFromNow(notBefore, days) {
|
|
3063
|
+
return new Date(notBefore.getTime() + days * 24 * 60 * 60 * 1e3);
|
|
3064
|
+
}
|
|
3065
|
+
function caSanUri(ca) {
|
|
3066
|
+
return `urn:${ca.subject.commonName || "NodeOPCUA-CA"}`;
|
|
3067
|
+
}
|
|
3068
|
+
function sanFromCsr(csrPem) {
|
|
3069
|
+
const request = new x509.Pkcs10CertificateRequest(csrPem);
|
|
3070
|
+
const extension = request.getExtension("2.5.29.17");
|
|
3071
|
+
const dns2 = [];
|
|
3072
|
+
const ip = [];
|
|
3073
|
+
let applicationUri;
|
|
3074
|
+
for (const name of extension?.names.toJSON() ?? []) {
|
|
3075
|
+
if (name.type === "dns") {
|
|
3076
|
+
dns2.push(name.value);
|
|
3077
|
+
} else if (name.type === "ip") {
|
|
3078
|
+
ip.push(name.value);
|
|
3079
|
+
} else if (name.type === "url" && applicationUri === void 0) {
|
|
3080
|
+
applicationUri = name.value;
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
return { dns: dns2, ip, applicationUri };
|
|
3084
|
+
}
|
|
3085
|
+
function toOpenSslSubjectString(subjectName) {
|
|
3086
|
+
const parts = [];
|
|
3087
|
+
for (const rdn of subjectName.toJSON()) {
|
|
3088
|
+
for (const [type, values] of Object.entries(rdn)) {
|
|
3089
|
+
for (const value of values) {
|
|
3090
|
+
parts.push(`${type}=${value}`);
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
return `/${parts.join("/")}`;
|
|
3095
|
+
}
|
|
3096
|
+
var CA_CERT_VALIDITY_DAYS, RSA_SHA256, REASON_TO_X509_CRL_REASON, NativeCaBackend;
|
|
3097
|
+
var init_native_ca_backend = __esm({
|
|
3098
|
+
"packages/node-opcua-pki/lib/ca/backends/native_ca_backend.ts"() {
|
|
3099
|
+
"use strict";
|
|
3100
|
+
init_esm_shims();
|
|
3101
|
+
init_toolbox();
|
|
3102
|
+
init_ca_database();
|
|
3103
|
+
CA_CERT_VALIDITY_DAYS = 3650;
|
|
3104
|
+
RSA_SHA256 = { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-256" } };
|
|
3105
|
+
REASON_TO_X509_CRL_REASON = {
|
|
3106
|
+
unspecified: x509.X509CrlReason.unspecified,
|
|
3107
|
+
keyCompromise: x509.X509CrlReason.keyCompromise,
|
|
3108
|
+
CACompromise: x509.X509CrlReason.cACompromise,
|
|
3109
|
+
affiliationChanged: x509.X509CrlReason.affiliationChanged,
|
|
3110
|
+
superseded: x509.X509CrlReason.superseded,
|
|
3111
|
+
cessationOfOperation: x509.X509CrlReason.cessationOfOperation,
|
|
3112
|
+
certificateHold: x509.X509CrlReason.certificateHold,
|
|
3113
|
+
removeFromCRL: x509.X509CrlReason.removeFromCRL
|
|
3114
|
+
};
|
|
3115
|
+
NativeCaBackend = class {
|
|
3116
|
+
/** Nothing to check: this backend spawns no process and needs no tool on PATH. */
|
|
3117
|
+
/** Signing goes through `ca._getSigningKey()`, which may be an external signer. */
|
|
3118
|
+
supportsExternalSigner = true;
|
|
3119
|
+
async preflight() {
|
|
3120
|
+
}
|
|
3121
|
+
/**
|
|
3122
|
+
* Write a CSR for the CA's own key, the `[v3_ca_req]` equivalent. The
|
|
3123
|
+
* key comes from the CA rather than from `privateKeyFile`: on a
|
|
3124
|
+
* signer-backed CA that file does not exist.
|
|
3125
|
+
*/
|
|
3126
|
+
async generateCaCsr(ca, _caRootDir, _privateKeyFile, csrFile) {
|
|
3127
|
+
displayTitle("Generate a certificate request for the CA key");
|
|
3128
|
+
const signingKey = await ca._getSigningKey();
|
|
3129
|
+
const { csr } = await createCertificateSigningRequest2({
|
|
3130
|
+
privateKey: signingKey,
|
|
3131
|
+
subject: ca.subject.toString(),
|
|
3132
|
+
applicationUri: caSanUri(ca),
|
|
3133
|
+
purpose: CertificatePurpose2.ForCertificateAuthority
|
|
3134
|
+
});
|
|
3135
|
+
await fs10.promises.writeFile(csrFile, csr);
|
|
3136
|
+
}
|
|
3137
|
+
async bootstrap(ca) {
|
|
3138
|
+
const caRootDir = path7.resolve(ca.rootDir);
|
|
3139
|
+
const csrFile = path7.join(caRootDir, "private/cakey.csr");
|
|
3140
|
+
await this.generateCaCsr(ca, caRootDir, path7.join(caRootDir, "private/cakey.pem"), csrFile);
|
|
3141
|
+
const issuerCA = ca._issuerCA;
|
|
3142
|
+
if (issuerCA) {
|
|
3143
|
+
displayTitle("Generate CA Certificate (signed by issuer CA)");
|
|
3144
|
+
const signWithIssuer = () => this.signSubordinateCsr(issuerCA, csrFile, ca.caCertificate, CA_CERT_VALIDITY_DAYS);
|
|
3145
|
+
if (path7.resolve(issuerCA.rootDir) === caRootDir) {
|
|
3146
|
+
await signWithIssuer();
|
|
3147
|
+
} else {
|
|
3148
|
+
await issuerCA._withCaDirectoryLock(signWithIssuer);
|
|
3149
|
+
}
|
|
3150
|
+
} else {
|
|
3151
|
+
displayTitle("Generate CA Certificate (self-signed)");
|
|
3152
|
+
const csrPem = await fs10.promises.readFile(csrFile, "utf-8");
|
|
3153
|
+
const signingKey = await ca._getSigningKey();
|
|
3154
|
+
const request = new x509.Pkcs10CertificateRequest(csrPem);
|
|
3155
|
+
const notBefore = /* @__PURE__ */ new Date();
|
|
3156
|
+
const { cert } = await createCertificateFromCsr({
|
|
3157
|
+
csr: csrPem,
|
|
3158
|
+
// the CSR's own parsed subject, so issuer and subject are
|
|
3159
|
+
// byte-identical on a self-signed root: re-encoding a string
|
|
3160
|
+
// form could differ and break chain building
|
|
3161
|
+
issuerName: request.subjectName,
|
|
3162
|
+
issuerPublicKey: request.publicKey,
|
|
3163
|
+
signingKey,
|
|
3164
|
+
signingAlgorithm: signingAlgorithmOf(signingKey),
|
|
3165
|
+
notBefore,
|
|
3166
|
+
notAfter: daysFromNow(notBefore, CA_CERT_VALIDITY_DAYS),
|
|
3167
|
+
purpose: CertificatePurpose2.ForCertificateAuthority,
|
|
3168
|
+
applicationUri: caSanUri(ca),
|
|
3169
|
+
revocation: {
|
|
3170
|
+
crlDistributionUrl: ca.crlDistributionUrl,
|
|
3171
|
+
ocspResponderUrl: ca.ocspResponderUrl,
|
|
3172
|
+
caIssuersUrl: ca.caIssuersUrl
|
|
3173
|
+
}
|
|
3174
|
+
});
|
|
3175
|
+
await fs10.promises.writeFile(ca.caCertificate, cert);
|
|
3176
|
+
}
|
|
3177
|
+
displaySubtitle("generate initial CRL (Certificate Revocation List)");
|
|
3178
|
+
await this.#regenerateCrlLocked(ca, new CaDatabase(ca.rootDir));
|
|
3179
|
+
displayTitle("Create Certificate Authority (CA) ---> DONE");
|
|
3180
|
+
}
|
|
3181
|
+
/** Sign a subordinate CA's request with this CA's key, the `[v3_ca]` equivalent. */
|
|
3182
|
+
async signSubordinateCsr(ca, csrFile, certFile, validityDays) {
|
|
3183
|
+
const { signingKey, signingAlgorithm, issuerPublicKey, issuerName } = await this.#issuerContext(ca);
|
|
3184
|
+
const csrPem = await fs10.promises.readFile(csrFile, "utf-8");
|
|
3185
|
+
const db = new CaDatabase(ca.rootDir);
|
|
3186
|
+
const serialNumber = db.nextSerial();
|
|
3187
|
+
const notBefore = /* @__PURE__ */ new Date();
|
|
3188
|
+
const notAfter = daysFromNow(notBefore, validityDays);
|
|
3189
|
+
const { cert } = await createCertificateFromCsr({
|
|
3190
|
+
csr: csrPem,
|
|
3191
|
+
issuerName,
|
|
3192
|
+
issuerPublicKey,
|
|
3193
|
+
signingKey,
|
|
3194
|
+
signingAlgorithm,
|
|
3195
|
+
serialNumber,
|
|
3196
|
+
notBefore,
|
|
3197
|
+
notAfter,
|
|
3198
|
+
purpose: CertificatePurpose2.ForCertificateAuthority,
|
|
3199
|
+
// the subordinate's own SAN, not the issuer's
|
|
3200
|
+
...sanFromCsr(csrPem),
|
|
3201
|
+
revocation: {
|
|
3202
|
+
crlDistributionUrl: ca.crlDistributionUrl,
|
|
3203
|
+
ocspResponderUrl: ca.ocspResponderUrl,
|
|
3204
|
+
caIssuersUrl: ca.caIssuersUrl
|
|
3205
|
+
}
|
|
3206
|
+
});
|
|
3207
|
+
await fs10.promises.writeFile(certFile, cert);
|
|
3208
|
+
this.#record(db, serialNumber, cert, notAfter);
|
|
3209
|
+
}
|
|
3210
|
+
/**
|
|
3211
|
+
* Self-sign a certificate with a caller-supplied key file and record it
|
|
3212
|
+
* in this CA's database: the native equivalent of `openssl req -new`
|
|
3213
|
+
* followed by `openssl ca -selfsign`, which likewise applies the
|
|
3214
|
+
* end-entity profile and updates `index.txt`.
|
|
3215
|
+
*/
|
|
3216
|
+
async createSelfSignedCertificate(ca, certificateFile, privateKeyFile, params) {
|
|
3217
|
+
displaySubtitle("- the certificate signing request");
|
|
3218
|
+
const key = await privateKeyToCryptoKey(readPrivateKey2(privateKeyFile, await ca._privateKeyPassphrase()));
|
|
3219
|
+
const { csr } = await createCertificateSigningRequest2({
|
|
3220
|
+
privateKey: key,
|
|
3221
|
+
subject: params.subject ? new Subject4(params.subject).toString() : ca.subject.toString(),
|
|
3222
|
+
dns: params.dns,
|
|
3223
|
+
ip: params.ip,
|
|
3224
|
+
applicationUri: params.applicationUri,
|
|
3225
|
+
purpose: CertificatePurpose2.ForApplication
|
|
3226
|
+
});
|
|
3227
|
+
displaySubtitle("- creating the self-signed certificate");
|
|
3228
|
+
const request = new x509.Pkcs10CertificateRequest(csr);
|
|
3229
|
+
const db = new CaDatabase(ca.rootDir);
|
|
3230
|
+
const serialNumber = db.nextSerial();
|
|
3231
|
+
const notBefore = params.startDate ?? /* @__PURE__ */ new Date();
|
|
3232
|
+
const notAfter = params.endDate ?? daysFromNow(notBefore, params.validity ?? 365);
|
|
3233
|
+
const { cert } = await createCertificateFromCsr({
|
|
3234
|
+
csr,
|
|
3235
|
+
issuerName: request.subjectName,
|
|
3236
|
+
issuerPublicKey: request.publicKey,
|
|
3237
|
+
signingKey: key,
|
|
3238
|
+
signingAlgorithm: RSA_SHA256,
|
|
3239
|
+
serialNumber,
|
|
3240
|
+
notBefore,
|
|
3241
|
+
notAfter,
|
|
3242
|
+
purpose: CertificatePurpose2.ForApplication,
|
|
3243
|
+
...sanFromCsr(csr)
|
|
3244
|
+
});
|
|
3245
|
+
await fs10.promises.writeFile(certificateFile, cert);
|
|
3246
|
+
this.#record(db, serialNumber, cert, notAfter);
|
|
3247
|
+
}
|
|
3248
|
+
/** `certs/<SERIAL>.pem` plus the `V` row, exactly as `openssl ca` writes them. */
|
|
3249
|
+
#record(db, serialNumber, certPem, notAfter) {
|
|
3250
|
+
db.storeCertificate(serialNumber, certPem);
|
|
3251
|
+
db.appendIssued({
|
|
3252
|
+
serial: serialNumber,
|
|
3253
|
+
expiryDate: notAfter,
|
|
3254
|
+
subject: toOpenSslSubjectString(new x509.X509Certificate(certPem).subjectName)
|
|
3255
|
+
});
|
|
3256
|
+
}
|
|
3257
|
+
/**
|
|
3258
|
+
* The CA's signing key and issuer identity, resolved fresh on every
|
|
3259
|
+
* call and never cached across operations. The identity is read back
|
|
3260
|
+
* from the CA certificate on disk so that the issuer field of anything
|
|
3261
|
+
* this CA signs is byte-identical to that certificate's subject.
|
|
3262
|
+
*/
|
|
3263
|
+
async #issuerContext(ca) {
|
|
3264
|
+
const signingKey = await ca._getSigningKey();
|
|
3265
|
+
const caCertPem = await fs10.promises.readFile(ca.caCertificate, "utf-8");
|
|
3266
|
+
const caCert = new x509.X509Certificate(caCertPem);
|
|
3267
|
+
return {
|
|
3268
|
+
signingKey,
|
|
3269
|
+
signingAlgorithm: signingAlgorithmOf(signingKey),
|
|
3270
|
+
issuerPublicKey: caCert.publicKey,
|
|
3271
|
+
issuerName: caCert.subjectName
|
|
3272
|
+
};
|
|
3273
|
+
}
|
|
3274
|
+
async signEndEntityCsr(ca, certificate, csr, params, sanOverride) {
|
|
3275
|
+
const { signingKey, signingAlgorithm, issuerPublicKey, issuerName } = await this.#issuerContext(ca);
|
|
3276
|
+
const db = new CaDatabase(ca.rootDir);
|
|
3277
|
+
const serialNumber = db.nextSerial();
|
|
3278
|
+
const csrPem = await fs10.promises.readFile(csr, "utf-8");
|
|
3279
|
+
const notBefore = params.startDate ?? /* @__PURE__ */ new Date();
|
|
3280
|
+
const notAfter = params.endDate ?? new Date(notBefore.getTime() + (params.validity ?? 365) * 24 * 60 * 60 * 1e3);
|
|
3281
|
+
const { cert } = await createCertificateFromCsr({
|
|
3282
|
+
csr: csrPem,
|
|
3283
|
+
issuerName,
|
|
3284
|
+
issuerPublicKey,
|
|
3285
|
+
signingKey,
|
|
3286
|
+
signingAlgorithm,
|
|
3287
|
+
serialNumber,
|
|
3288
|
+
notBefore,
|
|
3289
|
+
notAfter,
|
|
3290
|
+
purpose: CertificatePurpose2.ForApplication,
|
|
3291
|
+
dns: sanOverride.dns,
|
|
3292
|
+
ip: sanOverride.ip,
|
|
3293
|
+
applicationUri: sanOverride.applicationUri,
|
|
3294
|
+
revocation: {
|
|
3295
|
+
crlDistributionUrl: ca.crlDistributionUrl,
|
|
3296
|
+
ocspResponderUrl: ca.ocspResponderUrl,
|
|
3297
|
+
caIssuersUrl: ca.caIssuersUrl
|
|
3298
|
+
}
|
|
3299
|
+
});
|
|
3300
|
+
await fs10.promises.writeFile(certificate, cert);
|
|
3301
|
+
this.#record(db, serialNumber, cert, notAfter);
|
|
3302
|
+
}
|
|
3303
|
+
async #regenerateCrlLocked(ca, db) {
|
|
3304
|
+
const { signingKey, signingAlgorithm, issuerPublicKey, issuerName } = await this.#issuerContext(ca);
|
|
3305
|
+
const crlNumber = db.nextCrlNumber();
|
|
3306
|
+
const entries = db.readIndex().filter((r) => r.status === "revoked").map((r) => ({
|
|
3307
|
+
serialNumber: r.serial,
|
|
3308
|
+
revocationDate: r.revocationDate ? new Date(r.revocationDate) : /* @__PURE__ */ new Date(),
|
|
3309
|
+
reason: r.reason ? REASON_TO_X509_CRL_REASON[r.reason] : void 0
|
|
3310
|
+
}));
|
|
3311
|
+
const { crl } = await createCrl({
|
|
3312
|
+
issuerName,
|
|
3313
|
+
issuerPublicKey,
|
|
3314
|
+
signingKey,
|
|
3315
|
+
signingAlgorithm,
|
|
3316
|
+
crlNumber: BigInt(`0x${crlNumber}`),
|
|
3317
|
+
entries
|
|
3318
|
+
});
|
|
3319
|
+
await fs10.promises.writeFile(ca.revocationList, crl);
|
|
3320
|
+
const der = new x509.X509Crl(crl).rawData;
|
|
3321
|
+
await fs10.promises.writeFile(ca.revocationListDER, Buffer.from(der));
|
|
3322
|
+
}
|
|
3323
|
+
async regenerateCrl(ca) {
|
|
3324
|
+
const db = new CaDatabase(ca.rootDir);
|
|
3325
|
+
await this.#regenerateCrlLocked(ca, db);
|
|
3326
|
+
}
|
|
3327
|
+
async revoke(ca, certificate, reason) {
|
|
3328
|
+
const certPem = await fs10.promises.readFile(certificate, "utf-8");
|
|
3329
|
+
const cert = new x509.X509Certificate(certPem);
|
|
3330
|
+
const db = new CaDatabase(ca.rootDir);
|
|
3331
|
+
db.markRevoked(cert.serialNumber, /* @__PURE__ */ new Date(), reason);
|
|
3332
|
+
await this.#regenerateCrlLocked(ca, db);
|
|
3333
|
+
}
|
|
3334
|
+
};
|
|
3335
|
+
}
|
|
3336
|
+
});
|
|
3337
|
+
|
|
3338
|
+
// packages/node-opcua-pki/lib/ca/backends/openssl_ca_backend.ts
|
|
3339
|
+
import fs11 from "fs";
|
|
3340
|
+
import path8 from "path";
|
|
3341
|
+
import { Subject as Subject5 } from "node-opcua-crypto";
|
|
3342
|
+
function caAltName(ca) {
|
|
3343
|
+
return `URI:urn:${ca.subject.commonName || "NodeOPCUA-CA"}`;
|
|
3344
|
+
}
|
|
3345
|
+
function caConfigEnvOverrides(ca, altName = caAltName(ca)) {
|
|
3346
|
+
const aiaLegs = [];
|
|
3347
|
+
if (ca.ocspResponderUrl) {
|
|
3348
|
+
aiaLegs.push(`OCSP;URI:${ca.ocspResponderUrl}`);
|
|
3349
|
+
}
|
|
3350
|
+
if (ca.caIssuersUrl) {
|
|
3351
|
+
aiaLegs.push(`caIssuers;URI:${ca.caIssuersUrl}`);
|
|
3352
|
+
}
|
|
3353
|
+
return {
|
|
3354
|
+
ALTNAME: altName,
|
|
3355
|
+
CDP_URL: ca.crlDistributionUrl ?? "",
|
|
3356
|
+
AIA_VALUE: aiaLegs.join(",")
|
|
3357
|
+
};
|
|
3358
|
+
}
|
|
3359
|
+
var n4, OpenSslCaBackend;
|
|
3360
|
+
var init_openssl_ca_backend = __esm({
|
|
3361
|
+
"packages/node-opcua-pki/lib/ca/backends/openssl_ca_backend.ts"() {
|
|
3362
|
+
"use strict";
|
|
3363
|
+
init_esm_shims();
|
|
3364
|
+
init_toolbox();
|
|
3365
|
+
init_with_openssl();
|
|
3366
|
+
n4 = makePath;
|
|
3367
|
+
OpenSslCaBackend = class {
|
|
3368
|
+
/** This backend is the `openssl` executable, so it has to be there. */
|
|
3369
|
+
/**
|
|
3370
|
+
* `-passin env:` argv and env for an openssl call that has to load this
|
|
3371
|
+
* CA's key. Built here rather than on the CA: the passphrase is the
|
|
3372
|
+
* CA's business, but expressing it as an openssl argument is this
|
|
3373
|
+
* backend's, and the core has no reason to know the flag exists.
|
|
3374
|
+
*/
|
|
3375
|
+
async #passin(ca) {
|
|
3376
|
+
return passinArg(await ca._privateKeyPassphrase());
|
|
3377
|
+
}
|
|
3378
|
+
/** The openssl CLI loads its key from a file, so it cannot call out to an HSM. */
|
|
3379
|
+
supportsExternalSigner = false;
|
|
3380
|
+
async preflight() {
|
|
3381
|
+
await ensure_openssl_installed();
|
|
3382
|
+
}
|
|
3383
|
+
/**
|
|
3384
|
+
* Render `conf/caconfig.cnf` once with explicit overrides, run `fn`
|
|
3385
|
+
* with the rendered path, and always remove the temp file. Structural
|
|
3386
|
+
* guarantee that (a) every render carries the three required env
|
|
3387
|
+
* values and (b) no rendered `<name>.<pid>-<n>.tmp` file is ever left
|
|
3388
|
+
* behind — each was previously a per-method discipline.
|
|
3389
|
+
*/
|
|
3390
|
+
async #withConfig(ca, altName, fn) {
|
|
3391
|
+
const caRootDir = path8.resolve(ca.rootDir);
|
|
3392
|
+
const options = { cwd: caRootDir };
|
|
3393
|
+
const configFile = generateStaticConfig("conf/caconfig.cnf", options, caConfigEnvOverrides(ca, altName));
|
|
3394
|
+
try {
|
|
3395
|
+
return await fn(configFile, options);
|
|
3396
|
+
} finally {
|
|
3397
|
+
await cleanupStaticConfig(configFile, options);
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
async #generateCaCsrWith(ca, configFile, options, privateKeyFile, csrFile) {
|
|
3401
|
+
const passin = await this.#passin(ca);
|
|
3402
|
+
displayTitle("Generate a certificate request for the CA key");
|
|
3403
|
+
await execute_openssl(
|
|
3404
|
+
[
|
|
3405
|
+
"req",
|
|
3406
|
+
"-new",
|
|
3407
|
+
"-sha256",
|
|
3408
|
+
"-extensions",
|
|
3409
|
+
"v3_ca_req",
|
|
3410
|
+
"-config",
|
|
3411
|
+
n4(configFile),
|
|
3412
|
+
"-key",
|
|
3413
|
+
n4(privateKeyFile),
|
|
3414
|
+
"-out",
|
|
3415
|
+
n4(csrFile),
|
|
3416
|
+
"-subj",
|
|
3417
|
+
ca.subject.toString(),
|
|
3418
|
+
...passin.args
|
|
3419
|
+
],
|
|
3420
|
+
{ ...options, env: passin.env }
|
|
3421
|
+
);
|
|
3422
|
+
}
|
|
3423
|
+
/**
|
|
3424
|
+
* `openssl ca -gencrl` signs the CRL with the CA key it finds through the
|
|
3425
|
+
* config file (`private_key = $dir/private/cakey.pem`), so it needs the
|
|
3426
|
+
* CA passphrase like every other `openssl ca` invocation.
|
|
3427
|
+
*/
|
|
3428
|
+
async #regenerateCrlWith(ca, configFile, options) {
|
|
3429
|
+
const passin = await this.#passin(ca);
|
|
3430
|
+
displaySubtitle("regenerate CRL (Certificate Revocation List)");
|
|
3431
|
+
await execute_openssl(["ca", "-gencrl", "-config", n4(configFile), "-out", "crl/revocation_list.crl", ...passin.args], {
|
|
3432
|
+
...options,
|
|
3433
|
+
env: passin.env
|
|
3434
|
+
});
|
|
3435
|
+
await execute_openssl(
|
|
3436
|
+
["crl", "-in", "crl/revocation_list.crl", "-out", "crl/revocation_list.der", "-outform", "der"],
|
|
3437
|
+
options
|
|
3438
|
+
);
|
|
3439
|
+
displaySubtitle("Display (Certificate Revocation List)");
|
|
3440
|
+
await execute_openssl(["crl", "-in", n4(ca.revocationList), "-text", "-noout"], options);
|
|
3441
|
+
}
|
|
3442
|
+
async generateCaCsr(ca, _caRootDir, privateKeyFile, csrFile) {
|
|
3443
|
+
await this.#withConfig(
|
|
3444
|
+
ca,
|
|
3445
|
+
caAltName(ca),
|
|
3446
|
+
(configFile, options) => this.#generateCaCsrWith(ca, configFile, options, privateKeyFile, csrFile)
|
|
3447
|
+
);
|
|
3448
|
+
}
|
|
3449
|
+
async bootstrap(ca) {
|
|
3450
|
+
const caRootDir = path8.resolve(ca.rootDir);
|
|
3451
|
+
const privateKeyFilename = path8.join(caRootDir, "private/cakey.pem");
|
|
3452
|
+
const csrFilename = path8.join(caRootDir, "private/cakey.csr");
|
|
3453
|
+
await this.#withConfig(ca, caAltName(ca), async (configFile, options) => {
|
|
3454
|
+
await this.#generateCaCsrWith(ca, configFile, options, privateKeyFilename, csrFilename);
|
|
3455
|
+
const issuerCA = ca._issuerCA;
|
|
3456
|
+
if (issuerCA) {
|
|
3457
|
+
displayTitle("Generate CA Certificate (signed by issuer CA)");
|
|
3458
|
+
const issuerCert = path8.resolve(issuerCA.caCertificate);
|
|
3459
|
+
const issuerKey = path8.resolve(issuerCA.rootDir, "private/cakey.pem");
|
|
3460
|
+
const issuerSerial = path8.resolve(issuerCA.rootDir, "serial");
|
|
3461
|
+
const issuerPassin = await this.#passin(issuerCA);
|
|
3462
|
+
const signWithIssuer = async () => {
|
|
3463
|
+
await execute_openssl(
|
|
3464
|
+
[
|
|
3465
|
+
"x509",
|
|
3466
|
+
"-sha256",
|
|
3467
|
+
"-req",
|
|
3468
|
+
"-days",
|
|
3469
|
+
"3650",
|
|
3470
|
+
"-extensions",
|
|
3471
|
+
"v3_ca",
|
|
3472
|
+
"-extfile",
|
|
3473
|
+
n4(configFile),
|
|
3474
|
+
"-in",
|
|
3475
|
+
"private/cakey.csr",
|
|
3476
|
+
"-CA",
|
|
3477
|
+
n4(issuerCert),
|
|
3478
|
+
"-CAkey",
|
|
3479
|
+
n4(issuerKey),
|
|
3480
|
+
"-CAserial",
|
|
3481
|
+
n4(issuerSerial),
|
|
3482
|
+
"-out",
|
|
3483
|
+
"public/cacert.pem",
|
|
3484
|
+
...issuerPassin.args
|
|
3485
|
+
],
|
|
3486
|
+
{ ...options, env: issuerPassin.env }
|
|
3487
|
+
);
|
|
3488
|
+
};
|
|
3489
|
+
if (path8.resolve(issuerCA.rootDir) === caRootDir) {
|
|
3490
|
+
await signWithIssuer();
|
|
3491
|
+
} else {
|
|
3492
|
+
await issuerCA._withCaDirectoryLock(signWithIssuer);
|
|
3493
|
+
}
|
|
3494
|
+
} else {
|
|
3495
|
+
displayTitle("Generate CA Certificate (self-signed)");
|
|
3496
|
+
const passin = await this.#passin(ca);
|
|
3497
|
+
await execute_openssl(
|
|
3498
|
+
[
|
|
3499
|
+
"x509",
|
|
3500
|
+
"-sha256",
|
|
3501
|
+
"-req",
|
|
3502
|
+
"-days",
|
|
3503
|
+
"3650",
|
|
3504
|
+
"-extensions",
|
|
3505
|
+
"v3_ca",
|
|
3506
|
+
"-extfile",
|
|
3507
|
+
n4(configFile),
|
|
3508
|
+
"-in",
|
|
3509
|
+
"private/cakey.csr",
|
|
3510
|
+
"-signkey",
|
|
3511
|
+
n4(privateKeyFilename),
|
|
3512
|
+
"-out",
|
|
3513
|
+
"public/cacert.pem",
|
|
3514
|
+
...passin.args
|
|
3515
|
+
],
|
|
3516
|
+
{ ...options, env: passin.env }
|
|
3517
|
+
);
|
|
3518
|
+
}
|
|
3519
|
+
displaySubtitle("generate initial CRL (Certificate Revocation List)");
|
|
3520
|
+
await this.#regenerateCrlWith(ca, configFile, options);
|
|
3521
|
+
});
|
|
3522
|
+
displayTitle("Create Certificate Authority (CA) ---> DONE");
|
|
3523
|
+
}
|
|
3524
|
+
async regenerateCrl(ca) {
|
|
3525
|
+
await this.#withConfig(ca, caAltName(ca), (configFile, options) => this.#regenerateCrlWith(ca, configFile, options));
|
|
3526
|
+
}
|
|
3527
|
+
async signSubordinateCsr(ca, csrFile, certFile, validityDays) {
|
|
3528
|
+
await this.#withConfig(ca, caAltName(ca), async (configFile, options) => {
|
|
3529
|
+
const caRootDir = options.cwd;
|
|
3530
|
+
const passin = await this.#passin(ca);
|
|
3531
|
+
await execute_openssl(
|
|
3532
|
+
[
|
|
3533
|
+
"x509",
|
|
3534
|
+
"-sha256",
|
|
3535
|
+
"-req",
|
|
3536
|
+
"-days",
|
|
3537
|
+
String(validityDays),
|
|
3538
|
+
"-extensions",
|
|
3539
|
+
"v3_ca",
|
|
3540
|
+
"-extfile",
|
|
3541
|
+
n4(configFile),
|
|
3542
|
+
"-in",
|
|
3543
|
+
n4(csrFile),
|
|
3544
|
+
"-CA",
|
|
3545
|
+
n4(ca.caCertificate),
|
|
3546
|
+
"-CAkey",
|
|
3547
|
+
n4(path8.join(caRootDir, "private/cakey.pem")),
|
|
3548
|
+
"-CAserial",
|
|
3549
|
+
n4(path8.join(caRootDir, "serial")),
|
|
3550
|
+
"-out",
|
|
3551
|
+
n4(certFile),
|
|
3552
|
+
...passin.args
|
|
3553
|
+
],
|
|
3554
|
+
{ ...options, env: passin.env }
|
|
3555
|
+
);
|
|
3556
|
+
});
|
|
3557
|
+
}
|
|
3558
|
+
async signEndEntityCsr(ca, certificate, csr, params1, sanOverride) {
|
|
3559
|
+
await this.#withConfig(ca, buildSubjectAltNameString(sanOverride), async (configFile, options) => {
|
|
3560
|
+
displaySubtitle("- then we ask the authority to sign the certificate signing request");
|
|
3561
|
+
const passin = await this.#passin(ca);
|
|
3562
|
+
await execute_openssl(
|
|
3563
|
+
[
|
|
3564
|
+
"ca",
|
|
3565
|
+
"-config",
|
|
3566
|
+
configFile,
|
|
3567
|
+
"-startdate",
|
|
3568
|
+
x509Date(params1.startDate),
|
|
3569
|
+
"-enddate",
|
|
3570
|
+
x509Date(params1.endDate),
|
|
3571
|
+
"-batch",
|
|
3572
|
+
"-out",
|
|
3573
|
+
n4(certificate),
|
|
3574
|
+
"-in",
|
|
3575
|
+
n4(csr),
|
|
3576
|
+
...passin.args
|
|
3577
|
+
],
|
|
3578
|
+
{ ...options, env: passin.env }
|
|
3579
|
+
);
|
|
3580
|
+
displaySubtitle("- dump the certificate for a check");
|
|
3581
|
+
await execute_openssl(["x509", "-in", n4(certificate), "-dates", "-fingerprint", "-purpose", "-noout"], options);
|
|
3582
|
+
});
|
|
3583
|
+
}
|
|
3584
|
+
async revoke(ca, certificate, reason) {
|
|
3585
|
+
setEnv("RANDFILE", path8.join(ca.rootDir, "random.rnd"));
|
|
3586
|
+
await this.#withConfig(ca, caAltName(ca), async (configFile, options) => {
|
|
3587
|
+
displaySubtitle("Revoke certificate");
|
|
3588
|
+
const passin = await this.#passin(ca);
|
|
3589
|
+
await execute_openssl_no_failure(
|
|
3590
|
+
["ca", "-verbose", "-config", n4(configFile), "-revoke", certificate, "-crl_reason", reason, ...passin.args],
|
|
3591
|
+
{ ...options, env: passin.env }
|
|
3592
|
+
);
|
|
3593
|
+
await this.#regenerateCrlWith(ca, configFile, options);
|
|
3594
|
+
displaySubtitle("Verify that certificate is revoked");
|
|
3595
|
+
await execute_openssl_no_failure(
|
|
3596
|
+
[
|
|
3597
|
+
"verify",
|
|
3598
|
+
"-verbose",
|
|
3599
|
+
"-CRLfile",
|
|
3600
|
+
n4(ca.revocationList),
|
|
3601
|
+
"-CAfile",
|
|
3602
|
+
n4(ca.caCertificate),
|
|
3603
|
+
"-crl_check",
|
|
3604
|
+
n4(certificate)
|
|
3605
|
+
],
|
|
3606
|
+
options
|
|
3607
|
+
);
|
|
3608
|
+
displaySubtitle("Produce CRL in DER form ");
|
|
3609
|
+
await execute_openssl(
|
|
3610
|
+
["crl", "-in", n4(ca.revocationList), "-out", "crl/revocation_list.der", "-outform", "der"],
|
|
3611
|
+
options
|
|
3612
|
+
);
|
|
3613
|
+
displaySubtitle("Produce CRL in PEM form ");
|
|
3614
|
+
await execute_openssl(
|
|
3615
|
+
["crl", "-in", n4(ca.revocationList), "-out", "crl/revocation_list.pem", "-outform", "pem", "-text"],
|
|
3616
|
+
options
|
|
3617
|
+
);
|
|
3618
|
+
});
|
|
3619
|
+
}
|
|
3620
|
+
async createSelfSignedCertificate(ca, certificateFile, privateKeyFile, params) {
|
|
3621
|
+
const envOverrides = caConfigEnvOverrides(ca, buildSubjectAltNameString(params));
|
|
3622
|
+
const configFile = generateStaticConfig(ca.configFile, { cwd: ca.rootDir }, envOverrides);
|
|
3623
|
+
const options = {
|
|
3624
|
+
cwd: ca.rootDir,
|
|
3625
|
+
openssl_conf: makePath(configFile)
|
|
3626
|
+
};
|
|
3627
|
+
try {
|
|
3628
|
+
const subject = params.subject ? new Subject5(params.subject).toString() : "";
|
|
3629
|
+
const subjectOptions = subject && subject.length > 1 ? ["-subj", subject] : [];
|
|
3630
|
+
const csrFile = `${certificateFile}_csr`;
|
|
3631
|
+
const passin = await this.#passin(ca);
|
|
3632
|
+
displaySubtitle("- the certificate signing request");
|
|
3633
|
+
await execute_openssl(
|
|
3634
|
+
[
|
|
3635
|
+
"req",
|
|
3636
|
+
"-new",
|
|
3637
|
+
"-sha256",
|
|
3638
|
+
...subjectOptions,
|
|
3639
|
+
"-batch",
|
|
3640
|
+
"-key",
|
|
3641
|
+
n4(privateKeyFile),
|
|
3642
|
+
"-out",
|
|
3643
|
+
n4(csrFile),
|
|
3644
|
+
...passin.args
|
|
3645
|
+
],
|
|
3646
|
+
{ ...options, env: passin.env }
|
|
3647
|
+
);
|
|
3648
|
+
displaySubtitle("- creating the self-signed certificate");
|
|
3649
|
+
await execute_openssl(
|
|
3650
|
+
[
|
|
3651
|
+
"ca",
|
|
3652
|
+
"-selfsign",
|
|
3653
|
+
"-keyfile",
|
|
3654
|
+
n4(privateKeyFile),
|
|
3655
|
+
"-startdate",
|
|
3656
|
+
x509Date(params.startDate),
|
|
3657
|
+
"-enddate",
|
|
3658
|
+
x509Date(params.endDate),
|
|
3659
|
+
"-batch",
|
|
3660
|
+
"-out",
|
|
3661
|
+
n4(certificateFile),
|
|
3662
|
+
"-in",
|
|
3663
|
+
n4(csrFile),
|
|
3664
|
+
...passin.args
|
|
3665
|
+
],
|
|
3666
|
+
{ ...options, env: passin.env }
|
|
3667
|
+
);
|
|
3668
|
+
displaySubtitle("- dump the certificate for a check");
|
|
3669
|
+
await execute_openssl(["x509", "-in", n4(certificateFile), "-dates", "-fingerprint", "-purpose", "-noout"], {});
|
|
3670
|
+
displaySubtitle("- verify self-signed certificate");
|
|
3671
|
+
await execute_openssl_no_failure(["verify", "-verbose", "-CAfile", n4(certificateFile), n4(certificateFile)], options);
|
|
3672
|
+
await fs11.promises.unlink(csrFile);
|
|
3673
|
+
} finally {
|
|
3674
|
+
await cleanupStaticConfig(configFile, { cwd: ca.rootDir });
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
};
|
|
3678
|
+
}
|
|
3679
|
+
});
|
|
3680
|
+
|
|
3681
|
+
// packages/node-opcua-pki/lib/ca/templates/ca_config_template.cnf.ts
|
|
3682
|
+
var config2, ca_config_template_cnf_default;
|
|
3683
|
+
var init_ca_config_template_cnf = __esm({
|
|
3684
|
+
"packages/node-opcua-pki/lib/ca/templates/ca_config_template.cnf.ts"() {
|
|
3685
|
+
"use strict";
|
|
3686
|
+
init_esm_shims();
|
|
3687
|
+
config2 = `#.........DO NOT MODIFY BY HAND .........................
|
|
3688
|
+
[ ca ]
|
|
3689
|
+
default_ca = CA_default
|
|
3690
|
+
[ CA_default ]
|
|
3691
|
+
dir = "%%ROOT_FOLDER%%" # the main CA folder (quoted: see renderCaConfig)
|
|
3692
|
+
certs = $dir/certs # where to store certificates
|
|
3693
|
+
new_certs_dir = $dir/certs #
|
|
3694
|
+
database = $dir/index.txt # the certificate database
|
|
3695
|
+
serial = $dir/serial # the serial number counter
|
|
3696
|
+
certificate = $dir/public/cacert.pem # The root CA certificate
|
|
3697
|
+
private_key = $dir/private/cakey.pem # the CA private key
|
|
3698
|
+
x509_extensions = usr_cert #
|
|
3699
|
+
default_days = 3650 # default validity : 10 years
|
|
3700
|
+
|
|
3701
|
+
# default_md = sha1
|
|
3702
|
+
|
|
3703
|
+
default_md = sha256 # The default digest algorithm
|
|
3704
|
+
|
|
3705
|
+
preserve = no
|
|
3706
|
+
policy = policy_match
|
|
3707
|
+
# randfile = $dir/random.rnd
|
|
3708
|
+
# default_startdate = YYMMDDHHMMSSZ
|
|
3709
|
+
# default_enddate = YYMMDDHHMMSSZ
|
|
3710
|
+
crl_dir = $dir/crl
|
|
3711
|
+
crl_extensions = crl_ext
|
|
3712
|
+
crl = $dir/crl/revocation_list.crl # the Revocation list
|
|
3713
|
+
crlnumber = $dir/crlnumber # CRL number file
|
|
3714
|
+
default_crl_days = 30
|
|
3715
|
+
default_crl_hours = 24
|
|
3716
|
+
#msie_hack
|
|
3717
|
+
|
|
3718
|
+
[ policy_match ]
|
|
3719
|
+
countryName = optional
|
|
3720
|
+
stateOrProvinceName = optional
|
|
3721
|
+
localityName = optional
|
|
3722
|
+
organizationName = optional
|
|
3723
|
+
organizationalUnitName = optional
|
|
3724
|
+
commonName = optional
|
|
3725
|
+
emailAddress = optional
|
|
3726
|
+
|
|
3727
|
+
[ req ]
|
|
3728
|
+
default_bits = 4096 # Size of keys
|
|
3729
|
+
default_keyfile = key.pem # name of generated keys
|
|
3730
|
+
distinguished_name = req_distinguished_name
|
|
3731
|
+
attributes = req_attributes
|
|
3732
|
+
x509_extensions = v3_ca
|
|
3733
|
+
#input_password
|
|
3734
|
+
#output_password
|
|
3735
|
+
string_mask = nombstr # permitted characters
|
|
3736
|
+
req_extensions = v3_req
|
|
3737
|
+
|
|
3738
|
+
[ req_distinguished_name ]
|
|
3739
|
+
|
|
3740
|
+
#0 countryName = Country Name (2 letter code)
|
|
3741
|
+
# countryName_default = FR
|
|
3742
|
+
# countryName_min = 2
|
|
3743
|
+
# countryName_max = 2
|
|
3744
|
+
# stateOrProvinceName = State or Province Name (full name)
|
|
3745
|
+
# stateOrProvinceName_default = Ile de France
|
|
3746
|
+
# localityName = Locality Name (city, district)
|
|
3747
|
+
# localityName_default = Paris
|
|
3748
|
+
organizationName = Organization Name (company)
|
|
3749
|
+
organizationName_default = NodeOPCUA
|
|
3750
|
+
# organizationalUnitName = Organizational Unit Name (department, division)
|
|
3751
|
+
# organizationalUnitName_default = R&D
|
|
3752
|
+
commonName = Common Name (hostname, FQDN, IP, or your name)
|
|
3753
|
+
commonName_max = 256
|
|
3754
|
+
commonName_default = NodeOPCUA
|
|
3755
|
+
# emailAddress = Email Address
|
|
3756
|
+
# emailAddress_max = 40
|
|
3757
|
+
# emailAddress_default = node-opcua (at) node-opcua (dot) com
|
|
3758
|
+
|
|
3759
|
+
[ req_attributes ]
|
|
3760
|
+
#challengePassword = A challenge password
|
|
3761
|
+
#challengePassword_min = 4
|
|
3762
|
+
#challengePassword_max = 20
|
|
3763
|
+
#unstructuredName = An optional company name
|
|
3764
|
+
[ usr_cert ]
|
|
3765
|
+
basicConstraints = critical, CA:FALSE
|
|
3766
|
+
subjectKeyIdentifier = hash
|
|
3767
|
+
authorityKeyIdentifier = keyid,issuer:always
|
|
3768
|
+
#authorityKeyIdentifier = keyid
|
|
3769
|
+
subjectAltName = $ENV::ALTNAME
|
|
3770
|
+
# issuerAltName = issuer:copy
|
|
3771
|
+
nsComment = ''OpenSSL Generated Certificate''
|
|
3772
|
+
#nsCertType = client, email, objsign for ''everything including object signing''
|
|
3773
|
+
#nsCaRevocationUrl = http://www.domain.dom/ca-crl.pem
|
|
3774
|
+
#nsBaseUrl =
|
|
3775
|
+
#nsRenewalUrl =
|
|
3776
|
+
#nsCaPolicyUrl =
|
|
3777
|
+
#nsSslServerName =
|
|
3778
|
+
keyUsage = critical, digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment, keyAgreement
|
|
3779
|
+
extendedKeyUsage = critical,serverAuth ,clientAuth
|
|
3780
|
+
{{#CDP_URL}}crlDistributionPoints = URI:$ENV::CDP_URL
|
|
3781
|
+
{{/CDP_URL}}{{#AIA_VALUE}}authorityInfoAccess = $ENV::AIA_VALUE
|
|
3782
|
+
{{/AIA_VALUE}}
|
|
3783
|
+
[ v3_req ]
|
|
3784
|
+
basicConstraints = critical, CA:FALSE
|
|
3785
|
+
keyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment, keyAgreement
|
|
3786
|
+
extendedKeyUsage = critical,serverAuth ,clientAuth
|
|
3787
|
+
subjectAltName = $ENV::ALTNAME
|
|
3788
|
+
nsComment = "CA Generated by Node-OPCUA Certificate utility using openssl"
|
|
3789
|
+
[ v3_ca_req ]
|
|
3790
|
+
subjectKeyIdentifier = hash
|
|
3791
|
+
basicConstraints = CA:TRUE
|
|
3792
|
+
keyUsage = critical, cRLSign, keyCertSign
|
|
3793
|
+
subjectAltName = $ENV::ALTNAME
|
|
3794
|
+
nsComment = "CA CSR generated by Node-OPCUA Certificate utility using openssl"
|
|
3795
|
+
[ v3_ca ]
|
|
3796
|
+
subjectKeyIdentifier = hash
|
|
3797
|
+
authorityKeyIdentifier = keyid:always,issuer:always
|
|
3798
|
+
basicConstraints = CA:TRUE
|
|
3799
|
+
keyUsage = critical, cRLSign, keyCertSign
|
|
3800
|
+
subjectAltName = $ENV::ALTNAME
|
|
3801
|
+
nsComment = "CA Certificate generated by Node-OPCUA Certificate utility using openssl"
|
|
3802
|
+
#nsCertType = sslCA, emailCA
|
|
3803
|
+
#issuerAltName = issuer:copy
|
|
3804
|
+
#obj = DER:02:03
|
|
3805
|
+
{{#CDP_URL}}crlDistributionPoints = URI:$ENV::CDP_URL
|
|
3806
|
+
{{/CDP_URL}}{{#AIA_VALUE}}authorityInfoAccess = $ENV::AIA_VALUE
|
|
3807
|
+
{{/AIA_VALUE}}[ v3_selfsigned]
|
|
3808
|
+
basicConstraints = critical, CA:FALSE
|
|
3809
|
+
keyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment, keyAgreement
|
|
3810
|
+
extendedKeyUsage = critical,serverAuth ,clientAuth
|
|
3811
|
+
nsComment = "Self-signed certificate, generated by NodeOPCUA"
|
|
3812
|
+
subjectAltName = $ENV::ALTNAME
|
|
3813
|
+
|
|
3814
|
+
[ crl_ext ]
|
|
3815
|
+
#issuerAltName = issuer:copy
|
|
3816
|
+
authorityKeyIdentifier = keyid:always,issuer:always
|
|
3817
|
+
#authorityInfoAccess = @issuer_info`;
|
|
3818
|
+
ca_config_template_cnf_default = config2;
|
|
3819
|
+
}
|
|
3820
|
+
});
|
|
3821
|
+
|
|
3822
|
+
// packages/node-opcua-pki/lib/ca/core/certificate_authority_core.ts
|
|
3823
|
+
import assert9 from "assert";
|
|
3824
|
+
import fs12 from "fs";
|
|
3825
|
+
import os4 from "os";
|
|
3826
|
+
import path9 from "path";
|
|
3827
|
+
import { withLock as withLock2 } from "@ster5/global-mutex";
|
|
3828
|
+
import chalk6 from "chalk";
|
|
3829
|
+
import {
|
|
3830
|
+
CertificatePurpose as CertificatePurpose3,
|
|
3831
|
+
certificateMatchesPrivateKey,
|
|
3832
|
+
convertPEMtoDER as convertPEMtoDER2,
|
|
3833
|
+
createCertificateSigningRequest as createCertificateSigningRequest3,
|
|
3834
|
+
createPfx,
|
|
3835
|
+
exploreCertificate as exploreCertificate2,
|
|
3836
|
+
exploreCertificateSigningRequest,
|
|
3837
|
+
generatePrivateKeyFile as generatePrivateKeyFile2,
|
|
3838
|
+
privateKeyToCryptoKey as privateKeyToCryptoKey2,
|
|
3839
|
+
readCertificatePEM as readCertificatePEM2,
|
|
3840
|
+
readCertificateSigningRequest,
|
|
3841
|
+
readPrivateKey as readPrivateKey3,
|
|
3842
|
+
Subject as Subject6,
|
|
3843
|
+
toPem as toPem2,
|
|
3844
|
+
verifyCertificateSignature as verifyCertificateSignature2,
|
|
3845
|
+
writePrivateKeyFile as writePrivateKeyFile2,
|
|
3846
|
+
x509 as x5092
|
|
3847
|
+
} from "node-opcua-crypto";
|
|
3848
|
+
function escapeOpensslConfDoubleQuoted(value) {
|
|
3849
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
3850
|
+
}
|
|
3851
|
+
function renderCaConfig(caRootDir) {
|
|
3852
|
+
return configurationFileTemplate.replace(/%%ROOT_FOLDER%%/, escapeOpensslConfDoubleQuoted(makePath(caRootDir)));
|
|
3853
|
+
}
|
|
3854
|
+
function octetStringToIpAddress(a) {
|
|
3855
|
+
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();
|
|
3856
|
+
}
|
|
3857
|
+
function validateRevocationUrl(url, fieldName) {
|
|
3858
|
+
if (url === void 0) {
|
|
3859
|
+
return void 0;
|
|
3860
|
+
}
|
|
3861
|
+
if (url === "") {
|
|
3862
|
+
throw new Error(`${fieldName} must not be empty \u2014 pass undefined to disable the extension`);
|
|
3863
|
+
}
|
|
3864
|
+
let parsed;
|
|
3865
|
+
try {
|
|
3866
|
+
parsed = new URL(url);
|
|
3867
|
+
} catch {
|
|
3868
|
+
throw new Error(`${fieldName} is not a valid URL: ${url}`);
|
|
3869
|
+
}
|
|
3870
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
3871
|
+
throw new Error(`${fieldName} must use http: or https: (got ${parsed.protocol} in ${url})`);
|
|
3872
|
+
}
|
|
3873
|
+
if (!parsed.pathname || parsed.pathname === "/") {
|
|
3874
|
+
throw new Error(`${fieldName} must include a path component (got ${url})`);
|
|
3187
3875
|
}
|
|
3188
3876
|
const isLoopback = parsed.hostname === "localhost" || parsed.hostname === "::1" || parsed.hostname.startsWith("127.");
|
|
3189
3877
|
if (isLoopback) {
|
|
@@ -3193,27 +3881,25 @@ function validateRevocationUrl(url, fieldName) {
|
|
|
3193
3881
|
}
|
|
3194
3882
|
return url;
|
|
3195
3883
|
}
|
|
3196
|
-
var defaultSubject, configurationFileTemplate,
|
|
3197
|
-
var
|
|
3198
|
-
"packages/node-opcua-pki/lib/ca/
|
|
3884
|
+
var defaultSubject, configurationFileTemplate, config3, CA_LOCK_MAX_WAIT_MS, CA_LOCK_RETRY, CertificateAuthorityCore;
|
|
3885
|
+
var init_certificate_authority_core = __esm({
|
|
3886
|
+
"packages/node-opcua-pki/lib/ca/core/certificate_authority_core.ts"() {
|
|
3199
3887
|
"use strict";
|
|
3200
3888
|
init_esm_shims();
|
|
3201
|
-
init_toolbox_pfx();
|
|
3202
3889
|
init_toolbox();
|
|
3203
|
-
|
|
3204
|
-
init_simple_config_template_cnf();
|
|
3890
|
+
init_ca_database();
|
|
3205
3891
|
init_ca_config_template_cnf();
|
|
3206
3892
|
defaultSubject = "/C=FR/ST=IDF/L=Paris/O=Local NODE-OPCUA Certificate Authority/CN=NodeOPCUA-CA";
|
|
3207
3893
|
configurationFileTemplate = ca_config_template_cnf_default;
|
|
3208
|
-
configurationFileSimpleTemplate2 = simple_config_template_cnf_default;
|
|
3209
3894
|
config3 = {
|
|
3210
3895
|
certificateDir: "INVALID",
|
|
3211
3896
|
forceCA: false,
|
|
3212
3897
|
pkiDir: "INVALID"
|
|
3213
3898
|
};
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3899
|
+
CA_LOCK_MAX_WAIT_MS = 5 * 6e4;
|
|
3900
|
+
CA_LOCK_RETRY = { minTimeout: 20, maxTimeout: 250 };
|
|
3901
|
+
assert9(octetStringToIpAddress("c07b9179") === "192.123.145.121");
|
|
3902
|
+
CertificateAuthorityCore = class {
|
|
3217
3903
|
/** RSA key size used when generating the CA private key. */
|
|
3218
3904
|
keySize;
|
|
3219
3905
|
/** Root filesystem path of the CA directory structure. */
|
|
@@ -3230,14 +3916,41 @@ var init_certificate_authority = __esm({
|
|
|
3230
3916
|
/** resolved once (see `privateKeyPassphrase`); `#passphraseResolved` distinguishes "none" from "not yet" */
|
|
3231
3917
|
#resolvedPassphrase;
|
|
3232
3918
|
#passphraseResolved = false;
|
|
3919
|
+
/** Signing backend: `openssl` shells out to the CLI, `native` signs in-process. */
|
|
3920
|
+
#backend;
|
|
3921
|
+
/** External signing key (HSM/KMS), when one was supplied instead of a key file. */
|
|
3922
|
+
#signer;
|
|
3923
|
+
/** Read access to `index.txt` / `certs/<SERIAL>.pem`. */
|
|
3924
|
+
#db;
|
|
3233
3925
|
constructor(options) {
|
|
3234
|
-
|
|
3235
|
-
|
|
3926
|
+
assert9(Object.prototype.hasOwnProperty.call(options, "location"));
|
|
3927
|
+
assert9(Object.prototype.hasOwnProperty.call(options, "keySize"));
|
|
3236
3928
|
this.location = options.location;
|
|
3237
3929
|
this.keySize = options.keySize || 2048;
|
|
3238
|
-
this.subject = new
|
|
3930
|
+
this.subject = new Subject6(options.subject || defaultSubject);
|
|
3239
3931
|
this._issuerCA = options.issuerCA;
|
|
3240
3932
|
this.#privateKeyPassphrase = options.privateKeyPassphrase;
|
|
3933
|
+
this.#signer = options.signer;
|
|
3934
|
+
if (options.signer) {
|
|
3935
|
+
const algorithm = options.signer.algorithm;
|
|
3936
|
+
if (algorithm.name !== "RSASSA-PKCS1-v1_5" && algorithm.name !== "ECDSA") {
|
|
3937
|
+
throw new Error(
|
|
3938
|
+
`CertificateAuthority: signer algorithm ${algorithm.name} is not supported - use RSASSA-PKCS1-v1_5 or ECDSA.`
|
|
3939
|
+
);
|
|
3940
|
+
}
|
|
3941
|
+
if (algorithm.name === "ECDSA" && !algorithm.namedCurve) {
|
|
3942
|
+
throw new Error(
|
|
3943
|
+
"CertificateAuthority: an ECDSA signer must declare its namedCurve (P-256, P-384 or P-521) - importing the signer's public key needs the curve, and an SPKI import cannot infer it."
|
|
3944
|
+
);
|
|
3945
|
+
}
|
|
3946
|
+
}
|
|
3947
|
+
if (options.signer && !options.backend.supportsExternalSigner) {
|
|
3948
|
+
throw new Error(
|
|
3949
|
+
"CertificateAuthority: this backend cannot sign with an external signer - it loads its key from a file. Use a backend that supports one, such as NativeCaBackend."
|
|
3950
|
+
);
|
|
3951
|
+
}
|
|
3952
|
+
this.#backend = options.backend;
|
|
3953
|
+
this.#db = new CaDatabase(this.location);
|
|
3241
3954
|
if (options.crlDistributionUrl !== void 0) {
|
|
3242
3955
|
this.setCrlDistributionUrl(options.crlDistributionUrl);
|
|
3243
3956
|
}
|
|
@@ -3296,35 +4009,10 @@ var init_certificate_authority = __esm({
|
|
|
3296
4009
|
* the `authorityInfoAccess` extension on every subsequently-issued
|
|
3297
4010
|
* certificate. Pass `undefined` to disable.
|
|
3298
4011
|
*
|
|
3299
|
-
* @see US-202
|
|
3300
|
-
*/
|
|
3301
|
-
setCaIssuersUrl(url) {
|
|
3302
|
-
this._caIssuersUrl = validateRevocationUrl(url, "caIssuersUrl");
|
|
3303
|
-
}
|
|
3304
|
-
/**
|
|
3305
|
-
* @internal
|
|
3306
|
-
* Populate the OpenSSL config substitution env vars (`CDP_URL` and
|
|
3307
|
-
* `AIA_VALUE`) from the configured URLs, or unset them so the
|
|
3308
|
-
* matching `{{#KEY}}...{{/KEY}}` blocks in the templates are
|
|
3309
|
-
* stripped. MUST be called before every `generateStaticConfig`
|
|
3310
|
-
* invocation that signs a certificate.
|
|
4012
|
+
* @see US-202
|
|
3311
4013
|
*/
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
unsetEnv("AIA_VALUE");
|
|
3315
|
-
if (this._crlDistributionUrl) {
|
|
3316
|
-
setEnv("CDP_URL", this._crlDistributionUrl);
|
|
3317
|
-
}
|
|
3318
|
-
const aiaLegs = [];
|
|
3319
|
-
if (this._ocspResponderUrl) {
|
|
3320
|
-
aiaLegs.push(`OCSP;URI:${this._ocspResponderUrl}`);
|
|
3321
|
-
}
|
|
3322
|
-
if (this._caIssuersUrl) {
|
|
3323
|
-
aiaLegs.push(`caIssuers;URI:${this._caIssuersUrl}`);
|
|
3324
|
-
}
|
|
3325
|
-
if (aiaLegs.length > 0) {
|
|
3326
|
-
setEnv("AIA_VALUE", aiaLegs.join(","));
|
|
3327
|
-
}
|
|
4014
|
+
setCaIssuersUrl(url) {
|
|
4015
|
+
this._caIssuersUrl = validateRevocationUrl(url, "caIssuersUrl");
|
|
3328
4016
|
}
|
|
3329
4017
|
/** Absolute path to the CA root directory (alias for {@link location}). */
|
|
3330
4018
|
get rootDir() {
|
|
@@ -3332,11 +4020,11 @@ var init_certificate_authority = __esm({
|
|
|
3332
4020
|
}
|
|
3333
4021
|
/** Path to the OpenSSL configuration file (`conf/caconfig.cnf`). */
|
|
3334
4022
|
get configFile() {
|
|
3335
|
-
return
|
|
4023
|
+
return path9.normalize(path9.join(this.rootDir, "./conf/caconfig.cnf"));
|
|
3336
4024
|
}
|
|
3337
4025
|
/** Path to the CA private key (`private/cakey.pem`); may be passphrase-encrypted, see {@link getPrivateKey}. */
|
|
3338
4026
|
get privateKey() {
|
|
3339
|
-
return
|
|
4027
|
+
return path9.join(path9.resolve(this.rootDir), "private/cakey.pem");
|
|
3340
4028
|
}
|
|
3341
4029
|
/**
|
|
3342
4030
|
* The CA private key, decrypted with the configured `privateKeyPassphrase`
|
|
@@ -3344,7 +4032,51 @@ var init_certificate_authority = __esm({
|
|
|
3344
4032
|
* on an encrypted key with no or the wrong passphrase.
|
|
3345
4033
|
*/
|
|
3346
4034
|
async getPrivateKey() {
|
|
3347
|
-
|
|
4035
|
+
this.#assertKeyIsOnDisk("getPrivateKey");
|
|
4036
|
+
return readPrivateKey3(this.privateKey, await this._privateKeyPassphrase());
|
|
4037
|
+
}
|
|
4038
|
+
/**
|
|
4039
|
+
* True when this CA signs with an external {@link CaSigner} (HSM/KMS)
|
|
4040
|
+
* rather than with `private/cakey.pem`. There is then no private key
|
|
4041
|
+
* file on disk, and never was one.
|
|
4042
|
+
*/
|
|
4043
|
+
get hasExternalSigner() {
|
|
4044
|
+
return this.#signer !== void 0;
|
|
4045
|
+
}
|
|
4046
|
+
/**
|
|
4047
|
+
* Does `cert` certify the key this CA signs with? With the key on disk
|
|
4048
|
+
* that is a private-key match; with a signer there is no private key to
|
|
4049
|
+
* match, so compare the certified public key against the signer's own -
|
|
4050
|
+
* the same question, asked the only way an HSM allows.
|
|
4051
|
+
*/
|
|
4052
|
+
async #certificateMatchesOurKey(certPem, certDer) {
|
|
4053
|
+
if (this.#signer) {
|
|
4054
|
+
const ours = Buffer.from(await this.#signer.getPublicKey());
|
|
4055
|
+
const certified = Buffer.from(new x5092.X509Certificate(certPem).publicKey.rawData);
|
|
4056
|
+
return ours.equals(certified);
|
|
4057
|
+
}
|
|
4058
|
+
return certificateMatchesPrivateKey(certDer, await this.getPrivateKey());
|
|
4059
|
+
}
|
|
4060
|
+
/** Reject the key-file-only operations up front, rather than failing on a missing file later. */
|
|
4061
|
+
#assertKeyIsOnDisk(what) {
|
|
4062
|
+
if (this.#signer) {
|
|
4063
|
+
throw new Error(
|
|
4064
|
+
`CertificateAuthority.${what} is not available on a signer-backed CA: the private key lives in the signer (HSM/KMS) and is never written to disk.`
|
|
4065
|
+
);
|
|
4066
|
+
}
|
|
4067
|
+
}
|
|
4068
|
+
/**
|
|
4069
|
+
* @internal The key every signing operation goes through. An injected
|
|
4070
|
+
* {@link CaSigner} is returned as-is; otherwise the on-disk key is read
|
|
4071
|
+
* (and decrypted) and handed over as a plain `CryptoKey`. Both are
|
|
4072
|
+
* accepted by the `node-opcua-crypto` signing primitives, so callers
|
|
4073
|
+
* never branch on which one they got.
|
|
4074
|
+
*/
|
|
4075
|
+
async _getSigningKey() {
|
|
4076
|
+
if (this.#signer) {
|
|
4077
|
+
return this.#signer;
|
|
4078
|
+
}
|
|
4079
|
+
return privateKeyToCryptoKey2(await this.getPrivateKey());
|
|
3348
4080
|
}
|
|
3349
4081
|
/**
|
|
3350
4082
|
* Enable, disable, or rotate the passphrase protecting `private/cakey.pem`
|
|
@@ -3353,18 +4085,19 @@ var init_certificate_authority = __esm({
|
|
|
3353
4085
|
* passphrase to continue using it.
|
|
3354
4086
|
*/
|
|
3355
4087
|
async reencryptPrivateKey(oldPassphrase, newPassphrase) {
|
|
4088
|
+
this.#assertKeyIsOnDisk("reencryptPrivateKey");
|
|
3356
4089
|
const oldPass = await resolvePrivateKeyPassphrase(oldPassphrase);
|
|
3357
4090
|
const newPass = await resolvePrivateKeyPassphrase(newPassphrase);
|
|
3358
|
-
const key =
|
|
4091
|
+
const key = readPrivateKey3(this.privateKey, oldPass);
|
|
3359
4092
|
await this.#rewritePrivateKeyFile(key, newPass);
|
|
3360
4093
|
}
|
|
3361
4094
|
async #rewritePrivateKeyFile(privateKey, passphrase) {
|
|
3362
4095
|
const tmpFilename = `${this.privateKey}.${process.pid}-${Date.now()}.tmp`;
|
|
3363
4096
|
try {
|
|
3364
4097
|
await writePrivateKeyFile2(tmpFilename, privateKey, { passphrase });
|
|
3365
|
-
await
|
|
4098
|
+
await fs12.promises.rename(tmpFilename, this.privateKey);
|
|
3366
4099
|
} finally {
|
|
3367
|
-
await
|
|
4100
|
+
await fs12.promises.rm(tmpFilename, { force: true });
|
|
3368
4101
|
}
|
|
3369
4102
|
}
|
|
3370
4103
|
/** @internal resolve the configured passphrase, at most once per instance */
|
|
@@ -3375,10 +4108,6 @@ var init_certificate_authority = __esm({
|
|
|
3375
4108
|
}
|
|
3376
4109
|
return this.#resolvedPassphrase;
|
|
3377
4110
|
}
|
|
3378
|
-
/** @internal `-passin env:` argv + env for every openssl call that loads this CA's key (always emitted, empty when none) */
|
|
3379
|
-
async _opensslPassin() {
|
|
3380
|
-
return passinArg(await this._privateKeyPassphrase());
|
|
3381
|
-
}
|
|
3382
4111
|
/**
|
|
3383
4112
|
* @internal On an existing key: encrypt it in place if a passphrase is
|
|
3384
4113
|
* configured and it is still plaintext (secure by default: the option
|
|
@@ -3387,16 +4116,58 @@ var init_certificate_authority = __esm({
|
|
|
3387
4116
|
* first signing operation.
|
|
3388
4117
|
*/
|
|
3389
4118
|
async _ensurePrivateKeyProtection() {
|
|
3390
|
-
if (!
|
|
4119
|
+
if (this.#signer || !fs12.existsSync(this.privateKey)) {
|
|
3391
4120
|
return;
|
|
3392
4121
|
}
|
|
3393
4122
|
if (this.#privateKeyPassphrase !== void 0 && !isEncryptedPrivateKeyFile(this.privateKey)) {
|
|
3394
4123
|
warningLog("CertificateAuthority: private key is plaintext but a passphrase is configured; encrypting it in place");
|
|
3395
|
-
const plaintextKey =
|
|
4124
|
+
const plaintextKey = readPrivateKey3(this.privateKey);
|
|
3396
4125
|
await this.#rewritePrivateKeyFile(plaintextKey, await this._privateKeyPassphrase());
|
|
3397
4126
|
}
|
|
3398
4127
|
await this.getPrivateKey();
|
|
3399
4128
|
}
|
|
4129
|
+
/**
|
|
4130
|
+
* Acquire a file-based lock on this CA's directory for the duration of
|
|
4131
|
+
* `action` — serializes every operation that mutates the certificate
|
|
4132
|
+
* database (`index.txt`, `serial`, `crlnumber`, `certs/`,
|
|
4133
|
+
* `crl/revocation_list.*`) or the CA's own key/certificate, so
|
|
4134
|
+
* concurrent calls on one `CertificateAuthority` (or two instances
|
|
4135
|
+
* pointed at the same directory, in this or another process) cannot
|
|
4136
|
+
* interleave. Mirrors `CertificateManager.withLock2`.
|
|
4137
|
+
*
|
|
4138
|
+
* Only ever call this from a top-level public operation — nested calls
|
|
4139
|
+
* on the same instance would deadlock, since the underlying file lock
|
|
4140
|
+
* is not reentrant.
|
|
4141
|
+
*
|
|
4142
|
+
* The wait is bounded: a waiter gives up (throws) after
|
|
4143
|
+
* `CA_LOCK_MAX_WAIT_MS`. Without a bound, a holder whose openssl child
|
|
4144
|
+
* hangs would block every CA operation in every process forever and
|
|
4145
|
+
* silently — the lock library's keepalive keeps refreshing the lock
|
|
4146
|
+
* file's mtime as long as the holder process is alive, so stale-lock
|
|
4147
|
+
* recovery never fires for a live-but-stuck holder. A dead holder's
|
|
4148
|
+
* lock goes stale (default 2 minutes) and is taken over well within
|
|
4149
|
+
* this bound.
|
|
4150
|
+
*/
|
|
4151
|
+
async #withCaLock(action) {
|
|
4152
|
+
const lockFileName = path9.join(this.rootDir, ".ca.lock");
|
|
4153
|
+
return withLock2(
|
|
4154
|
+
{
|
|
4155
|
+
fileToLock: lockFileName,
|
|
4156
|
+
retries: { forever: true, maxRetryTime: CA_LOCK_MAX_WAIT_MS, ...CA_LOCK_RETRY }
|
|
4157
|
+
},
|
|
4158
|
+
action
|
|
4159
|
+
);
|
|
4160
|
+
}
|
|
4161
|
+
/**
|
|
4162
|
+
* @internal Run `action` under this CA's directory lock. For the one
|
|
4163
|
+
* legitimate cross-instance use: a subordinate CA's bootstrap signs its
|
|
4164
|
+
* certificate with `openssl x509 -CAserial`, which read-increment-writes
|
|
4165
|
+
* THIS issuer's `serial` file, and must therefore hold this issuer's
|
|
4166
|
+
* lock (ordering is always subordinate -> issuer, so no cycle).
|
|
4167
|
+
*/
|
|
4168
|
+
async _withCaDirectoryLock(action) {
|
|
4169
|
+
return this.#withCaLock(action);
|
|
4170
|
+
}
|
|
3400
4171
|
/** Path to the CA certificate in PEM format (`public/cacert.pem`). */
|
|
3401
4172
|
get caCertificate() {
|
|
3402
4173
|
return makePath(this.rootDir, "./public/cacert.pem");
|
|
@@ -3444,8 +4215,8 @@ var init_certificate_authority = __esm({
|
|
|
3444
4215
|
* (call {@link initialize} first).
|
|
3445
4216
|
*/
|
|
3446
4217
|
getCACertificateDER() {
|
|
3447
|
-
const pem =
|
|
3448
|
-
return
|
|
4218
|
+
const pem = readCertificatePEM2(this.caCertificate);
|
|
4219
|
+
return convertPEMtoDER2(pem);
|
|
3449
4220
|
}
|
|
3450
4221
|
/**
|
|
3451
4222
|
* Return the CA certificate as a PEM-encoded string.
|
|
@@ -3454,7 +4225,7 @@ var init_certificate_authority = __esm({
|
|
|
3454
4225
|
* (call {@link initialize} first).
|
|
3455
4226
|
*/
|
|
3456
4227
|
getCACertificatePEM() {
|
|
3457
|
-
const raw =
|
|
4228
|
+
const raw = readCertificatePEM2(this.caCertificate);
|
|
3458
4229
|
const beginMarker = "-----BEGIN CERTIFICATE-----";
|
|
3459
4230
|
const idx = raw.indexOf(beginMarker);
|
|
3460
4231
|
if (idx > 0) {
|
|
@@ -3470,10 +4241,10 @@ var init_certificate_authority = __esm({
|
|
|
3470
4241
|
*/
|
|
3471
4242
|
getCRLDER() {
|
|
3472
4243
|
const crlPath = this.revocationListDER;
|
|
3473
|
-
if (!
|
|
4244
|
+
if (!fs12.existsSync(crlPath)) {
|
|
3474
4245
|
return Buffer.alloc(0);
|
|
3475
4246
|
}
|
|
3476
|
-
return
|
|
4247
|
+
return fs12.readFileSync(crlPath);
|
|
3477
4248
|
}
|
|
3478
4249
|
/**
|
|
3479
4250
|
* Return the current Certificate Revocation List as a
|
|
@@ -3483,10 +4254,10 @@ var init_certificate_authority = __esm({
|
|
|
3483
4254
|
*/
|
|
3484
4255
|
getCRLPEM() {
|
|
3485
4256
|
const crlPath = this.revocationList;
|
|
3486
|
-
if (!
|
|
4257
|
+
if (!fs12.existsSync(crlPath)) {
|
|
3487
4258
|
return "";
|
|
3488
4259
|
}
|
|
3489
|
-
const raw =
|
|
4260
|
+
const raw = fs12.readFileSync(crlPath, "utf-8");
|
|
3490
4261
|
const beginMarker = "-----BEGIN X509 CRL-----";
|
|
3491
4262
|
const idx = raw.indexOf(beginMarker);
|
|
3492
4263
|
if (idx > 0) {
|
|
@@ -3505,14 +4276,14 @@ var init_certificate_authority = __esm({
|
|
|
3505
4276
|
* expiry date, and (for revoked certs) the revocation date.
|
|
3506
4277
|
*/
|
|
3507
4278
|
getIssuedCertificates() {
|
|
3508
|
-
return this.
|
|
4279
|
+
return this.#db.readIndex();
|
|
3509
4280
|
}
|
|
3510
4281
|
/**
|
|
3511
4282
|
* Return the total number of certificates recorded in
|
|
3512
4283
|
* `index.txt`.
|
|
3513
4284
|
*/
|
|
3514
4285
|
getIssuedCertificateCount() {
|
|
3515
|
-
return this.
|
|
4286
|
+
return this.#db.readIndex().length;
|
|
3516
4287
|
}
|
|
3517
4288
|
/**
|
|
3518
4289
|
* Return the status of a certificate by its serial number.
|
|
@@ -3522,9 +4293,7 @@ var init_certificate_authority = __esm({
|
|
|
3522
4293
|
* `undefined` if not found
|
|
3523
4294
|
*/
|
|
3524
4295
|
getCertificateStatus(serial) {
|
|
3525
|
-
|
|
3526
|
-
const record = this._parseIndexTxt().find((r) => r.serial.toUpperCase() === upper);
|
|
3527
|
-
return record?.status;
|
|
4296
|
+
return this.#db.findBySerial(serial)?.status;
|
|
3528
4297
|
}
|
|
3529
4298
|
/**
|
|
3530
4299
|
* Read a specific issued certificate by serial number and
|
|
@@ -3537,82 +4306,13 @@ var init_certificate_authority = __esm({
|
|
|
3537
4306
|
* @returns the DER buffer, or `undefined` if not found
|
|
3538
4307
|
*/
|
|
3539
4308
|
getCertificateBySerial(serial) {
|
|
3540
|
-
|
|
3541
|
-
const certFile = path6.join(this.rootDir, "certs", `${upper}.pem`);
|
|
3542
|
-
if (!fs10.existsSync(certFile)) {
|
|
3543
|
-
return void 0;
|
|
3544
|
-
}
|
|
3545
|
-
const pem = readCertificatePEM(certFile);
|
|
3546
|
-
return convertPEMtoDER(pem);
|
|
4309
|
+
return this.#db.getCertificateBySerial(serial);
|
|
3547
4310
|
}
|
|
3548
4311
|
/**
|
|
3549
4312
|
* Path to the OpenSSL certificate database file.
|
|
3550
4313
|
*/
|
|
3551
4314
|
get indexFile() {
|
|
3552
|
-
return
|
|
3553
|
-
}
|
|
3554
|
-
/**
|
|
3555
|
-
* Parse the OpenSSL `index.txt` certificate database.
|
|
3556
|
-
*
|
|
3557
|
-
* Each line has tab-separated fields:
|
|
3558
|
-
* ```
|
|
3559
|
-
* status expiry [revocationDate] serial unknown subject
|
|
3560
|
-
* ```
|
|
3561
|
-
*
|
|
3562
|
-
* - status: `V` (valid), `R` (revoked), `E` (expired)
|
|
3563
|
-
* - expiry: `YYMMDDHHmmssZ`
|
|
3564
|
-
* - revocationDate: present only for revoked certs
|
|
3565
|
-
* - serial: hex string
|
|
3566
|
-
* - unknown: always `"unknown"`
|
|
3567
|
-
* - subject: X.500 slash-delimited string
|
|
3568
|
-
*/
|
|
3569
|
-
_parseIndexTxt() {
|
|
3570
|
-
const indexPath = this.indexFile;
|
|
3571
|
-
if (!fs10.existsSync(indexPath)) {
|
|
3572
|
-
return [];
|
|
3573
|
-
}
|
|
3574
|
-
const content = fs10.readFileSync(indexPath, "utf-8");
|
|
3575
|
-
const lines = content.split("\n").filter((l) => l.trim().length > 0);
|
|
3576
|
-
const records = [];
|
|
3577
|
-
for (const line of lines) {
|
|
3578
|
-
const fields = line.split(" ");
|
|
3579
|
-
if (fields.length < 4) continue;
|
|
3580
|
-
const statusChar = fields[0];
|
|
3581
|
-
const expiryStr = fields[1];
|
|
3582
|
-
let serial;
|
|
3583
|
-
let subject;
|
|
3584
|
-
let revocationDate;
|
|
3585
|
-
if (statusChar === "R") {
|
|
3586
|
-
revocationDate = fields[2];
|
|
3587
|
-
serial = fields[3];
|
|
3588
|
-
subject = fields.length >= 6 ? fields[5] : "";
|
|
3589
|
-
} else {
|
|
3590
|
-
serial = fields[3];
|
|
3591
|
-
subject = fields.length >= 6 ? fields[5] : "";
|
|
3592
|
-
}
|
|
3593
|
-
let status;
|
|
3594
|
-
switch (statusChar) {
|
|
3595
|
-
case "V":
|
|
3596
|
-
status = "valid";
|
|
3597
|
-
break;
|
|
3598
|
-
case "R":
|
|
3599
|
-
status = "revoked";
|
|
3600
|
-
break;
|
|
3601
|
-
case "E":
|
|
3602
|
-
status = "expired";
|
|
3603
|
-
break;
|
|
3604
|
-
default:
|
|
3605
|
-
continue;
|
|
3606
|
-
}
|
|
3607
|
-
records.push({
|
|
3608
|
-
serial,
|
|
3609
|
-
status,
|
|
3610
|
-
subject,
|
|
3611
|
-
expiryDate: parseOpenSSLDate(expiryStr),
|
|
3612
|
-
revocationDate: revocationDate ? parseOpenSSLDate(revocationDate) : void 0
|
|
3613
|
-
});
|
|
3614
|
-
}
|
|
3615
|
-
return records;
|
|
4315
|
+
return this.#db.indexFile;
|
|
3616
4316
|
}
|
|
3617
4317
|
// ---------------------------------------------------------------
|
|
3618
4318
|
// Buffer-based CA operations (US-058)
|
|
@@ -3634,12 +4334,12 @@ var init_certificate_authority = __esm({
|
|
|
3634
4334
|
* @returns the signed certificate as a DER-encoded buffer
|
|
3635
4335
|
*/
|
|
3636
4336
|
async signCertificateRequestFromDER(csrDer, options) {
|
|
3637
|
-
const tmpDir = await
|
|
4337
|
+
const tmpDir = await fs12.promises.mkdtemp(path9.join(os4.tmpdir(), "pki-sign-"));
|
|
3638
4338
|
try {
|
|
3639
|
-
const csrFile =
|
|
3640
|
-
const certFile =
|
|
4339
|
+
const csrFile = path9.join(tmpDir, "request.csr");
|
|
4340
|
+
const certFile = path9.join(tmpDir, "certificate.pem");
|
|
3641
4341
|
const csrPem = toPem2(csrDer, "CERTIFICATE REQUEST");
|
|
3642
|
-
await
|
|
4342
|
+
await fs12.promises.writeFile(csrFile, csrPem, "utf-8");
|
|
3643
4343
|
const signingParams = {};
|
|
3644
4344
|
if (options?.validityMs !== void 0) signingParams.validityMs = options.validityMs;
|
|
3645
4345
|
else signingParams.validity = options?.validity ?? 365;
|
|
@@ -3649,10 +4349,10 @@ var init_certificate_authority = __esm({
|
|
|
3649
4349
|
if (options?.applicationUri) signingParams.applicationUri = options.applicationUri;
|
|
3650
4350
|
if (options?.subject) signingParams.subject = options.subject;
|
|
3651
4351
|
await this.signCertificateRequest(certFile, csrFile, signingParams);
|
|
3652
|
-
const certPem =
|
|
3653
|
-
return
|
|
4352
|
+
const certPem = readCertificatePEM2(certFile);
|
|
4353
|
+
return convertPEMtoDER2(certPem);
|
|
3654
4354
|
} finally {
|
|
3655
|
-
await
|
|
4355
|
+
await fs12.promises.rm(tmpDir, {
|
|
3656
4356
|
recursive: true,
|
|
3657
4357
|
force: true
|
|
3658
4358
|
});
|
|
@@ -3702,27 +4402,35 @@ var init_certificate_authority = __esm({
|
|
|
3702
4402
|
* @returns `{ certificateDer, privateKey }` — certificate as DER,
|
|
3703
4403
|
* private key as a branded `PrivateKey` buffer
|
|
3704
4404
|
*/
|
|
4405
|
+
/**
|
|
4406
|
+
* An ephemeral key and a CSR for it, written into `tmpDir`. Both
|
|
4407
|
+
* `generateKeyPairAndSign*` methods need exactly this, and neither
|
|
4408
|
+
* needs a subprocess for it: the key comes from node's crypto and the
|
|
4409
|
+
* request is built and self-signed in process, so no `openssl.cnf` has
|
|
4410
|
+
* to be rendered either.
|
|
4411
|
+
*/
|
|
4412
|
+
async #createEphemeralKeyAndCsr(tmpDir, keySize, options) {
|
|
4413
|
+
const privateKeyFile = path9.join(tmpDir, "private_key.pem");
|
|
4414
|
+
await generatePrivateKeyFile2(privateKeyFile, keySize);
|
|
4415
|
+
const { csr } = await createCertificateSigningRequest3({
|
|
4416
|
+
privateKey: await privateKeyToCryptoKey2(readPrivateKey3(privateKeyFile)),
|
|
4417
|
+
subject: options.subject ? new Subject6(options.subject).toString() : void 0,
|
|
4418
|
+
applicationUri: options.applicationUri,
|
|
4419
|
+
dns: options.dns ?? [],
|
|
4420
|
+
ip: options.ip ?? [],
|
|
4421
|
+
purpose: CertificatePurpose3.ForApplication
|
|
4422
|
+
});
|
|
4423
|
+
const csrFile = path9.join(tmpDir, "request.csr");
|
|
4424
|
+
await fs12.promises.writeFile(csrFile, csr);
|
|
4425
|
+
return { privateKeyFile, csrFile };
|
|
4426
|
+
}
|
|
3705
4427
|
async generateKeyPairAndSignDER(options) {
|
|
3706
4428
|
const keySize = options.keySize ?? 2048;
|
|
3707
4429
|
const startDate = options.startDate ?? /* @__PURE__ */ new Date();
|
|
3708
|
-
const tmpDir = await
|
|
4430
|
+
const tmpDir = await fs12.promises.mkdtemp(path9.join(os4.tmpdir(), "pki-keygen-"));
|
|
3709
4431
|
try {
|
|
3710
|
-
const privateKeyFile =
|
|
3711
|
-
|
|
3712
|
-
const configFile = path6.join(tmpDir, "openssl.cnf");
|
|
3713
|
-
await fs10.promises.writeFile(configFile, configurationFileSimpleTemplate2, "utf-8");
|
|
3714
|
-
const csrFile = path6.join(tmpDir, "request.csr");
|
|
3715
|
-
await createCertificateSigningRequestWithOpenSSL(csrFile, {
|
|
3716
|
-
rootDir: tmpDir,
|
|
3717
|
-
configFile,
|
|
3718
|
-
privateKey: privateKeyFile,
|
|
3719
|
-
applicationUri: options.applicationUri,
|
|
3720
|
-
subject: options.subject,
|
|
3721
|
-
dns: options.dns ?? [],
|
|
3722
|
-
ip: options.ip ?? [],
|
|
3723
|
-
purpose: CertificatePurpose2.ForApplication
|
|
3724
|
-
});
|
|
3725
|
-
const certFile = path6.join(tmpDir, "certificate.pem");
|
|
4432
|
+
const { privateKeyFile, csrFile } = await this.#createEphemeralKeyAndCsr(tmpDir, keySize, options);
|
|
4433
|
+
const certFile = path9.join(tmpDir, "certificate.pem");
|
|
3726
4434
|
const signingParams = {
|
|
3727
4435
|
applicationUri: options.applicationUri,
|
|
3728
4436
|
dns: options.dns,
|
|
@@ -3732,12 +4440,12 @@ var init_certificate_authority = __esm({
|
|
|
3732
4440
|
if (options.validityMs !== void 0) signingParams.validityMs = options.validityMs;
|
|
3733
4441
|
else signingParams.validity = options.validity ?? 365;
|
|
3734
4442
|
await this.signCertificateRequest(certFile, csrFile, signingParams);
|
|
3735
|
-
const certPem =
|
|
3736
|
-
const certificateDer =
|
|
3737
|
-
const privateKey =
|
|
4443
|
+
const certPem = readCertificatePEM2(certFile);
|
|
4444
|
+
const certificateDer = convertPEMtoDER2(certPem);
|
|
4445
|
+
const privateKey = readPrivateKey3(privateKeyFile);
|
|
3738
4446
|
return { certificateDer, privateKey };
|
|
3739
4447
|
} finally {
|
|
3740
|
-
await
|
|
4448
|
+
await fs12.promises.rm(tmpDir, {
|
|
3741
4449
|
recursive: true,
|
|
3742
4450
|
force: true
|
|
3743
4451
|
});
|
|
@@ -3758,24 +4466,10 @@ var init_certificate_authority = __esm({
|
|
|
3758
4466
|
const keySize = options.keySize ?? 2048;
|
|
3759
4467
|
const startDate = options.startDate ?? /* @__PURE__ */ new Date();
|
|
3760
4468
|
const passphrase = options.passphrase ?? "";
|
|
3761
|
-
const tmpDir = await
|
|
4469
|
+
const tmpDir = await fs12.promises.mkdtemp(path9.join(os4.tmpdir(), "pki-keygen-pfx-"));
|
|
3762
4470
|
try {
|
|
3763
|
-
const privateKeyFile =
|
|
3764
|
-
|
|
3765
|
-
const configFile = path6.join(tmpDir, "openssl.cnf");
|
|
3766
|
-
await fs10.promises.writeFile(configFile, configurationFileSimpleTemplate2, "utf-8");
|
|
3767
|
-
const csrFile = path6.join(tmpDir, "request.csr");
|
|
3768
|
-
await createCertificateSigningRequestWithOpenSSL(csrFile, {
|
|
3769
|
-
rootDir: tmpDir,
|
|
3770
|
-
configFile,
|
|
3771
|
-
privateKey: privateKeyFile,
|
|
3772
|
-
applicationUri: options.applicationUri,
|
|
3773
|
-
subject: options.subject,
|
|
3774
|
-
dns: options.dns ?? [],
|
|
3775
|
-
ip: options.ip ?? [],
|
|
3776
|
-
purpose: CertificatePurpose2.ForApplication
|
|
3777
|
-
});
|
|
3778
|
-
const certFile = path6.join(tmpDir, "certificate.pem");
|
|
4471
|
+
const { privateKeyFile, csrFile } = await this.#createEphemeralKeyAndCsr(tmpDir, keySize, options);
|
|
4472
|
+
const certFile = path9.join(tmpDir, "certificate.pem");
|
|
3779
4473
|
const signingParams = {
|
|
3780
4474
|
applicationUri: options.applicationUri,
|
|
3781
4475
|
dns: options.dns,
|
|
@@ -3785,17 +4479,20 @@ var init_certificate_authority = __esm({
|
|
|
3785
4479
|
if (options.validityMs !== void 0) signingParams.validityMs = options.validityMs;
|
|
3786
4480
|
else signingParams.validity = options.validity ?? 365;
|
|
3787
4481
|
await this.signCertificateRequest(certFile, csrFile, signingParams);
|
|
3788
|
-
const
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
4482
|
+
const blocks = (await fs12.promises.readFile(certFile, "utf-8")).match(
|
|
4483
|
+
/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g
|
|
4484
|
+
);
|
|
4485
|
+
if (!blocks || blocks.length === 0) {
|
|
4486
|
+
throw new Error(`generateKeyPairAndSignPFX: no certificate was produced in ${certFile}`);
|
|
4487
|
+
}
|
|
4488
|
+
return await createPfx({
|
|
4489
|
+
certificate: convertPEMtoDER2(blocks[0]),
|
|
4490
|
+
certificateChain: blocks.slice(1).map(convertPEMtoDER2),
|
|
4491
|
+
privateKey: readPrivateKey3(privateKeyFile),
|
|
4492
|
+
password: passphrase
|
|
3795
4493
|
});
|
|
3796
|
-
return await fs10.promises.readFile(pfxFile);
|
|
3797
4494
|
} finally {
|
|
3798
|
-
await
|
|
4495
|
+
await fs12.promises.rm(tmpDir, {
|
|
3799
4496
|
recursive: true,
|
|
3800
4497
|
force: true
|
|
3801
4498
|
});
|
|
@@ -3816,8 +4513,8 @@ var init_certificate_authority = __esm({
|
|
|
3816
4513
|
async revokeCertificateDER(certDer, reason) {
|
|
3817
4514
|
const info = exploreCertificate2(certDer);
|
|
3818
4515
|
const serial = info.tbsCertificate.serialNumber.replace(/:/g, "").toUpperCase();
|
|
3819
|
-
const storedCertFile =
|
|
3820
|
-
if (!
|
|
4516
|
+
const storedCertFile = path9.join(this.rootDir, "certs", `${serial}.pem`);
|
|
4517
|
+
if (!fs12.existsSync(storedCertFile)) {
|
|
3821
4518
|
throw new Error(`Cannot revoke: no stored certificate found for serial ${serial} at ${storedCertFile}`);
|
|
3822
4519
|
}
|
|
3823
4520
|
await this.revokeCertificate(storedCertFile, {
|
|
@@ -3830,7 +4527,69 @@ var init_certificate_authority = __esm({
|
|
|
3830
4527
|
* already exist.
|
|
3831
4528
|
*/
|
|
3832
4529
|
async initialize() {
|
|
3833
|
-
|
|
4530
|
+
mkdirRecursiveSync(path9.resolve(this.rootDir));
|
|
4531
|
+
await this.#withCaLock(() => this.#bootstrap());
|
|
4532
|
+
}
|
|
4533
|
+
/**
|
|
4534
|
+
* @internal Shared (backend-agnostic) part of `initialize()`: directory
|
|
4535
|
+
* layout, default database files, the "already initialized" / "partial
|
|
4536
|
+
* init" checks, and the openssl config file — then delegates CSR
|
|
4537
|
+
* generation, CA-certificate signing, and the initial CRL to
|
|
4538
|
+
* {@link CaBackend.bootstrap}. Must be called under {@link #withCaLock}.
|
|
4539
|
+
*/
|
|
4540
|
+
async #bootstrap() {
|
|
4541
|
+
const caRootDir = path9.resolve(this.rootDir);
|
|
4542
|
+
mkdirRecursiveSync(caRootDir);
|
|
4543
|
+
ensurePrivateDirectory(path9.join(caRootDir, "private"));
|
|
4544
|
+
mkdirRecursiveSync(path9.join(caRootDir, "public"));
|
|
4545
|
+
mkdirRecursiveSync(path9.join(caRootDir, "certs"));
|
|
4546
|
+
mkdirRecursiveSync(path9.join(caRootDir, "crl"));
|
|
4547
|
+
mkdirRecursiveSync(path9.join(caRootDir, "conf"));
|
|
4548
|
+
const serial = path9.join(caRootDir, "serial");
|
|
4549
|
+
if (!fs12.existsSync(serial)) {
|
|
4550
|
+
await fs12.promises.writeFile(serial, "1000");
|
|
4551
|
+
}
|
|
4552
|
+
const crlNumber = path9.join(caRootDir, "crlnumber");
|
|
4553
|
+
if (!fs12.existsSync(crlNumber)) {
|
|
4554
|
+
await fs12.promises.writeFile(crlNumber, "1000");
|
|
4555
|
+
}
|
|
4556
|
+
const indexFile = path9.join(caRootDir, "index.txt");
|
|
4557
|
+
if (!fs12.existsSync(indexFile)) {
|
|
4558
|
+
await fs12.promises.writeFile(indexFile, "");
|
|
4559
|
+
}
|
|
4560
|
+
const signerBacked = this.hasExternalSigner;
|
|
4561
|
+
const caKeyExists = signerBacked || fs12.existsSync(path9.join(caRootDir, "private/cakey.pem"));
|
|
4562
|
+
const caCertExists = fs12.existsSync(path9.join(caRootDir, "public/cacert.pem"));
|
|
4563
|
+
if (caKeyExists && caCertExists && !config3.forceCA) {
|
|
4564
|
+
if (!signerBacked) {
|
|
4565
|
+
restrictPrivateFilePermissions(path9.join(caRootDir, "private/cakey.pem"), 384);
|
|
4566
|
+
await this._ensurePrivateKeyProtection();
|
|
4567
|
+
}
|
|
4568
|
+
debugLog("CA private key and certificate already exist ... skipping");
|
|
4569
|
+
return;
|
|
4570
|
+
}
|
|
4571
|
+
if (!signerBacked && caKeyExists && !caCertExists) {
|
|
4572
|
+
debugLog("CA private key exists but cacert.pem is missing \u2014 rebuilding CA");
|
|
4573
|
+
fs12.unlinkSync(path9.join(caRootDir, "private/cakey.pem"));
|
|
4574
|
+
const staleCsr = path9.join(caRootDir, "private/cakey.csr");
|
|
4575
|
+
if (fs12.existsSync(staleCsr)) {
|
|
4576
|
+
fs12.unlinkSync(staleCsr);
|
|
4577
|
+
}
|
|
4578
|
+
}
|
|
4579
|
+
displayTitle("Create Certificate Authority (CA)");
|
|
4580
|
+
const indexFileAttr = path9.join(caRootDir, "index.txt.attr");
|
|
4581
|
+
if (!fs12.existsSync(indexFileAttr)) {
|
|
4582
|
+
await fs12.promises.writeFile(indexFileAttr, "unique_subject = no");
|
|
4583
|
+
}
|
|
4584
|
+
const caConfigFile = this.configFile;
|
|
4585
|
+
await fs12.promises.writeFile(caConfigFile, renderCaConfig(caRootDir));
|
|
4586
|
+
if (!signerBacked) {
|
|
4587
|
+
const privateKeyFilename = path9.join(caRootDir, "private/cakey.pem");
|
|
4588
|
+
displayTitle(`Generate the CA private Key - ${this.keySize}`);
|
|
4589
|
+
await generatePrivateKeyFile2(privateKeyFilename, this.keySize, { passphrase: await this._privateKeyPassphrase() });
|
|
4590
|
+
restrictPrivateFilePermissions(privateKeyFilename, 384);
|
|
4591
|
+
}
|
|
4592
|
+
await this.#backend.bootstrap(this);
|
|
3834
4593
|
}
|
|
3835
4594
|
/**
|
|
3836
4595
|
* Initialize the CA directory structure and generate the
|
|
@@ -3852,21 +4611,25 @@ var init_certificate_authority = __esm({
|
|
|
3852
4611
|
* @returns an {@link InitializeCSRResult} describing the CA state
|
|
3853
4612
|
*/
|
|
3854
4613
|
async initializeCSR() {
|
|
3855
|
-
const caRootDir =
|
|
4614
|
+
const caRootDir = path9.resolve(this.rootDir);
|
|
3856
4615
|
mkdirRecursiveSync(caRootDir);
|
|
4616
|
+
return this.#withCaLock(() => this.#initializeCSRLocked(caRootDir));
|
|
4617
|
+
}
|
|
4618
|
+
async #initializeCSRLocked(caRootDir) {
|
|
3857
4619
|
for (const dir of ["public", "certs", "crl", "conf"]) {
|
|
3858
|
-
mkdirRecursiveSync(
|
|
4620
|
+
mkdirRecursiveSync(path9.join(caRootDir, dir));
|
|
3859
4621
|
}
|
|
3860
|
-
ensurePrivateDirectory(
|
|
4622
|
+
ensurePrivateDirectory(path9.join(caRootDir, "private"));
|
|
3861
4623
|
const caCertFile = this.caCertificate;
|
|
3862
|
-
const privateKeyFile =
|
|
3863
|
-
const csrFile =
|
|
3864
|
-
|
|
4624
|
+
const privateKeyFile = path9.join(caRootDir, "private/cakey.pem");
|
|
4625
|
+
const csrFile = path9.join(caRootDir, "private/cakey.csr");
|
|
4626
|
+
const keyAvailable = this.hasExternalSigner || fs12.existsSync(privateKeyFile);
|
|
4627
|
+
if (!this.hasExternalSigner && fs12.existsSync(privateKeyFile)) {
|
|
3865
4628
|
restrictPrivateFilePermissions(privateKeyFile, 384);
|
|
3866
4629
|
await this._ensurePrivateKeyProtection();
|
|
3867
4630
|
}
|
|
3868
|
-
if (
|
|
3869
|
-
const certDer =
|
|
4631
|
+
if (fs12.existsSync(caCertFile)) {
|
|
4632
|
+
const certDer = convertPEMtoDER2(readCertificatePEM2(caCertFile));
|
|
3870
4633
|
const certInfo = exploreCertificate2(certDer);
|
|
3871
4634
|
const notAfter = certInfo.tbsCertificate.validity.notAfter;
|
|
3872
4635
|
if (notAfter.getTime() < Date.now()) {
|
|
@@ -3877,29 +4640,29 @@ var init_certificate_authority = __esm({
|
|
|
3877
4640
|
debugLog("CA certificate already exists and is valid \u2014 ready");
|
|
3878
4641
|
return { status: "ready" };
|
|
3879
4642
|
}
|
|
3880
|
-
if (
|
|
4643
|
+
if (keyAvailable && fs12.existsSync(csrFile)) {
|
|
3881
4644
|
debugLog("CA key + CSR already exist \u2014 pending external signing");
|
|
3882
4645
|
return { status: "pending", csrPath: csrFile };
|
|
3883
4646
|
}
|
|
3884
|
-
const serial =
|
|
3885
|
-
if (!
|
|
3886
|
-
await
|
|
4647
|
+
const serial = path9.join(caRootDir, "serial");
|
|
4648
|
+
if (!fs12.existsSync(serial)) {
|
|
4649
|
+
await fs12.promises.writeFile(serial, "1000");
|
|
3887
4650
|
}
|
|
3888
|
-
const crlNumber =
|
|
3889
|
-
if (!
|
|
3890
|
-
await
|
|
4651
|
+
const crlNumber = path9.join(caRootDir, "crlnumber");
|
|
4652
|
+
if (!fs12.existsSync(crlNumber)) {
|
|
4653
|
+
await fs12.promises.writeFile(crlNumber, "1000");
|
|
3891
4654
|
}
|
|
3892
|
-
const indexFile =
|
|
3893
|
-
if (!
|
|
3894
|
-
await
|
|
4655
|
+
const indexFile = path9.join(caRootDir, "index.txt");
|
|
4656
|
+
if (!fs12.existsSync(indexFile)) {
|
|
4657
|
+
await fs12.promises.writeFile(indexFile, "");
|
|
3895
4658
|
}
|
|
3896
|
-
const indexFileAttr =
|
|
3897
|
-
if (!
|
|
3898
|
-
await
|
|
4659
|
+
const indexFileAttr = path9.join(caRootDir, "index.txt.attr");
|
|
4660
|
+
if (!fs12.existsSync(indexFileAttr)) {
|
|
4661
|
+
await fs12.promises.writeFile(indexFileAttr, "unique_subject = no");
|
|
3899
4662
|
}
|
|
3900
4663
|
const caConfigFile = this.configFile;
|
|
3901
|
-
await
|
|
3902
|
-
if (!
|
|
4664
|
+
await fs12.promises.writeFile(caConfigFile, renderCaConfig(caRootDir));
|
|
4665
|
+
if (!keyAvailable) {
|
|
3903
4666
|
await generatePrivateKeyFile2(privateKeyFile, this.keySize, { passphrase: await this._privateKeyPassphrase() });
|
|
3904
4667
|
restrictPrivateFilePermissions(privateKeyFile, 384);
|
|
3905
4668
|
}
|
|
@@ -3920,53 +4683,34 @@ var init_certificate_authority = __esm({
|
|
|
3920
4683
|
* renewal is needed, `"ready"` if the cert is still valid
|
|
3921
4684
|
*/
|
|
3922
4685
|
async renewCSR(thresholdDays = 30) {
|
|
3923
|
-
const caRootDir =
|
|
4686
|
+
const caRootDir = path9.resolve(this.rootDir);
|
|
3924
4687
|
const caCertFile = this.caCertificate;
|
|
3925
|
-
const privateKeyFile =
|
|
3926
|
-
const csrFile =
|
|
3927
|
-
if (!
|
|
4688
|
+
const privateKeyFile = path9.join(caRootDir, "private/cakey.pem");
|
|
4689
|
+
const csrFile = path9.join(caRootDir, "private/cakey.csr");
|
|
4690
|
+
if (!fs12.existsSync(caCertFile)) {
|
|
3928
4691
|
return this.initializeCSR();
|
|
3929
4692
|
}
|
|
3930
|
-
const certDer =
|
|
4693
|
+
const certDer = convertPEMtoDER2(readCertificatePEM2(caCertFile));
|
|
3931
4694
|
const certInfo = exploreCertificate2(certDer);
|
|
3932
4695
|
const notAfter = certInfo.tbsCertificate.validity.notAfter;
|
|
3933
4696
|
const thresholdMs = thresholdDays * 24 * 60 * 60 * 1e3;
|
|
3934
4697
|
if (notAfter.getTime() - Date.now() < thresholdMs) {
|
|
3935
4698
|
debugLog(`CA certificate expires within ${thresholdDays} days \u2014 generating renewal CSR`);
|
|
3936
|
-
await this._generateCSR(caRootDir, privateKeyFile, csrFile);
|
|
4699
|
+
await this.#withCaLock(() => this._generateCSR(caRootDir, privateKeyFile, csrFile));
|
|
3937
4700
|
return { status: "expired", csrPath: csrFile, expiryDate: notAfter };
|
|
3938
4701
|
}
|
|
3939
4702
|
return { status: "ready" };
|
|
3940
4703
|
}
|
|
3941
4704
|
/**
|
|
3942
4705
|
* Generate a CSR using the existing private key.
|
|
4706
|
+
* Must be called under {@link #withCaLock} — the lock is taken by the
|
|
4707
|
+
* public callers (`initializeCSR`, `renewCSR`), not here, so that
|
|
4708
|
+
* `initializeCSR` can hold one lock across key generation AND CSR
|
|
4709
|
+
* generation without the non-reentrant file lock deadlocking.
|
|
3943
4710
|
* @internal
|
|
3944
4711
|
*/
|
|
3945
4712
|
async _generateCSR(caRootDir, privateKeyFile, csrFile) {
|
|
3946
|
-
|
|
3947
|
-
const options = { cwd: caRootDir };
|
|
3948
|
-
const configFile = generateStaticConfig("conf/caconfig.cnf", options);
|
|
3949
|
-
const passin = await this._opensslPassin();
|
|
3950
|
-
await execute_openssl(
|
|
3951
|
-
[
|
|
3952
|
-
"req",
|
|
3953
|
-
"-new",
|
|
3954
|
-
"-sha256",
|
|
3955
|
-
"-text",
|
|
3956
|
-
"-extensions",
|
|
3957
|
-
"v3_ca_req",
|
|
3958
|
-
"-config",
|
|
3959
|
-
n5(configFile),
|
|
3960
|
-
"-key",
|
|
3961
|
-
n5(privateKeyFile),
|
|
3962
|
-
"-out",
|
|
3963
|
-
n5(csrFile),
|
|
3964
|
-
"-subj",
|
|
3965
|
-
this.subject.toString(),
|
|
3966
|
-
...passin.args
|
|
3967
|
-
],
|
|
3968
|
-
{ ...options, env: passin.env }
|
|
3969
|
-
);
|
|
4713
|
+
await this.#backend.generateCaCsr(this, caRootDir, privateKeyFile, csrFile);
|
|
3970
4714
|
}
|
|
3971
4715
|
/**
|
|
3972
4716
|
* Install an externally-signed CA certificate and generate
|
|
@@ -3985,43 +4729,41 @@ var init_certificate_authority = __esm({
|
|
|
3985
4729
|
* `status: "success"` or `status: "error"` and a `reason`
|
|
3986
4730
|
*/
|
|
3987
4731
|
async installCACertificate(signedCertFile) {
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
}
|
|
4007
|
-
|
|
4008
|
-
await fs10.promises.writeFile(caCertFile, `${pemBlocks[0]}
|
|
4732
|
+
return this.#withCaLock(async () => {
|
|
4733
|
+
const caCertFile = this.caCertificate;
|
|
4734
|
+
const fullPem = await fs12.promises.readFile(signedCertFile, "utf8");
|
|
4735
|
+
const pemBlocks = fullPem.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g);
|
|
4736
|
+
if (!pemBlocks || pemBlocks.length === 0) {
|
|
4737
|
+
return {
|
|
4738
|
+
status: "error",
|
|
4739
|
+
reason: "no_certificate_found",
|
|
4740
|
+
message: "The provided file does not contain any PEM-encoded certificate."
|
|
4741
|
+
};
|
|
4742
|
+
}
|
|
4743
|
+
const certDer = convertPEMtoDER2(pemBlocks[0]);
|
|
4744
|
+
if (!await this.#certificateMatchesOurKey(pemBlocks[0], certDer)) {
|
|
4745
|
+
return {
|
|
4746
|
+
status: "error",
|
|
4747
|
+
reason: "certificate_key_mismatch",
|
|
4748
|
+
message: "The provided certificate does not match the CA private key. Ensure the certificate was signed from the CSR generated by initializeCSR()."
|
|
4749
|
+
};
|
|
4750
|
+
}
|
|
4751
|
+
await fs12.promises.writeFile(caCertFile, `${pemBlocks[0]}
|
|
4009
4752
|
`);
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4753
|
+
const issuerChainFile = this.issuerCertificateChain;
|
|
4754
|
+
if (pemBlocks.length > 1) {
|
|
4755
|
+
const issuerPem = `${pemBlocks.slice(1).join("\n")}
|
|
4013
4756
|
`;
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4757
|
+
await fs12.promises.writeFile(issuerChainFile, issuerPem);
|
|
4758
|
+
debugLog(`Stored ${pemBlocks.length - 1} issuer certificate(s) in issuer_chain.pem`);
|
|
4759
|
+
} else {
|
|
4760
|
+
if (fs12.existsSync(issuerChainFile)) {
|
|
4761
|
+
await fs12.promises.unlink(issuerChainFile);
|
|
4762
|
+
}
|
|
4019
4763
|
}
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
await regenerateCrl(this.revocationList, ["-config", n5(configFile)], options, await this._opensslPassin());
|
|
4024
|
-
return { status: "success" };
|
|
4764
|
+
await this.#backend.regenerateCrl(this);
|
|
4765
|
+
return { status: "success" };
|
|
4766
|
+
});
|
|
4025
4767
|
}
|
|
4026
4768
|
/**
|
|
4027
4769
|
* Sign a CSR with CA extensions (`v3_ca`), producing a
|
|
@@ -4037,39 +4779,11 @@ var init_certificate_authority = __esm({
|
|
|
4037
4779
|
* @param params - signing parameters
|
|
4038
4780
|
*/
|
|
4039
4781
|
async signCACertificateRequest(certFile, csrFile, params) {
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
const passin = await this._opensslPassin();
|
|
4046
|
-
await execute_openssl(
|
|
4047
|
-
[
|
|
4048
|
-
"x509",
|
|
4049
|
-
"-sha256",
|
|
4050
|
-
"-req",
|
|
4051
|
-
"-days",
|
|
4052
|
-
String(validity),
|
|
4053
|
-
"-text",
|
|
4054
|
-
"-extensions",
|
|
4055
|
-
"v3_ca",
|
|
4056
|
-
"-extfile",
|
|
4057
|
-
n5(configFile),
|
|
4058
|
-
"-in",
|
|
4059
|
-
n5(csrFile),
|
|
4060
|
-
"-CA",
|
|
4061
|
-
n5(this.caCertificate),
|
|
4062
|
-
"-CAkey",
|
|
4063
|
-
n5(path6.join(caRootDir, "private/cakey.pem")),
|
|
4064
|
-
"-CAserial",
|
|
4065
|
-
n5(path6.join(caRootDir, "serial")),
|
|
4066
|
-
"-out",
|
|
4067
|
-
n5(certFile),
|
|
4068
|
-
...passin.args
|
|
4069
|
-
],
|
|
4070
|
-
{ ...options, env: passin.env }
|
|
4071
|
-
);
|
|
4072
|
-
await this.constructCertificateChain(certFile);
|
|
4782
|
+
await this.#withCaLock(async () => {
|
|
4783
|
+
const validity = params.validity ?? 3650;
|
|
4784
|
+
await this.#backend.signSubordinateCsr(this, csrFile, certFile, validity);
|
|
4785
|
+
await this.constructCertificateChain(certFile);
|
|
4786
|
+
});
|
|
4073
4787
|
}
|
|
4074
4788
|
/**
|
|
4075
4789
|
* Rebuild the combined CA certificate + CRL file.
|
|
@@ -4080,13 +4794,13 @@ var init_certificate_authority = __esm({
|
|
|
4080
4794
|
*/
|
|
4081
4795
|
async constructCACertificateWithCRL() {
|
|
4082
4796
|
const cacertWithCRL = this.caCertificateWithCrl;
|
|
4083
|
-
if (
|
|
4084
|
-
await
|
|
4797
|
+
if (fs12.existsSync(this.revocationList)) {
|
|
4798
|
+
await fs12.promises.writeFile(
|
|
4085
4799
|
cacertWithCRL,
|
|
4086
|
-
|
|
4800
|
+
fs12.readFileSync(this.caCertificate, "utf8") + fs12.readFileSync(this.revocationList, "utf8")
|
|
4087
4801
|
);
|
|
4088
4802
|
} else {
|
|
4089
|
-
await
|
|
4803
|
+
await fs12.promises.writeFile(cacertWithCRL, fs12.readFileSync(this.caCertificate));
|
|
4090
4804
|
}
|
|
4091
4805
|
}
|
|
4092
4806
|
/**
|
|
@@ -4096,15 +4810,15 @@ var init_certificate_authority = __esm({
|
|
|
4096
4810
|
* @param certificate - path to the certificate file to extend
|
|
4097
4811
|
*/
|
|
4098
4812
|
async constructCertificateChain(certificate) {
|
|
4099
|
-
|
|
4100
|
-
|
|
4813
|
+
assert9(fs12.existsSync(certificate));
|
|
4814
|
+
assert9(fs12.existsSync(this.caCertificate));
|
|
4101
4815
|
debugLog(chalk6.yellow(" certificate file :"), chalk6.cyan(certificate));
|
|
4102
|
-
let chain = await
|
|
4103
|
-
chain += await
|
|
4104
|
-
if (
|
|
4105
|
-
chain += await
|
|
4816
|
+
let chain = await fs12.promises.readFile(certificate, "utf8");
|
|
4817
|
+
chain += await fs12.promises.readFile(this.caCertificate, "utf8");
|
|
4818
|
+
if (fs12.existsSync(this.issuerCertificateChain)) {
|
|
4819
|
+
chain += await fs12.promises.readFile(this.issuerCertificateChain, "utf8");
|
|
4106
4820
|
}
|
|
4107
|
-
await
|
|
4821
|
+
await fs12.promises.writeFile(certificate, chain);
|
|
4108
4822
|
}
|
|
4109
4823
|
/**
|
|
4110
4824
|
* Create a self-signed certificate using OpenSSL.
|
|
@@ -4114,52 +4828,30 @@ var init_certificate_authority = __esm({
|
|
|
4114
4828
|
* @param params - certificate parameters (subject, validity, SANs)
|
|
4115
4829
|
*/
|
|
4116
4830
|
async createSelfSignedCertificate(certificateFile, privateKey, params) {
|
|
4117
|
-
|
|
4118
|
-
|
|
4831
|
+
assert9(typeof privateKey === "string");
|
|
4832
|
+
assert9(fs12.existsSync(privateKey));
|
|
4119
4833
|
if (!certificateFileExist(certificateFile)) {
|
|
4120
4834
|
return;
|
|
4121
4835
|
}
|
|
4122
4836
|
adjustDate(params);
|
|
4123
4837
|
adjustApplicationUri(params);
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
)
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
[
|
|
4142
|
-
"ca",
|
|
4143
|
-
"-selfsign",
|
|
4144
|
-
"-keyfile",
|
|
4145
|
-
n5(privateKey),
|
|
4146
|
-
"-startdate",
|
|
4147
|
-
x509Date(params.startDate),
|
|
4148
|
-
"-enddate",
|
|
4149
|
-
x509Date(params.endDate),
|
|
4150
|
-
"-batch",
|
|
4151
|
-
"-out",
|
|
4152
|
-
n5(certificateFile),
|
|
4153
|
-
"-in",
|
|
4154
|
-
n5(csrFile)
|
|
4155
|
-
],
|
|
4156
|
-
options
|
|
4157
|
-
);
|
|
4158
|
-
displaySubtitle("- dump the certificate for a check");
|
|
4159
|
-
await execute_openssl(["x509", "-in", n5(certificateFile), "-dates", "-fingerprint", "-purpose", "-noout"], {});
|
|
4160
|
-
displaySubtitle("- verify self-signed certificate");
|
|
4161
|
-
await execute_openssl_no_failure(["verify", "-verbose", "-CAfile", n5(certificateFile), n5(certificateFile)], options);
|
|
4162
|
-
await fs10.promises.unlink(csrFile);
|
|
4838
|
+
params.dns = params.dns || [];
|
|
4839
|
+
params.ip = params.ip || [];
|
|
4840
|
+
await this.#withCaLock(async () => {
|
|
4841
|
+
await this.#backend.createSelfSignedCertificate(this, certificateFile, privateKey, params);
|
|
4842
|
+
});
|
|
4843
|
+
}
|
|
4844
|
+
/**
|
|
4845
|
+
* Regenerate `crl/revocation_list.{crl,der}` from the current database
|
|
4846
|
+
* state, without changing any certificate's status. Normally
|
|
4847
|
+
* unnecessary — `revokeCertificate` already regenerates the CRL as
|
|
4848
|
+
* part of revoking — but useful to force a refresh (e.g. after
|
|
4849
|
+
* switching a CA's `backend` between `"openssl"` and `"native"`).
|
|
4850
|
+
*/
|
|
4851
|
+
async regenerateCrl() {
|
|
4852
|
+
await this.#withCaLock(async () => {
|
|
4853
|
+
await this.#backend.regenerateCrl(this);
|
|
4854
|
+
});
|
|
4163
4855
|
}
|
|
4164
4856
|
/**
|
|
4165
4857
|
* Revoke a certificate and regenerate the CRL.
|
|
@@ -4180,49 +4872,12 @@ var init_certificate_authority = __esm({
|
|
|
4180
4872
|
"certificateHold",
|
|
4181
4873
|
"removeFromCRL"
|
|
4182
4874
|
];
|
|
4183
|
-
const configFile = generateStaticConfig("conf/caconfig.cnf", { cwd: this.rootDir });
|
|
4184
|
-
const options = {
|
|
4185
|
-
cwd: this.rootDir,
|
|
4186
|
-
openssl_conf: makePath(configFile)
|
|
4187
|
-
};
|
|
4188
|
-
setEnv("ALTNAME", "");
|
|
4189
|
-
const randomFile = path6.join(this.rootDir, "random.rnd");
|
|
4190
|
-
setEnv("RANDFILE", randomFile);
|
|
4191
|
-
const configOption = ["-config", n5(configFile)];
|
|
4192
4875
|
const reason = params.reason || "keyCompromise";
|
|
4193
|
-
|
|
4876
|
+
assert9(crlReasons.indexOf(reason) >= 0);
|
|
4194
4877
|
displayTitle(`Revoking certificate ${certificate}`);
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
["ca", "-verbose", ...configOption, "-revoke", certificate, "-crl_reason", reason, ...passin.args],
|
|
4199
|
-
{ ...options, env: passin.env }
|
|
4200
|
-
);
|
|
4201
|
-
await regenerateCrl(this.revocationList, configOption, options, passin);
|
|
4202
|
-
displaySubtitle("Verify that certificate is revoked");
|
|
4203
|
-
await execute_openssl_no_failure(
|
|
4204
|
-
[
|
|
4205
|
-
"verify",
|
|
4206
|
-
"-verbose",
|
|
4207
|
-
"-CRLfile",
|
|
4208
|
-
n5(this.revocationList),
|
|
4209
|
-
"-CAfile",
|
|
4210
|
-
n5(this.caCertificate),
|
|
4211
|
-
"-crl_check",
|
|
4212
|
-
n5(certificate)
|
|
4213
|
-
],
|
|
4214
|
-
options
|
|
4215
|
-
);
|
|
4216
|
-
displaySubtitle("Produce CRL in DER form ");
|
|
4217
|
-
await execute_openssl(
|
|
4218
|
-
["crl", "-in", n5(this.revocationList), "-out", "crl/revocation_list.der", "-outform", "der"],
|
|
4219
|
-
options
|
|
4220
|
-
);
|
|
4221
|
-
displaySubtitle("Produce CRL in PEM form ");
|
|
4222
|
-
await execute_openssl(
|
|
4223
|
-
["crl", "-in", n5(this.revocationList), "-out", "crl/revocation_list.pem", "-outform", "pem", "-text"],
|
|
4224
|
-
options
|
|
4225
|
-
);
|
|
4878
|
+
await this.#withCaLock(async () => {
|
|
4879
|
+
await this.#backend.revoke(this, certificate, reason);
|
|
4880
|
+
});
|
|
4226
4881
|
}
|
|
4227
4882
|
/**
|
|
4228
4883
|
* Sign a Certificate Signing Request (CSR) with this CA.
|
|
@@ -4237,15 +4892,13 @@ var init_certificate_authority = __esm({
|
|
|
4237
4892
|
* @returns the path to the signed certificate
|
|
4238
4893
|
*/
|
|
4239
4894
|
async signCertificateRequest(certificate, certificateSigningRequestFilename, params1) {
|
|
4240
|
-
await
|
|
4241
|
-
|
|
4895
|
+
await this.#backend.preflight();
|
|
4896
|
+
assert9(fs12.existsSync(certificateSigningRequestFilename));
|
|
4242
4897
|
if (!certificateFileExist(certificate)) {
|
|
4243
4898
|
return "";
|
|
4244
4899
|
}
|
|
4245
4900
|
adjustDate(params1);
|
|
4246
4901
|
adjustApplicationUri(params1);
|
|
4247
|
-
processAltNames(params1);
|
|
4248
|
-
const options = { cwd: this.rootDir };
|
|
4249
4902
|
const csr = await readCertificateSigningRequest(certificateSigningRequestFilename);
|
|
4250
4903
|
const csrInfo = exploreCertificateSigningRequest(csr);
|
|
4251
4904
|
const applicationUri = csrInfo.extensionRequest.subjectAltName.uniformResourceIdentifier ? csrInfo.extensionRequest.subjectAltName.uniformResourceIdentifier[0] : void 0;
|
|
@@ -4255,73 +4908,122 @@ var init_certificate_authority = __esm({
|
|
|
4255
4908
|
const dns2 = csrInfo.extensionRequest.subjectAltName.dNSName || [];
|
|
4256
4909
|
let ip = csrInfo.extensionRequest.subjectAltName.iPAddress || [];
|
|
4257
4910
|
ip = ip.map(octetStringToIpAddress);
|
|
4258
|
-
const
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
[
|
|
4270
|
-
"ca",
|
|
4271
|
-
"-config",
|
|
4272
|
-
configFile,
|
|
4273
|
-
"-startdate",
|
|
4274
|
-
x509Date(params1.startDate),
|
|
4275
|
-
"-enddate",
|
|
4276
|
-
x509Date(params1.endDate),
|
|
4277
|
-
"-batch",
|
|
4278
|
-
"-out",
|
|
4279
|
-
n5(certificate),
|
|
4280
|
-
"-in",
|
|
4281
|
-
n5(certificateSigningRequestFilename),
|
|
4282
|
-
...passin.args
|
|
4283
|
-
],
|
|
4284
|
-
{ ...options, env: passin.env }
|
|
4285
|
-
);
|
|
4286
|
-
displaySubtitle("- dump the certificate for a check");
|
|
4287
|
-
await execute_openssl(["x509", "-in", n5(certificate), "-dates", "-fingerprint", "-purpose", "-noout"], options);
|
|
4288
|
-
displaySubtitle("- construct CA certificate with CRL");
|
|
4289
|
-
await this.constructCACertificateWithCRL();
|
|
4290
|
-
displaySubtitle("- construct certificate chain");
|
|
4291
|
-
await this.constructCertificateChain(certificate);
|
|
4292
|
-
displaySubtitle("- verify certificate against the root CA");
|
|
4293
|
-
await this.verifyCertificate(certificate);
|
|
4294
|
-
return certificate;
|
|
4911
|
+
const sanOverride = { applicationUri, dns: dns2, ip };
|
|
4912
|
+
return this.#withCaLock(async () => {
|
|
4913
|
+
await this.#backend.signEndEntityCsr(this, certificate, certificateSigningRequestFilename, params1, sanOverride);
|
|
4914
|
+
displaySubtitle("- construct CA certificate with CRL");
|
|
4915
|
+
await this.constructCACertificateWithCRL();
|
|
4916
|
+
displaySubtitle("- construct certificate chain");
|
|
4917
|
+
await this.constructCertificateChain(certificate);
|
|
4918
|
+
displaySubtitle("- verify certificate against the root CA");
|
|
4919
|
+
await this.verifyCertificate(certificate);
|
|
4920
|
+
return certificate;
|
|
4921
|
+
});
|
|
4295
4922
|
}
|
|
4296
4923
|
/**
|
|
4297
|
-
*
|
|
4924
|
+
* Check that `certificate` really was signed by this CA, and throw if it
|
|
4925
|
+
* was not.
|
|
4926
|
+
*
|
|
4927
|
+
* This used to do nothing: `openssl verify` crashes on Windows, so the
|
|
4928
|
+
* check was left as a placeholder. It no longer needs a subprocess -
|
|
4929
|
+
* `verifyCertificateSignature` does it in pure JS, the same way
|
|
4930
|
+
* {@link CertificateManager} already validates a chain - so the check
|
|
4931
|
+
* that `signCertificateRequest` always claimed to perform now actually
|
|
4932
|
+
* happens on every issuance, whichever backend did the signing.
|
|
4933
|
+
*
|
|
4934
|
+
* Only the leading certificate is examined: the file may be a chain,
|
|
4935
|
+
* and the rest of it is this CA's own certificate and its issuers.
|
|
4298
4936
|
*
|
|
4299
|
-
* @param certificate - path to the certificate
|
|
4937
|
+
* @param certificate - path to the certificate (or chain) to verify
|
|
4300
4938
|
*/
|
|
4301
4939
|
async verifyCertificate(certificate) {
|
|
4302
|
-
const
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4940
|
+
const pem = await fs12.promises.readFile(certificate, "utf-8");
|
|
4941
|
+
const blocks = pem.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g);
|
|
4942
|
+
if (!blocks || blocks.length === 0) {
|
|
4943
|
+
throw new Error(`verifyCertificate: ${certificate} contains no PEM-encoded certificate`);
|
|
4944
|
+
}
|
|
4945
|
+
const caCertificateDer = convertPEMtoDER2(readCertificatePEM2(this.caCertificate));
|
|
4946
|
+
if (!verifyCertificateSignature2(convertPEMtoDER2(blocks[0]), caCertificateDer)) {
|
|
4947
|
+
throw new Error(
|
|
4948
|
+
`verifyCertificate: ${certificate} was not signed by this certificate authority (${this.caCertificate})`
|
|
4949
|
+
);
|
|
4950
|
+
}
|
|
4951
|
+
}
|
|
4952
|
+
};
|
|
4953
|
+
}
|
|
4954
|
+
});
|
|
4955
|
+
|
|
4956
|
+
// packages/node-opcua-pki/lib/ca/certificate_authority.ts
|
|
4957
|
+
var CertificateAuthority;
|
|
4958
|
+
var init_certificate_authority = __esm({
|
|
4959
|
+
"packages/node-opcua-pki/lib/ca/certificate_authority.ts"() {
|
|
4960
|
+
"use strict";
|
|
4961
|
+
init_esm_shims();
|
|
4962
|
+
init_with_openssl();
|
|
4963
|
+
init_native_ca_backend();
|
|
4964
|
+
init_openssl_ca_backend();
|
|
4965
|
+
init_certificate_authority_core();
|
|
4966
|
+
init_certificate_authority_core();
|
|
4967
|
+
CertificateAuthority = class extends CertificateAuthorityCore {
|
|
4968
|
+
constructor(options) {
|
|
4969
|
+
if (options.signer && options.backend === "openssl") {
|
|
4970
|
+
throw new Error(
|
|
4971
|
+
"CertificateAuthority: backend 'openssl' cannot be combined with 'signer' - the openssl CLI loads its key from a file and cannot call an external signer. Drop the backend option (a signer implies 'native') or drop the signer."
|
|
4310
4972
|
);
|
|
4311
4973
|
}
|
|
4974
|
+
const backend = options.signer || options.backend === "native" ? new NativeCaBackend() : new OpenSslCaBackend();
|
|
4975
|
+
super({ ...options, backend });
|
|
4976
|
+
}
|
|
4977
|
+
/**
|
|
4978
|
+
* @internal `-passin env:` argv + env for an openssl call that loads
|
|
4979
|
+
* this CA's key (always emitted, empty when none).
|
|
4980
|
+
*
|
|
4981
|
+
* The openssl backend builds its own now; this remains so that external
|
|
4982
|
+
* code calling it keeps working, and lives here rather than on the core
|
|
4983
|
+
* because the flag means nothing to a backend that spawns nothing.
|
|
4984
|
+
*/
|
|
4985
|
+
async _opensslPassin() {
|
|
4986
|
+
return passinArg(await this._privateKeyPassphrase());
|
|
4987
|
+
}
|
|
4988
|
+
/**
|
|
4989
|
+
* @internal
|
|
4990
|
+
* Legacy shim: publish the `CDP_URL` / `AIA_VALUE` config substitution
|
|
4991
|
+
* values to the shared env registry, or unset them so the matching
|
|
4992
|
+
* `{{#KEY}}...{{/KEY}}` blocks are stripped. Nothing in this package
|
|
4993
|
+
* reads the registry any more - every openssl config render receives
|
|
4994
|
+
* these values explicitly, from the same {@link caConfigEnvOverrides}
|
|
4995
|
+
* builder this delegates to, so the two cannot drift - kept only for
|
|
4996
|
+
* external code that renders openssl config templates against the
|
|
4997
|
+
* registry directly.
|
|
4998
|
+
*
|
|
4999
|
+
* It lives on this class rather than the core because it is meaningful
|
|
5000
|
+
* only to the openssl backend.
|
|
5001
|
+
*/
|
|
5002
|
+
_wireRevocationEnvVars() {
|
|
5003
|
+
const overrides = caConfigEnvOverrides(this);
|
|
5004
|
+
if (overrides.CDP_URL) {
|
|
5005
|
+
setEnv("CDP_URL", overrides.CDP_URL);
|
|
5006
|
+
} else {
|
|
5007
|
+
unsetEnv("CDP_URL");
|
|
5008
|
+
}
|
|
5009
|
+
if (overrides.AIA_VALUE) {
|
|
5010
|
+
setEnv("AIA_VALUE", overrides.AIA_VALUE);
|
|
5011
|
+
} else {
|
|
5012
|
+
unsetEnv("AIA_VALUE");
|
|
5013
|
+
}
|
|
4312
5014
|
}
|
|
4313
5015
|
};
|
|
4314
5016
|
}
|
|
4315
5017
|
});
|
|
4316
5018
|
|
|
4317
5019
|
// packages/node-opcua-pki/lib/ca/crypto_create_CA.ts
|
|
4318
|
-
import
|
|
4319
|
-
import
|
|
5020
|
+
import assert10 from "assert";
|
|
5021
|
+
import fs13 from "fs";
|
|
4320
5022
|
import { createRequire } from "module";
|
|
4321
5023
|
import os5 from "os";
|
|
4322
|
-
import
|
|
5024
|
+
import path10 from "path";
|
|
4323
5025
|
import chalk7 from "chalk";
|
|
4324
|
-
import { CertificatePurpose as
|
|
5026
|
+
import { CertificatePurpose as CertificatePurpose4, generatePrivateKeyFile as generatePrivateKeyFile3, Subject as Subject7 } from "node-opcua-crypto";
|
|
4325
5027
|
import commandLineArgs from "command-line-args";
|
|
4326
5028
|
import commandLineUsage from "command-line-usage";
|
|
4327
5029
|
function get_offset_date(date, nbDays) {
|
|
@@ -4329,9 +5031,9 @@ function get_offset_date(date, nbDays) {
|
|
|
4329
5031
|
d.setDate(d.getDate() + nbDays);
|
|
4330
5032
|
return d;
|
|
4331
5033
|
}
|
|
4332
|
-
async function
|
|
4333
|
-
|
|
4334
|
-
|
|
5034
|
+
async function construct_CertificateAuthority(subject) {
|
|
5035
|
+
assert10(typeof gLocalConfig.CAFolder === "string", "expecting a CAFolder in config");
|
|
5036
|
+
assert10(typeof gLocalConfig.keySize === "number", "expecting a keySize in config");
|
|
4335
5037
|
if (!g_certificateAuthority) {
|
|
4336
5038
|
g_certificateAuthority = new CertificateAuthority({
|
|
4337
5039
|
keySize: gLocalConfig.keySize,
|
|
@@ -4342,7 +5044,7 @@ async function construct_CertificateAuthority2(subject) {
|
|
|
4342
5044
|
}
|
|
4343
5045
|
}
|
|
4344
5046
|
async function construct_CertificateManager() {
|
|
4345
|
-
|
|
5047
|
+
assert10(typeof gLocalConfig.PKIFolder === "string", "expecting a PKIFolder in config");
|
|
4346
5048
|
if (!certificateManager) {
|
|
4347
5049
|
certificateManager = new CertificateManager({
|
|
4348
5050
|
keySize: gLocalConfig.keySize,
|
|
@@ -4353,35 +5055,35 @@ async function construct_CertificateManager() {
|
|
|
4353
5055
|
}
|
|
4354
5056
|
function default_template_content() {
|
|
4355
5057
|
if (process.pkg?.entrypoint) {
|
|
4356
|
-
const a =
|
|
5058
|
+
const a = fs13.readFileSync(path10.join(__dirname, "../../bin/pki_config.example.js"), "utf8");
|
|
4357
5059
|
return a;
|
|
4358
5060
|
}
|
|
4359
5061
|
function find_default_config_template() {
|
|
4360
5062
|
const rootFolder = find_module_root_folder();
|
|
4361
5063
|
const configName = "pki_config.example.js";
|
|
4362
|
-
let default_config_template2 =
|
|
4363
|
-
if (!
|
|
4364
|
-
default_config_template2 =
|
|
4365
|
-
if (!
|
|
4366
|
-
default_config_template2 =
|
|
5064
|
+
let default_config_template2 = path10.join(rootFolder, "bin", configName);
|
|
5065
|
+
if (!fs13.existsSync(default_config_template2)) {
|
|
5066
|
+
default_config_template2 = path10.join(__dirname, "..", configName);
|
|
5067
|
+
if (!fs13.existsSync(default_config_template2)) {
|
|
5068
|
+
default_config_template2 = path10.join(__dirname, `../bin/${configName}`);
|
|
4367
5069
|
}
|
|
4368
5070
|
}
|
|
4369
5071
|
return default_config_template2;
|
|
4370
5072
|
}
|
|
4371
5073
|
const default_config_template = find_default_config_template();
|
|
4372
|
-
|
|
4373
|
-
const default_config_template_content =
|
|
5074
|
+
assert10(fs13.existsSync(default_config_template));
|
|
5075
|
+
const default_config_template_content = fs13.readFileSync(default_config_template, "utf8");
|
|
4374
5076
|
return default_config_template_content;
|
|
4375
5077
|
}
|
|
4376
5078
|
function find_module_root_folder() {
|
|
4377
|
-
let rootFolder =
|
|
5079
|
+
let rootFolder = path10.join(__dirname);
|
|
4378
5080
|
for (let i = 0; i < 4; i++) {
|
|
4379
|
-
if (
|
|
5081
|
+
if (fs13.existsSync(path10.join(rootFolder, "package.json"))) {
|
|
4380
5082
|
return rootFolder;
|
|
4381
5083
|
}
|
|
4382
|
-
rootFolder =
|
|
5084
|
+
rootFolder = path10.join(rootFolder, "..");
|
|
4383
5085
|
}
|
|
4384
|
-
|
|
5086
|
+
assert10(fs13.existsSync(path10.join(rootFolder, "package.json")), "root folder must have a package.json file");
|
|
4385
5087
|
return rootFolder;
|
|
4386
5088
|
}
|
|
4387
5089
|
async function readConfiguration(argv) {
|
|
@@ -4406,41 +5108,41 @@ async function readConfiguration(argv) {
|
|
|
4406
5108
|
return str;
|
|
4407
5109
|
}
|
|
4408
5110
|
function prepare(file) {
|
|
4409
|
-
const tmp =
|
|
5111
|
+
const tmp = path10.resolve(performSubstitution(file));
|
|
4410
5112
|
return makePath(tmp);
|
|
4411
5113
|
}
|
|
4412
5114
|
certificateDir = argv.root;
|
|
4413
|
-
|
|
5115
|
+
assert10(typeof certificateDir === "string");
|
|
4414
5116
|
certificateDir = prepare(certificateDir);
|
|
4415
5117
|
mkdirRecursiveSync(certificateDir);
|
|
4416
|
-
|
|
4417
|
-
const default_config =
|
|
4418
|
-
if (!
|
|
5118
|
+
assert10(fs13.existsSync(certificateDir));
|
|
5119
|
+
const default_config = path10.join(certificateDir, "config.js");
|
|
5120
|
+
if (!fs13.existsSync(default_config)) {
|
|
4419
5121
|
debugLog(chalk7.yellow(" Creating default g_config file "), chalk7.cyan(default_config));
|
|
4420
5122
|
const default_config_template_content = default_template_content();
|
|
4421
|
-
|
|
5123
|
+
fs13.writeFileSync(default_config, default_config_template_content);
|
|
4422
5124
|
} else {
|
|
4423
5125
|
debugLog(chalk7.yellow(" using g_config file "), chalk7.cyan(default_config));
|
|
4424
5126
|
}
|
|
4425
|
-
if (!
|
|
5127
|
+
if (!fs13.existsSync(default_config)) {
|
|
4426
5128
|
debugLog(chalk7.redBright(" cannot find config file ", default_config));
|
|
4427
5129
|
}
|
|
4428
|
-
const defaultRandomFile =
|
|
5130
|
+
const defaultRandomFile = path10.join(path10.dirname(default_config), "random.rnd");
|
|
4429
5131
|
setEnv("RANDFILE", defaultRandomFile);
|
|
4430
5132
|
const _require = createRequire(__filename);
|
|
4431
5133
|
gLocalConfig = _require(default_config);
|
|
4432
|
-
gLocalConfig.subject = new
|
|
5134
|
+
gLocalConfig.subject = new Subject7(gLocalConfig.subject || "");
|
|
4433
5135
|
if (argv.subject) {
|
|
4434
|
-
gLocalConfig.subject = new
|
|
5136
|
+
gLocalConfig.subject = new Subject7(argv.subject);
|
|
4435
5137
|
}
|
|
4436
5138
|
if (!gLocalConfig.subject.commonName) {
|
|
4437
5139
|
throw new Error("subject must have a Common Name");
|
|
4438
5140
|
}
|
|
4439
5141
|
gLocalConfig.certificateDir = certificateDir;
|
|
4440
|
-
let CAFolder = argv.CAFolder ||
|
|
5142
|
+
let CAFolder = argv.CAFolder || path10.join(certificateDir, "CA");
|
|
4441
5143
|
CAFolder = prepare(CAFolder);
|
|
4442
5144
|
gLocalConfig.CAFolder = CAFolder;
|
|
4443
|
-
gLocalConfig.PKIFolder =
|
|
5145
|
+
gLocalConfig.PKIFolder = path10.join(gLocalConfig.certificateDir, "PKI");
|
|
4444
5146
|
if (argv.PKIFolder) {
|
|
4445
5147
|
gLocalConfig.PKIFolder = prepare(argv.PKIFolder);
|
|
4446
5148
|
}
|
|
@@ -4478,7 +5180,7 @@ async function readConfiguration(argv) {
|
|
|
4478
5180
|
}
|
|
4479
5181
|
}
|
|
4480
5182
|
async function createDefaultCertificate(base_name, prefix, key_length, applicationUri, dev) {
|
|
4481
|
-
|
|
5183
|
+
assert10(key_length === 1024 || key_length === 2048 || key_length === 3072 || key_length === 4096);
|
|
4482
5184
|
const private_key_file = makePath(base_name, `${prefix}key_${key_length}.pem`);
|
|
4483
5185
|
const public_key_file = makePath(base_name, `${prefix}public_key_${key_length}.pub`);
|
|
4484
5186
|
const certificate_file = makePath(base_name, `${prefix}cert_${key_length}.pem`);
|
|
@@ -4498,7 +5200,7 @@ async function createDefaultCertificate(base_name, prefix, key_length, applicati
|
|
|
4498
5200
|
}
|
|
4499
5201
|
const ip = [];
|
|
4500
5202
|
async function createCertificateIfNotExist(certificate, private_key, applicationUri2, startDate, validity) {
|
|
4501
|
-
if (
|
|
5203
|
+
if (fs13.existsSync(certificate)) {
|
|
4502
5204
|
warningLog(chalk7.yellow(" certificate"), chalk7.cyan(certificate), chalk7.yellow(" already exists => skipping"));
|
|
4503
5205
|
return "";
|
|
4504
5206
|
} else {
|
|
@@ -4517,7 +5219,7 @@ async function createDefaultCertificate(base_name, prefix, key_length, applicati
|
|
|
4517
5219
|
configFile,
|
|
4518
5220
|
dns: dns3,
|
|
4519
5221
|
ip: ip2,
|
|
4520
|
-
purpose:
|
|
5222
|
+
purpose: CertificatePurpose4.ForApplication
|
|
4521
5223
|
};
|
|
4522
5224
|
await createCertificateSigningRequestWithOpenSSL(certificateSigningRequestFile, params);
|
|
4523
5225
|
return await g_certificateAuthority.signCertificateRequest(certificate, certificateSigningRequestFile, {
|
|
@@ -4541,7 +5243,7 @@ async function createDefaultCertificate(base_name, prefix, key_length, applicati
|
|
|
4541
5243
|
await g_certificateAuthority.revokeCertificate(certificate, {});
|
|
4542
5244
|
}
|
|
4543
5245
|
async function createPrivateKeyIfNotExist(privateKey, keyLength) {
|
|
4544
|
-
if (
|
|
5246
|
+
if (fs13.existsSync(privateKey)) {
|
|
4545
5247
|
warningLog(chalk7.yellow(" privateKey"), chalk7.cyan(privateKey), chalk7.yellow(" already exists => skipping"));
|
|
4546
5248
|
return;
|
|
4547
5249
|
} else {
|
|
@@ -4555,14 +5257,14 @@ async function createDefaultCertificate(base_name, prefix, key_length, applicati
|
|
|
4555
5257
|
displaySubtitle(` create Certificate ${certificate_file}`);
|
|
4556
5258
|
await createCertificateIfNotExist(certificate_file, private_key_file, applicationUri, yesterday, 365);
|
|
4557
5259
|
displaySubtitle(` create self signed Certificate ${self_signed_certificate_file}`);
|
|
4558
|
-
if (
|
|
5260
|
+
if (fs13.existsSync(self_signed_certificate_file)) {
|
|
4559
5261
|
return;
|
|
4560
5262
|
}
|
|
4561
5263
|
await createSelfSignedCertificate2(self_signed_certificate_file, private_key_file, applicationUri, yesterday, 365);
|
|
4562
5264
|
if (dev) {
|
|
4563
5265
|
await createCertificateIfNotExist(certificate_file_outofdate, private_key_file, applicationUri, two_years_ago, 365);
|
|
4564
5266
|
await createCertificateIfNotExist(certificate_file_not_active_yet, private_key_file, applicationUri, next_year, 365);
|
|
4565
|
-
if (!
|
|
5267
|
+
if (!fs13.existsSync(certificate_revoked)) {
|
|
4566
5268
|
const certificate = await createCertificateIfNotExist(
|
|
4567
5269
|
certificate_revoked,
|
|
4568
5270
|
private_key_file,
|
|
@@ -4584,9 +5286,9 @@ async function wrap(func) {
|
|
|
4584
5286
|
}
|
|
4585
5287
|
}
|
|
4586
5288
|
async function create_default_certificates(dev) {
|
|
4587
|
-
|
|
5289
|
+
assert10(gLocalConfig);
|
|
4588
5290
|
const base_name = gLocalConfig.certificateDir || "";
|
|
4589
|
-
|
|
5291
|
+
assert10(fs13.existsSync(base_name));
|
|
4590
5292
|
let clientURN;
|
|
4591
5293
|
let serverURN;
|
|
4592
5294
|
let discoveryServerURN;
|
|
@@ -4617,7 +5319,7 @@ async function create_default_certificates(dev) {
|
|
|
4617
5319
|
});
|
|
4618
5320
|
}
|
|
4619
5321
|
async function createDefaultCertificates(dev) {
|
|
4620
|
-
await
|
|
5322
|
+
await construct_CertificateAuthority("");
|
|
4621
5323
|
await construct_CertificateManager();
|
|
4622
5324
|
await create_default_certificates(dev);
|
|
4623
5325
|
}
|
|
@@ -4683,7 +5385,7 @@ ${epilog}`
|
|
|
4683
5385
|
}
|
|
4684
5386
|
if (command === "version") {
|
|
4685
5387
|
const rootFolder = find_module_root_folder();
|
|
4686
|
-
const pkg = JSON.parse(
|
|
5388
|
+
const pkg = JSON.parse(fs13.readFileSync(path10.join(rootFolder, "package.json"), "utf-8"));
|
|
4687
5389
|
console.log(pkg.version);
|
|
4688
5390
|
return;
|
|
4689
5391
|
}
|
|
@@ -4708,12 +5410,12 @@ ${epilog}`
|
|
|
4708
5410
|
await readConfiguration(local_argv);
|
|
4709
5411
|
if (local_argv.clean) {
|
|
4710
5412
|
displayTitle("Cleaning old certificates");
|
|
4711
|
-
|
|
5413
|
+
assert10(gLocalConfig);
|
|
4712
5414
|
const certificateDir = gLocalConfig.certificateDir || "";
|
|
4713
|
-
const files = await
|
|
5415
|
+
const files = await fs13.promises.readdir(certificateDir);
|
|
4714
5416
|
for (const file of files) {
|
|
4715
5417
|
if (file.includes(".pem") || file.includes(".pub")) {
|
|
4716
|
-
await
|
|
5418
|
+
await fs13.promises.unlink(path10.join(certificateDir, file));
|
|
4717
5419
|
}
|
|
4718
5420
|
}
|
|
4719
5421
|
mkdirRecursiveSync(certificateDir);
|
|
@@ -4734,7 +5436,7 @@ ${epilog}`
|
|
|
4734
5436
|
await wrap(async () => {
|
|
4735
5437
|
await ensure_openssl_installed();
|
|
4736
5438
|
await readConfiguration(local_argv);
|
|
4737
|
-
await
|
|
5439
|
+
await construct_CertificateAuthority(local_argv.subject);
|
|
4738
5440
|
});
|
|
4739
5441
|
return;
|
|
4740
5442
|
}
|
|
@@ -4803,7 +5505,7 @@ ${epilog}`
|
|
|
4803
5505
|
await readConfiguration(local_argv2);
|
|
4804
5506
|
await construct_CertificateManager();
|
|
4805
5507
|
displaySubtitle(` create self signed Certificate ${gLocalConfig.outputFile}`);
|
|
4806
|
-
let subject = local_argv2.subject && local_argv2.subject.length > 1 ? new
|
|
5508
|
+
let subject = local_argv2.subject && local_argv2.subject.length > 1 ? new Subject7(local_argv2.subject) : gLocalConfig.subject || "";
|
|
4807
5509
|
subject = JSON.parse(JSON.stringify(subject));
|
|
4808
5510
|
const params = {
|
|
4809
5511
|
applicationUri: gLocalConfig.applicationUri || "",
|
|
@@ -4819,8 +5521,8 @@ ${epilog}`
|
|
|
4819
5521
|
async function command_full_certificate(local_argv2) {
|
|
4820
5522
|
await readConfiguration(local_argv2);
|
|
4821
5523
|
await construct_CertificateManager();
|
|
4822
|
-
await
|
|
4823
|
-
|
|
5524
|
+
await construct_CertificateAuthority("");
|
|
5525
|
+
assert10(fs13.existsSync(gLocalConfig.CAFolder || ""), " CA folder must exist");
|
|
4824
5526
|
gLocalConfig.privateKey = void 0;
|
|
4825
5527
|
gLocalConfig.subject = local_argv2.subject && local_argv2.subject.length > 1 ? local_argv2.subject : gLocalConfig.subject;
|
|
4826
5528
|
const csr_file = await certificateManager.createCertificateRequest(
|
|
@@ -4831,7 +5533,7 @@ ${epilog}`
|
|
|
4831
5533
|
}
|
|
4832
5534
|
warningLog(" csr_file = ", csr_file);
|
|
4833
5535
|
const certificate = csr_file.replace(".csr", ".pem");
|
|
4834
|
-
if (
|
|
5536
|
+
if (fs13.existsSync(certificate)) {
|
|
4835
5537
|
throw new Error(` File ${certificate} already exist`);
|
|
4836
5538
|
}
|
|
4837
5539
|
await g_certificateAuthority.signCertificateRequest(
|
|
@@ -4839,8 +5541,8 @@ ${epilog}`
|
|
|
4839
5541
|
csr_file,
|
|
4840
5542
|
gLocalConfig
|
|
4841
5543
|
);
|
|
4842
|
-
|
|
4843
|
-
|
|
5544
|
+
assert10(typeof gLocalConfig.outputFile === "string");
|
|
5545
|
+
fs13.writeFileSync(gLocalConfig.outputFile || "", fs13.readFileSync(certificate, "ascii"));
|
|
4844
5546
|
}
|
|
4845
5547
|
await wrap(async () => await command_certificate(local_argv));
|
|
4846
5548
|
return;
|
|
@@ -4859,13 +5561,13 @@ ${epilog}`
|
|
|
4859
5561
|
await g_certificateAuthority.revokeCertificate(certificate, {});
|
|
4860
5562
|
}
|
|
4861
5563
|
await wrap(async () => {
|
|
4862
|
-
const certificate =
|
|
5564
|
+
const certificate = path10.resolve(local_argv.certificateFile);
|
|
4863
5565
|
warningLog(chalk7.yellow(" Certificate to revoke : "), chalk7.cyan(certificate));
|
|
4864
|
-
if (!
|
|
5566
|
+
if (!fs13.existsSync(certificate)) {
|
|
4865
5567
|
throw new Error(`cannot find certificate to revoke ${certificate}`);
|
|
4866
5568
|
}
|
|
4867
5569
|
await readConfiguration(local_argv);
|
|
4868
|
-
await
|
|
5570
|
+
await construct_CertificateAuthority("");
|
|
4869
5571
|
await revoke_certificate(certificate);
|
|
4870
5572
|
warningLog("done ... ");
|
|
4871
5573
|
warningLog(" crl = ", g_certificateAuthority.revocationList);
|
|
@@ -4908,11 +5610,11 @@ ${epilog}`
|
|
|
4908
5610
|
if (local_argv.help) return showHelp("csr", "create a certificate signing request", optionsDef);
|
|
4909
5611
|
await wrap(async () => {
|
|
4910
5612
|
await readConfiguration(local_argv);
|
|
4911
|
-
if (!
|
|
5613
|
+
if (!fs13.existsSync(gLocalConfig.PKIFolder || "")) {
|
|
4912
5614
|
warningLog("PKI folder must exist");
|
|
4913
5615
|
}
|
|
4914
5616
|
await construct_CertificateManager();
|
|
4915
|
-
if (!gLocalConfig.outputFile ||
|
|
5617
|
+
if (!gLocalConfig.outputFile || fs13.existsSync(gLocalConfig.outputFile)) {
|
|
4916
5618
|
throw new Error(` File ${gLocalConfig.outputFile} already exist`);
|
|
4917
5619
|
}
|
|
4918
5620
|
gLocalConfig.privateKey = void 0;
|
|
@@ -4927,8 +5629,8 @@ ${epilog}`
|
|
|
4927
5629
|
warningLog("please specify a output file");
|
|
4928
5630
|
return;
|
|
4929
5631
|
}
|
|
4930
|
-
const csr = await
|
|
4931
|
-
|
|
5632
|
+
const csr = await fs13.promises.readFile(internal_csr_file, "utf-8");
|
|
5633
|
+
fs13.writeFileSync(gLocalConfig.outputFile || "", csr, "utf-8");
|
|
4932
5634
|
warningLog("Subject = ", gLocalConfig.subject);
|
|
4933
5635
|
warningLog("applicationUri = ", gLocalConfig.applicationUri);
|
|
4934
5636
|
warningLog("altNames = ", gLocalConfig.altNames);
|
|
@@ -4956,16 +5658,16 @@ ${epilog}`
|
|
|
4956
5658
|
return showHelp("sign", "validate a certificate signing request and generate a certificate", optionsDef);
|
|
4957
5659
|
await wrap(async () => {
|
|
4958
5660
|
await readConfiguration(local_argv);
|
|
4959
|
-
if (!
|
|
5661
|
+
if (!fs13.existsSync(gLocalConfig.CAFolder || "")) {
|
|
4960
5662
|
throw new Error(`CA folder must exist:${gLocalConfig.CAFolder}`);
|
|
4961
5663
|
}
|
|
4962
|
-
await
|
|
4963
|
-
const csr_file =
|
|
4964
|
-
if (!
|
|
5664
|
+
await construct_CertificateAuthority("");
|
|
5665
|
+
const csr_file = path10.resolve(local_argv.csr || "");
|
|
5666
|
+
if (!fs13.existsSync(csr_file)) {
|
|
4965
5667
|
throw new Error(`Certificate signing request doesn't exist: ${csr_file}`);
|
|
4966
5668
|
}
|
|
4967
|
-
const certificate =
|
|
4968
|
-
if (
|
|
5669
|
+
const certificate = path10.resolve(local_argv.output || csr_file.replace(".csr", ".pem"));
|
|
5670
|
+
if (fs13.existsSync(certificate)) {
|
|
4969
5671
|
throw new Error(` File ${certificate} already exist`);
|
|
4970
5672
|
}
|
|
4971
5673
|
await g_certificateAuthority.signCertificateRequest(
|
|
@@ -4973,8 +5675,8 @@ ${epilog}`
|
|
|
4973
5675
|
csr_file,
|
|
4974
5676
|
gLocalConfig
|
|
4975
5677
|
);
|
|
4976
|
-
|
|
4977
|
-
|
|
5678
|
+
assert10(typeof gLocalConfig.outputFile === "string");
|
|
5679
|
+
fs13.writeFileSync(gLocalConfig.outputFile || "", fs13.readFileSync(certificate, "ascii"));
|
|
4978
5680
|
});
|
|
4979
5681
|
return;
|
|
4980
5682
|
}
|