node-opcua-pki 6.19.0 → 6.20.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/index.js CHANGED
@@ -34,6 +34,7 @@ __export(lib_exports, {
34
34
  CertificateManager: () => CertificateManager,
35
35
  CertificateManagerState: () => CertificateManagerState,
36
36
  ChainCompletionStatus: () => ChainCompletionStatus,
37
+ PrivateKeyPassphraseRequiredError: () => import_node_opcua_crypto6.PrivateKeyPassphraseRequiredError,
37
38
  Subject: () => import_node_opcua_crypto.Subject,
38
39
  VerificationStatus: () => VerificationStatus,
39
40
  adjustApplicationUri: () => adjustApplicationUri,
@@ -52,9 +53,11 @@ __export(lib_exports, {
52
53
  isIssuer: () => isIssuer,
53
54
  isRootIssuer: () => isRootIssuer,
54
55
  makeFingerprint: () => makeFingerprint,
55
- quote: () => quote
56
+ quote: () => quote,
57
+ resolvePrivateKeyPassphrase: () => resolvePrivateKeyPassphrase
56
58
  });
57
59
  module.exports = __toCommonJS(lib_exports);
60
+ var import_node_opcua_crypto6 = require("node-opcua-crypto");
58
61
 
59
62
  // packages/node-opcua-pki/lib/ca/certificate_authority.ts
60
63
  var import_node_assert7 = __toESM(require("assert"));
@@ -65,41 +68,11 @@ var import_chalk5 = __toESM(require("chalk"));
65
68
  var import_node_opcua_crypto2 = require("node-opcua-crypto");
66
69
 
67
70
  // packages/node-opcua-pki/lib/pki/toolbox_pfx.ts
68
- var import_node_assert4 = __toESM(require("assert"));
71
+ var import_node_assert3 = __toESM(require("assert"));
69
72
  var import_node_fs4 = __toESM(require("fs"));
70
73
 
71
- // packages/node-opcua-pki/lib/toolbox/common.ts
72
- var import_node_assert = __toESM(require("assert"));
73
- function quote(str) {
74
- return `"${str || ""}"`;
75
- }
76
- function adjustDate(params) {
77
- (0, import_node_assert.default)(params instanceof Object);
78
- params.startDate = params.startDate || /* @__PURE__ */ new Date();
79
- (0, import_node_assert.default)(params.startDate instanceof Date);
80
- if (params.validityMs !== void 0) {
81
- if (params.validityMs <= 0) {
82
- throw new RangeError(`validityMs must be > 0 (got ${params.validityMs})`);
83
- }
84
- params.endDate = new Date(params.startDate.getTime() + params.validityMs);
85
- params.validity = Math.ceil(params.validityMs / 864e5);
86
- } else {
87
- params.validity = params.validity || 365;
88
- params.endDate = new Date(params.startDate.getTime());
89
- params.endDate.setDate(params.startDate.getDate() + params.validity);
90
- }
91
- (0, import_node_assert.default)(params.endDate instanceof Date);
92
- (0, import_node_assert.default)(params.startDate instanceof Date);
93
- }
94
- function adjustApplicationUri(params) {
95
- const applicationUri = params.applicationUri || "";
96
- if (applicationUri.length > 200) {
97
- throw new Error(`Openssl doesn't support urn with length greater than 200${applicationUri}`);
98
- }
99
- }
100
-
101
74
  // packages/node-opcua-pki/lib/toolbox/common2.ts
102
- var import_node_assert2 = __toESM(require("assert"));
75
+ var import_node_assert = __toESM(require("assert"));
103
76
  var import_node_fs = __toESM(require("fs"));
104
77
  var import_node_path = __toESM(require("path"));
105
78
  var import_chalk = __toESM(require("chalk"));
@@ -140,12 +113,31 @@ function mkdirRecursiveSync(folder) {
140
113
  import_node_fs.default.mkdirSync(folder, { recursive: true });
141
114
  }
142
115
  }
116
+ function restrictPrivateFilePermissions(target, mode) {
117
+ if (process.platform === "win32") {
118
+ return;
119
+ }
120
+ try {
121
+ import_node_fs.default.chmodSync(target, mode);
122
+ } catch (err) {
123
+ warningLog(import_chalk.default.yellow(" could not restrict permissions on "), target, err.message);
124
+ }
125
+ }
126
+ function ensurePrivateDirectory(dir) {
127
+ if (!import_node_fs.default.existsSync(dir)) {
128
+ import_node_fs.default.mkdirSync(dir, { recursive: true, mode: 448 });
129
+ }
130
+ restrictPrivateFilePermissions(dir, 448);
131
+ }
132
+ function isEncryptedPrivateKeyFile(filename) {
133
+ return import_node_fs.default.readFileSync(filename, "utf-8").includes("-----BEGIN ENCRYPTED PRIVATE KEY-----");
134
+ }
143
135
  function makePath(folderName, filename) {
144
136
  let s;
145
137
  if (filename) {
146
138
  s = import_node_path.default.join(import_node_path.default.normalize(folderName), filename);
147
139
  } else {
148
- (0, import_node_assert2.default)(folderName);
140
+ (0, import_node_assert.default)(folderName);
149
141
  s = folderName;
150
142
  }
151
143
  s = s.replace(/\\/g, "/");
@@ -153,7 +145,7 @@ function makePath(folderName, filename) {
153
145
  }
154
146
 
155
147
  // packages/node-opcua-pki/lib/toolbox/with_openssl/execute_openssl.ts
156
- var import_node_assert3 = __toESM(require("assert"));
148
+ var import_node_assert2 = __toESM(require("assert"));
157
149
  var import_node_child_process2 = __toESM(require("child_process"));
158
150
  var import_node_fs3 = __toESM(require("fs"));
159
151
  var import_node_os2 = __toESM(require("os"));
@@ -161,6 +153,50 @@ var import_byline2 = __toESM(require("byline"));
161
153
  var import_chalk3 = __toESM(require("chalk"));
162
154
 
163
155
  // packages/node-opcua-pki/lib/toolbox/with_openssl/_env.ts
156
+ var SAFE_ENV_PASSTHROUGH = /* @__PURE__ */ new Set([
157
+ // POSIX/Windows shell and process essentials
158
+ "path",
159
+ "home",
160
+ "userprofile",
161
+ "temp",
162
+ "tmp",
163
+ "tmpdir",
164
+ "systemroot",
165
+ "windir",
166
+ "comspec",
167
+ "pathext",
168
+ "appdata",
169
+ "localappdata",
170
+ // dynamic loader: a custom-built or relocated openssl (e.g. under /opt,
171
+ // or Homebrew on macOS) may need these to find its own libcrypto/libssl
172
+ "ld_library_path",
173
+ "dyld_library_path",
174
+ "dyld_fallback_library_path",
175
+ // locale, so openssl's textual output stays parseable
176
+ "lang",
177
+ "lc_all",
178
+ "lc_ctype",
179
+ // openssl configuration this app, or the host OS/user, may rely on
180
+ "openssl_conf",
181
+ "randfile",
182
+ "openssl_modules",
183
+ "openssl_engines",
184
+ "ssl_cert_file",
185
+ "ssl_cert_dir"
186
+ ]);
187
+ function buildChildEnv(extra) {
188
+ const env = {};
189
+ for (const key of Object.keys(process.env)) {
190
+ if (SAFE_ENV_PASSTHROUGH.has(key.toLowerCase())) {
191
+ env[key] = process.env[key];
192
+ }
193
+ }
194
+ return { ...env, ...extra };
195
+ }
196
+ function redactEnvForLog(options) {
197
+ const { env, ...rest } = options;
198
+ return env ? { ...rest, env: Object.keys(env) } : rest;
199
+ }
164
200
  var exportedEnvVars = {};
165
201
  function setEnv(varName, value) {
166
202
  if (!g_config.silent) {
@@ -215,24 +251,17 @@ var import_byline = __toESM(require("byline"));
215
251
  var import_chalk2 = __toESM(require("chalk"));
216
252
  var import_yauzl = __toESM(require("yauzl"));
217
253
  var doDebug2 = process.env.NODEOPCUAPKIDEBUG || false;
218
- async function execute(cmd, cwd) {
254
+ async function execute(file, args, cwd) {
219
255
  let output = "";
220
- const options = {
221
- cwd,
222
- windowsHide: true
223
- };
224
256
  return await new Promise((resolve, reject) => {
225
- const child = import_node_child_process.default.exec(
226
- cmd,
227
- options,
228
- (err) => {
229
- const exitCode = err === null ? 0 : typeof err.code === "number" ? err.code : 1;
230
- if (err) reject(err);
231
- else {
232
- resolve({ exitCode, output });
233
- }
234
- }
235
- );
257
+ const child = import_node_child_process.default.spawn(file, args, {
258
+ cwd,
259
+ windowsHide: true,
260
+ env: buildChildEnv(),
261
+ stdio: ["ignore", "pipe", "pipe"]
262
+ });
263
+ child.on("error", (err) => reject(err));
264
+ child.on("close", (code) => resolve({ exitCode: code ?? 1, output }));
236
265
  const stream1 = (0, import_byline.default)(child.stdout);
237
266
  stream1.on("data", (line) => {
238
267
  output += `${line}
@@ -244,16 +273,13 @@ async function execute(cmd, cwd) {
244
273
  });
245
274
  });
246
275
  }
247
- function quote2(str) {
248
- return `"${str.replace(/\\/g, "/")}"`;
249
- }
250
276
  function is_expected_openssl_version(strVersion) {
251
277
  return !!strVersion.match(/OpenSSL \d/);
252
278
  }
253
279
  async function getopensslExecPath() {
254
280
  let result1;
255
281
  try {
256
- result1 = await execute("which openssl");
282
+ result1 = await execute("which", ["openssl"]);
257
283
  } catch (err) {
258
284
  warningLog("warning: ", err.message);
259
285
  throw new Error("Cannot find openssl");
@@ -270,11 +296,10 @@ async function getopensslExecPath() {
270
296
  }
271
297
  async function check_system_openssl_version() {
272
298
  const opensslExecPath = await getopensslExecPath();
273
- const q_opensslExecPath = quote2(opensslExecPath);
274
299
  if (doDebug2) {
275
300
  warningLog(` OpenSSL found in : ${import_chalk2.default.yellow(opensslExecPath)}`);
276
301
  }
277
- const result = await execute(`${q_opensslExecPath} version`);
302
+ const result = await execute(opensslExecPath, ["version"]);
278
303
  const exitCode = result?.exitCode;
279
304
  const output = result?.output;
280
305
  const version = output.trim();
@@ -314,9 +339,8 @@ async function install_and_check_win32_openssl_version() {
314
339
  version: `cannot find file ${opensslExecPath2}`
315
340
  };
316
341
  } else {
317
- const q_openssl_exe_path = quote2(opensslExecPath2);
318
342
  const cwd = ".";
319
- const { exitCode, output } = await execute(`${q_openssl_exe_path} version`, cwd);
343
+ const { exitCode, output } = await execute(opensslExecPath2, ["version"], cwd);
320
344
  const version = output.trim();
321
345
  if (doDebug2) {
322
346
  warningLog(" Version = ", version);
@@ -329,7 +353,7 @@ async function install_and_check_win32_openssl_version() {
329
353
  }
330
354
  async function find_system_openssl_win32() {
331
355
  try {
332
- const result = await execute("where openssl");
356
+ const result = await execute("where", ["openssl"]);
333
357
  if (result.exitCode !== 0) {
334
358
  return void 0;
335
359
  }
@@ -337,8 +361,7 @@ async function install_and_check_win32_openssl_version() {
337
361
  if (!opensslPath2 || !import_node_fs2.default.existsSync(opensslPath2)) {
338
362
  return void 0;
339
363
  }
340
- const q4 = quote2(opensslPath2);
341
- const versionResult = await execute(`${q4} version`);
364
+ const versionResult = await execute(opensslPath2, ["version"]);
342
365
  const version = versionResult.output.trim();
343
366
  if (versionResult.exitCode === 0 && is_expected_openssl_version(version)) {
344
367
  warningLog(
@@ -501,36 +524,55 @@ async function get_openssl_exec_path() {
501
524
  // packages/node-opcua-pki/lib/toolbox/with_openssl/execute_openssl.ts
502
525
  var opensslPath;
503
526
  var n = makePath;
504
- async function execute2(cmd, options) {
527
+ var PASSIN_ENV_VAR = "NODE_OPCUA_PKI_OPENSSL_PASSIN";
528
+ var PASSOUT_ENV_VAR = "NODE_OPCUA_PKI_OPENSSL_PASSOUT";
529
+ function passinArg(passphrase = "") {
530
+ return { args: ["-passin", `env:${PASSIN_ENV_VAR}`], env: { [PASSIN_ENV_VAR]: passphrase } };
531
+ }
532
+ function passoutArg(passphrase = "") {
533
+ return { args: ["-passout", `env:${PASSOUT_ENV_VAR}`], env: { [PASSOUT_ENV_VAR]: passphrase } };
534
+ }
535
+ function renderForDisplay(file, args) {
536
+ return [file, ...args].map((a) => a === "" || /[\s"'`$\\]/.test(a) ? JSON.stringify(a) : a).join(" ");
537
+ }
538
+ async function execute2(file, args, options) {
505
539
  const from = new Error();
506
540
  options.cwd = options.cwd || process.cwd();
507
541
  if (!g_config.silent) {
508
542
  warningLog(import_chalk3.default.cyan(" CWD "), options.cwd);
509
543
  }
510
544
  const outputs = [];
545
+ const errorOutputs = [];
546
+ const display2 = renderForDisplay(file, args);
511
547
  return await new Promise((resolve, reject) => {
512
- const child = import_node_child_process2.default.exec(
513
- cmd,
514
- {
515
- cwd: options.cwd,
516
- windowsHide: true
517
- },
518
- (err) => {
519
- if (err) {
520
- if (!options.hideErrorMessage) {
521
- const fence = "###########################################";
522
- console.error(import_chalk3.default.bgWhiteBright.redBright(`${fence} OPENSSL ERROR ${fence}`));
523
- console.error(import_chalk3.default.bgWhiteBright.redBright(`CWD = ${options.cwd}`));
524
- console.error(import_chalk3.default.bgWhiteBright.redBright(err.message));
525
- console.error(import_chalk3.default.bgWhiteBright.redBright(`${fence} OPENSSL ERROR ${fence}`));
526
- console.error(from.stack);
527
- }
528
- reject(new Error(err.message));
529
- return;
530
- }
548
+ const child = import_node_child_process2.default.spawn(file, [...args], {
549
+ cwd: options.cwd,
550
+ windowsHide: true,
551
+ env: buildChildEnv(options.env),
552
+ stdio: ["ignore", "pipe", "pipe"]
553
+ });
554
+ const fail = (message) => {
555
+ if (!options.hideErrorMessage) {
556
+ const fence = "###########################################";
557
+ console.error(import_chalk3.default.bgWhiteBright.redBright(`${fence} OPENSSL ERROR ${fence}`));
558
+ console.error(import_chalk3.default.bgWhiteBright.redBright(`CWD = ${options.cwd}`));
559
+ console.error(import_chalk3.default.bgWhiteBright.redBright(message));
560
+ console.error(import_chalk3.default.bgWhiteBright.redBright(`${fence} OPENSSL ERROR ${fence}`));
561
+ console.error(from.stack);
562
+ }
563
+ reject(new Error(message));
564
+ };
565
+ child.on("error", (err) => fail(`Command failed: ${display2}
566
+ ${err.message}`));
567
+ child.on("close", (code, signal) => {
568
+ if (code === 0) {
531
569
  resolve(outputs.join(""));
570
+ return;
532
571
  }
533
- );
572
+ const why = signal ? `signal ${signal}` : `exit code ${code}`;
573
+ fail(`Command failed: ${display2}
574
+ ${errorOutputs.join("")}(${why})`);
575
+ });
534
576
  if (child.stdout) {
535
577
  const stream2 = (0, import_byline2.default)(child.stdout);
536
578
  stream2.on("data", (line) => {
@@ -547,17 +589,17 @@ async function execute2(cmd, options) {
547
589
  });
548
590
  }
549
591
  }
550
- if (!g_config.silent) {
551
- if (child.stderr) {
552
- const stream1 = (0, import_byline2.default)(child.stderr);
553
- stream1.on("data", (line) => {
554
- line = line.toString();
555
- if (displayError) {
556
- process.stdout.write(`${import_chalk3.default.white(" stderr ") + import_chalk3.default.red(line)}
592
+ if (child.stderr) {
593
+ const stream1 = (0, import_byline2.default)(child.stderr);
594
+ stream1.on("data", (line) => {
595
+ line = line.toString();
596
+ errorOutputs.push(`${line}
557
597
  `);
558
- }
559
- });
560
- }
598
+ if (!g_config.silent && displayError) {
599
+ process.stdout.write(`${import_chalk3.default.white(" stderr ") + import_chalk3.default.red(line)}
600
+ `);
601
+ }
602
+ });
561
603
  }
562
604
  });
563
605
  }
@@ -567,18 +609,18 @@ async function find_openssl() {
567
609
  async function ensure_openssl_installed() {
568
610
  if (!opensslPath) {
569
611
  opensslPath = await find_openssl();
570
- const outputs = await execute_openssl("version", { cwd: "." });
612
+ const outputs = await execute_openssl(["version"], { cwd: "." });
571
613
  g_config.opensslVersion = outputs.trim();
572
614
  if (doDebug) {
573
615
  warningLog("OpenSSL version : ", g_config.opensslVersion);
574
616
  }
575
617
  }
576
618
  }
577
- async function execute_openssl_no_failure(cmd, options) {
619
+ async function execute_openssl_no_failure(args, options) {
578
620
  options = options || {};
579
621
  options.hideErrorMessage = true;
580
622
  try {
581
- return await execute_openssl(cmd, options);
623
+ return await execute_openssl(args, options);
582
624
  } catch (err) {
583
625
  debugLog(" (ignored error = ERROR : )", err.message);
584
626
  }
@@ -586,62 +628,64 @@ async function execute_openssl_no_failure(cmd, options) {
586
628
  function getTempFolder() {
587
629
  return import_node_os2.default.tmpdir();
588
630
  }
589
- async function execute_openssl(cmd, options) {
590
- debugLog("execute_openssl", cmd, options);
631
+ async function execute_openssl(args, options) {
632
+ debugLog("execute_openssl", args, redactEnvForLog(options));
591
633
  const empty_config_file = n(getTempFolder(), "empty_config.cnf");
592
634
  if (!import_node_fs3.default.existsSync(empty_config_file)) {
593
635
  await import_node_fs3.default.promises.writeFile(empty_config_file, "# empty config file");
594
636
  }
595
637
  options = options || {};
596
638
  options.openssl_conf = options.openssl_conf || empty_config_file;
597
- (0, import_node_assert3.default)(options.openssl_conf);
639
+ (0, import_node_assert2.default)(options.openssl_conf);
598
640
  setEnv("OPENSSL_CONF", options.openssl_conf);
599
641
  if (!g_config.silent) {
600
642
  warningLog(import_chalk3.default.cyan(" OPENSSL_CONF"), process.env.OPENSSL_CONF);
601
643
  warningLog(import_chalk3.default.cyan(" RANDFILE "), process.env.RANDFILE);
602
- warningLog(import_chalk3.default.cyan(" CMD openssl "), import_chalk3.default.cyanBright(cmd));
644
+ warningLog(import_chalk3.default.cyan(" CMD "), import_chalk3.default.cyanBright(renderForDisplay("openssl", args)));
603
645
  }
604
646
  await ensure_openssl_installed();
605
- return await execute2(`${quote(opensslPath)} ${cmd}`, options);
647
+ return await execute2(opensslPath, args, options);
606
648
  }
607
649
 
608
650
  // packages/node-opcua-pki/lib/pki/toolbox_pfx.ts
609
- var q = quote;
610
651
  var n2 = makePath;
611
652
  async function createPFX(options) {
612
- const { certificateFile, privateKeyFile, outputFile, passphrase = "", caCertificateFiles } = options;
613
- (0, import_node_assert4.default)(import_node_fs4.default.existsSync(certificateFile), `Certificate file does not exist: ${certificateFile}`);
614
- (0, import_node_assert4.default)(import_node_fs4.default.existsSync(privateKeyFile), `Private key file does not exist: ${privateKeyFile}`);
615
- let cmd = `pkcs12 -export`;
616
- cmd += ` -in ${q(n2(certificateFile))}`;
617
- cmd += ` -inkey ${q(n2(privateKeyFile))}`;
653
+ const { certificateFile, privateKeyFile, privateKeyPassphrase, outputFile, passphrase = "", caCertificateFiles } = options;
654
+ (0, import_node_assert3.default)(import_node_fs4.default.existsSync(certificateFile), `Certificate file does not exist: ${certificateFile}`);
655
+ (0, import_node_assert3.default)(import_node_fs4.default.existsSync(privateKeyFile), `Private key file does not exist: ${privateKeyFile}`);
656
+ const args = ["pkcs12", "-export", "-in", n2(certificateFile), "-inkey", n2(privateKeyFile)];
618
657
  if (caCertificateFiles) {
619
658
  for (const caFile of caCertificateFiles) {
620
- (0, import_node_assert4.default)(import_node_fs4.default.existsSync(caFile), `CA certificate file does not exist: ${caFile}`);
621
- cmd += ` -certfile ${q(n2(caFile))}`;
659
+ (0, import_node_assert3.default)(import_node_fs4.default.existsSync(caFile), `CA certificate file does not exist: ${caFile}`);
660
+ args.push("-certfile", n2(caFile));
622
661
  }
623
662
  }
624
- cmd += ` -out ${q(n2(outputFile))}`;
625
- cmd += ` -passout pass:${passphrase}`;
626
- await execute_openssl(cmd, {});
663
+ const passin = passinArg(privateKeyPassphrase);
664
+ const passout = passoutArg(passphrase);
665
+ args.push("-out", n2(outputFile), ...passin.args, ...passout.args);
666
+ await execute_openssl(args, { env: { ...passin.env, ...passout.env } });
627
667
  }
628
668
  async function extractCertificateFromPFX(options) {
629
669
  const { pfxFile, passphrase = "" } = options;
630
- (0, import_node_assert4.default)(import_node_fs4.default.existsSync(pfxFile), `PFX file does not exist: ${pfxFile}`);
631
- const cmd = `pkcs12 -in ${q(n2(pfxFile))} -clcerts -nokeys -nodes -passin pass:${passphrase}`;
632
- return await execute_openssl(cmd, {});
670
+ (0, import_node_assert3.default)(import_node_fs4.default.existsSync(pfxFile), `PFX file does not exist: ${pfxFile}`);
671
+ const passin = passinArg(passphrase);
672
+ return await execute_openssl(["pkcs12", "-in", n2(pfxFile), "-clcerts", "-nokeys", "-nodes", ...passin.args], {
673
+ env: passin.env
674
+ });
633
675
  }
634
676
  async function extractPrivateKeyFromPFX(options) {
635
677
  const { pfxFile, passphrase = "" } = options;
636
- (0, import_node_assert4.default)(import_node_fs4.default.existsSync(pfxFile), `PFX file does not exist: ${pfxFile}`);
637
- const cmd = `pkcs12 -in ${q(n2(pfxFile))} -nocerts -nodes -passin pass:${passphrase}`;
638
- return await execute_openssl(cmd, {});
678
+ (0, import_node_assert3.default)(import_node_fs4.default.existsSync(pfxFile), `PFX file does not exist: ${pfxFile}`);
679
+ const passin = passinArg(passphrase);
680
+ return await execute_openssl(["pkcs12", "-in", n2(pfxFile), "-nocerts", "-nodes", ...passin.args], { env: passin.env });
639
681
  }
640
682
  async function extractCACertificatesFromPFX(options) {
641
683
  const { pfxFile, passphrase = "" } = options;
642
- (0, import_node_assert4.default)(import_node_fs4.default.existsSync(pfxFile), `PFX file does not exist: ${pfxFile}`);
643
- const cmd = `pkcs12 -in ${q(n2(pfxFile))} -cacerts -nokeys -nodes -passin pass:${passphrase}`;
644
- return await execute_openssl(cmd, {});
684
+ (0, import_node_assert3.default)(import_node_fs4.default.existsSync(pfxFile), `PFX file does not exist: ${pfxFile}`);
685
+ const passin = passinArg(passphrase);
686
+ return await execute_openssl(["pkcs12", "-in", n2(pfxFile), "-cacerts", "-nokeys", "-nodes", ...passin.args], {
687
+ env: passin.env
688
+ });
645
689
  }
646
690
  async function extractAllFromPFX(options) {
647
691
  const [certificate, privateKey, caCertificates] = await Promise.all([
@@ -652,14 +696,50 @@ async function extractAllFromPFX(options) {
652
696
  return { certificate, privateKey, caCertificates };
653
697
  }
654
698
  async function convertPFXtoPEM(pfxFile, pemFile, passphrase = "") {
655
- (0, import_node_assert4.default)(import_node_fs4.default.existsSync(pfxFile), `PFX file does not exist: ${pfxFile}`);
656
- const cmd = `pkcs12 -in ${q(n2(pfxFile))} -out ${q(n2(pemFile))} -nodes -passin pass:${passphrase}`;
657
- await execute_openssl(cmd, {});
699
+ (0, import_node_assert3.default)(import_node_fs4.default.existsSync(pfxFile), `PFX file does not exist: ${pfxFile}`);
700
+ const passin = passinArg(passphrase);
701
+ await execute_openssl(["pkcs12", "-in", n2(pfxFile), "-out", n2(pemFile), "-nodes", ...passin.args], { env: passin.env });
658
702
  }
659
703
  async function dumpPFX(pfxFile, passphrase = "") {
660
- (0, import_node_assert4.default)(import_node_fs4.default.existsSync(pfxFile), `PFX file does not exist: ${pfxFile}`);
661
- const cmd = `pkcs12 -in ${q(n2(pfxFile))} -info -nodes -passin pass:${passphrase}`;
662
- return await execute_openssl(cmd, {});
704
+ (0, import_node_assert3.default)(import_node_fs4.default.existsSync(pfxFile), `PFX file does not exist: ${pfxFile}`);
705
+ const passin = passinArg(passphrase);
706
+ return await execute_openssl(["pkcs12", "-in", n2(pfxFile), "-info", "-nodes", ...passin.args], { env: passin.env });
707
+ }
708
+
709
+ // packages/node-opcua-pki/lib/toolbox/common.ts
710
+ var import_node_assert4 = __toESM(require("assert"));
711
+ function quote(str) {
712
+ return `"${str || ""}"`;
713
+ }
714
+ async function resolvePrivateKeyPassphrase(passphrase) {
715
+ if (passphrase === void 0) {
716
+ return void 0;
717
+ }
718
+ return typeof passphrase === "function" ? await passphrase() : passphrase;
719
+ }
720
+ function adjustDate(params) {
721
+ (0, import_node_assert4.default)(params instanceof Object);
722
+ params.startDate = params.startDate || /* @__PURE__ */ new Date();
723
+ (0, import_node_assert4.default)(params.startDate instanceof Date);
724
+ if (params.validityMs !== void 0) {
725
+ if (params.validityMs <= 0) {
726
+ throw new RangeError(`validityMs must be > 0 (got ${params.validityMs})`);
727
+ }
728
+ params.endDate = new Date(params.startDate.getTime() + params.validityMs);
729
+ params.validity = Math.ceil(params.validityMs / 864e5);
730
+ } else {
731
+ params.validity = params.validity || 365;
732
+ params.endDate = new Date(params.startDate.getTime());
733
+ params.endDate.setDate(params.startDate.getDate() + params.validity);
734
+ }
735
+ (0, import_node_assert4.default)(params.endDate instanceof Date);
736
+ (0, import_node_assert4.default)(params.startDate instanceof Date);
737
+ }
738
+ function adjustApplicationUri(params) {
739
+ const applicationUri = params.applicationUri || "";
740
+ if (applicationUri.length > 200) {
741
+ throw new Error(`Openssl doesn't support urn with length greater than 200${applicationUri}`);
742
+ }
663
743
  }
664
744
 
665
745
  // packages/node-opcua-pki/lib/toolbox/display.ts
@@ -751,7 +831,6 @@ function x509Date(date) {
751
831
  }
752
832
 
753
833
  // packages/node-opcua-pki/lib/toolbox/with_openssl/create_certificate_signing_request.ts
754
- var q2 = quote;
755
834
  var n3 = makePath;
756
835
  async function createCertificateSigningRequestWithOpenSSL(certificateSigningRequestFilename, params) {
757
836
  (0, import_node_assert6.default)(params);
@@ -766,12 +845,23 @@ async function createCertificateSigningRequestWithOpenSSL(certificateSigningRequ
766
845
  processAltNames(params);
767
846
  const configFile = generateStaticConfig(params.configFile, { cwd: params.rootDir });
768
847
  const options = { cwd: params.rootDir, openssl_conf: import_node_path4.default.relative(params.rootDir, configFile) };
769
- const configOption = ` -config ${q2(n3(configFile))}`;
770
848
  const subject = params.subject ? new import_node_opcua_crypto.Subject(params.subject).toString() : void 0;
771
- const subjectOptions = subject ? ` -subj "${subject}"` : "";
772
849
  displaySubtitle("- Creating a Certificate Signing Request with openssl");
773
850
  await execute_openssl(
774
- "req -new -sha256 -batch -text " + configOption + " -key " + q2(n3(params.privateKey)) + subjectOptions + " -out " + q2(n3(certificateSigningRequestFilename)),
851
+ [
852
+ "req",
853
+ "-new",
854
+ "-sha256",
855
+ "-batch",
856
+ "-text",
857
+ "-config",
858
+ n3(configFile),
859
+ "-key",
860
+ n3(params.privateKey),
861
+ ...subject ? ["-subj", subject] : [],
862
+ "-out",
863
+ n3(certificateSigningRequestFilename)
864
+ ],
775
865
  options
776
866
  );
777
867
  }
@@ -785,7 +875,7 @@ var config2 = `#.........DO NOT MODIFY BY HAND .........................
785
875
  [ ca ]
786
876
  default_ca = CA_default
787
877
  [ CA_default ]
788
- dir = %%ROOT_FOLDER%% # the main CA folder
878
+ dir = "%%ROOT_FOLDER%%" # the main CA folder (quoted: see renderCaConfig)
789
879
  certs = $dir/certs # where to store certificates
790
880
  new_certs_dir = $dir/certs #
791
881
  database = $dir/index.txt # the certificate database
@@ -918,13 +1008,18 @@ var ca_config_template_cnf_default = config2;
918
1008
  var defaultSubject = "/C=FR/ST=IDF/L=Paris/O=Local NODE-OPCUA Certificate Authority/CN=NodeOPCUA-CA";
919
1009
  var configurationFileTemplate = ca_config_template_cnf_default;
920
1010
  var configurationFileSimpleTemplate = simple_config_template_cnf_default;
1011
+ function escapeOpensslConfDoubleQuoted(value) {
1012
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1013
+ }
1014
+ function renderCaConfig(caRootDir) {
1015
+ return configurationFileTemplate.replace(/%%ROOT_FOLDER%%/, escapeOpensslConfDoubleQuoted(makePath(caRootDir)));
1016
+ }
921
1017
  var config3 = {
922
1018
  certificateDir: "INVALID",
923
1019
  forceCA: false,
924
1020
  pkiDir: "INVALID"
925
1021
  };
926
1022
  var n4 = makePath;
927
- var q3 = quote;
928
1023
  function octetStringToIpAddress(a) {
929
1024
  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();
930
1025
  }
@@ -934,7 +1029,7 @@ async function construct_CertificateAuthority(certificateAuthority) {
934
1029
  const caRootDir = import_node_path5.default.resolve(certificateAuthority.rootDir);
935
1030
  async function make_folders() {
936
1031
  mkdirRecursiveSync(caRootDir);
937
- mkdirRecursiveSync(import_node_path5.default.join(caRootDir, "private"));
1032
+ ensurePrivateDirectory(import_node_path5.default.join(caRootDir, "private"));
938
1033
  mkdirRecursiveSync(import_node_path5.default.join(caRootDir, "public"));
939
1034
  mkdirRecursiveSync(import_node_path5.default.join(caRootDir, "certs"));
940
1035
  mkdirRecursiveSync(import_node_path5.default.join(caRootDir, "crl"));
@@ -959,6 +1054,8 @@ async function construct_CertificateAuthority(certificateAuthority) {
959
1054
  const caKeyExists = import_node_fs7.default.existsSync(import_node_path5.default.join(caRootDir, "private/cakey.pem"));
960
1055
  const caCertExists = import_node_fs7.default.existsSync(import_node_path5.default.join(caRootDir, "public/cacert.pem"));
961
1056
  if (caKeyExists && caCertExists && !config3.forceCA) {
1057
+ restrictPrivateFilePermissions(import_node_path5.default.join(caRootDir, "private/cakey.pem"), 384);
1058
+ await certificateAuthority._ensurePrivateKeyProtection();
962
1059
  debugLog("CA private key and certificate already exist ... skipping");
963
1060
  return;
964
1061
  }
@@ -977,26 +1074,40 @@ async function construct_CertificateAuthority(certificateAuthority) {
977
1074
  }
978
1075
  const caConfigFile = certificateAuthority.configFile;
979
1076
  if (1) {
980
- let data = configurationFileTemplate;
981
- data = makePath(data.replace(/%%ROOT_FOLDER%%/, caRootDir));
982
- await import_node_fs7.default.promises.writeFile(caConfigFile, data);
1077
+ await import_node_fs7.default.promises.writeFile(caConfigFile, renderCaConfig(caRootDir));
983
1078
  }
984
- const subjectOpt = ` -subj "${subject.toString()}" `;
1079
+ const subjectOpt = ["-subj", subject.toString()];
985
1080
  const caCommonName = subject.commonName || "NodeOPCUA-CA";
986
1081
  setEnv("ALTNAME", `URI:urn:${caCommonName}`);
987
1082
  certificateAuthority._wireRevocationEnvVars();
988
1083
  const options = { cwd: caRootDir };
989
1084
  const configFile = generateStaticConfig("conf/caconfig.cnf", options);
990
- const configOption = ` -config ${q3(n4(configFile))}`;
1085
+ const configOption = ["-config", n4(configFile)];
991
1086
  const keySize = certificateAuthority.keySize;
992
1087
  const privateKeyFilename = import_node_path5.default.join(caRootDir, "private/cakey.pem");
993
1088
  const csrFilename = import_node_path5.default.join(caRootDir, "private/cakey.csr");
994
1089
  displayTitle(`Generate the CA private Key - ${keySize}`);
995
- await (0, import_node_opcua_crypto2.generatePrivateKeyFile)(privateKeyFilename, keySize);
1090
+ const passin = await certificateAuthority._opensslPassin();
1091
+ await (0, import_node_opcua_crypto2.generatePrivateKeyFile)(privateKeyFilename, keySize, { passphrase: await certificateAuthority._privateKeyPassphrase() });
1092
+ restrictPrivateFilePermissions(privateKeyFilename, 384);
996
1093
  displayTitle("Generate a certificate request for the CA key");
997
1094
  await execute_openssl(
998
- "req -new -sha256 -text -extensions v3_ca_req" + configOption + " -key " + q3(n4(privateKeyFilename)) + " -out " + q3(n4(csrFilename)) + " " + subjectOpt,
999
- options
1095
+ [
1096
+ "req",
1097
+ "-new",
1098
+ "-sha256",
1099
+ "-text",
1100
+ "-extensions",
1101
+ "v3_ca_req",
1102
+ ...configOption,
1103
+ "-key",
1104
+ n4(privateKeyFilename),
1105
+ "-out",
1106
+ n4(csrFilename),
1107
+ ...subjectOpt,
1108
+ ...passin.args
1109
+ ],
1110
+ { ...options, env: passin.env }
1000
1111
  );
1001
1112
  const issuerCA = certificateAuthority._issuerCA;
1002
1113
  if (issuerCA) {
@@ -1004,27 +1115,71 @@ async function construct_CertificateAuthority(certificateAuthority) {
1004
1115
  const issuerCert = import_node_path5.default.resolve(issuerCA.caCertificate);
1005
1116
  const issuerKey = import_node_path5.default.resolve(issuerCA.rootDir, "private/cakey.pem");
1006
1117
  const issuerSerial = import_node_path5.default.resolve(issuerCA.rootDir, "serial");
1118
+ const issuerPassin = await issuerCA._opensslPassin();
1007
1119
  await execute_openssl(
1008
- " x509 -sha256 -req -days 3650 -text -extensions v3_ca -extfile " + q3(n4(configFile)) + " -in private/cakey.csr -CA " + q3(n4(issuerCert)) + " -CAkey " + q3(n4(issuerKey)) + " -CAserial " + q3(n4(issuerSerial)) + " -out public/cacert.pem",
1009
- options
1120
+ [
1121
+ "x509",
1122
+ "-sha256",
1123
+ "-req",
1124
+ "-days",
1125
+ "3650",
1126
+ "-text",
1127
+ "-extensions",
1128
+ "v3_ca",
1129
+ "-extfile",
1130
+ n4(configFile),
1131
+ "-in",
1132
+ "private/cakey.csr",
1133
+ "-CA",
1134
+ n4(issuerCert),
1135
+ "-CAkey",
1136
+ n4(issuerKey),
1137
+ "-CAserial",
1138
+ n4(issuerSerial),
1139
+ "-out",
1140
+ "public/cacert.pem",
1141
+ ...issuerPassin.args
1142
+ ],
1143
+ { ...options, env: issuerPassin.env }
1010
1144
  );
1011
1145
  } else {
1012
1146
  displayTitle("Generate CA Certificate (self-signed)");
1013
1147
  await execute_openssl(
1014
- " x509 -sha256 -req -days 3650 -text -extensions v3_ca -extfile " + q3(n4(configFile)) + " -in private/cakey.csr -signkey " + q3(n4(privateKeyFilename)) + " -out public/cacert.pem",
1015
- options
1148
+ [
1149
+ "x509",
1150
+ "-sha256",
1151
+ "-req",
1152
+ "-days",
1153
+ "3650",
1154
+ "-text",
1155
+ "-extensions",
1156
+ "v3_ca",
1157
+ "-extfile",
1158
+ n4(configFile),
1159
+ "-in",
1160
+ "private/cakey.csr",
1161
+ "-signkey",
1162
+ n4(privateKeyFilename),
1163
+ "-out",
1164
+ "public/cacert.pem",
1165
+ ...passin.args
1166
+ ],
1167
+ { ...options, env: passin.env }
1016
1168
  );
1017
1169
  }
1018
1170
  displaySubtitle("generate initial CRL (Certificate Revocation List)");
1019
- await regenerateCrl(certificateAuthority.revocationList, configOption, options);
1171
+ await regenerateCrl(certificateAuthority.revocationList, configOption, options, passin);
1020
1172
  displayTitle("Create Certificate Authority (CA) ---> DONE");
1021
1173
  }
1022
- async function regenerateCrl(revocationList, configOption, options) {
1174
+ async function regenerateCrl(revocationList, configOption, options, passin) {
1023
1175
  displaySubtitle("regenerate CRL (Certificate Revocation List)");
1024
- await execute_openssl(`ca -gencrl ${configOption} -out crl/revocation_list.crl`, options);
1025
- await execute_openssl("crl -in crl/revocation_list.crl -out crl/revocation_list.der -outform der", options);
1176
+ await execute_openssl(["ca", "-gencrl", ...configOption, "-out", "crl/revocation_list.crl", ...passin.args], {
1177
+ ...options,
1178
+ env: passin.env
1179
+ });
1180
+ await execute_openssl(["crl", "-in", "crl/revocation_list.crl", "-out", "crl/revocation_list.der", "-outform", "der"], options);
1026
1181
  displaySubtitle("Display (Certificate Revocation List)");
1027
- await execute_openssl(`crl -in ${q3(n4(revocationList))} -text -noout`, options);
1182
+ await execute_openssl(["crl", "-in", n4(revocationList), "-text", "-noout"], options);
1028
1183
  }
1029
1184
  function parseOpenSSLDate(dateStr) {
1030
1185
  const raw = dateStr?.split(",")[0] ?? "";
@@ -1078,6 +1233,10 @@ var CertificateAuthority = class {
1078
1233
  _crlDistributionUrl;
1079
1234
  _ocspResponderUrl;
1080
1235
  _caIssuersUrl;
1236
+ #privateKeyPassphrase;
1237
+ /** resolved once (see `privateKeyPassphrase`); `#passphraseResolved` distinguishes "none" from "not yet" */
1238
+ #resolvedPassphrase;
1239
+ #passphraseResolved = false;
1081
1240
  constructor(options) {
1082
1241
  (0, import_node_assert7.default)(Object.prototype.hasOwnProperty.call(options, "location"));
1083
1242
  (0, import_node_assert7.default)(Object.prototype.hasOwnProperty.call(options, "keySize"));
@@ -1085,6 +1244,7 @@ var CertificateAuthority = class {
1085
1244
  this.keySize = options.keySize || 2048;
1086
1245
  this.subject = new import_node_opcua_crypto2.Subject(options.subject || defaultSubject);
1087
1246
  this._issuerCA = options.issuerCA;
1247
+ this.#privateKeyPassphrase = options.privateKeyPassphrase;
1088
1248
  if (options.crlDistributionUrl !== void 0) {
1089
1249
  this.setCrlDistributionUrl(options.crlDistributionUrl);
1090
1250
  }
@@ -1181,6 +1341,69 @@ var CertificateAuthority = class {
1181
1341
  get configFile() {
1182
1342
  return import_node_path5.default.normalize(import_node_path5.default.join(this.rootDir, "./conf/caconfig.cnf"));
1183
1343
  }
1344
+ /** Path to the CA private key (`private/cakey.pem`); may be passphrase-encrypted, see {@link getPrivateKey}. */
1345
+ get privateKey() {
1346
+ return import_node_path5.default.join(import_node_path5.default.resolve(this.rootDir), "private/cakey.pem");
1347
+ }
1348
+ /**
1349
+ * The CA private key, decrypted with the configured `privateKeyPassphrase`
1350
+ * if it is encrypted. Fails closed (`PrivateKeyPassphraseRequiredError`)
1351
+ * on an encrypted key with no or the wrong passphrase.
1352
+ */
1353
+ async getPrivateKey() {
1354
+ return (0, import_node_opcua_crypto2.readPrivateKey)(this.privateKey, await this._privateKeyPassphrase());
1355
+ }
1356
+ /**
1357
+ * Enable, disable, or rotate the passphrase protecting `private/cakey.pem`
1358
+ * (temp file + atomic rename, temp file removed on failure). Only
1359
+ * rewrites the file: construct a new `CertificateAuthority` with the new
1360
+ * passphrase to continue using it.
1361
+ */
1362
+ async reencryptPrivateKey(oldPassphrase, newPassphrase) {
1363
+ const oldPass = await resolvePrivateKeyPassphrase(oldPassphrase);
1364
+ const newPass = await resolvePrivateKeyPassphrase(newPassphrase);
1365
+ const key = (0, import_node_opcua_crypto2.readPrivateKey)(this.privateKey, oldPass);
1366
+ await this.#rewritePrivateKeyFile(key, newPass);
1367
+ }
1368
+ async #rewritePrivateKeyFile(privateKey, passphrase) {
1369
+ const tmpFilename = `${this.privateKey}.${process.pid}-${Date.now()}.tmp`;
1370
+ try {
1371
+ await (0, import_node_opcua_crypto2.writePrivateKeyFile)(tmpFilename, privateKey, { passphrase });
1372
+ await import_node_fs7.default.promises.rename(tmpFilename, this.privateKey);
1373
+ } finally {
1374
+ await import_node_fs7.default.promises.rm(tmpFilename, { force: true });
1375
+ }
1376
+ }
1377
+ /** @internal resolve the configured passphrase, at most once per instance */
1378
+ async _privateKeyPassphrase() {
1379
+ if (!this.#passphraseResolved) {
1380
+ this.#resolvedPassphrase = await resolvePrivateKeyPassphrase(this.#privateKeyPassphrase);
1381
+ this.#passphraseResolved = true;
1382
+ }
1383
+ return this.#resolvedPassphrase;
1384
+ }
1385
+ /** @internal `-passin env:` argv + env for every openssl call that loads this CA's key (always emitted, empty when none) */
1386
+ async _opensslPassin() {
1387
+ return passinArg(await this._privateKeyPassphrase());
1388
+ }
1389
+ /**
1390
+ * @internal On an existing key: encrypt it in place if a passphrase is
1391
+ * configured and it is still plaintext (secure by default: the option
1392
+ * means "protect this key", not "ignore me"), then read it back so a
1393
+ * wrong or missing passphrase fails initialize() closed rather than the
1394
+ * first signing operation.
1395
+ */
1396
+ async _ensurePrivateKeyProtection() {
1397
+ if (!import_node_fs7.default.existsSync(this.privateKey)) {
1398
+ return;
1399
+ }
1400
+ if (this.#privateKeyPassphrase !== void 0 && !isEncryptedPrivateKeyFile(this.privateKey)) {
1401
+ warningLog("CertificateAuthority: private key is plaintext but a passphrase is configured; encrypting it in place");
1402
+ const plaintextKey = (0, import_node_opcua_crypto2.readPrivateKey)(this.privateKey);
1403
+ await this.#rewritePrivateKeyFile(plaintextKey, await this._privateKeyPassphrase());
1404
+ }
1405
+ await this.getPrivateKey();
1406
+ }
1184
1407
  /** Path to the CA certificate in PEM format (`public/cacert.pem`). */
1185
1408
  get caCertificate() {
1186
1409
  return makePath(this.rootDir, "./public/cacert.pem");
@@ -1638,12 +1861,17 @@ var CertificateAuthority = class {
1638
1861
  async initializeCSR() {
1639
1862
  const caRootDir = import_node_path5.default.resolve(this.rootDir);
1640
1863
  mkdirRecursiveSync(caRootDir);
1641
- for (const dir of ["private", "public", "certs", "crl", "conf"]) {
1864
+ for (const dir of ["public", "certs", "crl", "conf"]) {
1642
1865
  mkdirRecursiveSync(import_node_path5.default.join(caRootDir, dir));
1643
1866
  }
1867
+ ensurePrivateDirectory(import_node_path5.default.join(caRootDir, "private"));
1644
1868
  const caCertFile = this.caCertificate;
1645
1869
  const privateKeyFile = import_node_path5.default.join(caRootDir, "private/cakey.pem");
1646
1870
  const csrFile = import_node_path5.default.join(caRootDir, "private/cakey.csr");
1871
+ if (import_node_fs7.default.existsSync(privateKeyFile)) {
1872
+ restrictPrivateFilePermissions(privateKeyFile, 384);
1873
+ await this._ensurePrivateKeyProtection();
1874
+ }
1647
1875
  if (import_node_fs7.default.existsSync(caCertFile)) {
1648
1876
  const certDer = (0, import_node_opcua_crypto2.convertPEMtoDER)((0, import_node_opcua_crypto2.readCertificatePEM)(caCertFile));
1649
1877
  const certInfo = (0, import_node_opcua_crypto2.exploreCertificate)(certDer);
@@ -1677,11 +1905,10 @@ var CertificateAuthority = class {
1677
1905
  await import_node_fs7.default.promises.writeFile(indexFileAttr, "unique_subject = no");
1678
1906
  }
1679
1907
  const caConfigFile = this.configFile;
1680
- let data = configurationFileTemplate;
1681
- data = makePath(data.replace(/%%ROOT_FOLDER%%/, caRootDir));
1682
- await import_node_fs7.default.promises.writeFile(caConfigFile, data);
1908
+ await import_node_fs7.default.promises.writeFile(caConfigFile, renderCaConfig(caRootDir));
1683
1909
  if (!import_node_fs7.default.existsSync(privateKeyFile)) {
1684
- await (0, import_node_opcua_crypto2.generatePrivateKeyFile)(privateKeyFile, this.keySize);
1910
+ await (0, import_node_opcua_crypto2.generatePrivateKeyFile)(privateKeyFile, this.keySize, { passphrase: await this._privateKeyPassphrase() });
1911
+ restrictPrivateFilePermissions(privateKeyFile, 384);
1685
1912
  }
1686
1913
  await this._generateCSR(caRootDir, privateKeyFile, csrFile);
1687
1914
  return { status: "created", csrPath: csrFile };
@@ -1723,14 +1950,29 @@ var CertificateAuthority = class {
1723
1950
  * @internal
1724
1951
  */
1725
1952
  async _generateCSR(caRootDir, privateKeyFile, csrFile) {
1726
- const subjectOpt = ` -subj "${this.subject.toString()}" `;
1727
1953
  processAltNames({});
1728
1954
  const options = { cwd: caRootDir };
1729
1955
  const configFile = generateStaticConfig("conf/caconfig.cnf", options);
1730
- const configOption = ` -config ${q3(n4(configFile))}`;
1956
+ const passin = await this._opensslPassin();
1731
1957
  await execute_openssl(
1732
- "req -new -sha256 -text -extensions v3_ca_req" + configOption + " -key " + q3(n4(privateKeyFile)) + " -out " + q3(n4(csrFile)) + " " + subjectOpt,
1733
- options
1958
+ [
1959
+ "req",
1960
+ "-new",
1961
+ "-sha256",
1962
+ "-text",
1963
+ "-extensions",
1964
+ "v3_ca_req",
1965
+ "-config",
1966
+ n4(configFile),
1967
+ "-key",
1968
+ n4(privateKeyFile),
1969
+ "-out",
1970
+ n4(csrFile),
1971
+ "-subj",
1972
+ this.subject.toString(),
1973
+ ...passin.args
1974
+ ],
1975
+ { ...options, env: passin.env }
1734
1976
  );
1735
1977
  }
1736
1978
  /**
@@ -1752,7 +1994,6 @@ var CertificateAuthority = class {
1752
1994
  async installCACertificate(signedCertFile) {
1753
1995
  const caRootDir = import_node_path5.default.resolve(this.rootDir);
1754
1996
  const caCertFile = this.caCertificate;
1755
- const privateKeyFile = import_node_path5.default.join(caRootDir, "private/cakey.pem");
1756
1997
  const fullPem = await import_node_fs7.default.promises.readFile(signedCertFile, "utf8");
1757
1998
  const pemBlocks = fullPem.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g);
1758
1999
  if (!pemBlocks || pemBlocks.length === 0) {
@@ -1763,7 +2004,7 @@ var CertificateAuthority = class {
1763
2004
  };
1764
2005
  }
1765
2006
  const certDer = (0, import_node_opcua_crypto2.convertPEMtoDER)(pemBlocks[0]);
1766
- const privateKey = (0, import_node_opcua_crypto2.readPrivateKey)(privateKeyFile);
2007
+ const privateKey = await this.getPrivateKey();
1767
2008
  if (!(0, import_node_opcua_crypto2.certificateMatchesPrivateKey)(certDer, privateKey)) {
1768
2009
  return {
1769
2010
  status: "error",
@@ -1786,8 +2027,7 @@ var CertificateAuthority = class {
1786
2027
  }
1787
2028
  const options = { cwd: caRootDir };
1788
2029
  const configFile = generateStaticConfig("conf/caconfig.cnf", options);
1789
- const configOption = ` -config ${q3(n4(configFile))}`;
1790
- await regenerateCrl(this.revocationList, configOption, options);
2030
+ await regenerateCrl(this.revocationList, ["-config", n4(configFile)], options, await this._opensslPassin());
1791
2031
  return { status: "success" };
1792
2032
  }
1793
2033
  /**
@@ -1809,9 +2049,32 @@ var CertificateAuthority = class {
1809
2049
  this._wireRevocationEnvVars();
1810
2050
  const configFile = generateStaticConfig("conf/caconfig.cnf", options);
1811
2051
  const validity = params.validity ?? 3650;
2052
+ const passin = await this._opensslPassin();
1812
2053
  await execute_openssl(
1813
- ` x509 -sha256 -req -days ${validity} -text -extensions v3_ca -extfile ` + q3(n4(configFile)) + " -in " + q3(n4(csrFile)) + " -CA " + q3(n4(this.caCertificate)) + " -CAkey " + q3(n4(import_node_path5.default.join(caRootDir, "private/cakey.pem"))) + " -CAserial " + q3(n4(import_node_path5.default.join(caRootDir, "serial"))) + " -out " + q3(n4(certFile)),
1814
- options
2054
+ [
2055
+ "x509",
2056
+ "-sha256",
2057
+ "-req",
2058
+ "-days",
2059
+ String(validity),
2060
+ "-text",
2061
+ "-extensions",
2062
+ "v3_ca",
2063
+ "-extfile",
2064
+ n4(configFile),
2065
+ "-in",
2066
+ n4(csrFile),
2067
+ "-CA",
2068
+ n4(this.caCertificate),
2069
+ "-CAkey",
2070
+ n4(import_node_path5.default.join(caRootDir, "private/cakey.pem")),
2071
+ "-CAserial",
2072
+ n4(import_node_path5.default.join(caRootDir, "serial")),
2073
+ "-out",
2074
+ n4(certFile),
2075
+ ...passin.args
2076
+ ],
2077
+ { ...options, env: passin.env }
1815
2078
  );
1816
2079
  await this.constructCertificateChain(certFile);
1817
2080
  }
@@ -1873,23 +2136,36 @@ var CertificateAuthority = class {
1873
2136
  cwd: this.rootDir,
1874
2137
  openssl_conf: makePath(configFile)
1875
2138
  };
1876
- const configOption = "";
1877
2139
  const subject = params.subject ? new import_node_opcua_crypto2.Subject(params.subject).toString() : "";
1878
- const subjectOptions = subject && subject.length > 1 ? ` -subj ${subject} ` : "";
2140
+ const subjectOptions = subject && subject.length > 1 ? ["-subj", subject] : [];
1879
2141
  displaySubtitle("- the certificate signing request");
1880
2142
  await execute_openssl(
1881
- "req -new -sha256 -text " + configOption + subjectOptions + " -batch -key " + q3(n4(privateKey)) + " -out " + q3(n4(csrFile)),
2143
+ ["req", "-new", "-sha256", "-text", ...subjectOptions, "-batch", "-key", n4(privateKey), "-out", n4(csrFile)],
1882
2144
  options
1883
2145
  );
1884
2146
  displaySubtitle("- creating the self-signed certificate");
1885
2147
  await execute_openssl(
1886
- "ca -selfsign -keyfile " + q3(n4(privateKey)) + " -startdate " + x509Date(params.startDate) + " -enddate " + x509Date(params.endDate) + " -batch -out " + q3(n4(certificateFile)) + " -in " + q3(n4(csrFile)),
2148
+ [
2149
+ "ca",
2150
+ "-selfsign",
2151
+ "-keyfile",
2152
+ n4(privateKey),
2153
+ "-startdate",
2154
+ x509Date(params.startDate),
2155
+ "-enddate",
2156
+ x509Date(params.endDate),
2157
+ "-batch",
2158
+ "-out",
2159
+ n4(certificateFile),
2160
+ "-in",
2161
+ n4(csrFile)
2162
+ ],
1887
2163
  options
1888
2164
  );
1889
2165
  displaySubtitle("- dump the certificate for a check");
1890
- await execute_openssl(`x509 -in ${q3(n4(certificateFile))} -dates -fingerprint -purpose -noout`, {});
2166
+ await execute_openssl(["x509", "-in", n4(certificateFile), "-dates", "-fingerprint", "-purpose", "-noout"], {});
1891
2167
  displaySubtitle("- verify self-signed certificate");
1892
- await execute_openssl_no_failure(`verify -verbose -CAfile ${q3(n4(certificateFile))} ${q3(n4(certificateFile))}`, options);
2168
+ await execute_openssl_no_failure(["verify", "-verbose", "-CAfile", n4(certificateFile), n4(certificateFile)], options);
1893
2169
  await import_node_fs7.default.promises.unlink(csrFile);
1894
2170
  }
1895
2171
  /**
@@ -1919,22 +2195,41 @@ var CertificateAuthority = class {
1919
2195
  setEnv("ALTNAME", "");
1920
2196
  const randomFile = import_node_path5.default.join(this.rootDir, "random.rnd");
1921
2197
  setEnv("RANDFILE", randomFile);
1922
- const configOption = ` -config ${q3(n4(configFile))}`;
2198
+ const configOption = ["-config", n4(configFile)];
1923
2199
  const reason = params.reason || "keyCompromise";
1924
2200
  (0, import_node_assert7.default)(crlReasons.indexOf(reason) >= 0);
1925
2201
  displayTitle(`Revoking certificate ${certificate}`);
1926
2202
  displaySubtitle("Revoke certificate");
1927
- await execute_openssl_no_failure(`ca -verbose ${configOption} -revoke ${q3(certificate)} -crl_reason ${reason}`, options);
1928
- await regenerateCrl(this.revocationList, configOption, options);
2203
+ const passin = await this._opensslPassin();
2204
+ await execute_openssl_no_failure(
2205
+ ["ca", "-verbose", ...configOption, "-revoke", certificate, "-crl_reason", reason, ...passin.args],
2206
+ { ...options, env: passin.env }
2207
+ );
2208
+ await regenerateCrl(this.revocationList, configOption, options, passin);
1929
2209
  displaySubtitle("Verify that certificate is revoked");
1930
2210
  await execute_openssl_no_failure(
1931
- "verify -verbose -CRLfile " + q3(n4(this.revocationList)) + " -CAfile " + q3(n4(this.caCertificate)) + " -crl_check " + q3(n4(certificate)),
2211
+ [
2212
+ "verify",
2213
+ "-verbose",
2214
+ "-CRLfile",
2215
+ n4(this.revocationList),
2216
+ "-CAfile",
2217
+ n4(this.caCertificate),
2218
+ "-crl_check",
2219
+ n4(certificate)
2220
+ ],
1932
2221
  options
1933
2222
  );
1934
2223
  displaySubtitle("Produce CRL in DER form ");
1935
- await execute_openssl(`crl -in ${q3(n4(this.revocationList))} -out crl/revocation_list.der -outform der`, options);
2224
+ await execute_openssl(
2225
+ ["crl", "-in", n4(this.revocationList), "-out", "crl/revocation_list.der", "-outform", "der"],
2226
+ options
2227
+ );
1936
2228
  displaySubtitle("Produce CRL in PEM form ");
1937
- await execute_openssl(`crl -in ${q3(n4(this.revocationList))} -out crl/revocation_list.pem -outform pem -text `, options);
2229
+ await execute_openssl(
2230
+ ["crl", "-in", n4(this.revocationList), "-out", "crl/revocation_list.pem", "-outform", "pem", "-text"],
2231
+ options
2232
+ );
1938
2233
  }
1939
2234
  /**
1940
2235
  * Sign a Certificate Signing Request (CSR) with this CA.
@@ -1976,13 +2271,27 @@ var CertificateAuthority = class {
1976
2271
  this._wireRevocationEnvVars();
1977
2272
  const configFile = generateStaticConfig("conf/caconfig.cnf", options);
1978
2273
  displaySubtitle("- then we ask the authority to sign the certificate signing request");
1979
- const configOption = ` -config ${configFile}`;
2274
+ const passin = await this._opensslPassin();
1980
2275
  await execute_openssl(
1981
- "ca " + configOption + " -startdate " + x509Date(params1.startDate) + " -enddate " + x509Date(params1.endDate) + " -batch -out " + q3(n4(certificate)) + " -in " + q3(n4(certificateSigningRequestFilename)),
1982
- options
2276
+ [
2277
+ "ca",
2278
+ "-config",
2279
+ configFile,
2280
+ "-startdate",
2281
+ x509Date(params1.startDate),
2282
+ "-enddate",
2283
+ x509Date(params1.endDate),
2284
+ "-batch",
2285
+ "-out",
2286
+ n4(certificate),
2287
+ "-in",
2288
+ n4(certificateSigningRequestFilename),
2289
+ ...passin.args
2290
+ ],
2291
+ { ...options, env: passin.env }
1983
2292
  );
1984
2293
  displaySubtitle("- dump the certificate for a check");
1985
- await execute_openssl(`x509 -in ${q3(n4(certificate))} -dates -fingerprint -purpose -noout`, options);
2294
+ await execute_openssl(["x509", "-in", n4(certificate), "-dates", "-fingerprint", "-purpose", "-noout"], options);
1986
2295
  displaySubtitle("- construct CA certificate with CRL");
1987
2296
  await this.constructCACertificateWithCRL();
1988
2297
  displaySubtitle("- construct certificate chain");
@@ -2002,10 +2311,8 @@ var CertificateAuthority = class {
2002
2311
  const options = { cwd: this.rootDir };
2003
2312
  const configFile = generateStaticConfig("conf/caconfig.cnf", options);
2004
2313
  setEnv("OPENSSL_CONF", makePath(configFile));
2005
- const _configOption = ` -config ${configFile}`;
2006
- _configOption;
2007
2314
  await execute_openssl_no_failure(
2008
- `verify -verbose -CAfile ${q3(n4(this.caCertificateWithCrl))} ${q3(n4(certificate))}`,
2315
+ ["verify", "-verbose", "-CAfile", n4(this.caCertificateWithCrl), n4(certificate)],
2009
2316
  options
2010
2317
  );
2011
2318
  }
@@ -2030,13 +2337,14 @@ async function createCertificateSigningRequestAsync(certificateSigningRequestFil
2030
2337
  (0, import_node_assert8.default)(params.rootDir);
2031
2338
  (0, import_node_assert8.default)(params.configFile);
2032
2339
  (0, import_node_assert8.default)(params.privateKey);
2033
- (0, import_node_assert8.default)(typeof params.privateKey === "string");
2034
- (0, import_node_assert8.default)(import_node_fs8.default.existsSync(params.privateKey), `Private key must exist${params.privateKey}`);
2340
+ if (typeof params.privateKey === "string") {
2341
+ (0, import_node_assert8.default)(import_node_fs8.default.existsSync(params.privateKey), `Private key must exist${params.privateKey}`);
2342
+ }
2035
2343
  (0, import_node_assert8.default)(import_node_fs8.default.existsSync(params.rootDir), "RootDir key must exist");
2036
2344
  (0, import_node_assert8.default)(typeof certificateSigningRequestFilename === "string");
2037
2345
  const subject = params.subject ? new import_node_opcua_crypto3.Subject(params.subject).toString() : void 0;
2038
2346
  displaySubtitle("- Creating a Certificate Signing Request with subtile");
2039
- const privateKeyPem = await import_node_fs8.default.promises.readFile(params.privateKey, "utf-8");
2347
+ const privateKeyPem = typeof params.privateKey === "string" ? await import_node_fs8.default.promises.readFile(params.privateKey, "utf-8") : (0, import_node_opcua_crypto3.coercePrivateKeyPem)(params.privateKey);
2040
2348
  const privateKey = await (0, import_node_opcua_crypto3.pemToPrivateKey)(privateKeyPem);
2041
2349
  const { csr } = await (0, import_node_opcua_crypto3.createCertificateSigningRequest)({
2042
2350
  privateKey,
@@ -2047,7 +2355,7 @@ async function createCertificateSigningRequestAsync(certificateSigningRequestFil
2047
2355
  purpose: params.purpose
2048
2356
  });
2049
2357
  await import_node_fs8.default.promises.writeFile(certificateSigningRequestFilename, csr, "utf-8");
2050
- display(`- privateKey ${params.privateKey}`);
2358
+ display(`- privateKey ${typeof params.privateKey === "string" ? params.privateKey : "<in-memory>"}`);
2051
2359
  display(`- certificateSigningRequestFilename ${certificateSigningRequestFilename}`);
2052
2360
  }
2053
2361
 
@@ -2060,7 +2368,9 @@ async function createSelfSignedCertificateAsync(certificate, params) {
2060
2368
  (0, import_node_assert9.default)(params.purpose, "Please provide a Certificate Purpose");
2061
2369
  (0, import_node_assert9.default)(import_node_fs9.default.existsSync(params.configFile));
2062
2370
  (0, import_node_assert9.default)(import_node_fs9.default.existsSync(params.rootDir));
2063
- (0, import_node_assert9.default)(import_node_fs9.default.existsSync(params.privateKey));
2371
+ if (typeof params.privateKey === "string") {
2372
+ (0, import_node_assert9.default)(import_node_fs9.default.existsSync(params.privateKey));
2373
+ }
2064
2374
  if (!params.subject) {
2065
2375
  throw Error("Missing subject");
2066
2376
  }
@@ -2072,7 +2382,7 @@ async function createSelfSignedCertificateAsync(certificate, params) {
2072
2382
  subject = subject.toString();
2073
2383
  const purpose = params.purpose;
2074
2384
  displayTitle("Generate a certificate request");
2075
- const privateKeyPem = await import_node_fs9.default.promises.readFile(params.privateKey, "utf-8");
2385
+ const privateKeyPem = typeof params.privateKey === "string" ? await import_node_fs9.default.promises.readFile(params.privateKey, "utf-8") : (0, import_node_opcua_crypto4.coercePrivateKeyPem)(params.privateKey);
2076
2386
  const privateKey = await (0, import_node_opcua_crypto4.pemToPrivateKey)(privateKeyPem);
2077
2387
  const { cert } = await (0, import_node_opcua_crypto4.createSelfSignedCertificate)({
2078
2388
  privateKey,
@@ -2339,6 +2649,15 @@ var CertificateManager = class _CertificateManager extends import_node_events.Ev
2339
2649
  #initializingPromise;
2340
2650
  #addCertValidation;
2341
2651
  #disableFileWatchers;
2652
+ #privateKeyPassphrase;
2653
+ #privateKeyProvider;
2654
+ /**
2655
+ * The on-disk key, decrypted once and kept for the instance's lifetime,
2656
+ * so the passphrase (or its resolver function) is consulted at most
2657
+ * once. Cleared by `dispose()`. Not used when a provider is configured:
2658
+ * the provider is the authority on the current key.
2659
+ */
2660
+ #cachedPrivateKey;
2342
2661
  #thumbs = {
2343
2662
  rejected: /* @__PURE__ */ new Map(),
2344
2663
  trusted: /* @__PURE__ */ new Map(),
@@ -2373,6 +2692,8 @@ var CertificateManager = class _CertificateManager extends import_node_events.Ev
2373
2692
  maxChainLength: v.maxChainLength ?? 5
2374
2693
  };
2375
2694
  this.#disableFileWatchers = options.disableFileWatchers ?? process.env.OPCUA_PKI_DISABLE_FILE_WATCHERS === "true";
2695
+ this.#privateKeyPassphrase = options.privateKeyPassphrase;
2696
+ this.#privateKeyProvider = options.privateKeyProvider;
2376
2697
  mkdirRecursiveSync(options.location);
2377
2698
  if (!import_node_fs10.default.existsSync(this.#location)) {
2378
2699
  throw new Error(`CertificateManager cannot access location ${this.#location}`);
@@ -2386,10 +2707,108 @@ var CertificateManager = class _CertificateManager extends import_node_events.Ev
2386
2707
  get rootDir() {
2387
2708
  return this.#location;
2388
2709
  }
2389
- /** Path to the private key file (`own/private/private_key.pem`). */
2710
+ /**
2711
+ * Path to the private key file (`own/private/private_key.pem`).
2712
+ *
2713
+ * Kept for backward compatibility with code that reads the key
2714
+ * directly from disk. When a passphrase or a `privateKeyProvider` is
2715
+ * configured, prefer {@link getPrivateKey} instead — this getter still
2716
+ * returns the on-disk path even if a provider is configured (there may
2717
+ * be no meaningful file in that case).
2718
+ */
2390
2719
  get privateKey() {
2391
2720
  return import_node_path6.default.join(this.rootDir, "own/private/private_key.pem");
2392
2721
  }
2722
+ /**
2723
+ * Resolve the private key: from `privateKeyProvider` if configured,
2724
+ * otherwise from disk (decrypting with `privateKeyPassphrase` if the
2725
+ * key is encrypted). Fails closed — throws
2726
+ * `PrivateKeyPassphraseRequiredError` — if the on-disk key is encrypted
2727
+ * and no passphrase is configured, or if the wrong passphrase is
2728
+ * configured.
2729
+ *
2730
+ * The on-disk key is read and decrypted once and then cached for the
2731
+ * lifetime of this instance, so a `privateKeyPassphrase` function is
2732
+ * called at most once (concurrent first calls share the same read). A
2733
+ * failed read is not cached, so a caller can fix the passphrase and
2734
+ * retry. A `privateKeyProvider` is consulted on every call: it is the
2735
+ * authority on what the current key is.
2736
+ */
2737
+ async getPrivateKey() {
2738
+ if (this.#privateKeyProvider) {
2739
+ return await this.#privateKeyProvider.getPrivateKey();
2740
+ }
2741
+ if (this.#cachedPrivateKey) {
2742
+ return this.#cachedPrivateKey;
2743
+ }
2744
+ if (!this.#privateKeyPromise) {
2745
+ this.#privateKeyPromise = (async () => {
2746
+ const passphrase = await resolvePrivateKeyPassphrase(this.#privateKeyPassphrase);
2747
+ return (0, import_node_opcua_crypto5.readPrivateKey)(this.privateKey, passphrase);
2748
+ })().then(
2749
+ (key) => {
2750
+ this.#cachedPrivateKey = key;
2751
+ return key;
2752
+ },
2753
+ (err) => {
2754
+ this.#privateKeyPromise = void 0;
2755
+ throw err;
2756
+ }
2757
+ );
2758
+ }
2759
+ return await this.#privateKeyPromise;
2760
+ }
2761
+ /** In-flight first read of the on-disk key, so concurrent callers share one passphrase resolution. */
2762
+ #privateKeyPromise;
2763
+ /**
2764
+ * Enable, disable, or rotate the passphrase protecting the on-disk
2765
+ * private key: decrypt with `oldPassphrase` (omit if the key is
2766
+ * currently unencrypted), then write back encrypted with
2767
+ * `newPassphrase` (omit to leave it unencrypted). The write goes to a
2768
+ * temporary file in the same directory and is atomically renamed into
2769
+ * place, so a crash mid-rotation cannot leave a partially-written key;
2770
+ * the temporary file is removed if anything fails, so a rotation *to*
2771
+ * plaintext can never leave a stray cleartext copy behind. Runs under
2772
+ * the same lock as `initialize()`.
2773
+ *
2774
+ * This only rewrites the on-disk file — it does not update this
2775
+ * instance's own `privateKeyPassphrase` (set at construction), and it
2776
+ * drops this instance's cached key so that disk stays the source of
2777
+ * truth. Construct a new `CertificateManager` with the new passphrase to
2778
+ * continue using it afterward.
2779
+ *
2780
+ * Not supported when a `privateKeyProvider` is configured (there is no
2781
+ * disk file for this method to rewrite).
2782
+ */
2783
+ async reencryptPrivateKey(oldPassphrase, newPassphrase) {
2784
+ if (this.#privateKeyProvider) {
2785
+ throw new Error("reencryptPrivateKey: not supported when a privateKeyProvider is configured");
2786
+ }
2787
+ const oldPass = await resolvePrivateKeyPassphrase(oldPassphrase);
2788
+ const newPass = await resolvePrivateKeyPassphrase(newPassphrase);
2789
+ await this.withLock2(async () => {
2790
+ const privateKey = (0, import_node_opcua_crypto5.readPrivateKey)(this.privateKey, oldPass);
2791
+ await this.#rewritePrivateKeyFile(privateKey, newPass);
2792
+ });
2793
+ this.#cachedPrivateKey = void 0;
2794
+ this.#privateKeyPromise = void 0;
2795
+ }
2796
+ /**
2797
+ * Atomically replace the on-disk private key with `privateKey`, written
2798
+ * as PKCS#8 (encrypted with `passphrase` if given). Temp file next to the
2799
+ * target, `0600`, renamed into place; the temp file is unlinked on any
2800
+ * failure so no partial or cleartext copy can be left behind.
2801
+ * Caller must hold the lock.
2802
+ */
2803
+ async #rewritePrivateKeyFile(privateKey, passphrase) {
2804
+ const tmpFilename = `${this.privateKey}.${process.pid}-${Date.now()}.tmp`;
2805
+ try {
2806
+ await (0, import_node_opcua_crypto5.writePrivateKeyFile)(tmpFilename, privateKey, { passphrase });
2807
+ await import_node_fs10.default.promises.rename(tmpFilename, this.privateKey);
2808
+ } finally {
2809
+ await import_node_fs10.default.promises.rm(tmpFilename, { force: true });
2810
+ }
2811
+ }
2393
2812
  /** Path to the OpenSSL random seed file. */
2394
2813
  get randomFile() {
2395
2814
  return import_node_path6.default.join(this.rootDir, "./random.rnd");
@@ -2689,19 +3108,24 @@ var CertificateManager = class _CertificateManager extends import_node_events.Ev
2689
3108
  }
2690
3109
  this.state = 1 /* Initializing */;
2691
3110
  this.#initializingPromise = this.#initialize();
2692
- await this.#initializingPromise;
3111
+ try {
3112
+ await this.#initializingPromise;
3113
+ } catch (err) {
3114
+ this.#initializingPromise = void 0;
3115
+ this.state = 0 /* Uninitialized */;
3116
+ throw err;
3117
+ }
2693
3118
  this.#initializingPromise = void 0;
2694
3119
  this.state = 2 /* Initialized */;
2695
3120
  _CertificateManager.#activeInstances.add(this);
2696
3121
  _CertificateManager.#installProcessCleanup();
2697
3122
  }
2698
3123
  async #initialize() {
2699
- this.state = 1 /* Initializing */;
2700
3124
  const pkiDir = this.#location;
2701
3125
  mkdirRecursiveSync(pkiDir);
2702
3126
  mkdirRecursiveSync(import_node_path6.default.join(pkiDir, "own"));
2703
3127
  mkdirRecursiveSync(import_node_path6.default.join(pkiDir, "own/certs"));
2704
- mkdirRecursiveSync(import_node_path6.default.join(pkiDir, "own/private"));
3128
+ ensurePrivateDirectory(import_node_path6.default.join(pkiDir, "own/private"));
2705
3129
  mkdirRecursiveSync(import_node_path6.default.join(pkiDir, "rejected"));
2706
3130
  mkdirRecursiveSync(import_node_path6.default.join(pkiDir, "trusted"));
2707
3131
  mkdirRecursiveSync(import_node_path6.default.join(pkiDir, "trusted/certs"));
@@ -2709,7 +3133,10 @@ var CertificateManager = class _CertificateManager extends import_node_events.Ev
2709
3133
  mkdirRecursiveSync(import_node_path6.default.join(pkiDir, "issuers"));
2710
3134
  mkdirRecursiveSync(import_node_path6.default.join(pkiDir, "issuers/certs"));
2711
3135
  mkdirRecursiveSync(import_node_path6.default.join(pkiDir, "issuers/crl"));
2712
- if (!import_node_fs10.default.existsSync(this.configFile) || !import_node_fs10.default.existsSync(this.privateKey)) {
3136
+ const ownsDiskKey = !this.#privateKeyProvider;
3137
+ const needsKeyGeneration = ownsDiskKey && !import_node_fs10.default.existsSync(this.privateKey);
3138
+ const needsKeyEncryption = ownsDiskKey && !needsKeyGeneration && this.#privateKeyPassphrase !== void 0 && !isEncryptedPrivateKeyFile(this.privateKey);
3139
+ if (!import_node_fs10.default.existsSync(this.configFile) || needsKeyGeneration || needsKeyEncryption) {
2713
3140
  return await this.withLock2(async () => {
2714
3141
  if (this.state === 3 /* Disposing */ || this.state === 4 /* Disposed */) {
2715
3142
  return;
@@ -2717,15 +3144,29 @@ var CertificateManager = class _CertificateManager extends import_node_events.Ev
2717
3144
  if (!import_node_fs10.default.existsSync(this.configFile)) {
2718
3145
  import_node_fs10.default.writeFileSync(this.configFile, configurationFileSimpleTemplate2);
2719
3146
  }
2720
- if (!import_node_fs10.default.existsSync(this.privateKey)) {
3147
+ if (ownsDiskKey && !import_node_fs10.default.existsSync(this.privateKey)) {
2721
3148
  debugLog("generating private key ...");
2722
- await (0, import_node_opcua_crypto5.generatePrivateKeyFile)(this.privateKey, this.keySize);
2723
- await this.#readCertificates();
2724
- } else {
2725
- await this.#readCertificates();
3149
+ const passphrase = await resolvePrivateKeyPassphrase(this.#privateKeyPassphrase);
3150
+ await (0, import_node_opcua_crypto5.generatePrivateKeyFile)(this.privateKey, this.keySize, { passphrase });
3151
+ this.#cachedPrivateKey = (0, import_node_opcua_crypto5.readPrivateKey)(this.privateKey, passphrase);
3152
+ } else if (ownsDiskKey && this.#privateKeyPassphrase !== void 0 && !isEncryptedPrivateKeyFile(this.privateKey)) {
3153
+ warningLog("initialize: private key is plaintext but a passphrase is configured; encrypting it in place");
3154
+ const passphrase = await resolvePrivateKeyPassphrase(this.#privateKeyPassphrase);
3155
+ const plaintextKey = (0, import_node_opcua_crypto5.readPrivateKey)(this.privateKey);
3156
+ await this.#rewritePrivateKeyFile(plaintextKey, passphrase);
3157
+ this.#cachedPrivateKey = plaintextKey;
3158
+ }
3159
+ if (ownsDiskKey) {
3160
+ restrictPrivateFilePermissions(this.privateKey, 384);
2726
3161
  }
3162
+ await this.getPrivateKey();
3163
+ await this.#readCertificates();
2727
3164
  });
2728
3165
  } else {
3166
+ if (ownsDiskKey) {
3167
+ restrictPrivateFilePermissions(this.privateKey, 384);
3168
+ }
3169
+ await this.getPrivateKey();
2729
3170
  await this.#readCertificates();
2730
3171
  }
2731
3172
  }
@@ -2761,6 +3202,8 @@ var CertificateManager = class _CertificateManager extends import_node_events.Ev
2761
3202
  this.#watchers.splice(0);
2762
3203
  } finally {
2763
3204
  this.state = 4 /* Disposed */;
3205
+ this.#cachedPrivateKey = void 0;
3206
+ this.#privateKeyPromise = void 0;
2764
3207
  _CertificateManager.#activeInstances.delete(this);
2765
3208
  }
2766
3209
  }
@@ -2807,16 +3250,18 @@ var CertificateManager = class _CertificateManager extends import_node_events.Ev
2807
3250
  if (typeof params.applicationUri !== "string") {
2808
3251
  throw new Error("createSelfSignedCertificate: expecting applicationUri to be a string");
2809
3252
  }
2810
- if (!import_node_fs10.default.existsSync(this.privateKey)) {
3253
+ if (!this.#privateKeyProvider && !import_node_fs10.default.existsSync(this.privateKey)) {
2811
3254
  throw new Error(`Cannot find private key ${this.privateKey}`);
2812
3255
  }
2813
3256
  let certificateFilename = import_node_path6.default.join(this.rootDir, "own/certs/self_signed_certificate.pem");
2814
3257
  certificateFilename = params.outputFile || certificateFilename;
2815
- const _params = params;
2816
- _params.rootDir = this.rootDir;
2817
- _params.configFile = this.configFile;
2818
- _params.privateKey = this.privateKey;
2819
- _params.subject = params.subject || "CN=FIXME";
3258
+ const _params = {
3259
+ ...params,
3260
+ rootDir: this.rootDir,
3261
+ configFile: this.configFile,
3262
+ privateKey: await this.getPrivateKey(),
3263
+ subject: params.subject || "CN=FIXME"
3264
+ };
2820
3265
  await this.withLock2(async () => {
2821
3266
  await createSelfSignedCertificate(certificateFilename, _params);
2822
3267
  });
@@ -2835,13 +3280,15 @@ var CertificateManager = class _CertificateManager extends import_node_events.Ev
2835
3280
  if (!params) {
2836
3281
  throw new Error("params is required");
2837
3282
  }
2838
- const _params = params;
2839
- if (Object.prototype.hasOwnProperty.call(_params, "rootDir")) {
3283
+ if (Object.prototype.hasOwnProperty.call(params, "rootDir")) {
2840
3284
  throw new Error("rootDir should not be specified ");
2841
3285
  }
2842
- _params.rootDir = import_node_path6.default.resolve(this.rootDir);
2843
- _params.configFile = import_node_path6.default.resolve(this.configFile);
2844
- _params.privateKey = import_node_path6.default.resolve(this.privateKey);
3286
+ const _params = {
3287
+ ...params,
3288
+ rootDir: import_node_path6.default.resolve(this.rootDir),
3289
+ configFile: import_node_path6.default.resolve(this.configFile),
3290
+ privateKey: await this.getPrivateKey()
3291
+ };
2845
3292
  return await this.withLock2(async () => {
2846
3293
  const now = /* @__PURE__ */ new Date();
2847
3294
  const today = `${now.toISOString().slice(0, 10)}_${now.getTime()}`;
@@ -3692,6 +4139,7 @@ var CertificateManager = class _CertificateManager extends import_node_events.Ev
3692
4139
  CertificateManager,
3693
4140
  CertificateManagerState,
3694
4141
  ChainCompletionStatus,
4142
+ PrivateKeyPassphraseRequiredError,
3695
4143
  Subject,
3696
4144
  VerificationStatus,
3697
4145
  adjustApplicationUri,
@@ -3710,6 +4158,7 @@ var CertificateManager = class _CertificateManager extends import_node_events.Ev
3710
4158
  isIssuer,
3711
4159
  isRootIssuer,
3712
4160
  makeFingerprint,
3713
- quote
4161
+ quote,
4162
+ resolvePrivateKeyPassphrase
3714
4163
  });
3715
4164
  //# sourceMappingURL=index.js.map