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/bin/pki.mjs +668 -227
- package/dist/bin/pki.mjs.map +1 -1
- package/dist/index.d.mts +201 -21
- package/dist/index.d.ts +201 -21
- package/dist/index.js +663 -214
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +669 -216
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/readme.md +4 -0
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// packages/node-opcua-pki/lib/index.ts
|
|
2
|
+
import { PrivateKeyPassphraseRequiredError } from "node-opcua-crypto";
|
|
3
|
+
|
|
1
4
|
// packages/node-opcua-pki/lib/ca/certificate_authority.ts
|
|
2
5
|
import assert7 from "assert";
|
|
3
6
|
import fs7 from "fs";
|
|
@@ -15,45 +18,16 @@ import {
|
|
|
15
18
|
readCertificateSigningRequest,
|
|
16
19
|
readPrivateKey,
|
|
17
20
|
Subject as Subject2,
|
|
18
|
-
toPem
|
|
21
|
+
toPem,
|
|
22
|
+
writePrivateKeyFile
|
|
19
23
|
} from "node-opcua-crypto";
|
|
20
24
|
|
|
21
25
|
// packages/node-opcua-pki/lib/pki/toolbox_pfx.ts
|
|
22
|
-
import
|
|
26
|
+
import assert3 from "assert";
|
|
23
27
|
import fs4 from "fs";
|
|
24
28
|
|
|
25
|
-
// packages/node-opcua-pki/lib/toolbox/common.ts
|
|
26
|
-
import assert from "assert";
|
|
27
|
-
function quote(str) {
|
|
28
|
-
return `"${str || ""}"`;
|
|
29
|
-
}
|
|
30
|
-
function adjustDate(params) {
|
|
31
|
-
assert(params instanceof Object);
|
|
32
|
-
params.startDate = params.startDate || /* @__PURE__ */ new Date();
|
|
33
|
-
assert(params.startDate instanceof Date);
|
|
34
|
-
if (params.validityMs !== void 0) {
|
|
35
|
-
if (params.validityMs <= 0) {
|
|
36
|
-
throw new RangeError(`validityMs must be > 0 (got ${params.validityMs})`);
|
|
37
|
-
}
|
|
38
|
-
params.endDate = new Date(params.startDate.getTime() + params.validityMs);
|
|
39
|
-
params.validity = Math.ceil(params.validityMs / 864e5);
|
|
40
|
-
} else {
|
|
41
|
-
params.validity = params.validity || 365;
|
|
42
|
-
params.endDate = new Date(params.startDate.getTime());
|
|
43
|
-
params.endDate.setDate(params.startDate.getDate() + params.validity);
|
|
44
|
-
}
|
|
45
|
-
assert(params.endDate instanceof Date);
|
|
46
|
-
assert(params.startDate instanceof Date);
|
|
47
|
-
}
|
|
48
|
-
function adjustApplicationUri(params) {
|
|
49
|
-
const applicationUri = params.applicationUri || "";
|
|
50
|
-
if (applicationUri.length > 200) {
|
|
51
|
-
throw new Error(`Openssl doesn't support urn with length greater than 200${applicationUri}`);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
29
|
// packages/node-opcua-pki/lib/toolbox/common2.ts
|
|
56
|
-
import
|
|
30
|
+
import assert from "assert";
|
|
57
31
|
import fs from "fs";
|
|
58
32
|
import path from "path";
|
|
59
33
|
import chalk from "chalk";
|
|
@@ -94,12 +68,31 @@ function mkdirRecursiveSync(folder) {
|
|
|
94
68
|
fs.mkdirSync(folder, { recursive: true });
|
|
95
69
|
}
|
|
96
70
|
}
|
|
71
|
+
function restrictPrivateFilePermissions(target, mode) {
|
|
72
|
+
if (process.platform === "win32") {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
fs.chmodSync(target, mode);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
warningLog(chalk.yellow(" could not restrict permissions on "), target, err.message);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function ensurePrivateDirectory(dir) {
|
|
82
|
+
if (!fs.existsSync(dir)) {
|
|
83
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
84
|
+
}
|
|
85
|
+
restrictPrivateFilePermissions(dir, 448);
|
|
86
|
+
}
|
|
87
|
+
function isEncryptedPrivateKeyFile(filename) {
|
|
88
|
+
return fs.readFileSync(filename, "utf-8").includes("-----BEGIN ENCRYPTED PRIVATE KEY-----");
|
|
89
|
+
}
|
|
97
90
|
function makePath(folderName, filename) {
|
|
98
91
|
let s;
|
|
99
92
|
if (filename) {
|
|
100
93
|
s = path.join(path.normalize(folderName), filename);
|
|
101
94
|
} else {
|
|
102
|
-
|
|
95
|
+
assert(folderName);
|
|
103
96
|
s = folderName;
|
|
104
97
|
}
|
|
105
98
|
s = s.replace(/\\/g, "/");
|
|
@@ -107,7 +100,7 @@ function makePath(folderName, filename) {
|
|
|
107
100
|
}
|
|
108
101
|
|
|
109
102
|
// packages/node-opcua-pki/lib/toolbox/with_openssl/execute_openssl.ts
|
|
110
|
-
import
|
|
103
|
+
import assert2 from "assert";
|
|
111
104
|
import child_process2 from "child_process";
|
|
112
105
|
import fs3 from "fs";
|
|
113
106
|
import os2 from "os";
|
|
@@ -115,6 +108,50 @@ import byline2 from "byline";
|
|
|
115
108
|
import chalk3 from "chalk";
|
|
116
109
|
|
|
117
110
|
// packages/node-opcua-pki/lib/toolbox/with_openssl/_env.ts
|
|
111
|
+
var SAFE_ENV_PASSTHROUGH = /* @__PURE__ */ new Set([
|
|
112
|
+
// POSIX/Windows shell and process essentials
|
|
113
|
+
"path",
|
|
114
|
+
"home",
|
|
115
|
+
"userprofile",
|
|
116
|
+
"temp",
|
|
117
|
+
"tmp",
|
|
118
|
+
"tmpdir",
|
|
119
|
+
"systemroot",
|
|
120
|
+
"windir",
|
|
121
|
+
"comspec",
|
|
122
|
+
"pathext",
|
|
123
|
+
"appdata",
|
|
124
|
+
"localappdata",
|
|
125
|
+
// dynamic loader: a custom-built or relocated openssl (e.g. under /opt,
|
|
126
|
+
// or Homebrew on macOS) may need these to find its own libcrypto/libssl
|
|
127
|
+
"ld_library_path",
|
|
128
|
+
"dyld_library_path",
|
|
129
|
+
"dyld_fallback_library_path",
|
|
130
|
+
// locale, so openssl's textual output stays parseable
|
|
131
|
+
"lang",
|
|
132
|
+
"lc_all",
|
|
133
|
+
"lc_ctype",
|
|
134
|
+
// openssl configuration this app, or the host OS/user, may rely on
|
|
135
|
+
"openssl_conf",
|
|
136
|
+
"randfile",
|
|
137
|
+
"openssl_modules",
|
|
138
|
+
"openssl_engines",
|
|
139
|
+
"ssl_cert_file",
|
|
140
|
+
"ssl_cert_dir"
|
|
141
|
+
]);
|
|
142
|
+
function buildChildEnv(extra) {
|
|
143
|
+
const env = {};
|
|
144
|
+
for (const key of Object.keys(process.env)) {
|
|
145
|
+
if (SAFE_ENV_PASSTHROUGH.has(key.toLowerCase())) {
|
|
146
|
+
env[key] = process.env[key];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return { ...env, ...extra };
|
|
150
|
+
}
|
|
151
|
+
function redactEnvForLog(options) {
|
|
152
|
+
const { env, ...rest } = options;
|
|
153
|
+
return env ? { ...rest, env: Object.keys(env) } : rest;
|
|
154
|
+
}
|
|
118
155
|
var exportedEnvVars = {};
|
|
119
156
|
function setEnv(varName, value) {
|
|
120
157
|
if (!g_config.silent) {
|
|
@@ -169,24 +206,17 @@ import byline from "byline";
|
|
|
169
206
|
import chalk2 from "chalk";
|
|
170
207
|
import yauzl from "yauzl";
|
|
171
208
|
var doDebug2 = process.env.NODEOPCUAPKIDEBUG || false;
|
|
172
|
-
async function execute(
|
|
209
|
+
async function execute(file, args, cwd) {
|
|
173
210
|
let output = "";
|
|
174
|
-
const options = {
|
|
175
|
-
cwd,
|
|
176
|
-
windowsHide: true
|
|
177
|
-
};
|
|
178
211
|
return await new Promise((resolve, reject) => {
|
|
179
|
-
const child = child_process.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
);
|
|
212
|
+
const child = child_process.spawn(file, args, {
|
|
213
|
+
cwd,
|
|
214
|
+
windowsHide: true,
|
|
215
|
+
env: buildChildEnv(),
|
|
216
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
217
|
+
});
|
|
218
|
+
child.on("error", (err) => reject(err));
|
|
219
|
+
child.on("close", (code) => resolve({ exitCode: code ?? 1, output }));
|
|
190
220
|
const stream1 = byline(child.stdout);
|
|
191
221
|
stream1.on("data", (line) => {
|
|
192
222
|
output += `${line}
|
|
@@ -198,16 +228,13 @@ async function execute(cmd, cwd) {
|
|
|
198
228
|
});
|
|
199
229
|
});
|
|
200
230
|
}
|
|
201
|
-
function quote2(str) {
|
|
202
|
-
return `"${str.replace(/\\/g, "/")}"`;
|
|
203
|
-
}
|
|
204
231
|
function is_expected_openssl_version(strVersion) {
|
|
205
232
|
return !!strVersion.match(/OpenSSL \d/);
|
|
206
233
|
}
|
|
207
234
|
async function getopensslExecPath() {
|
|
208
235
|
let result1;
|
|
209
236
|
try {
|
|
210
|
-
result1 = await execute("which openssl");
|
|
237
|
+
result1 = await execute("which", ["openssl"]);
|
|
211
238
|
} catch (err) {
|
|
212
239
|
warningLog("warning: ", err.message);
|
|
213
240
|
throw new Error("Cannot find openssl");
|
|
@@ -224,11 +251,10 @@ async function getopensslExecPath() {
|
|
|
224
251
|
}
|
|
225
252
|
async function check_system_openssl_version() {
|
|
226
253
|
const opensslExecPath = await getopensslExecPath();
|
|
227
|
-
const q_opensslExecPath = quote2(opensslExecPath);
|
|
228
254
|
if (doDebug2) {
|
|
229
255
|
warningLog(` OpenSSL found in : ${chalk2.yellow(opensslExecPath)}`);
|
|
230
256
|
}
|
|
231
|
-
const result = await execute(
|
|
257
|
+
const result = await execute(opensslExecPath, ["version"]);
|
|
232
258
|
const exitCode = result?.exitCode;
|
|
233
259
|
const output = result?.output;
|
|
234
260
|
const version = output.trim();
|
|
@@ -268,9 +294,8 @@ async function install_and_check_win32_openssl_version() {
|
|
|
268
294
|
version: `cannot find file ${opensslExecPath2}`
|
|
269
295
|
};
|
|
270
296
|
} else {
|
|
271
|
-
const q_openssl_exe_path = quote2(opensslExecPath2);
|
|
272
297
|
const cwd = ".";
|
|
273
|
-
const { exitCode, output } = await execute(
|
|
298
|
+
const { exitCode, output } = await execute(opensslExecPath2, ["version"], cwd);
|
|
274
299
|
const version = output.trim();
|
|
275
300
|
if (doDebug2) {
|
|
276
301
|
warningLog(" Version = ", version);
|
|
@@ -283,7 +308,7 @@ async function install_and_check_win32_openssl_version() {
|
|
|
283
308
|
}
|
|
284
309
|
async function find_system_openssl_win32() {
|
|
285
310
|
try {
|
|
286
|
-
const result = await execute("where openssl");
|
|
311
|
+
const result = await execute("where", ["openssl"]);
|
|
287
312
|
if (result.exitCode !== 0) {
|
|
288
313
|
return void 0;
|
|
289
314
|
}
|
|
@@ -291,8 +316,7 @@ async function install_and_check_win32_openssl_version() {
|
|
|
291
316
|
if (!opensslPath2 || !fs2.existsSync(opensslPath2)) {
|
|
292
317
|
return void 0;
|
|
293
318
|
}
|
|
294
|
-
const
|
|
295
|
-
const versionResult = await execute(`${q4} version`);
|
|
319
|
+
const versionResult = await execute(opensslPath2, ["version"]);
|
|
296
320
|
const version = versionResult.output.trim();
|
|
297
321
|
if (versionResult.exitCode === 0 && is_expected_openssl_version(version)) {
|
|
298
322
|
warningLog(
|
|
@@ -455,36 +479,55 @@ async function get_openssl_exec_path() {
|
|
|
455
479
|
// packages/node-opcua-pki/lib/toolbox/with_openssl/execute_openssl.ts
|
|
456
480
|
var opensslPath;
|
|
457
481
|
var n = makePath;
|
|
458
|
-
|
|
482
|
+
var PASSIN_ENV_VAR = "NODE_OPCUA_PKI_OPENSSL_PASSIN";
|
|
483
|
+
var PASSOUT_ENV_VAR = "NODE_OPCUA_PKI_OPENSSL_PASSOUT";
|
|
484
|
+
function passinArg(passphrase = "") {
|
|
485
|
+
return { args: ["-passin", `env:${PASSIN_ENV_VAR}`], env: { [PASSIN_ENV_VAR]: passphrase } };
|
|
486
|
+
}
|
|
487
|
+
function passoutArg(passphrase = "") {
|
|
488
|
+
return { args: ["-passout", `env:${PASSOUT_ENV_VAR}`], env: { [PASSOUT_ENV_VAR]: passphrase } };
|
|
489
|
+
}
|
|
490
|
+
function renderForDisplay(file, args) {
|
|
491
|
+
return [file, ...args].map((a) => a === "" || /[\s"'`$\\]/.test(a) ? JSON.stringify(a) : a).join(" ");
|
|
492
|
+
}
|
|
493
|
+
async function execute2(file, args, options) {
|
|
459
494
|
const from = new Error();
|
|
460
495
|
options.cwd = options.cwd || process.cwd();
|
|
461
496
|
if (!g_config.silent) {
|
|
462
497
|
warningLog(chalk3.cyan(" CWD "), options.cwd);
|
|
463
498
|
}
|
|
464
499
|
const outputs = [];
|
|
500
|
+
const errorOutputs = [];
|
|
501
|
+
const display2 = renderForDisplay(file, args);
|
|
465
502
|
return await new Promise((resolve, reject) => {
|
|
466
|
-
const child = child_process2.
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
503
|
+
const child = child_process2.spawn(file, [...args], {
|
|
504
|
+
cwd: options.cwd,
|
|
505
|
+
windowsHide: true,
|
|
506
|
+
env: buildChildEnv(options.env),
|
|
507
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
508
|
+
});
|
|
509
|
+
const fail = (message) => {
|
|
510
|
+
if (!options.hideErrorMessage) {
|
|
511
|
+
const fence = "###########################################";
|
|
512
|
+
console.error(chalk3.bgWhiteBright.redBright(`${fence} OPENSSL ERROR ${fence}`));
|
|
513
|
+
console.error(chalk3.bgWhiteBright.redBright(`CWD = ${options.cwd}`));
|
|
514
|
+
console.error(chalk3.bgWhiteBright.redBright(message));
|
|
515
|
+
console.error(chalk3.bgWhiteBright.redBright(`${fence} OPENSSL ERROR ${fence}`));
|
|
516
|
+
console.error(from.stack);
|
|
517
|
+
}
|
|
518
|
+
reject(new Error(message));
|
|
519
|
+
};
|
|
520
|
+
child.on("error", (err) => fail(`Command failed: ${display2}
|
|
521
|
+
${err.message}`));
|
|
522
|
+
child.on("close", (code, signal) => {
|
|
523
|
+
if (code === 0) {
|
|
485
524
|
resolve(outputs.join(""));
|
|
525
|
+
return;
|
|
486
526
|
}
|
|
487
|
-
|
|
527
|
+
const why = signal ? `signal ${signal}` : `exit code ${code}`;
|
|
528
|
+
fail(`Command failed: ${display2}
|
|
529
|
+
${errorOutputs.join("")}(${why})`);
|
|
530
|
+
});
|
|
488
531
|
if (child.stdout) {
|
|
489
532
|
const stream2 = byline2(child.stdout);
|
|
490
533
|
stream2.on("data", (line) => {
|
|
@@ -501,17 +544,17 @@ async function execute2(cmd, options) {
|
|
|
501
544
|
});
|
|
502
545
|
}
|
|
503
546
|
}
|
|
504
|
-
if (
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
if (displayError) {
|
|
510
|
-
process.stdout.write(`${chalk3.white(" stderr ") + chalk3.red(line)}
|
|
547
|
+
if (child.stderr) {
|
|
548
|
+
const stream1 = byline2(child.stderr);
|
|
549
|
+
stream1.on("data", (line) => {
|
|
550
|
+
line = line.toString();
|
|
551
|
+
errorOutputs.push(`${line}
|
|
511
552
|
`);
|
|
512
|
-
|
|
513
|
-
}
|
|
514
|
-
|
|
553
|
+
if (!g_config.silent && displayError) {
|
|
554
|
+
process.stdout.write(`${chalk3.white(" stderr ") + chalk3.red(line)}
|
|
555
|
+
`);
|
|
556
|
+
}
|
|
557
|
+
});
|
|
515
558
|
}
|
|
516
559
|
});
|
|
517
560
|
}
|
|
@@ -521,18 +564,18 @@ async function find_openssl() {
|
|
|
521
564
|
async function ensure_openssl_installed() {
|
|
522
565
|
if (!opensslPath) {
|
|
523
566
|
opensslPath = await find_openssl();
|
|
524
|
-
const outputs = await execute_openssl("version", { cwd: "." });
|
|
567
|
+
const outputs = await execute_openssl(["version"], { cwd: "." });
|
|
525
568
|
g_config.opensslVersion = outputs.trim();
|
|
526
569
|
if (doDebug) {
|
|
527
570
|
warningLog("OpenSSL version : ", g_config.opensslVersion);
|
|
528
571
|
}
|
|
529
572
|
}
|
|
530
573
|
}
|
|
531
|
-
async function execute_openssl_no_failure(
|
|
574
|
+
async function execute_openssl_no_failure(args, options) {
|
|
532
575
|
options = options || {};
|
|
533
576
|
options.hideErrorMessage = true;
|
|
534
577
|
try {
|
|
535
|
-
return await execute_openssl(
|
|
578
|
+
return await execute_openssl(args, options);
|
|
536
579
|
} catch (err) {
|
|
537
580
|
debugLog(" (ignored error = ERROR : )", err.message);
|
|
538
581
|
}
|
|
@@ -540,62 +583,64 @@ async function execute_openssl_no_failure(cmd, options) {
|
|
|
540
583
|
function getTempFolder() {
|
|
541
584
|
return os2.tmpdir();
|
|
542
585
|
}
|
|
543
|
-
async function execute_openssl(
|
|
544
|
-
debugLog("execute_openssl",
|
|
586
|
+
async function execute_openssl(args, options) {
|
|
587
|
+
debugLog("execute_openssl", args, redactEnvForLog(options));
|
|
545
588
|
const empty_config_file = n(getTempFolder(), "empty_config.cnf");
|
|
546
589
|
if (!fs3.existsSync(empty_config_file)) {
|
|
547
590
|
await fs3.promises.writeFile(empty_config_file, "# empty config file");
|
|
548
591
|
}
|
|
549
592
|
options = options || {};
|
|
550
593
|
options.openssl_conf = options.openssl_conf || empty_config_file;
|
|
551
|
-
|
|
594
|
+
assert2(options.openssl_conf);
|
|
552
595
|
setEnv("OPENSSL_CONF", options.openssl_conf);
|
|
553
596
|
if (!g_config.silent) {
|
|
554
597
|
warningLog(chalk3.cyan(" OPENSSL_CONF"), process.env.OPENSSL_CONF);
|
|
555
598
|
warningLog(chalk3.cyan(" RANDFILE "), process.env.RANDFILE);
|
|
556
|
-
warningLog(chalk3.cyan(" CMD
|
|
599
|
+
warningLog(chalk3.cyan(" CMD "), chalk3.cyanBright(renderForDisplay("openssl", args)));
|
|
557
600
|
}
|
|
558
601
|
await ensure_openssl_installed();
|
|
559
|
-
return await execute2(
|
|
602
|
+
return await execute2(opensslPath, args, options);
|
|
560
603
|
}
|
|
561
604
|
|
|
562
605
|
// packages/node-opcua-pki/lib/pki/toolbox_pfx.ts
|
|
563
|
-
var q = quote;
|
|
564
606
|
var n2 = makePath;
|
|
565
607
|
async function createPFX(options) {
|
|
566
|
-
const { certificateFile, privateKeyFile, outputFile, passphrase = "", caCertificateFiles } = options;
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
cmd += ` -in ${q(n2(certificateFile))}`;
|
|
571
|
-
cmd += ` -inkey ${q(n2(privateKeyFile))}`;
|
|
608
|
+
const { certificateFile, privateKeyFile, privateKeyPassphrase, outputFile, passphrase = "", caCertificateFiles } = options;
|
|
609
|
+
assert3(fs4.existsSync(certificateFile), `Certificate file does not exist: ${certificateFile}`);
|
|
610
|
+
assert3(fs4.existsSync(privateKeyFile), `Private key file does not exist: ${privateKeyFile}`);
|
|
611
|
+
const args = ["pkcs12", "-export", "-in", n2(certificateFile), "-inkey", n2(privateKeyFile)];
|
|
572
612
|
if (caCertificateFiles) {
|
|
573
613
|
for (const caFile of caCertificateFiles) {
|
|
574
|
-
|
|
575
|
-
|
|
614
|
+
assert3(fs4.existsSync(caFile), `CA certificate file does not exist: ${caFile}`);
|
|
615
|
+
args.push("-certfile", n2(caFile));
|
|
576
616
|
}
|
|
577
617
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
618
|
+
const passin = passinArg(privateKeyPassphrase);
|
|
619
|
+
const passout = passoutArg(passphrase);
|
|
620
|
+
args.push("-out", n2(outputFile), ...passin.args, ...passout.args);
|
|
621
|
+
await execute_openssl(args, { env: { ...passin.env, ...passout.env } });
|
|
581
622
|
}
|
|
582
623
|
async function extractCertificateFromPFX(options) {
|
|
583
624
|
const { pfxFile, passphrase = "" } = options;
|
|
584
|
-
|
|
585
|
-
const
|
|
586
|
-
return await execute_openssl(
|
|
625
|
+
assert3(fs4.existsSync(pfxFile), `PFX file does not exist: ${pfxFile}`);
|
|
626
|
+
const passin = passinArg(passphrase);
|
|
627
|
+
return await execute_openssl(["pkcs12", "-in", n2(pfxFile), "-clcerts", "-nokeys", "-nodes", ...passin.args], {
|
|
628
|
+
env: passin.env
|
|
629
|
+
});
|
|
587
630
|
}
|
|
588
631
|
async function extractPrivateKeyFromPFX(options) {
|
|
589
632
|
const { pfxFile, passphrase = "" } = options;
|
|
590
|
-
|
|
591
|
-
const
|
|
592
|
-
return await execute_openssl(
|
|
633
|
+
assert3(fs4.existsSync(pfxFile), `PFX file does not exist: ${pfxFile}`);
|
|
634
|
+
const passin = passinArg(passphrase);
|
|
635
|
+
return await execute_openssl(["pkcs12", "-in", n2(pfxFile), "-nocerts", "-nodes", ...passin.args], { env: passin.env });
|
|
593
636
|
}
|
|
594
637
|
async function extractCACertificatesFromPFX(options) {
|
|
595
638
|
const { pfxFile, passphrase = "" } = options;
|
|
596
|
-
|
|
597
|
-
const
|
|
598
|
-
return await execute_openssl(
|
|
639
|
+
assert3(fs4.existsSync(pfxFile), `PFX file does not exist: ${pfxFile}`);
|
|
640
|
+
const passin = passinArg(passphrase);
|
|
641
|
+
return await execute_openssl(["pkcs12", "-in", n2(pfxFile), "-cacerts", "-nokeys", "-nodes", ...passin.args], {
|
|
642
|
+
env: passin.env
|
|
643
|
+
});
|
|
599
644
|
}
|
|
600
645
|
async function extractAllFromPFX(options) {
|
|
601
646
|
const [certificate, privateKey, caCertificates] = await Promise.all([
|
|
@@ -606,14 +651,50 @@ async function extractAllFromPFX(options) {
|
|
|
606
651
|
return { certificate, privateKey, caCertificates };
|
|
607
652
|
}
|
|
608
653
|
async function convertPFXtoPEM(pfxFile, pemFile, passphrase = "") {
|
|
609
|
-
|
|
610
|
-
const
|
|
611
|
-
await execute_openssl(
|
|
654
|
+
assert3(fs4.existsSync(pfxFile), `PFX file does not exist: ${pfxFile}`);
|
|
655
|
+
const passin = passinArg(passphrase);
|
|
656
|
+
await execute_openssl(["pkcs12", "-in", n2(pfxFile), "-out", n2(pemFile), "-nodes", ...passin.args], { env: passin.env });
|
|
612
657
|
}
|
|
613
658
|
async function dumpPFX(pfxFile, passphrase = "") {
|
|
614
|
-
|
|
615
|
-
const
|
|
616
|
-
return await execute_openssl(
|
|
659
|
+
assert3(fs4.existsSync(pfxFile), `PFX file does not exist: ${pfxFile}`);
|
|
660
|
+
const passin = passinArg(passphrase);
|
|
661
|
+
return await execute_openssl(["pkcs12", "-in", n2(pfxFile), "-info", "-nodes", ...passin.args], { env: passin.env });
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// packages/node-opcua-pki/lib/toolbox/common.ts
|
|
665
|
+
import assert4 from "assert";
|
|
666
|
+
function quote(str) {
|
|
667
|
+
return `"${str || ""}"`;
|
|
668
|
+
}
|
|
669
|
+
async function resolvePrivateKeyPassphrase(passphrase) {
|
|
670
|
+
if (passphrase === void 0) {
|
|
671
|
+
return void 0;
|
|
672
|
+
}
|
|
673
|
+
return typeof passphrase === "function" ? await passphrase() : passphrase;
|
|
674
|
+
}
|
|
675
|
+
function adjustDate(params) {
|
|
676
|
+
assert4(params instanceof Object);
|
|
677
|
+
params.startDate = params.startDate || /* @__PURE__ */ new Date();
|
|
678
|
+
assert4(params.startDate instanceof Date);
|
|
679
|
+
if (params.validityMs !== void 0) {
|
|
680
|
+
if (params.validityMs <= 0) {
|
|
681
|
+
throw new RangeError(`validityMs must be > 0 (got ${params.validityMs})`);
|
|
682
|
+
}
|
|
683
|
+
params.endDate = new Date(params.startDate.getTime() + params.validityMs);
|
|
684
|
+
params.validity = Math.ceil(params.validityMs / 864e5);
|
|
685
|
+
} else {
|
|
686
|
+
params.validity = params.validity || 365;
|
|
687
|
+
params.endDate = new Date(params.startDate.getTime());
|
|
688
|
+
params.endDate.setDate(params.startDate.getDate() + params.validity);
|
|
689
|
+
}
|
|
690
|
+
assert4(params.endDate instanceof Date);
|
|
691
|
+
assert4(params.startDate instanceof Date);
|
|
692
|
+
}
|
|
693
|
+
function adjustApplicationUri(params) {
|
|
694
|
+
const applicationUri = params.applicationUri || "";
|
|
695
|
+
if (applicationUri.length > 200) {
|
|
696
|
+
throw new Error(`Openssl doesn't support urn with length greater than 200${applicationUri}`);
|
|
697
|
+
}
|
|
617
698
|
}
|
|
618
699
|
|
|
619
700
|
// packages/node-opcua-pki/lib/toolbox/display.ts
|
|
@@ -705,7 +786,6 @@ function x509Date(date) {
|
|
|
705
786
|
}
|
|
706
787
|
|
|
707
788
|
// packages/node-opcua-pki/lib/toolbox/with_openssl/create_certificate_signing_request.ts
|
|
708
|
-
var q2 = quote;
|
|
709
789
|
var n3 = makePath;
|
|
710
790
|
async function createCertificateSigningRequestWithOpenSSL(certificateSigningRequestFilename, params) {
|
|
711
791
|
assert6(params);
|
|
@@ -720,12 +800,23 @@ async function createCertificateSigningRequestWithOpenSSL(certificateSigningRequ
|
|
|
720
800
|
processAltNames(params);
|
|
721
801
|
const configFile = generateStaticConfig(params.configFile, { cwd: params.rootDir });
|
|
722
802
|
const options = { cwd: params.rootDir, openssl_conf: path4.relative(params.rootDir, configFile) };
|
|
723
|
-
const configOption = ` -config ${q2(n3(configFile))}`;
|
|
724
803
|
const subject = params.subject ? new Subject(params.subject).toString() : void 0;
|
|
725
|
-
const subjectOptions = subject ? ` -subj "${subject}"` : "";
|
|
726
804
|
displaySubtitle("- Creating a Certificate Signing Request with openssl");
|
|
727
805
|
await execute_openssl(
|
|
728
|
-
|
|
806
|
+
[
|
|
807
|
+
"req",
|
|
808
|
+
"-new",
|
|
809
|
+
"-sha256",
|
|
810
|
+
"-batch",
|
|
811
|
+
"-text",
|
|
812
|
+
"-config",
|
|
813
|
+
n3(configFile),
|
|
814
|
+
"-key",
|
|
815
|
+
n3(params.privateKey),
|
|
816
|
+
...subject ? ["-subj", subject] : [],
|
|
817
|
+
"-out",
|
|
818
|
+
n3(certificateSigningRequestFilename)
|
|
819
|
+
],
|
|
729
820
|
options
|
|
730
821
|
);
|
|
731
822
|
}
|
|
@@ -739,7 +830,7 @@ var config2 = `#.........DO NOT MODIFY BY HAND .........................
|
|
|
739
830
|
[ ca ]
|
|
740
831
|
default_ca = CA_default
|
|
741
832
|
[ CA_default ]
|
|
742
|
-
dir = %%ROOT_FOLDER%%
|
|
833
|
+
dir = "%%ROOT_FOLDER%%" # the main CA folder (quoted: see renderCaConfig)
|
|
743
834
|
certs = $dir/certs # where to store certificates
|
|
744
835
|
new_certs_dir = $dir/certs #
|
|
745
836
|
database = $dir/index.txt # the certificate database
|
|
@@ -872,13 +963,18 @@ var ca_config_template_cnf_default = config2;
|
|
|
872
963
|
var defaultSubject = "/C=FR/ST=IDF/L=Paris/O=Local NODE-OPCUA Certificate Authority/CN=NodeOPCUA-CA";
|
|
873
964
|
var configurationFileTemplate = ca_config_template_cnf_default;
|
|
874
965
|
var configurationFileSimpleTemplate = simple_config_template_cnf_default;
|
|
966
|
+
function escapeOpensslConfDoubleQuoted(value) {
|
|
967
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
968
|
+
}
|
|
969
|
+
function renderCaConfig(caRootDir) {
|
|
970
|
+
return configurationFileTemplate.replace(/%%ROOT_FOLDER%%/, escapeOpensslConfDoubleQuoted(makePath(caRootDir)));
|
|
971
|
+
}
|
|
875
972
|
var config3 = {
|
|
876
973
|
certificateDir: "INVALID",
|
|
877
974
|
forceCA: false,
|
|
878
975
|
pkiDir: "INVALID"
|
|
879
976
|
};
|
|
880
977
|
var n4 = makePath;
|
|
881
|
-
var q3 = quote;
|
|
882
978
|
function octetStringToIpAddress(a) {
|
|
883
979
|
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();
|
|
884
980
|
}
|
|
@@ -888,7 +984,7 @@ async function construct_CertificateAuthority(certificateAuthority) {
|
|
|
888
984
|
const caRootDir = path5.resolve(certificateAuthority.rootDir);
|
|
889
985
|
async function make_folders() {
|
|
890
986
|
mkdirRecursiveSync(caRootDir);
|
|
891
|
-
|
|
987
|
+
ensurePrivateDirectory(path5.join(caRootDir, "private"));
|
|
892
988
|
mkdirRecursiveSync(path5.join(caRootDir, "public"));
|
|
893
989
|
mkdirRecursiveSync(path5.join(caRootDir, "certs"));
|
|
894
990
|
mkdirRecursiveSync(path5.join(caRootDir, "crl"));
|
|
@@ -913,6 +1009,8 @@ async function construct_CertificateAuthority(certificateAuthority) {
|
|
|
913
1009
|
const caKeyExists = fs7.existsSync(path5.join(caRootDir, "private/cakey.pem"));
|
|
914
1010
|
const caCertExists = fs7.existsSync(path5.join(caRootDir, "public/cacert.pem"));
|
|
915
1011
|
if (caKeyExists && caCertExists && !config3.forceCA) {
|
|
1012
|
+
restrictPrivateFilePermissions(path5.join(caRootDir, "private/cakey.pem"), 384);
|
|
1013
|
+
await certificateAuthority._ensurePrivateKeyProtection();
|
|
916
1014
|
debugLog("CA private key and certificate already exist ... skipping");
|
|
917
1015
|
return;
|
|
918
1016
|
}
|
|
@@ -931,26 +1029,40 @@ async function construct_CertificateAuthority(certificateAuthority) {
|
|
|
931
1029
|
}
|
|
932
1030
|
const caConfigFile = certificateAuthority.configFile;
|
|
933
1031
|
if (1) {
|
|
934
|
-
|
|
935
|
-
data = makePath(data.replace(/%%ROOT_FOLDER%%/, caRootDir));
|
|
936
|
-
await fs7.promises.writeFile(caConfigFile, data);
|
|
1032
|
+
await fs7.promises.writeFile(caConfigFile, renderCaConfig(caRootDir));
|
|
937
1033
|
}
|
|
938
|
-
const subjectOpt =
|
|
1034
|
+
const subjectOpt = ["-subj", subject.toString()];
|
|
939
1035
|
const caCommonName = subject.commonName || "NodeOPCUA-CA";
|
|
940
1036
|
setEnv("ALTNAME", `URI:urn:${caCommonName}`);
|
|
941
1037
|
certificateAuthority._wireRevocationEnvVars();
|
|
942
1038
|
const options = { cwd: caRootDir };
|
|
943
1039
|
const configFile = generateStaticConfig("conf/caconfig.cnf", options);
|
|
944
|
-
const configOption =
|
|
1040
|
+
const configOption = ["-config", n4(configFile)];
|
|
945
1041
|
const keySize = certificateAuthority.keySize;
|
|
946
1042
|
const privateKeyFilename = path5.join(caRootDir, "private/cakey.pem");
|
|
947
1043
|
const csrFilename = path5.join(caRootDir, "private/cakey.csr");
|
|
948
1044
|
displayTitle(`Generate the CA private Key - ${keySize}`);
|
|
949
|
-
await
|
|
1045
|
+
const passin = await certificateAuthority._opensslPassin();
|
|
1046
|
+
await generatePrivateKeyFile(privateKeyFilename, keySize, { passphrase: await certificateAuthority._privateKeyPassphrase() });
|
|
1047
|
+
restrictPrivateFilePermissions(privateKeyFilename, 384);
|
|
950
1048
|
displayTitle("Generate a certificate request for the CA key");
|
|
951
1049
|
await execute_openssl(
|
|
952
|
-
|
|
953
|
-
|
|
1050
|
+
[
|
|
1051
|
+
"req",
|
|
1052
|
+
"-new",
|
|
1053
|
+
"-sha256",
|
|
1054
|
+
"-text",
|
|
1055
|
+
"-extensions",
|
|
1056
|
+
"v3_ca_req",
|
|
1057
|
+
...configOption,
|
|
1058
|
+
"-key",
|
|
1059
|
+
n4(privateKeyFilename),
|
|
1060
|
+
"-out",
|
|
1061
|
+
n4(csrFilename),
|
|
1062
|
+
...subjectOpt,
|
|
1063
|
+
...passin.args
|
|
1064
|
+
],
|
|
1065
|
+
{ ...options, env: passin.env }
|
|
954
1066
|
);
|
|
955
1067
|
const issuerCA = certificateAuthority._issuerCA;
|
|
956
1068
|
if (issuerCA) {
|
|
@@ -958,27 +1070,71 @@ async function construct_CertificateAuthority(certificateAuthority) {
|
|
|
958
1070
|
const issuerCert = path5.resolve(issuerCA.caCertificate);
|
|
959
1071
|
const issuerKey = path5.resolve(issuerCA.rootDir, "private/cakey.pem");
|
|
960
1072
|
const issuerSerial = path5.resolve(issuerCA.rootDir, "serial");
|
|
1073
|
+
const issuerPassin = await issuerCA._opensslPassin();
|
|
961
1074
|
await execute_openssl(
|
|
962
|
-
|
|
963
|
-
|
|
1075
|
+
[
|
|
1076
|
+
"x509",
|
|
1077
|
+
"-sha256",
|
|
1078
|
+
"-req",
|
|
1079
|
+
"-days",
|
|
1080
|
+
"3650",
|
|
1081
|
+
"-text",
|
|
1082
|
+
"-extensions",
|
|
1083
|
+
"v3_ca",
|
|
1084
|
+
"-extfile",
|
|
1085
|
+
n4(configFile),
|
|
1086
|
+
"-in",
|
|
1087
|
+
"private/cakey.csr",
|
|
1088
|
+
"-CA",
|
|
1089
|
+
n4(issuerCert),
|
|
1090
|
+
"-CAkey",
|
|
1091
|
+
n4(issuerKey),
|
|
1092
|
+
"-CAserial",
|
|
1093
|
+
n4(issuerSerial),
|
|
1094
|
+
"-out",
|
|
1095
|
+
"public/cacert.pem",
|
|
1096
|
+
...issuerPassin.args
|
|
1097
|
+
],
|
|
1098
|
+
{ ...options, env: issuerPassin.env }
|
|
964
1099
|
);
|
|
965
1100
|
} else {
|
|
966
1101
|
displayTitle("Generate CA Certificate (self-signed)");
|
|
967
1102
|
await execute_openssl(
|
|
968
|
-
|
|
969
|
-
|
|
1103
|
+
[
|
|
1104
|
+
"x509",
|
|
1105
|
+
"-sha256",
|
|
1106
|
+
"-req",
|
|
1107
|
+
"-days",
|
|
1108
|
+
"3650",
|
|
1109
|
+
"-text",
|
|
1110
|
+
"-extensions",
|
|
1111
|
+
"v3_ca",
|
|
1112
|
+
"-extfile",
|
|
1113
|
+
n4(configFile),
|
|
1114
|
+
"-in",
|
|
1115
|
+
"private/cakey.csr",
|
|
1116
|
+
"-signkey",
|
|
1117
|
+
n4(privateKeyFilename),
|
|
1118
|
+
"-out",
|
|
1119
|
+
"public/cacert.pem",
|
|
1120
|
+
...passin.args
|
|
1121
|
+
],
|
|
1122
|
+
{ ...options, env: passin.env }
|
|
970
1123
|
);
|
|
971
1124
|
}
|
|
972
1125
|
displaySubtitle("generate initial CRL (Certificate Revocation List)");
|
|
973
|
-
await regenerateCrl(certificateAuthority.revocationList, configOption, options);
|
|
1126
|
+
await regenerateCrl(certificateAuthority.revocationList, configOption, options, passin);
|
|
974
1127
|
displayTitle("Create Certificate Authority (CA) ---> DONE");
|
|
975
1128
|
}
|
|
976
|
-
async function regenerateCrl(revocationList, configOption, options) {
|
|
1129
|
+
async function regenerateCrl(revocationList, configOption, options, passin) {
|
|
977
1130
|
displaySubtitle("regenerate CRL (Certificate Revocation List)");
|
|
978
|
-
await execute_openssl(
|
|
979
|
-
|
|
1131
|
+
await execute_openssl(["ca", "-gencrl", ...configOption, "-out", "crl/revocation_list.crl", ...passin.args], {
|
|
1132
|
+
...options,
|
|
1133
|
+
env: passin.env
|
|
1134
|
+
});
|
|
1135
|
+
await execute_openssl(["crl", "-in", "crl/revocation_list.crl", "-out", "crl/revocation_list.der", "-outform", "der"], options);
|
|
980
1136
|
displaySubtitle("Display (Certificate Revocation List)");
|
|
981
|
-
await execute_openssl(
|
|
1137
|
+
await execute_openssl(["crl", "-in", n4(revocationList), "-text", "-noout"], options);
|
|
982
1138
|
}
|
|
983
1139
|
function parseOpenSSLDate(dateStr) {
|
|
984
1140
|
const raw = dateStr?.split(",")[0] ?? "";
|
|
@@ -1032,6 +1188,10 @@ var CertificateAuthority = class {
|
|
|
1032
1188
|
_crlDistributionUrl;
|
|
1033
1189
|
_ocspResponderUrl;
|
|
1034
1190
|
_caIssuersUrl;
|
|
1191
|
+
#privateKeyPassphrase;
|
|
1192
|
+
/** resolved once (see `privateKeyPassphrase`); `#passphraseResolved` distinguishes "none" from "not yet" */
|
|
1193
|
+
#resolvedPassphrase;
|
|
1194
|
+
#passphraseResolved = false;
|
|
1035
1195
|
constructor(options) {
|
|
1036
1196
|
assert7(Object.prototype.hasOwnProperty.call(options, "location"));
|
|
1037
1197
|
assert7(Object.prototype.hasOwnProperty.call(options, "keySize"));
|
|
@@ -1039,6 +1199,7 @@ var CertificateAuthority = class {
|
|
|
1039
1199
|
this.keySize = options.keySize || 2048;
|
|
1040
1200
|
this.subject = new Subject2(options.subject || defaultSubject);
|
|
1041
1201
|
this._issuerCA = options.issuerCA;
|
|
1202
|
+
this.#privateKeyPassphrase = options.privateKeyPassphrase;
|
|
1042
1203
|
if (options.crlDistributionUrl !== void 0) {
|
|
1043
1204
|
this.setCrlDistributionUrl(options.crlDistributionUrl);
|
|
1044
1205
|
}
|
|
@@ -1135,6 +1296,69 @@ var CertificateAuthority = class {
|
|
|
1135
1296
|
get configFile() {
|
|
1136
1297
|
return path5.normalize(path5.join(this.rootDir, "./conf/caconfig.cnf"));
|
|
1137
1298
|
}
|
|
1299
|
+
/** Path to the CA private key (`private/cakey.pem`); may be passphrase-encrypted, see {@link getPrivateKey}. */
|
|
1300
|
+
get privateKey() {
|
|
1301
|
+
return path5.join(path5.resolve(this.rootDir), "private/cakey.pem");
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* The CA private key, decrypted with the configured `privateKeyPassphrase`
|
|
1305
|
+
* if it is encrypted. Fails closed (`PrivateKeyPassphraseRequiredError`)
|
|
1306
|
+
* on an encrypted key with no or the wrong passphrase.
|
|
1307
|
+
*/
|
|
1308
|
+
async getPrivateKey() {
|
|
1309
|
+
return readPrivateKey(this.privateKey, await this._privateKeyPassphrase());
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Enable, disable, or rotate the passphrase protecting `private/cakey.pem`
|
|
1313
|
+
* (temp file + atomic rename, temp file removed on failure). Only
|
|
1314
|
+
* rewrites the file: construct a new `CertificateAuthority` with the new
|
|
1315
|
+
* passphrase to continue using it.
|
|
1316
|
+
*/
|
|
1317
|
+
async reencryptPrivateKey(oldPassphrase, newPassphrase) {
|
|
1318
|
+
const oldPass = await resolvePrivateKeyPassphrase(oldPassphrase);
|
|
1319
|
+
const newPass = await resolvePrivateKeyPassphrase(newPassphrase);
|
|
1320
|
+
const key = readPrivateKey(this.privateKey, oldPass);
|
|
1321
|
+
await this.#rewritePrivateKeyFile(key, newPass);
|
|
1322
|
+
}
|
|
1323
|
+
async #rewritePrivateKeyFile(privateKey, passphrase) {
|
|
1324
|
+
const tmpFilename = `${this.privateKey}.${process.pid}-${Date.now()}.tmp`;
|
|
1325
|
+
try {
|
|
1326
|
+
await writePrivateKeyFile(tmpFilename, privateKey, { passphrase });
|
|
1327
|
+
await fs7.promises.rename(tmpFilename, this.privateKey);
|
|
1328
|
+
} finally {
|
|
1329
|
+
await fs7.promises.rm(tmpFilename, { force: true });
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
/** @internal resolve the configured passphrase, at most once per instance */
|
|
1333
|
+
async _privateKeyPassphrase() {
|
|
1334
|
+
if (!this.#passphraseResolved) {
|
|
1335
|
+
this.#resolvedPassphrase = await resolvePrivateKeyPassphrase(this.#privateKeyPassphrase);
|
|
1336
|
+
this.#passphraseResolved = true;
|
|
1337
|
+
}
|
|
1338
|
+
return this.#resolvedPassphrase;
|
|
1339
|
+
}
|
|
1340
|
+
/** @internal `-passin env:` argv + env for every openssl call that loads this CA's key (always emitted, empty when none) */
|
|
1341
|
+
async _opensslPassin() {
|
|
1342
|
+
return passinArg(await this._privateKeyPassphrase());
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* @internal On an existing key: encrypt it in place if a passphrase is
|
|
1346
|
+
* configured and it is still plaintext (secure by default: the option
|
|
1347
|
+
* means "protect this key", not "ignore me"), then read it back so a
|
|
1348
|
+
* wrong or missing passphrase fails initialize() closed rather than the
|
|
1349
|
+
* first signing operation.
|
|
1350
|
+
*/
|
|
1351
|
+
async _ensurePrivateKeyProtection() {
|
|
1352
|
+
if (!fs7.existsSync(this.privateKey)) {
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
if (this.#privateKeyPassphrase !== void 0 && !isEncryptedPrivateKeyFile(this.privateKey)) {
|
|
1356
|
+
warningLog("CertificateAuthority: private key is plaintext but a passphrase is configured; encrypting it in place");
|
|
1357
|
+
const plaintextKey = readPrivateKey(this.privateKey);
|
|
1358
|
+
await this.#rewritePrivateKeyFile(plaintextKey, await this._privateKeyPassphrase());
|
|
1359
|
+
}
|
|
1360
|
+
await this.getPrivateKey();
|
|
1361
|
+
}
|
|
1138
1362
|
/** Path to the CA certificate in PEM format (`public/cacert.pem`). */
|
|
1139
1363
|
get caCertificate() {
|
|
1140
1364
|
return makePath(this.rootDir, "./public/cacert.pem");
|
|
@@ -1592,12 +1816,17 @@ var CertificateAuthority = class {
|
|
|
1592
1816
|
async initializeCSR() {
|
|
1593
1817
|
const caRootDir = path5.resolve(this.rootDir);
|
|
1594
1818
|
mkdirRecursiveSync(caRootDir);
|
|
1595
|
-
for (const dir of ["
|
|
1819
|
+
for (const dir of ["public", "certs", "crl", "conf"]) {
|
|
1596
1820
|
mkdirRecursiveSync(path5.join(caRootDir, dir));
|
|
1597
1821
|
}
|
|
1822
|
+
ensurePrivateDirectory(path5.join(caRootDir, "private"));
|
|
1598
1823
|
const caCertFile = this.caCertificate;
|
|
1599
1824
|
const privateKeyFile = path5.join(caRootDir, "private/cakey.pem");
|
|
1600
1825
|
const csrFile = path5.join(caRootDir, "private/cakey.csr");
|
|
1826
|
+
if (fs7.existsSync(privateKeyFile)) {
|
|
1827
|
+
restrictPrivateFilePermissions(privateKeyFile, 384);
|
|
1828
|
+
await this._ensurePrivateKeyProtection();
|
|
1829
|
+
}
|
|
1601
1830
|
if (fs7.existsSync(caCertFile)) {
|
|
1602
1831
|
const certDer = convertPEMtoDER(readCertificatePEM(caCertFile));
|
|
1603
1832
|
const certInfo = exploreCertificate(certDer);
|
|
@@ -1631,11 +1860,10 @@ var CertificateAuthority = class {
|
|
|
1631
1860
|
await fs7.promises.writeFile(indexFileAttr, "unique_subject = no");
|
|
1632
1861
|
}
|
|
1633
1862
|
const caConfigFile = this.configFile;
|
|
1634
|
-
|
|
1635
|
-
data = makePath(data.replace(/%%ROOT_FOLDER%%/, caRootDir));
|
|
1636
|
-
await fs7.promises.writeFile(caConfigFile, data);
|
|
1863
|
+
await fs7.promises.writeFile(caConfigFile, renderCaConfig(caRootDir));
|
|
1637
1864
|
if (!fs7.existsSync(privateKeyFile)) {
|
|
1638
|
-
await generatePrivateKeyFile(privateKeyFile, this.keySize);
|
|
1865
|
+
await generatePrivateKeyFile(privateKeyFile, this.keySize, { passphrase: await this._privateKeyPassphrase() });
|
|
1866
|
+
restrictPrivateFilePermissions(privateKeyFile, 384);
|
|
1639
1867
|
}
|
|
1640
1868
|
await this._generateCSR(caRootDir, privateKeyFile, csrFile);
|
|
1641
1869
|
return { status: "created", csrPath: csrFile };
|
|
@@ -1677,14 +1905,29 @@ var CertificateAuthority = class {
|
|
|
1677
1905
|
* @internal
|
|
1678
1906
|
*/
|
|
1679
1907
|
async _generateCSR(caRootDir, privateKeyFile, csrFile) {
|
|
1680
|
-
const subjectOpt = ` -subj "${this.subject.toString()}" `;
|
|
1681
1908
|
processAltNames({});
|
|
1682
1909
|
const options = { cwd: caRootDir };
|
|
1683
1910
|
const configFile = generateStaticConfig("conf/caconfig.cnf", options);
|
|
1684
|
-
const
|
|
1911
|
+
const passin = await this._opensslPassin();
|
|
1685
1912
|
await execute_openssl(
|
|
1686
|
-
|
|
1687
|
-
|
|
1913
|
+
[
|
|
1914
|
+
"req",
|
|
1915
|
+
"-new",
|
|
1916
|
+
"-sha256",
|
|
1917
|
+
"-text",
|
|
1918
|
+
"-extensions",
|
|
1919
|
+
"v3_ca_req",
|
|
1920
|
+
"-config",
|
|
1921
|
+
n4(configFile),
|
|
1922
|
+
"-key",
|
|
1923
|
+
n4(privateKeyFile),
|
|
1924
|
+
"-out",
|
|
1925
|
+
n4(csrFile),
|
|
1926
|
+
"-subj",
|
|
1927
|
+
this.subject.toString(),
|
|
1928
|
+
...passin.args
|
|
1929
|
+
],
|
|
1930
|
+
{ ...options, env: passin.env }
|
|
1688
1931
|
);
|
|
1689
1932
|
}
|
|
1690
1933
|
/**
|
|
@@ -1706,7 +1949,6 @@ var CertificateAuthority = class {
|
|
|
1706
1949
|
async installCACertificate(signedCertFile) {
|
|
1707
1950
|
const caRootDir = path5.resolve(this.rootDir);
|
|
1708
1951
|
const caCertFile = this.caCertificate;
|
|
1709
|
-
const privateKeyFile = path5.join(caRootDir, "private/cakey.pem");
|
|
1710
1952
|
const fullPem = await fs7.promises.readFile(signedCertFile, "utf8");
|
|
1711
1953
|
const pemBlocks = fullPem.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g);
|
|
1712
1954
|
if (!pemBlocks || pemBlocks.length === 0) {
|
|
@@ -1717,7 +1959,7 @@ var CertificateAuthority = class {
|
|
|
1717
1959
|
};
|
|
1718
1960
|
}
|
|
1719
1961
|
const certDer = convertPEMtoDER(pemBlocks[0]);
|
|
1720
|
-
const privateKey =
|
|
1962
|
+
const privateKey = await this.getPrivateKey();
|
|
1721
1963
|
if (!certificateMatchesPrivateKey(certDer, privateKey)) {
|
|
1722
1964
|
return {
|
|
1723
1965
|
status: "error",
|
|
@@ -1740,8 +1982,7 @@ var CertificateAuthority = class {
|
|
|
1740
1982
|
}
|
|
1741
1983
|
const options = { cwd: caRootDir };
|
|
1742
1984
|
const configFile = generateStaticConfig("conf/caconfig.cnf", options);
|
|
1743
|
-
|
|
1744
|
-
await regenerateCrl(this.revocationList, configOption, options);
|
|
1985
|
+
await regenerateCrl(this.revocationList, ["-config", n4(configFile)], options, await this._opensslPassin());
|
|
1745
1986
|
return { status: "success" };
|
|
1746
1987
|
}
|
|
1747
1988
|
/**
|
|
@@ -1763,9 +2004,32 @@ var CertificateAuthority = class {
|
|
|
1763
2004
|
this._wireRevocationEnvVars();
|
|
1764
2005
|
const configFile = generateStaticConfig("conf/caconfig.cnf", options);
|
|
1765
2006
|
const validity = params.validity ?? 3650;
|
|
2007
|
+
const passin = await this._opensslPassin();
|
|
1766
2008
|
await execute_openssl(
|
|
1767
|
-
|
|
1768
|
-
|
|
2009
|
+
[
|
|
2010
|
+
"x509",
|
|
2011
|
+
"-sha256",
|
|
2012
|
+
"-req",
|
|
2013
|
+
"-days",
|
|
2014
|
+
String(validity),
|
|
2015
|
+
"-text",
|
|
2016
|
+
"-extensions",
|
|
2017
|
+
"v3_ca",
|
|
2018
|
+
"-extfile",
|
|
2019
|
+
n4(configFile),
|
|
2020
|
+
"-in",
|
|
2021
|
+
n4(csrFile),
|
|
2022
|
+
"-CA",
|
|
2023
|
+
n4(this.caCertificate),
|
|
2024
|
+
"-CAkey",
|
|
2025
|
+
n4(path5.join(caRootDir, "private/cakey.pem")),
|
|
2026
|
+
"-CAserial",
|
|
2027
|
+
n4(path5.join(caRootDir, "serial")),
|
|
2028
|
+
"-out",
|
|
2029
|
+
n4(certFile),
|
|
2030
|
+
...passin.args
|
|
2031
|
+
],
|
|
2032
|
+
{ ...options, env: passin.env }
|
|
1769
2033
|
);
|
|
1770
2034
|
await this.constructCertificateChain(certFile);
|
|
1771
2035
|
}
|
|
@@ -1827,23 +2091,36 @@ var CertificateAuthority = class {
|
|
|
1827
2091
|
cwd: this.rootDir,
|
|
1828
2092
|
openssl_conf: makePath(configFile)
|
|
1829
2093
|
};
|
|
1830
|
-
const configOption = "";
|
|
1831
2094
|
const subject = params.subject ? new Subject2(params.subject).toString() : "";
|
|
1832
|
-
const subjectOptions = subject && subject.length > 1 ?
|
|
2095
|
+
const subjectOptions = subject && subject.length > 1 ? ["-subj", subject] : [];
|
|
1833
2096
|
displaySubtitle("- the certificate signing request");
|
|
1834
2097
|
await execute_openssl(
|
|
1835
|
-
"req
|
|
2098
|
+
["req", "-new", "-sha256", "-text", ...subjectOptions, "-batch", "-key", n4(privateKey), "-out", n4(csrFile)],
|
|
1836
2099
|
options
|
|
1837
2100
|
);
|
|
1838
2101
|
displaySubtitle("- creating the self-signed certificate");
|
|
1839
2102
|
await execute_openssl(
|
|
1840
|
-
|
|
2103
|
+
[
|
|
2104
|
+
"ca",
|
|
2105
|
+
"-selfsign",
|
|
2106
|
+
"-keyfile",
|
|
2107
|
+
n4(privateKey),
|
|
2108
|
+
"-startdate",
|
|
2109
|
+
x509Date(params.startDate),
|
|
2110
|
+
"-enddate",
|
|
2111
|
+
x509Date(params.endDate),
|
|
2112
|
+
"-batch",
|
|
2113
|
+
"-out",
|
|
2114
|
+
n4(certificateFile),
|
|
2115
|
+
"-in",
|
|
2116
|
+
n4(csrFile)
|
|
2117
|
+
],
|
|
1841
2118
|
options
|
|
1842
2119
|
);
|
|
1843
2120
|
displaySubtitle("- dump the certificate for a check");
|
|
1844
|
-
await execute_openssl(
|
|
2121
|
+
await execute_openssl(["x509", "-in", n4(certificateFile), "-dates", "-fingerprint", "-purpose", "-noout"], {});
|
|
1845
2122
|
displaySubtitle("- verify self-signed certificate");
|
|
1846
|
-
await execute_openssl_no_failure(
|
|
2123
|
+
await execute_openssl_no_failure(["verify", "-verbose", "-CAfile", n4(certificateFile), n4(certificateFile)], options);
|
|
1847
2124
|
await fs7.promises.unlink(csrFile);
|
|
1848
2125
|
}
|
|
1849
2126
|
/**
|
|
@@ -1873,22 +2150,41 @@ var CertificateAuthority = class {
|
|
|
1873
2150
|
setEnv("ALTNAME", "");
|
|
1874
2151
|
const randomFile = path5.join(this.rootDir, "random.rnd");
|
|
1875
2152
|
setEnv("RANDFILE", randomFile);
|
|
1876
|
-
const configOption =
|
|
2153
|
+
const configOption = ["-config", n4(configFile)];
|
|
1877
2154
|
const reason = params.reason || "keyCompromise";
|
|
1878
2155
|
assert7(crlReasons.indexOf(reason) >= 0);
|
|
1879
2156
|
displayTitle(`Revoking certificate ${certificate}`);
|
|
1880
2157
|
displaySubtitle("Revoke certificate");
|
|
1881
|
-
|
|
1882
|
-
await
|
|
2158
|
+
const passin = await this._opensslPassin();
|
|
2159
|
+
await execute_openssl_no_failure(
|
|
2160
|
+
["ca", "-verbose", ...configOption, "-revoke", certificate, "-crl_reason", reason, ...passin.args],
|
|
2161
|
+
{ ...options, env: passin.env }
|
|
2162
|
+
);
|
|
2163
|
+
await regenerateCrl(this.revocationList, configOption, options, passin);
|
|
1883
2164
|
displaySubtitle("Verify that certificate is revoked");
|
|
1884
2165
|
await execute_openssl_no_failure(
|
|
1885
|
-
|
|
2166
|
+
[
|
|
2167
|
+
"verify",
|
|
2168
|
+
"-verbose",
|
|
2169
|
+
"-CRLfile",
|
|
2170
|
+
n4(this.revocationList),
|
|
2171
|
+
"-CAfile",
|
|
2172
|
+
n4(this.caCertificate),
|
|
2173
|
+
"-crl_check",
|
|
2174
|
+
n4(certificate)
|
|
2175
|
+
],
|
|
1886
2176
|
options
|
|
1887
2177
|
);
|
|
1888
2178
|
displaySubtitle("Produce CRL in DER form ");
|
|
1889
|
-
await execute_openssl(
|
|
2179
|
+
await execute_openssl(
|
|
2180
|
+
["crl", "-in", n4(this.revocationList), "-out", "crl/revocation_list.der", "-outform", "der"],
|
|
2181
|
+
options
|
|
2182
|
+
);
|
|
1890
2183
|
displaySubtitle("Produce CRL in PEM form ");
|
|
1891
|
-
await execute_openssl(
|
|
2184
|
+
await execute_openssl(
|
|
2185
|
+
["crl", "-in", n4(this.revocationList), "-out", "crl/revocation_list.pem", "-outform", "pem", "-text"],
|
|
2186
|
+
options
|
|
2187
|
+
);
|
|
1892
2188
|
}
|
|
1893
2189
|
/**
|
|
1894
2190
|
* Sign a Certificate Signing Request (CSR) with this CA.
|
|
@@ -1930,13 +2226,27 @@ var CertificateAuthority = class {
|
|
|
1930
2226
|
this._wireRevocationEnvVars();
|
|
1931
2227
|
const configFile = generateStaticConfig("conf/caconfig.cnf", options);
|
|
1932
2228
|
displaySubtitle("- then we ask the authority to sign the certificate signing request");
|
|
1933
|
-
const
|
|
2229
|
+
const passin = await this._opensslPassin();
|
|
1934
2230
|
await execute_openssl(
|
|
1935
|
-
|
|
1936
|
-
|
|
2231
|
+
[
|
|
2232
|
+
"ca",
|
|
2233
|
+
"-config",
|
|
2234
|
+
configFile,
|
|
2235
|
+
"-startdate",
|
|
2236
|
+
x509Date(params1.startDate),
|
|
2237
|
+
"-enddate",
|
|
2238
|
+
x509Date(params1.endDate),
|
|
2239
|
+
"-batch",
|
|
2240
|
+
"-out",
|
|
2241
|
+
n4(certificate),
|
|
2242
|
+
"-in",
|
|
2243
|
+
n4(certificateSigningRequestFilename),
|
|
2244
|
+
...passin.args
|
|
2245
|
+
],
|
|
2246
|
+
{ ...options, env: passin.env }
|
|
1937
2247
|
);
|
|
1938
2248
|
displaySubtitle("- dump the certificate for a check");
|
|
1939
|
-
await execute_openssl(
|
|
2249
|
+
await execute_openssl(["x509", "-in", n4(certificate), "-dates", "-fingerprint", "-purpose", "-noout"], options);
|
|
1940
2250
|
displaySubtitle("- construct CA certificate with CRL");
|
|
1941
2251
|
await this.constructCACertificateWithCRL();
|
|
1942
2252
|
displaySubtitle("- construct certificate chain");
|
|
@@ -1956,10 +2266,8 @@ var CertificateAuthority = class {
|
|
|
1956
2266
|
const options = { cwd: this.rootDir };
|
|
1957
2267
|
const configFile = generateStaticConfig("conf/caconfig.cnf", options);
|
|
1958
2268
|
setEnv("OPENSSL_CONF", makePath(configFile));
|
|
1959
|
-
const _configOption = ` -config ${configFile}`;
|
|
1960
|
-
_configOption;
|
|
1961
2269
|
await execute_openssl_no_failure(
|
|
1962
|
-
|
|
2270
|
+
["verify", "-verbose", "-CAfile", n4(this.caCertificateWithCrl), n4(certificate)],
|
|
1963
2271
|
options
|
|
1964
2272
|
);
|
|
1965
2273
|
}
|
|
@@ -1982,27 +2290,30 @@ import {
|
|
|
1982
2290
|
readCertificateChain,
|
|
1983
2291
|
readCertificateChainAsync,
|
|
1984
2292
|
readCertificateRevocationList,
|
|
2293
|
+
readPrivateKey as readPrivateKey2,
|
|
1985
2294
|
split_der,
|
|
1986
2295
|
toPem as toPem2,
|
|
1987
|
-
verifyCertificateSignature
|
|
2296
|
+
verifyCertificateSignature,
|
|
2297
|
+
writePrivateKeyFile as writePrivateKeyFile2
|
|
1988
2298
|
} from "node-opcua-crypto";
|
|
1989
2299
|
|
|
1990
2300
|
// packages/node-opcua-pki/lib/toolbox/without_openssl/create_certificate_signing_request.ts
|
|
1991
2301
|
import assert8 from "assert";
|
|
1992
2302
|
import fs8 from "fs";
|
|
1993
|
-
import { createCertificateSigningRequest, pemToPrivateKey, Subject as Subject3 } from "node-opcua-crypto";
|
|
2303
|
+
import { coercePrivateKeyPem, createCertificateSigningRequest, pemToPrivateKey, Subject as Subject3 } from "node-opcua-crypto";
|
|
1994
2304
|
async function createCertificateSigningRequestAsync(certificateSigningRequestFilename, params) {
|
|
1995
2305
|
assert8(params);
|
|
1996
2306
|
assert8(params.rootDir);
|
|
1997
2307
|
assert8(params.configFile);
|
|
1998
2308
|
assert8(params.privateKey);
|
|
1999
|
-
|
|
2000
|
-
|
|
2309
|
+
if (typeof params.privateKey === "string") {
|
|
2310
|
+
assert8(fs8.existsSync(params.privateKey), `Private key must exist${params.privateKey}`);
|
|
2311
|
+
}
|
|
2001
2312
|
assert8(fs8.existsSync(params.rootDir), "RootDir key must exist");
|
|
2002
2313
|
assert8(typeof certificateSigningRequestFilename === "string");
|
|
2003
2314
|
const subject = params.subject ? new Subject3(params.subject).toString() : void 0;
|
|
2004
2315
|
displaySubtitle("- Creating a Certificate Signing Request with subtile");
|
|
2005
|
-
const privateKeyPem = await fs8.promises.readFile(params.privateKey, "utf-8");
|
|
2316
|
+
const privateKeyPem = typeof params.privateKey === "string" ? await fs8.promises.readFile(params.privateKey, "utf-8") : coercePrivateKeyPem(params.privateKey);
|
|
2006
2317
|
const privateKey = await pemToPrivateKey(privateKeyPem);
|
|
2007
2318
|
const { csr } = await createCertificateSigningRequest({
|
|
2008
2319
|
privateKey,
|
|
@@ -2013,7 +2324,7 @@ async function createCertificateSigningRequestAsync(certificateSigningRequestFil
|
|
|
2013
2324
|
purpose: params.purpose
|
|
2014
2325
|
});
|
|
2015
2326
|
await fs8.promises.writeFile(certificateSigningRequestFilename, csr, "utf-8");
|
|
2016
|
-
display(`- privateKey ${params.privateKey}`);
|
|
2327
|
+
display(`- privateKey ${typeof params.privateKey === "string" ? params.privateKey : "<in-memory>"}`);
|
|
2017
2328
|
display(`- certificateSigningRequestFilename ${certificateSigningRequestFilename}`);
|
|
2018
2329
|
}
|
|
2019
2330
|
|
|
@@ -2022,6 +2333,7 @@ import assert9 from "assert";
|
|
|
2022
2333
|
import fs9 from "fs";
|
|
2023
2334
|
import {
|
|
2024
2335
|
CertificatePurpose as CertificatePurpose2,
|
|
2336
|
+
coercePrivateKeyPem as coercePrivateKeyPem2,
|
|
2025
2337
|
createSelfSignedCertificate as createSelfSignedCertificate1,
|
|
2026
2338
|
pemToPrivateKey as pemToPrivateKey2,
|
|
2027
2339
|
Subject as Subject4
|
|
@@ -2031,7 +2343,9 @@ async function createSelfSignedCertificateAsync(certificate, params) {
|
|
|
2031
2343
|
assert9(params.purpose, "Please provide a Certificate Purpose");
|
|
2032
2344
|
assert9(fs9.existsSync(params.configFile));
|
|
2033
2345
|
assert9(fs9.existsSync(params.rootDir));
|
|
2034
|
-
|
|
2346
|
+
if (typeof params.privateKey === "string") {
|
|
2347
|
+
assert9(fs9.existsSync(params.privateKey));
|
|
2348
|
+
}
|
|
2035
2349
|
if (!params.subject) {
|
|
2036
2350
|
throw Error("Missing subject");
|
|
2037
2351
|
}
|
|
@@ -2043,7 +2357,7 @@ async function createSelfSignedCertificateAsync(certificate, params) {
|
|
|
2043
2357
|
subject = subject.toString();
|
|
2044
2358
|
const purpose = params.purpose;
|
|
2045
2359
|
displayTitle("Generate a certificate request");
|
|
2046
|
-
const privateKeyPem = await fs9.promises.readFile(params.privateKey, "utf-8");
|
|
2360
|
+
const privateKeyPem = typeof params.privateKey === "string" ? await fs9.promises.readFile(params.privateKey, "utf-8") : coercePrivateKeyPem2(params.privateKey);
|
|
2047
2361
|
const privateKey = await pemToPrivateKey2(privateKeyPem);
|
|
2048
2362
|
const { cert } = await createSelfSignedCertificate1({
|
|
2049
2363
|
privateKey,
|
|
@@ -2310,6 +2624,15 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
2310
2624
|
#initializingPromise;
|
|
2311
2625
|
#addCertValidation;
|
|
2312
2626
|
#disableFileWatchers;
|
|
2627
|
+
#privateKeyPassphrase;
|
|
2628
|
+
#privateKeyProvider;
|
|
2629
|
+
/**
|
|
2630
|
+
* The on-disk key, decrypted once and kept for the instance's lifetime,
|
|
2631
|
+
* so the passphrase (or its resolver function) is consulted at most
|
|
2632
|
+
* once. Cleared by `dispose()`. Not used when a provider is configured:
|
|
2633
|
+
* the provider is the authority on the current key.
|
|
2634
|
+
*/
|
|
2635
|
+
#cachedPrivateKey;
|
|
2313
2636
|
#thumbs = {
|
|
2314
2637
|
rejected: /* @__PURE__ */ new Map(),
|
|
2315
2638
|
trusted: /* @__PURE__ */ new Map(),
|
|
@@ -2344,6 +2667,8 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
2344
2667
|
maxChainLength: v.maxChainLength ?? 5
|
|
2345
2668
|
};
|
|
2346
2669
|
this.#disableFileWatchers = options.disableFileWatchers ?? process.env.OPCUA_PKI_DISABLE_FILE_WATCHERS === "true";
|
|
2670
|
+
this.#privateKeyPassphrase = options.privateKeyPassphrase;
|
|
2671
|
+
this.#privateKeyProvider = options.privateKeyProvider;
|
|
2347
2672
|
mkdirRecursiveSync(options.location);
|
|
2348
2673
|
if (!fs10.existsSync(this.#location)) {
|
|
2349
2674
|
throw new Error(`CertificateManager cannot access location ${this.#location}`);
|
|
@@ -2357,10 +2682,108 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
2357
2682
|
get rootDir() {
|
|
2358
2683
|
return this.#location;
|
|
2359
2684
|
}
|
|
2360
|
-
/**
|
|
2685
|
+
/**
|
|
2686
|
+
* Path to the private key file (`own/private/private_key.pem`).
|
|
2687
|
+
*
|
|
2688
|
+
* Kept for backward compatibility with code that reads the key
|
|
2689
|
+
* directly from disk. When a passphrase or a `privateKeyProvider` is
|
|
2690
|
+
* configured, prefer {@link getPrivateKey} instead — this getter still
|
|
2691
|
+
* returns the on-disk path even if a provider is configured (there may
|
|
2692
|
+
* be no meaningful file in that case).
|
|
2693
|
+
*/
|
|
2361
2694
|
get privateKey() {
|
|
2362
2695
|
return path6.join(this.rootDir, "own/private/private_key.pem");
|
|
2363
2696
|
}
|
|
2697
|
+
/**
|
|
2698
|
+
* Resolve the private key: from `privateKeyProvider` if configured,
|
|
2699
|
+
* otherwise from disk (decrypting with `privateKeyPassphrase` if the
|
|
2700
|
+
* key is encrypted). Fails closed — throws
|
|
2701
|
+
* `PrivateKeyPassphraseRequiredError` — if the on-disk key is encrypted
|
|
2702
|
+
* and no passphrase is configured, or if the wrong passphrase is
|
|
2703
|
+
* configured.
|
|
2704
|
+
*
|
|
2705
|
+
* The on-disk key is read and decrypted once and then cached for the
|
|
2706
|
+
* lifetime of this instance, so a `privateKeyPassphrase` function is
|
|
2707
|
+
* called at most once (concurrent first calls share the same read). A
|
|
2708
|
+
* failed read is not cached, so a caller can fix the passphrase and
|
|
2709
|
+
* retry. A `privateKeyProvider` is consulted on every call: it is the
|
|
2710
|
+
* authority on what the current key is.
|
|
2711
|
+
*/
|
|
2712
|
+
async getPrivateKey() {
|
|
2713
|
+
if (this.#privateKeyProvider) {
|
|
2714
|
+
return await this.#privateKeyProvider.getPrivateKey();
|
|
2715
|
+
}
|
|
2716
|
+
if (this.#cachedPrivateKey) {
|
|
2717
|
+
return this.#cachedPrivateKey;
|
|
2718
|
+
}
|
|
2719
|
+
if (!this.#privateKeyPromise) {
|
|
2720
|
+
this.#privateKeyPromise = (async () => {
|
|
2721
|
+
const passphrase = await resolvePrivateKeyPassphrase(this.#privateKeyPassphrase);
|
|
2722
|
+
return readPrivateKey2(this.privateKey, passphrase);
|
|
2723
|
+
})().then(
|
|
2724
|
+
(key) => {
|
|
2725
|
+
this.#cachedPrivateKey = key;
|
|
2726
|
+
return key;
|
|
2727
|
+
},
|
|
2728
|
+
(err) => {
|
|
2729
|
+
this.#privateKeyPromise = void 0;
|
|
2730
|
+
throw err;
|
|
2731
|
+
}
|
|
2732
|
+
);
|
|
2733
|
+
}
|
|
2734
|
+
return await this.#privateKeyPromise;
|
|
2735
|
+
}
|
|
2736
|
+
/** In-flight first read of the on-disk key, so concurrent callers share one passphrase resolution. */
|
|
2737
|
+
#privateKeyPromise;
|
|
2738
|
+
/**
|
|
2739
|
+
* Enable, disable, or rotate the passphrase protecting the on-disk
|
|
2740
|
+
* private key: decrypt with `oldPassphrase` (omit if the key is
|
|
2741
|
+
* currently unencrypted), then write back encrypted with
|
|
2742
|
+
* `newPassphrase` (omit to leave it unencrypted). The write goes to a
|
|
2743
|
+
* temporary file in the same directory and is atomically renamed into
|
|
2744
|
+
* place, so a crash mid-rotation cannot leave a partially-written key;
|
|
2745
|
+
* the temporary file is removed if anything fails, so a rotation *to*
|
|
2746
|
+
* plaintext can never leave a stray cleartext copy behind. Runs under
|
|
2747
|
+
* the same lock as `initialize()`.
|
|
2748
|
+
*
|
|
2749
|
+
* This only rewrites the on-disk file — it does not update this
|
|
2750
|
+
* instance's own `privateKeyPassphrase` (set at construction), and it
|
|
2751
|
+
* drops this instance's cached key so that disk stays the source of
|
|
2752
|
+
* truth. Construct a new `CertificateManager` with the new passphrase to
|
|
2753
|
+
* continue using it afterward.
|
|
2754
|
+
*
|
|
2755
|
+
* Not supported when a `privateKeyProvider` is configured (there is no
|
|
2756
|
+
* disk file for this method to rewrite).
|
|
2757
|
+
*/
|
|
2758
|
+
async reencryptPrivateKey(oldPassphrase, newPassphrase) {
|
|
2759
|
+
if (this.#privateKeyProvider) {
|
|
2760
|
+
throw new Error("reencryptPrivateKey: not supported when a privateKeyProvider is configured");
|
|
2761
|
+
}
|
|
2762
|
+
const oldPass = await resolvePrivateKeyPassphrase(oldPassphrase);
|
|
2763
|
+
const newPass = await resolvePrivateKeyPassphrase(newPassphrase);
|
|
2764
|
+
await this.withLock2(async () => {
|
|
2765
|
+
const privateKey = readPrivateKey2(this.privateKey, oldPass);
|
|
2766
|
+
await this.#rewritePrivateKeyFile(privateKey, newPass);
|
|
2767
|
+
});
|
|
2768
|
+
this.#cachedPrivateKey = void 0;
|
|
2769
|
+
this.#privateKeyPromise = void 0;
|
|
2770
|
+
}
|
|
2771
|
+
/**
|
|
2772
|
+
* Atomically replace the on-disk private key with `privateKey`, written
|
|
2773
|
+
* as PKCS#8 (encrypted with `passphrase` if given). Temp file next to the
|
|
2774
|
+
* target, `0600`, renamed into place; the temp file is unlinked on any
|
|
2775
|
+
* failure so no partial or cleartext copy can be left behind.
|
|
2776
|
+
* Caller must hold the lock.
|
|
2777
|
+
*/
|
|
2778
|
+
async #rewritePrivateKeyFile(privateKey, passphrase) {
|
|
2779
|
+
const tmpFilename = `${this.privateKey}.${process.pid}-${Date.now()}.tmp`;
|
|
2780
|
+
try {
|
|
2781
|
+
await writePrivateKeyFile2(tmpFilename, privateKey, { passphrase });
|
|
2782
|
+
await fs10.promises.rename(tmpFilename, this.privateKey);
|
|
2783
|
+
} finally {
|
|
2784
|
+
await fs10.promises.rm(tmpFilename, { force: true });
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2364
2787
|
/** Path to the OpenSSL random seed file. */
|
|
2365
2788
|
get randomFile() {
|
|
2366
2789
|
return path6.join(this.rootDir, "./random.rnd");
|
|
@@ -2660,19 +3083,24 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
2660
3083
|
}
|
|
2661
3084
|
this.state = 1 /* Initializing */;
|
|
2662
3085
|
this.#initializingPromise = this.#initialize();
|
|
2663
|
-
|
|
3086
|
+
try {
|
|
3087
|
+
await this.#initializingPromise;
|
|
3088
|
+
} catch (err) {
|
|
3089
|
+
this.#initializingPromise = void 0;
|
|
3090
|
+
this.state = 0 /* Uninitialized */;
|
|
3091
|
+
throw err;
|
|
3092
|
+
}
|
|
2664
3093
|
this.#initializingPromise = void 0;
|
|
2665
3094
|
this.state = 2 /* Initialized */;
|
|
2666
3095
|
_CertificateManager.#activeInstances.add(this);
|
|
2667
3096
|
_CertificateManager.#installProcessCleanup();
|
|
2668
3097
|
}
|
|
2669
3098
|
async #initialize() {
|
|
2670
|
-
this.state = 1 /* Initializing */;
|
|
2671
3099
|
const pkiDir = this.#location;
|
|
2672
3100
|
mkdirRecursiveSync(pkiDir);
|
|
2673
3101
|
mkdirRecursiveSync(path6.join(pkiDir, "own"));
|
|
2674
3102
|
mkdirRecursiveSync(path6.join(pkiDir, "own/certs"));
|
|
2675
|
-
|
|
3103
|
+
ensurePrivateDirectory(path6.join(pkiDir, "own/private"));
|
|
2676
3104
|
mkdirRecursiveSync(path6.join(pkiDir, "rejected"));
|
|
2677
3105
|
mkdirRecursiveSync(path6.join(pkiDir, "trusted"));
|
|
2678
3106
|
mkdirRecursiveSync(path6.join(pkiDir, "trusted/certs"));
|
|
@@ -2680,7 +3108,10 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
2680
3108
|
mkdirRecursiveSync(path6.join(pkiDir, "issuers"));
|
|
2681
3109
|
mkdirRecursiveSync(path6.join(pkiDir, "issuers/certs"));
|
|
2682
3110
|
mkdirRecursiveSync(path6.join(pkiDir, "issuers/crl"));
|
|
2683
|
-
|
|
3111
|
+
const ownsDiskKey = !this.#privateKeyProvider;
|
|
3112
|
+
const needsKeyGeneration = ownsDiskKey && !fs10.existsSync(this.privateKey);
|
|
3113
|
+
const needsKeyEncryption = ownsDiskKey && !needsKeyGeneration && this.#privateKeyPassphrase !== void 0 && !isEncryptedPrivateKeyFile(this.privateKey);
|
|
3114
|
+
if (!fs10.existsSync(this.configFile) || needsKeyGeneration || needsKeyEncryption) {
|
|
2684
3115
|
return await this.withLock2(async () => {
|
|
2685
3116
|
if (this.state === 3 /* Disposing */ || this.state === 4 /* Disposed */) {
|
|
2686
3117
|
return;
|
|
@@ -2688,15 +3119,29 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
2688
3119
|
if (!fs10.existsSync(this.configFile)) {
|
|
2689
3120
|
fs10.writeFileSync(this.configFile, configurationFileSimpleTemplate2);
|
|
2690
3121
|
}
|
|
2691
|
-
if (!fs10.existsSync(this.privateKey)) {
|
|
3122
|
+
if (ownsDiskKey && !fs10.existsSync(this.privateKey)) {
|
|
2692
3123
|
debugLog("generating private key ...");
|
|
2693
|
-
await
|
|
2694
|
-
await this
|
|
2695
|
-
|
|
2696
|
-
|
|
3124
|
+
const passphrase = await resolvePrivateKeyPassphrase(this.#privateKeyPassphrase);
|
|
3125
|
+
await generatePrivateKeyFile2(this.privateKey, this.keySize, { passphrase });
|
|
3126
|
+
this.#cachedPrivateKey = readPrivateKey2(this.privateKey, passphrase);
|
|
3127
|
+
} else if (ownsDiskKey && this.#privateKeyPassphrase !== void 0 && !isEncryptedPrivateKeyFile(this.privateKey)) {
|
|
3128
|
+
warningLog("initialize: private key is plaintext but a passphrase is configured; encrypting it in place");
|
|
3129
|
+
const passphrase = await resolvePrivateKeyPassphrase(this.#privateKeyPassphrase);
|
|
3130
|
+
const plaintextKey = readPrivateKey2(this.privateKey);
|
|
3131
|
+
await this.#rewritePrivateKeyFile(plaintextKey, passphrase);
|
|
3132
|
+
this.#cachedPrivateKey = plaintextKey;
|
|
3133
|
+
}
|
|
3134
|
+
if (ownsDiskKey) {
|
|
3135
|
+
restrictPrivateFilePermissions(this.privateKey, 384);
|
|
2697
3136
|
}
|
|
3137
|
+
await this.getPrivateKey();
|
|
3138
|
+
await this.#readCertificates();
|
|
2698
3139
|
});
|
|
2699
3140
|
} else {
|
|
3141
|
+
if (ownsDiskKey) {
|
|
3142
|
+
restrictPrivateFilePermissions(this.privateKey, 384);
|
|
3143
|
+
}
|
|
3144
|
+
await this.getPrivateKey();
|
|
2700
3145
|
await this.#readCertificates();
|
|
2701
3146
|
}
|
|
2702
3147
|
}
|
|
@@ -2732,6 +3177,8 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
2732
3177
|
this.#watchers.splice(0);
|
|
2733
3178
|
} finally {
|
|
2734
3179
|
this.state = 4 /* Disposed */;
|
|
3180
|
+
this.#cachedPrivateKey = void 0;
|
|
3181
|
+
this.#privateKeyPromise = void 0;
|
|
2735
3182
|
_CertificateManager.#activeInstances.delete(this);
|
|
2736
3183
|
}
|
|
2737
3184
|
}
|
|
@@ -2778,16 +3225,18 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
2778
3225
|
if (typeof params.applicationUri !== "string") {
|
|
2779
3226
|
throw new Error("createSelfSignedCertificate: expecting applicationUri to be a string");
|
|
2780
3227
|
}
|
|
2781
|
-
if (!fs10.existsSync(this.privateKey)) {
|
|
3228
|
+
if (!this.#privateKeyProvider && !fs10.existsSync(this.privateKey)) {
|
|
2782
3229
|
throw new Error(`Cannot find private key ${this.privateKey}`);
|
|
2783
3230
|
}
|
|
2784
3231
|
let certificateFilename = path6.join(this.rootDir, "own/certs/self_signed_certificate.pem");
|
|
2785
3232
|
certificateFilename = params.outputFile || certificateFilename;
|
|
2786
|
-
const _params =
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
3233
|
+
const _params = {
|
|
3234
|
+
...params,
|
|
3235
|
+
rootDir: this.rootDir,
|
|
3236
|
+
configFile: this.configFile,
|
|
3237
|
+
privateKey: await this.getPrivateKey(),
|
|
3238
|
+
subject: params.subject || "CN=FIXME"
|
|
3239
|
+
};
|
|
2791
3240
|
await this.withLock2(async () => {
|
|
2792
3241
|
await createSelfSignedCertificate(certificateFilename, _params);
|
|
2793
3242
|
});
|
|
@@ -2806,13 +3255,15 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
2806
3255
|
if (!params) {
|
|
2807
3256
|
throw new Error("params is required");
|
|
2808
3257
|
}
|
|
2809
|
-
|
|
2810
|
-
if (Object.prototype.hasOwnProperty.call(_params, "rootDir")) {
|
|
3258
|
+
if (Object.prototype.hasOwnProperty.call(params, "rootDir")) {
|
|
2811
3259
|
throw new Error("rootDir should not be specified ");
|
|
2812
3260
|
}
|
|
2813
|
-
_params
|
|
2814
|
-
|
|
2815
|
-
|
|
3261
|
+
const _params = {
|
|
3262
|
+
...params,
|
|
3263
|
+
rootDir: path6.resolve(this.rootDir),
|
|
3264
|
+
configFile: path6.resolve(this.configFile),
|
|
3265
|
+
privateKey: await this.getPrivateKey()
|
|
3266
|
+
};
|
|
2816
3267
|
return await this.withLock2(async () => {
|
|
2817
3268
|
const now = /* @__PURE__ */ new Date();
|
|
2818
3269
|
const today = `${now.toISOString().slice(0, 10)}_${now.getTime()}`;
|
|
@@ -3662,6 +4113,7 @@ export {
|
|
|
3662
4113
|
CertificateManager,
|
|
3663
4114
|
CertificateManagerState,
|
|
3664
4115
|
ChainCompletionStatus,
|
|
4116
|
+
PrivateKeyPassphraseRequiredError,
|
|
3665
4117
|
Subject,
|
|
3666
4118
|
VerificationStatus,
|
|
3667
4119
|
adjustApplicationUri,
|
|
@@ -3680,6 +4132,7 @@ export {
|
|
|
3680
4132
|
isIssuer,
|
|
3681
4133
|
isRootIssuer,
|
|
3682
4134
|
makeFingerprint,
|
|
3683
|
-
quote
|
|
4135
|
+
quote,
|
|
4136
|
+
resolvePrivateKeyPassphrase
|
|
3684
4137
|
};
|
|
3685
4138
|
//# sourceMappingURL=index.mjs.map
|