node-opcua-pki 6.20.0 → 6.22.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 +1627 -821
- package/dist/bin/pki.mjs.map +1 -1
- package/dist/chunk-EQ4DGI4M.mjs +1917 -0
- package/dist/chunk-EQ4DGI4M.mjs.map +1 -0
- package/dist/core-BwqSJPHc.d.mts +1041 -0
- package/dist/core-BwqSJPHc.d.ts +1041 -0
- package/dist/core.d.mts +2 -0
- package/dist/core.d.ts +2 -0
- package/dist/core.js +1902 -0
- package/dist/core.js.map +1 -0
- package/dist/core.mjs +21 -0
- package/dist/core.mjs.map +1 -0
- package/dist/index.d.mts +136 -833
- package/dist/index.d.ts +136 -833
- package/dist/index.js +1890 -1086
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +890 -1922
- package/dist/index.mjs.map +1 -1
- package/package.json +14 -3
package/dist/index.mjs
CHANGED
|
@@ -1,111 +1,37 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
CertificateAuthorityCore,
|
|
3
|
+
NativeCaBackend,
|
|
4
|
+
Subject,
|
|
5
|
+
adjustApplicationUri,
|
|
6
|
+
adjustDate,
|
|
7
|
+
debugLog,
|
|
8
|
+
display,
|
|
9
|
+
displayError,
|
|
10
|
+
displaySubtitle,
|
|
11
|
+
displayTitle,
|
|
12
|
+
doDebug,
|
|
13
|
+
ensurePrivateDirectory,
|
|
14
|
+
g_config,
|
|
15
|
+
isEncryptedPrivateKeyFile,
|
|
16
|
+
isOpaqueSigner,
|
|
17
|
+
makePath,
|
|
18
|
+
mkdirRecursiveSync,
|
|
19
|
+
quote,
|
|
20
|
+
resolvePrivateKeyPassphrase,
|
|
21
|
+
restrictPrivateFilePermissions,
|
|
22
|
+
warningLog
|
|
23
|
+
} from "./chunk-EQ4DGI4M.mjs";
|
|
3
24
|
|
|
4
|
-
// packages/node-opcua-pki/lib/
|
|
5
|
-
import assert7 from "assert";
|
|
6
|
-
import fs7 from "fs";
|
|
7
|
-
import os3 from "os";
|
|
8
|
-
import path5 from "path";
|
|
9
|
-
import chalk5 from "chalk";
|
|
25
|
+
// packages/node-opcua-pki/lib/index.ts
|
|
10
26
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
convertPEMtoDER,
|
|
14
|
-
exploreCertificate,
|
|
15
|
-
exploreCertificateSigningRequest,
|
|
16
|
-
generatePrivateKeyFile,
|
|
17
|
-
readCertificatePEM,
|
|
18
|
-
readCertificateSigningRequest,
|
|
19
|
-
readPrivateKey,
|
|
20
|
-
Subject as Subject2,
|
|
21
|
-
toPem,
|
|
22
|
-
writePrivateKeyFile
|
|
27
|
+
PrivateKeyPassphraseRequiredError,
|
|
28
|
+
PrivateKeyUnavailableError as PrivateKeyUnavailableError2
|
|
23
29
|
} from "node-opcua-crypto";
|
|
24
30
|
|
|
25
|
-
// packages/node-opcua-pki/lib/
|
|
26
|
-
import assert3 from "assert";
|
|
31
|
+
// packages/node-opcua-pki/lib/ca/backends/openssl_ca_backend.ts
|
|
27
32
|
import fs4 from "fs";
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
import assert from "assert";
|
|
31
|
-
import fs from "fs";
|
|
32
|
-
import path from "path";
|
|
33
|
-
import chalk from "chalk";
|
|
34
|
-
|
|
35
|
-
// packages/node-opcua-pki/lib/toolbox/config.ts
|
|
36
|
-
var g_config = {
|
|
37
|
-
opensslVersion: "unset",
|
|
38
|
-
silent: process.env.VERBOSE ? !process.env.VERBOSE : true,
|
|
39
|
-
force: false
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
// packages/node-opcua-pki/lib/toolbox/debug.ts
|
|
43
|
-
var doDebug = process.env.NODEOPCUAPKIDEBUG || false;
|
|
44
|
-
var displayError = true;
|
|
45
|
-
var displayDebug = !!process.env.NODEOPCUAPKIDEBUG || false;
|
|
46
|
-
function debugLog(...args) {
|
|
47
|
-
if (displayDebug) {
|
|
48
|
-
console.log.apply(null, args);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
function warningLog(...args) {
|
|
52
|
-
console.log.apply(null, args);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// packages/node-opcua-pki/lib/toolbox/common2.ts
|
|
56
|
-
function certificateFileExist(certificateFile) {
|
|
57
|
-
if (fs.existsSync(certificateFile) && !g_config.force) {
|
|
58
|
-
warningLog(
|
|
59
|
-
chalk.yellow(" certificate ") + chalk.cyan(certificateFile) + chalk.yellow(" already exists => do not overwrite")
|
|
60
|
-
);
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
return true;
|
|
64
|
-
}
|
|
65
|
-
function mkdirRecursiveSync(folder) {
|
|
66
|
-
if (!fs.existsSync(folder)) {
|
|
67
|
-
debugLog(chalk.white(" .. constructing "), folder);
|
|
68
|
-
fs.mkdirSync(folder, { recursive: true });
|
|
69
|
-
}
|
|
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
|
-
}
|
|
90
|
-
function makePath(folderName, filename) {
|
|
91
|
-
let s;
|
|
92
|
-
if (filename) {
|
|
93
|
-
s = path.join(path.normalize(folderName), filename);
|
|
94
|
-
} else {
|
|
95
|
-
assert(folderName);
|
|
96
|
-
s = folderName;
|
|
97
|
-
}
|
|
98
|
-
s = s.replace(/\\/g, "/");
|
|
99
|
-
return s;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// packages/node-opcua-pki/lib/toolbox/with_openssl/execute_openssl.ts
|
|
103
|
-
import assert2 from "assert";
|
|
104
|
-
import child_process2 from "child_process";
|
|
105
|
-
import fs3 from "fs";
|
|
106
|
-
import os2 from "os";
|
|
107
|
-
import byline2 from "byline";
|
|
108
|
-
import chalk3 from "chalk";
|
|
33
|
+
import path3 from "path";
|
|
34
|
+
import { Subject as Subject2 } from "node-opcua-crypto";
|
|
109
35
|
|
|
110
36
|
// packages/node-opcua-pki/lib/toolbox/with_openssl/_env.ts
|
|
111
37
|
var SAFE_ENV_PASSTHROUGH = /* @__PURE__ */ new Set([
|
|
@@ -179,31 +105,30 @@ function getEnvironmentVarNames() {
|
|
|
179
105
|
return { key: varName, pattern: `\\$ENV\\:\\:${varName}` };
|
|
180
106
|
});
|
|
181
107
|
}
|
|
182
|
-
function
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
subjectAltName,
|
|
189
|
-
params.dns.map((d) => `DNS:${d}`)
|
|
190
|
-
);
|
|
191
|
-
subjectAltName = [].concat(
|
|
192
|
-
subjectAltName,
|
|
193
|
-
params.ip.map((d) => `IP:${d}`)
|
|
194
|
-
);
|
|
195
|
-
const subjectAltNameString = subjectAltName.join(", ");
|
|
196
|
-
setEnv("ALTNAME", subjectAltNameString);
|
|
108
|
+
function buildSubjectAltNameString(params) {
|
|
109
|
+
return [
|
|
110
|
+
`URI:${params.applicationUri}`,
|
|
111
|
+
...(params.dns ?? []).map((d) => `DNS:${d}`),
|
|
112
|
+
...(params.ip ?? []).map((d) => `IP:${d}`)
|
|
113
|
+
].join(", ");
|
|
197
114
|
}
|
|
198
115
|
|
|
116
|
+
// packages/node-opcua-pki/lib/toolbox/with_openssl/execute_openssl.ts
|
|
117
|
+
import assert from "assert";
|
|
118
|
+
import child_process2 from "child_process";
|
|
119
|
+
import fs2 from "fs";
|
|
120
|
+
import os2 from "os";
|
|
121
|
+
import byline2 from "byline";
|
|
122
|
+
import chalk2 from "chalk";
|
|
123
|
+
|
|
199
124
|
// packages/node-opcua-pki/lib/toolbox/with_openssl/install_prerequisite.ts
|
|
200
125
|
import child_process from "child_process";
|
|
201
|
-
import
|
|
126
|
+
import fs from "fs";
|
|
202
127
|
import os from "os";
|
|
203
|
-
import
|
|
128
|
+
import path from "path";
|
|
204
129
|
import { pipeline } from "stream/promises";
|
|
205
130
|
import byline from "byline";
|
|
206
|
-
import
|
|
131
|
+
import chalk from "chalk";
|
|
207
132
|
import yauzl from "yauzl";
|
|
208
133
|
var doDebug2 = process.env.NODEOPCUAPKIDEBUG || false;
|
|
209
134
|
async function execute(file, args, cwd) {
|
|
@@ -222,7 +147,7 @@ async function execute(file, args, cwd) {
|
|
|
222
147
|
output += `${line}
|
|
223
148
|
`;
|
|
224
149
|
if (doDebug2) {
|
|
225
|
-
process.stdout.write(` stdout ${
|
|
150
|
+
process.stdout.write(` stdout ${chalk.yellow(line)}
|
|
226
151
|
`);
|
|
227
152
|
}
|
|
228
153
|
});
|
|
@@ -242,8 +167,8 @@ async function getopensslExecPath() {
|
|
|
242
167
|
const exitCode = result1?.exitCode;
|
|
243
168
|
const output = result1?.output;
|
|
244
169
|
if (exitCode !== 0) {
|
|
245
|
-
warningLog(
|
|
246
|
-
warningLog(
|
|
170
|
+
warningLog(chalk.yellow(" it seems that ") + chalk.cyan("openssl") + chalk.yellow(" is not installed on your computer "));
|
|
171
|
+
warningLog(chalk.yellow("Please install it before running this programs"));
|
|
247
172
|
throw new Error("Cannot find openssl");
|
|
248
173
|
}
|
|
249
174
|
const opensslExecPath = output.replace(/\n\r/g, "").trim();
|
|
@@ -252,7 +177,7 @@ async function getopensslExecPath() {
|
|
|
252
177
|
async function check_system_openssl_version() {
|
|
253
178
|
const opensslExecPath = await getopensslExecPath();
|
|
254
179
|
if (doDebug2) {
|
|
255
|
-
warningLog(` OpenSSL found in : ${
|
|
180
|
+
warningLog(` OpenSSL found in : ${chalk.yellow(opensslExecPath)}`);
|
|
256
181
|
}
|
|
257
182
|
const result = await execute(opensslExecPath, ["version"]);
|
|
258
183
|
const exitCode = result?.exitCode;
|
|
@@ -260,35 +185,35 @@ async function check_system_openssl_version() {
|
|
|
260
185
|
const version = output.trim();
|
|
261
186
|
const versionOK = exitCode === 0 && is_expected_openssl_version(version);
|
|
262
187
|
if (!versionOK) {
|
|
263
|
-
let message =
|
|
188
|
+
let message = chalk.whiteBright("Warning !!!!!!!!!!!! ") + "\nyour version of openssl is " + version + ". It doesn't match the expected version";
|
|
264
189
|
if (process.platform === "darwin") {
|
|
265
|
-
message +=
|
|
190
|
+
message += chalk.cyan("\nplease refer to :") + chalk.yellow(" https://github.com/node-opcua/node-opcua/wiki/installing-node-opcua-or-node-red-on-MacOS");
|
|
266
191
|
}
|
|
267
192
|
console.log(message);
|
|
268
193
|
}
|
|
269
194
|
return output;
|
|
270
195
|
}
|
|
271
196
|
async function install_and_check_win32_openssl_version() {
|
|
272
|
-
const downloadFolder =
|
|
197
|
+
const downloadFolder = path.join(os.tmpdir(), ".");
|
|
273
198
|
function get_openssl_folder_win32() {
|
|
274
199
|
if (process.env.LOCALAPPDATA) {
|
|
275
|
-
const userProgramFolder =
|
|
276
|
-
if (
|
|
277
|
-
return
|
|
200
|
+
const userProgramFolder = path.join(process.env.LOCALAPPDATA, "Programs");
|
|
201
|
+
if (fs.existsSync(userProgramFolder)) {
|
|
202
|
+
return path.join(userProgramFolder, "openssl");
|
|
278
203
|
}
|
|
279
204
|
}
|
|
280
|
-
return
|
|
205
|
+
return path.join(process.cwd(), "openssl");
|
|
281
206
|
}
|
|
282
207
|
function get_openssl_exec_path_win32() {
|
|
283
208
|
const opensslFolder2 = get_openssl_folder_win32();
|
|
284
|
-
return
|
|
209
|
+
return path.join(opensslFolder2, "openssl.exe");
|
|
285
210
|
}
|
|
286
211
|
async function check_openssl_win32() {
|
|
287
212
|
const opensslExecPath2 = get_openssl_exec_path_win32();
|
|
288
|
-
const exists =
|
|
213
|
+
const exists = fs.existsSync(opensslExecPath2);
|
|
289
214
|
if (!exists) {
|
|
290
215
|
warningLog("checking presence of ", opensslExecPath2);
|
|
291
|
-
warningLog(
|
|
216
|
+
warningLog(chalk.red(" cannot find file ") + opensslExecPath2);
|
|
292
217
|
return {
|
|
293
218
|
opensslOk: false,
|
|
294
219
|
version: `cannot find file ${opensslExecPath2}`
|
|
@@ -313,18 +238,18 @@ async function install_and_check_win32_openssl_version() {
|
|
|
313
238
|
return void 0;
|
|
314
239
|
}
|
|
315
240
|
const opensslPath2 = result.output.split(/\r?\n/)[0].trim();
|
|
316
|
-
if (!opensslPath2 || !
|
|
241
|
+
if (!opensslPath2 || !fs.existsSync(opensslPath2)) {
|
|
317
242
|
return void 0;
|
|
318
243
|
}
|
|
319
244
|
const versionResult = await execute(opensslPath2, ["version"]);
|
|
320
245
|
const version = versionResult.output.trim();
|
|
321
246
|
if (versionResult.exitCode === 0 && is_expected_openssl_version(version)) {
|
|
322
247
|
warningLog(
|
|
323
|
-
|
|
248
|
+
chalk.green("Using system OpenSSL: ") + chalk.cyan(version) + chalk.green(" at ") + chalk.cyan(opensslPath2)
|
|
324
249
|
);
|
|
325
250
|
return opensslPath2;
|
|
326
251
|
}
|
|
327
|
-
warningLog(
|
|
252
|
+
warningLog(chalk.yellow("System OpenSSL found but version not accepted: ") + version);
|
|
328
253
|
return void 0;
|
|
329
254
|
} catch (_err) {
|
|
330
255
|
return void 0;
|
|
@@ -344,9 +269,9 @@ async function install_and_check_win32_openssl_version() {
|
|
|
344
269
|
}
|
|
345
270
|
async function download_openssl() {
|
|
346
271
|
const url = win32or64() === 64 ? "https://github.com/node-opcua/node-opcua-pki/releases/download/2.14.2/openssl-1.0.2u-x64_86-win64.zip" : "https://github.com/node-opcua/node-opcua-pki/releases/download/2.14.2/openssl-1.0.2u-i386-win32.zip";
|
|
347
|
-
const outputFilename =
|
|
348
|
-
warningLog(`downloading ${
|
|
349
|
-
if (
|
|
272
|
+
const outputFilename = path.join(downloadFolder, path.basename(url));
|
|
273
|
+
warningLog(`downloading ${chalk.yellow(url)} to ${outputFilename}`);
|
|
274
|
+
if (fs.existsSync(outputFilename)) {
|
|
350
275
|
return { downloadedFile: outputFilename };
|
|
351
276
|
}
|
|
352
277
|
const response = await fetch(url, { redirect: "follow" });
|
|
@@ -356,7 +281,7 @@ async function install_and_check_win32_openssl_version() {
|
|
|
356
281
|
const contentLength = parseInt(response.headers.get("content-length") || "0", 10);
|
|
357
282
|
let downloaded = 0;
|
|
358
283
|
let lastPercent = -1;
|
|
359
|
-
const fileStream =
|
|
284
|
+
const fileStream = fs.createWriteStream(outputFilename);
|
|
360
285
|
const body = response.body;
|
|
361
286
|
body.on("data", (chunk) => {
|
|
362
287
|
downloaded += chunk.length;
|
|
@@ -369,12 +294,12 @@ async function install_and_check_win32_openssl_version() {
|
|
|
369
294
|
}
|
|
370
295
|
});
|
|
371
296
|
await pipeline(body, fileStream);
|
|
372
|
-
const stat =
|
|
297
|
+
const stat = fs.statSync(outputFilename);
|
|
373
298
|
if (stat.size === 0) {
|
|
374
|
-
|
|
299
|
+
fs.unlinkSync(outputFilename);
|
|
375
300
|
throw new Error(`Downloaded file is empty: ${outputFilename}`);
|
|
376
301
|
}
|
|
377
|
-
warningLog(
|
|
302
|
+
warningLog(chalk.green("Download complete: ") + `${stat.size} bytes`);
|
|
378
303
|
return { downloadedFile: outputFilename };
|
|
379
304
|
}
|
|
380
305
|
async function unzip_openssl(zipFilename) {
|
|
@@ -411,11 +336,11 @@ async function install_and_check_win32_openssl_version() {
|
|
|
411
336
|
if (err) {
|
|
412
337
|
return reject(err);
|
|
413
338
|
}
|
|
414
|
-
const file =
|
|
339
|
+
const file = path.join(opensslFolder2, entry.fileName);
|
|
415
340
|
if (doDebug2) {
|
|
416
341
|
warningLog(" unzipping :", file);
|
|
417
342
|
}
|
|
418
|
-
const writeStream =
|
|
343
|
+
const writeStream = fs.createWriteStream(file, "binary");
|
|
419
344
|
readStream?.pipe(writeStream);
|
|
420
345
|
writeStream.on("close", () => {
|
|
421
346
|
zipFile.readEntry();
|
|
@@ -430,29 +355,29 @@ async function install_and_check_win32_openssl_version() {
|
|
|
430
355
|
}
|
|
431
356
|
const opensslFolder = get_openssl_folder_win32();
|
|
432
357
|
const opensslExecPath = get_openssl_exec_path_win32();
|
|
433
|
-
if (!
|
|
358
|
+
if (!fs.existsSync(opensslFolder)) {
|
|
434
359
|
if (doDebug2) {
|
|
435
360
|
warningLog("creating openssl_folder", opensslFolder);
|
|
436
361
|
}
|
|
437
|
-
|
|
362
|
+
fs.mkdirSync(opensslFolder);
|
|
438
363
|
}
|
|
439
364
|
const { opensslOk, version: _version } = await check_openssl_win32();
|
|
440
365
|
if (!opensslOk) {
|
|
441
|
-
warningLog(
|
|
366
|
+
warningLog(chalk.yellow("openssl seems to be missing and need to be installed"));
|
|
442
367
|
const { downloadedFile } = await download_openssl();
|
|
443
368
|
if (doDebug2) {
|
|
444
|
-
warningLog("deflating ",
|
|
369
|
+
warningLog("deflating ", chalk.yellow(downloadedFile));
|
|
445
370
|
}
|
|
446
371
|
await unzip_openssl(downloadedFile);
|
|
447
|
-
const opensslExists = !!
|
|
372
|
+
const opensslExists = !!fs.existsSync(opensslExecPath);
|
|
448
373
|
if (doDebug2) {
|
|
449
|
-
warningLog("verifying ", opensslExists, opensslExists ?
|
|
374
|
+
warningLog("verifying ", opensslExists, opensslExists ? chalk.green("OK ") : chalk.red(" Error"), opensslExecPath);
|
|
450
375
|
}
|
|
451
376
|
const _opensslExecPath2 = await check_openssl_win32();
|
|
452
377
|
return opensslExecPath;
|
|
453
378
|
} else {
|
|
454
379
|
if (doDebug2) {
|
|
455
|
-
warningLog(
|
|
380
|
+
warningLog(chalk.green("openssl is already installed and have the expected version."));
|
|
456
381
|
}
|
|
457
382
|
return opensslExecPath;
|
|
458
383
|
}
|
|
@@ -467,7 +392,7 @@ async function install_prerequisite() {
|
|
|
467
392
|
async function get_openssl_exec_path() {
|
|
468
393
|
if (process.platform === "win32") {
|
|
469
394
|
const opensslExecPath = await install_prerequisite();
|
|
470
|
-
if (!
|
|
395
|
+
if (!fs.existsSync(opensslExecPath)) {
|
|
471
396
|
throw new Error(`internal error cannot find ${opensslExecPath}`);
|
|
472
397
|
}
|
|
473
398
|
return opensslExecPath;
|
|
@@ -480,13 +405,9 @@ async function get_openssl_exec_path() {
|
|
|
480
405
|
var opensslPath;
|
|
481
406
|
var n = makePath;
|
|
482
407
|
var PASSIN_ENV_VAR = "NODE_OPCUA_PKI_OPENSSL_PASSIN";
|
|
483
|
-
var PASSOUT_ENV_VAR = "NODE_OPCUA_PKI_OPENSSL_PASSOUT";
|
|
484
408
|
function passinArg(passphrase = "") {
|
|
485
409
|
return { args: ["-passin", `env:${PASSIN_ENV_VAR}`], env: { [PASSIN_ENV_VAR]: passphrase } };
|
|
486
410
|
}
|
|
487
|
-
function passoutArg(passphrase = "") {
|
|
488
|
-
return { args: ["-passout", `env:${PASSOUT_ENV_VAR}`], env: { [PASSOUT_ENV_VAR]: passphrase } };
|
|
489
|
-
}
|
|
490
411
|
function renderForDisplay(file, args) {
|
|
491
412
|
return [file, ...args].map((a) => a === "" || /[\s"'`$\\]/.test(a) ? JSON.stringify(a) : a).join(" ");
|
|
492
413
|
}
|
|
@@ -494,7 +415,7 @@ async function execute2(file, args, options) {
|
|
|
494
415
|
const from = new Error();
|
|
495
416
|
options.cwd = options.cwd || process.cwd();
|
|
496
417
|
if (!g_config.silent) {
|
|
497
|
-
warningLog(
|
|
418
|
+
warningLog(chalk2.cyan(" CWD "), options.cwd);
|
|
498
419
|
}
|
|
499
420
|
const outputs = [];
|
|
500
421
|
const errorOutputs = [];
|
|
@@ -507,12 +428,12 @@ async function execute2(file, args, options) {
|
|
|
507
428
|
stdio: ["ignore", "pipe", "pipe"]
|
|
508
429
|
});
|
|
509
430
|
const fail = (message) => {
|
|
510
|
-
if (!options.hideErrorMessage) {
|
|
431
|
+
if (!options.hideErrorMessage && !g_config.silent) {
|
|
511
432
|
const fence = "###########################################";
|
|
512
|
-
console.error(
|
|
513
|
-
console.error(
|
|
514
|
-
console.error(
|
|
515
|
-
console.error(
|
|
433
|
+
console.error(chalk2.bgWhiteBright.redBright(`${fence} OPENSSL ERROR ${fence}`));
|
|
434
|
+
console.error(chalk2.bgWhiteBright.redBright(`CWD = ${options.cwd}`));
|
|
435
|
+
console.error(chalk2.bgWhiteBright.redBright(message));
|
|
436
|
+
console.error(chalk2.bgWhiteBright.redBright(`${fence} OPENSSL ERROR ${fence}`));
|
|
516
437
|
console.error(from.stack);
|
|
517
438
|
}
|
|
518
439
|
reject(new Error(message));
|
|
@@ -538,7 +459,7 @@ ${errorOutputs.join("")}(${why})`);
|
|
|
538
459
|
stream2.on("data", (line) => {
|
|
539
460
|
line = line.toString();
|
|
540
461
|
if (doDebug) {
|
|
541
|
-
process.stdout.write(`${
|
|
462
|
+
process.stdout.write(`${chalk2.white(" stdout ") + chalk2.whiteBright(line)}
|
|
542
463
|
`);
|
|
543
464
|
}
|
|
544
465
|
});
|
|
@@ -551,7 +472,7 @@ ${errorOutputs.join("")}(${why})`);
|
|
|
551
472
|
errorOutputs.push(`${line}
|
|
552
473
|
`);
|
|
553
474
|
if (!g_config.silent && displayError) {
|
|
554
|
-
process.stdout.write(`${
|
|
475
|
+
process.stdout.write(`${chalk2.white(" stderr ") + chalk2.red(line)}
|
|
555
476
|
`);
|
|
556
477
|
}
|
|
557
478
|
});
|
|
@@ -586,154 +507,26 @@ function getTempFolder() {
|
|
|
586
507
|
async function execute_openssl(args, options) {
|
|
587
508
|
debugLog("execute_openssl", args, redactEnvForLog(options));
|
|
588
509
|
const empty_config_file = n(getTempFolder(), "empty_config.cnf");
|
|
589
|
-
if (!
|
|
590
|
-
await
|
|
510
|
+
if (!fs2.existsSync(empty_config_file)) {
|
|
511
|
+
await fs2.promises.writeFile(empty_config_file, "# empty config file");
|
|
591
512
|
}
|
|
592
513
|
options = options || {};
|
|
593
514
|
options.openssl_conf = options.openssl_conf || empty_config_file;
|
|
594
|
-
|
|
515
|
+
assert(options.openssl_conf);
|
|
595
516
|
setEnv("OPENSSL_CONF", options.openssl_conf);
|
|
596
517
|
if (!g_config.silent) {
|
|
597
|
-
warningLog(
|
|
598
|
-
warningLog(
|
|
599
|
-
warningLog(
|
|
518
|
+
warningLog(chalk2.cyan(" OPENSSL_CONF"), process.env.OPENSSL_CONF);
|
|
519
|
+
warningLog(chalk2.cyan(" RANDFILE "), process.env.RANDFILE);
|
|
520
|
+
warningLog(chalk2.cyan(" CMD "), chalk2.cyanBright(renderForDisplay("openssl", args)));
|
|
600
521
|
}
|
|
601
522
|
await ensure_openssl_installed();
|
|
602
523
|
return await execute2(opensslPath, args, options);
|
|
603
524
|
}
|
|
604
525
|
|
|
605
|
-
// packages/node-opcua-pki/lib/pki/toolbox_pfx.ts
|
|
606
|
-
var n2 = makePath;
|
|
607
|
-
async function createPFX(options) {
|
|
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)];
|
|
612
|
-
if (caCertificateFiles) {
|
|
613
|
-
for (const caFile of caCertificateFiles) {
|
|
614
|
-
assert3(fs4.existsSync(caFile), `CA certificate file does not exist: ${caFile}`);
|
|
615
|
-
args.push("-certfile", n2(caFile));
|
|
616
|
-
}
|
|
617
|
-
}
|
|
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 } });
|
|
622
|
-
}
|
|
623
|
-
async function extractCertificateFromPFX(options) {
|
|
624
|
-
const { pfxFile, passphrase = "" } = options;
|
|
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
|
-
});
|
|
630
|
-
}
|
|
631
|
-
async function extractPrivateKeyFromPFX(options) {
|
|
632
|
-
const { pfxFile, passphrase = "" } = options;
|
|
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 });
|
|
636
|
-
}
|
|
637
|
-
async function extractCACertificatesFromPFX(options) {
|
|
638
|
-
const { pfxFile, passphrase = "" } = options;
|
|
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
|
-
});
|
|
644
|
-
}
|
|
645
|
-
async function extractAllFromPFX(options) {
|
|
646
|
-
const [certificate, privateKey, caCertificates] = await Promise.all([
|
|
647
|
-
extractCertificateFromPFX(options),
|
|
648
|
-
extractPrivateKeyFromPFX(options),
|
|
649
|
-
extractCACertificatesFromPFX(options)
|
|
650
|
-
]);
|
|
651
|
-
return { certificate, privateKey, caCertificates };
|
|
652
|
-
}
|
|
653
|
-
async function convertPFXtoPEM(pfxFile, pemFile, passphrase = "") {
|
|
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 });
|
|
657
|
-
}
|
|
658
|
-
async function dumpPFX(pfxFile, passphrase = "") {
|
|
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
|
-
}
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
// packages/node-opcua-pki/lib/toolbox/display.ts
|
|
701
|
-
import chalk4 from "chalk";
|
|
702
|
-
function displayTitle(str) {
|
|
703
|
-
if (!g_config.silent) {
|
|
704
|
-
warningLog("");
|
|
705
|
-
warningLog(chalk4.yellowBright(str));
|
|
706
|
-
warningLog(chalk4.yellow(new Array(str.length + 1).join("=")), "\n");
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
function displaySubtitle(str) {
|
|
710
|
-
if (!g_config.silent) {
|
|
711
|
-
warningLog("");
|
|
712
|
-
warningLog(` ${chalk4.yellowBright(str)}`);
|
|
713
|
-
warningLog(` ${chalk4.white(new Array(str.length + 1).join("-"))}`, "\n");
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
function display(str) {
|
|
717
|
-
if (!g_config.silent) {
|
|
718
|
-
warningLog(` ${str}`);
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
// packages/node-opcua-pki/lib/toolbox/with_openssl/index.ts
|
|
723
|
-
import exp from "constants";
|
|
724
|
-
|
|
725
|
-
// packages/node-opcua-pki/lib/toolbox/with_openssl/create_certificate_signing_request.ts
|
|
726
|
-
import assert6 from "assert";
|
|
727
|
-
import fs6 from "fs";
|
|
728
|
-
import path4 from "path";
|
|
729
|
-
|
|
730
|
-
// packages/node-opcua-pki/lib/misc/subject.ts
|
|
731
|
-
import { Subject } from "node-opcua-crypto";
|
|
732
|
-
|
|
733
526
|
// packages/node-opcua-pki/lib/toolbox/with_openssl/toolbox.ts
|
|
734
|
-
import
|
|
735
|
-
import
|
|
736
|
-
import
|
|
527
|
+
import assert2 from "assert";
|
|
528
|
+
import fs3 from "fs";
|
|
529
|
+
import path2 from "path";
|
|
737
530
|
function openssl_require2DigitYearInDate() {
|
|
738
531
|
if (!g_config.opensslVersion) {
|
|
739
532
|
throw new Error(
|
|
@@ -744,29 +537,40 @@ function openssl_require2DigitYearInDate() {
|
|
|
744
537
|
}
|
|
745
538
|
g_config.opensslVersion = "";
|
|
746
539
|
var _counter = 0;
|
|
747
|
-
function stripConditionalBlocks(template) {
|
|
540
|
+
function stripConditionalBlocks(template, envOverrides) {
|
|
748
541
|
return template.replace(/\{\{#([A-Z_][A-Z0-9_]*)\}\}([\s\S]*?)\{\{\/\1\}\}\r?\n?/g, (_match, key, content) => {
|
|
749
|
-
const keep = hasEnv(key) && getEnv(key) !== "";
|
|
542
|
+
const keep = envOverrides && Object.prototype.hasOwnProperty.call(envOverrides, key) ? envOverrides[key] !== "" : hasEnv(key) && getEnv(key) !== "";
|
|
750
543
|
return keep ? content : "";
|
|
751
544
|
});
|
|
752
545
|
}
|
|
753
|
-
function generateStaticConfig(configPath, options) {
|
|
546
|
+
function generateStaticConfig(configPath, options, envOverrides) {
|
|
754
547
|
const prePath = options?.cwd || "";
|
|
755
|
-
const originalFilename = !
|
|
756
|
-
let staticConfig =
|
|
757
|
-
staticConfig = stripConditionalBlocks(staticConfig);
|
|
548
|
+
const originalFilename = !path2.isAbsolute(configPath) ? path2.join(prePath, configPath) : configPath;
|
|
549
|
+
let staticConfig = fs3.readFileSync(originalFilename, { encoding: "utf8" });
|
|
550
|
+
staticConfig = stripConditionalBlocks(staticConfig, envOverrides);
|
|
758
551
|
for (const envVar of getEnvironmentVarNames()) {
|
|
759
|
-
|
|
552
|
+
if (envOverrides && Object.prototype.hasOwnProperty.call(envOverrides, envVar.key)) {
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
staticConfig = staticConfig.replace(new RegExp(envVar.pattern, "gi"), () => getEnv(envVar.key));
|
|
556
|
+
}
|
|
557
|
+
if (envOverrides) {
|
|
558
|
+
for (const [key, value] of Object.entries(envOverrides)) {
|
|
559
|
+
staticConfig = staticConfig.replace(new RegExp(`\\$ENV\\:\\:${key}`, "gi"), () => value);
|
|
560
|
+
}
|
|
760
561
|
}
|
|
761
562
|
const staticConfigPath = `${configPath}.${process.pid}-${_counter++}.tmp`;
|
|
762
|
-
const temporaryConfigPath = !
|
|
763
|
-
|
|
563
|
+
const temporaryConfigPath = !path2.isAbsolute(configPath) ? path2.join(prePath, staticConfigPath) : staticConfigPath;
|
|
564
|
+
fs3.writeFileSync(temporaryConfigPath, staticConfig);
|
|
764
565
|
if (options?.cwd) {
|
|
765
|
-
return
|
|
566
|
+
return path2.relative(options.cwd, temporaryConfigPath);
|
|
766
567
|
} else {
|
|
767
568
|
return temporaryConfigPath;
|
|
768
569
|
}
|
|
769
570
|
}
|
|
571
|
+
async function cleanupStaticConfig(configFile, options) {
|
|
572
|
+
await fs3.promises.rm(path2.resolve(options?.cwd ?? "", configFile), { force: true });
|
|
573
|
+
}
|
|
770
574
|
function x509Date(date) {
|
|
771
575
|
date = date || /* @__PURE__ */ new Date();
|
|
772
576
|
const Y = date.getUTCFullYear();
|
|
@@ -785,1536 +589,433 @@ function x509Date(date) {
|
|
|
785
589
|
}
|
|
786
590
|
}
|
|
787
591
|
|
|
788
|
-
// packages/node-opcua-pki/lib/
|
|
789
|
-
var
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
assert6(fs6.existsSync(params.privateKey), `Private key must exist${params.privateKey}`);
|
|
798
|
-
assert6(fs6.existsSync(params.rootDir), "RootDir key must exist");
|
|
799
|
-
assert6(typeof certificateSigningRequestFilename === "string");
|
|
800
|
-
processAltNames(params);
|
|
801
|
-
const configFile = generateStaticConfig(params.configFile, { cwd: params.rootDir });
|
|
802
|
-
const options = { cwd: params.rootDir, openssl_conf: path4.relative(params.rootDir, configFile) };
|
|
803
|
-
const subject = params.subject ? new Subject(params.subject).toString() : void 0;
|
|
804
|
-
displaySubtitle("- Creating a Certificate Signing Request with openssl");
|
|
805
|
-
await execute_openssl(
|
|
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
|
-
],
|
|
820
|
-
options
|
|
821
|
-
);
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
// packages/node-opcua-pki/lib/pki/templates/simple_config_template.cnf.ts
|
|
825
|
-
var config = '##################################################################################################\n## SIMPLE OPENSSL CONFIG FILE FOR SELF-SIGNED CERTIFICATE GENERATION\n################################################################################################################\n\ndistinguished_name = req_distinguished_name\ndefault_md = sha1\n\ndefault_md = sha256 # The default digest algorithm\n\n[ v3_ca ]\nsubjectKeyIdentifier = hash\nauthorityKeyIdentifier = keyid:always,issuer:always\n\n# authorityKeyIdentifier = keyid\nbasicConstraints = CA:TRUE\nkeyUsage = critical, cRLSign, keyCertSign\nnsComment = "Self-signed Certificate for CA generated by Node-OPCUA Certificate utility"\n#nsCertType = sslCA, emailCA\n#subjectAltName = email:copy\n#issuerAltName = issuer:copy\n#obj = DER:02:03\n# crlDistributionPoints = @crl_info\n# [ crl_info ]\n# URI.0 = http://localhost:8900/crl.pem\nsubjectAltName = $ENV::ALTNAME\n\n[ req ]\ndays = 390\nreq_extensions = v3_req\nx509_extensions = v3_ca\n\n[v3_req]\nbasicConstraints = CA:false\nkeyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment\nsubjectAltName = $ENV::ALTNAME\n\n[ v3_ca_signed]\nsubjectKeyIdentifier = hash\nauthorityKeyIdentifier = keyid,issuer\nbasicConstraints = critical, CA:FALSE\nkeyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment\nextendedKeyUsage = clientAuth,serverAuth \nnsComment = "certificate generated by Node-OPCUA Certificate utility and signed by a CA"\nsubjectAltName = $ENV::ALTNAME\n[ v3_selfsigned]\nsubjectKeyIdentifier = hash\nauthorityKeyIdentifier = keyid,issuer\nbasicConstraints = critical, CA:FALSE\nkeyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment\nextendedKeyUsage = clientAuth,serverAuth \nnsComment = "Self-signed certificate generated by Node-OPCUA Certificate utility"\nsubjectAltName = $ENV::ALTNAME\n[ req_distinguished_name ]\ncountryName = Country Name (2 letter code)\ncountryName_default = FR\ncountryName_min = 2\ncountryName_max = 2\n# stateOrProvinceName = State or Province Name (full name)\n# stateOrProvinceName_default = Ile de France\n# localityName = Locality Name (city, district)\n# localityName_default = Paris\norganizationName = Organization Name (company)\norganizationName_default = NodeOPCUA\n# organizationalUnitName = Organizational Unit Name (department, division)\n# organizationalUnitName_default = R&D\ncommonName = Common Name (hostname, FQDN, IP, or your name)\ncommonName_max = 256\ncommonName_default = NodeOPCUA\n# emailAddress = Email Address\n# emailAddress_max = 40\n# emailAddress_default = node-opcua (at) node-opcua (dot) com\nsubjectAltName = $ENV::ALTNAME';
|
|
826
|
-
var simple_config_template_cnf_default = config;
|
|
827
|
-
|
|
828
|
-
// packages/node-opcua-pki/lib/ca/templates/ca_config_template.cnf.ts
|
|
829
|
-
var config2 = `#.........DO NOT MODIFY BY HAND .........................
|
|
830
|
-
[ ca ]
|
|
831
|
-
default_ca = CA_default
|
|
832
|
-
[ CA_default ]
|
|
833
|
-
dir = "%%ROOT_FOLDER%%" # the main CA folder (quoted: see renderCaConfig)
|
|
834
|
-
certs = $dir/certs # where to store certificates
|
|
835
|
-
new_certs_dir = $dir/certs #
|
|
836
|
-
database = $dir/index.txt # the certificate database
|
|
837
|
-
serial = $dir/serial # the serial number counter
|
|
838
|
-
certificate = $dir/public/cacert.pem # The root CA certificate
|
|
839
|
-
private_key = $dir/private/cakey.pem # the CA private key
|
|
840
|
-
x509_extensions = usr_cert #
|
|
841
|
-
default_days = 3650 # default validity : 10 years
|
|
842
|
-
|
|
843
|
-
# default_md = sha1
|
|
844
|
-
|
|
845
|
-
default_md = sha256 # The default digest algorithm
|
|
846
|
-
|
|
847
|
-
preserve = no
|
|
848
|
-
policy = policy_match
|
|
849
|
-
# randfile = $dir/random.rnd
|
|
850
|
-
# default_startdate = YYMMDDHHMMSSZ
|
|
851
|
-
# default_enddate = YYMMDDHHMMSSZ
|
|
852
|
-
crl_dir = $dir/crl
|
|
853
|
-
crl_extensions = crl_ext
|
|
854
|
-
crl = $dir/revocation_list.crl # the Revocation list
|
|
855
|
-
crlnumber = $dir/crlnumber # CRL number file
|
|
856
|
-
default_crl_days = 30
|
|
857
|
-
default_crl_hours = 24
|
|
858
|
-
#msie_hack
|
|
859
|
-
|
|
860
|
-
[ policy_match ]
|
|
861
|
-
countryName = optional
|
|
862
|
-
stateOrProvinceName = optional
|
|
863
|
-
localityName = optional
|
|
864
|
-
organizationName = optional
|
|
865
|
-
organizationalUnitName = optional
|
|
866
|
-
commonName = optional
|
|
867
|
-
emailAddress = optional
|
|
868
|
-
|
|
869
|
-
[ req ]
|
|
870
|
-
default_bits = 4096 # Size of keys
|
|
871
|
-
default_keyfile = key.pem # name of generated keys
|
|
872
|
-
distinguished_name = req_distinguished_name
|
|
873
|
-
attributes = req_attributes
|
|
874
|
-
x509_extensions = v3_ca
|
|
875
|
-
#input_password
|
|
876
|
-
#output_password
|
|
877
|
-
string_mask = nombstr # permitted characters
|
|
878
|
-
req_extensions = v3_req
|
|
879
|
-
|
|
880
|
-
[ req_distinguished_name ]
|
|
881
|
-
|
|
882
|
-
#0 countryName = Country Name (2 letter code)
|
|
883
|
-
# countryName_default = FR
|
|
884
|
-
# countryName_min = 2
|
|
885
|
-
# countryName_max = 2
|
|
886
|
-
# stateOrProvinceName = State or Province Name (full name)
|
|
887
|
-
# stateOrProvinceName_default = Ile de France
|
|
888
|
-
# localityName = Locality Name (city, district)
|
|
889
|
-
# localityName_default = Paris
|
|
890
|
-
organizationName = Organization Name (company)
|
|
891
|
-
organizationName_default = NodeOPCUA
|
|
892
|
-
# organizationalUnitName = Organizational Unit Name (department, division)
|
|
893
|
-
# organizationalUnitName_default = R&D
|
|
894
|
-
commonName = Common Name (hostname, FQDN, IP, or your name)
|
|
895
|
-
commonName_max = 256
|
|
896
|
-
commonName_default = NodeOPCUA
|
|
897
|
-
# emailAddress = Email Address
|
|
898
|
-
# emailAddress_max = 40
|
|
899
|
-
# emailAddress_default = node-opcua (at) node-opcua (dot) com
|
|
900
|
-
|
|
901
|
-
[ req_attributes ]
|
|
902
|
-
#challengePassword = A challenge password
|
|
903
|
-
#challengePassword_min = 4
|
|
904
|
-
#challengePassword_max = 20
|
|
905
|
-
#unstructuredName = An optional company name
|
|
906
|
-
[ usr_cert ]
|
|
907
|
-
basicConstraints = critical, CA:FALSE
|
|
908
|
-
subjectKeyIdentifier = hash
|
|
909
|
-
authorityKeyIdentifier = keyid,issuer:always
|
|
910
|
-
#authorityKeyIdentifier = keyid
|
|
911
|
-
subjectAltName = $ENV::ALTNAME
|
|
912
|
-
# issuerAltName = issuer:copy
|
|
913
|
-
nsComment = ''OpenSSL Generated Certificate''
|
|
914
|
-
#nsCertType = client, email, objsign for ''everything including object signing''
|
|
915
|
-
#nsCaRevocationUrl = http://www.domain.dom/ca-crl.pem
|
|
916
|
-
#nsBaseUrl =
|
|
917
|
-
#nsRenewalUrl =
|
|
918
|
-
#nsCaPolicyUrl =
|
|
919
|
-
#nsSslServerName =
|
|
920
|
-
keyUsage = critical, digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment, keyAgreement
|
|
921
|
-
extendedKeyUsage = critical,serverAuth ,clientAuth
|
|
922
|
-
{{#CDP_URL}}crlDistributionPoints = URI:$ENV::CDP_URL
|
|
923
|
-
{{/CDP_URL}}{{#AIA_VALUE}}authorityInfoAccess = $ENV::AIA_VALUE
|
|
924
|
-
{{/AIA_VALUE}}
|
|
925
|
-
[ v3_req ]
|
|
926
|
-
basicConstraints = critical, CA:FALSE
|
|
927
|
-
keyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment, keyAgreement
|
|
928
|
-
extendedKeyUsage = critical,serverAuth ,clientAuth
|
|
929
|
-
subjectAltName = $ENV::ALTNAME
|
|
930
|
-
nsComment = "CA Generated by Node-OPCUA Certificate utility using openssl"
|
|
931
|
-
[ v3_ca_req ]
|
|
932
|
-
subjectKeyIdentifier = hash
|
|
933
|
-
basicConstraints = CA:TRUE
|
|
934
|
-
keyUsage = critical, cRLSign, keyCertSign
|
|
935
|
-
subjectAltName = $ENV::ALTNAME
|
|
936
|
-
nsComment = "CA CSR generated by Node-OPCUA Certificate utility using openssl"
|
|
937
|
-
[ v3_ca ]
|
|
938
|
-
subjectKeyIdentifier = hash
|
|
939
|
-
authorityKeyIdentifier = keyid:always,issuer:always
|
|
940
|
-
basicConstraints = CA:TRUE
|
|
941
|
-
keyUsage = critical, cRLSign, keyCertSign
|
|
942
|
-
subjectAltName = $ENV::ALTNAME
|
|
943
|
-
nsComment = "CA Certificate generated by Node-OPCUA Certificate utility using openssl"
|
|
944
|
-
#nsCertType = sslCA, emailCA
|
|
945
|
-
#issuerAltName = issuer:copy
|
|
946
|
-
#obj = DER:02:03
|
|
947
|
-
{{#CDP_URL}}crlDistributionPoints = URI:$ENV::CDP_URL
|
|
948
|
-
{{/CDP_URL}}{{#AIA_VALUE}}authorityInfoAccess = $ENV::AIA_VALUE
|
|
949
|
-
{{/AIA_VALUE}}[ v3_selfsigned]
|
|
950
|
-
basicConstraints = critical, CA:FALSE
|
|
951
|
-
keyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment, keyAgreement
|
|
952
|
-
extendedKeyUsage = critical,serverAuth ,clientAuth
|
|
953
|
-
nsComment = "Self-signed certificate, generated by NodeOPCUA"
|
|
954
|
-
subjectAltName = $ENV::ALTNAME
|
|
955
|
-
|
|
956
|
-
[ crl_ext ]
|
|
957
|
-
#issuerAltName = issuer:copy
|
|
958
|
-
authorityKeyIdentifier = keyid:always,issuer:always
|
|
959
|
-
#authorityInfoAccess = @issuer_info`;
|
|
960
|
-
var ca_config_template_cnf_default = config2;
|
|
961
|
-
|
|
962
|
-
// packages/node-opcua-pki/lib/ca/certificate_authority.ts
|
|
963
|
-
var defaultSubject = "/C=FR/ST=IDF/L=Paris/O=Local NODE-OPCUA Certificate Authority/CN=NodeOPCUA-CA";
|
|
964
|
-
var configurationFileTemplate = ca_config_template_cnf_default;
|
|
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
|
-
}
|
|
972
|
-
var config3 = {
|
|
973
|
-
certificateDir: "INVALID",
|
|
974
|
-
forceCA: false,
|
|
975
|
-
pkiDir: "INVALID"
|
|
976
|
-
};
|
|
977
|
-
var n4 = makePath;
|
|
978
|
-
function octetStringToIpAddress(a) {
|
|
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();
|
|
980
|
-
}
|
|
981
|
-
assert7(octetStringToIpAddress("c07b9179") === "192.123.145.121");
|
|
982
|
-
async function construct_CertificateAuthority(certificateAuthority) {
|
|
983
|
-
const subject = certificateAuthority.subject;
|
|
984
|
-
const caRootDir = path5.resolve(certificateAuthority.rootDir);
|
|
985
|
-
async function make_folders() {
|
|
986
|
-
mkdirRecursiveSync(caRootDir);
|
|
987
|
-
ensurePrivateDirectory(path5.join(caRootDir, "private"));
|
|
988
|
-
mkdirRecursiveSync(path5.join(caRootDir, "public"));
|
|
989
|
-
mkdirRecursiveSync(path5.join(caRootDir, "certs"));
|
|
990
|
-
mkdirRecursiveSync(path5.join(caRootDir, "crl"));
|
|
991
|
-
mkdirRecursiveSync(path5.join(caRootDir, "conf"));
|
|
992
|
-
}
|
|
993
|
-
await make_folders();
|
|
994
|
-
async function construct_default_files() {
|
|
995
|
-
const serial = path5.join(caRootDir, "serial");
|
|
996
|
-
if (!fs7.existsSync(serial)) {
|
|
997
|
-
await fs7.promises.writeFile(serial, "1000");
|
|
998
|
-
}
|
|
999
|
-
const crlNumber = path5.join(caRootDir, "crlnumber");
|
|
1000
|
-
if (!fs7.existsSync(crlNumber)) {
|
|
1001
|
-
await fs7.promises.writeFile(crlNumber, "1000");
|
|
1002
|
-
}
|
|
1003
|
-
const indexFile = path5.join(caRootDir, "index.txt");
|
|
1004
|
-
if (!fs7.existsSync(indexFile)) {
|
|
1005
|
-
await fs7.promises.writeFile(indexFile, "");
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
await construct_default_files();
|
|
1009
|
-
const caKeyExists = fs7.existsSync(path5.join(caRootDir, "private/cakey.pem"));
|
|
1010
|
-
const caCertExists = fs7.existsSync(path5.join(caRootDir, "public/cacert.pem"));
|
|
1011
|
-
if (caKeyExists && caCertExists && !config3.forceCA) {
|
|
1012
|
-
restrictPrivateFilePermissions(path5.join(caRootDir, "private/cakey.pem"), 384);
|
|
1013
|
-
await certificateAuthority._ensurePrivateKeyProtection();
|
|
1014
|
-
debugLog("CA private key and certificate already exist ... skipping");
|
|
1015
|
-
return;
|
|
1016
|
-
}
|
|
1017
|
-
if (caKeyExists && !caCertExists) {
|
|
1018
|
-
debugLog("CA private key exists but cacert.pem is missing \u2014 rebuilding CA");
|
|
1019
|
-
fs7.unlinkSync(path5.join(caRootDir, "private/cakey.pem"));
|
|
1020
|
-
const staleCsr = path5.join(caRootDir, "private/cakey.csr");
|
|
1021
|
-
if (fs7.existsSync(staleCsr)) {
|
|
1022
|
-
fs7.unlinkSync(staleCsr);
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
displayTitle("Create Certificate Authority (CA)");
|
|
1026
|
-
const indexFileAttr = path5.join(caRootDir, "index.txt.attr");
|
|
1027
|
-
if (!fs7.existsSync(indexFileAttr)) {
|
|
1028
|
-
await fs7.promises.writeFile(indexFileAttr, "unique_subject = no");
|
|
1029
|
-
}
|
|
1030
|
-
const caConfigFile = certificateAuthority.configFile;
|
|
1031
|
-
if (1) {
|
|
1032
|
-
await fs7.promises.writeFile(caConfigFile, renderCaConfig(caRootDir));
|
|
1033
|
-
}
|
|
1034
|
-
const subjectOpt = ["-subj", subject.toString()];
|
|
1035
|
-
const caCommonName = subject.commonName || "NodeOPCUA-CA";
|
|
1036
|
-
setEnv("ALTNAME", `URI:urn:${caCommonName}`);
|
|
1037
|
-
certificateAuthority._wireRevocationEnvVars();
|
|
1038
|
-
const options = { cwd: caRootDir };
|
|
1039
|
-
const configFile = generateStaticConfig("conf/caconfig.cnf", options);
|
|
1040
|
-
const configOption = ["-config", n4(configFile)];
|
|
1041
|
-
const keySize = certificateAuthority.keySize;
|
|
1042
|
-
const privateKeyFilename = path5.join(caRootDir, "private/cakey.pem");
|
|
1043
|
-
const csrFilename = path5.join(caRootDir, "private/cakey.csr");
|
|
1044
|
-
displayTitle(`Generate the CA private Key - ${keySize}`);
|
|
1045
|
-
const passin = await certificateAuthority._opensslPassin();
|
|
1046
|
-
await generatePrivateKeyFile(privateKeyFilename, keySize, { passphrase: await certificateAuthority._privateKeyPassphrase() });
|
|
1047
|
-
restrictPrivateFilePermissions(privateKeyFilename, 384);
|
|
1048
|
-
displayTitle("Generate a certificate request for the CA key");
|
|
1049
|
-
await execute_openssl(
|
|
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 }
|
|
1066
|
-
);
|
|
1067
|
-
const issuerCA = certificateAuthority._issuerCA;
|
|
1068
|
-
if (issuerCA) {
|
|
1069
|
-
displayTitle("Generate CA Certificate (signed by issuer CA)");
|
|
1070
|
-
const issuerCert = path5.resolve(issuerCA.caCertificate);
|
|
1071
|
-
const issuerKey = path5.resolve(issuerCA.rootDir, "private/cakey.pem");
|
|
1072
|
-
const issuerSerial = path5.resolve(issuerCA.rootDir, "serial");
|
|
1073
|
-
const issuerPassin = await issuerCA._opensslPassin();
|
|
1074
|
-
await execute_openssl(
|
|
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 }
|
|
1099
|
-
);
|
|
1100
|
-
} else {
|
|
1101
|
-
displayTitle("Generate CA Certificate (self-signed)");
|
|
1102
|
-
await execute_openssl(
|
|
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 }
|
|
1123
|
-
);
|
|
1124
|
-
}
|
|
1125
|
-
displaySubtitle("generate initial CRL (Certificate Revocation List)");
|
|
1126
|
-
await regenerateCrl(certificateAuthority.revocationList, configOption, options, passin);
|
|
1127
|
-
displayTitle("Create Certificate Authority (CA) ---> DONE");
|
|
1128
|
-
}
|
|
1129
|
-
async function regenerateCrl(revocationList, configOption, options, passin) {
|
|
1130
|
-
displaySubtitle("regenerate CRL (Certificate Revocation List)");
|
|
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);
|
|
1136
|
-
displaySubtitle("Display (Certificate Revocation List)");
|
|
1137
|
-
await execute_openssl(["crl", "-in", n4(revocationList), "-text", "-noout"], options);
|
|
1138
|
-
}
|
|
1139
|
-
function parseOpenSSLDate(dateStr) {
|
|
1140
|
-
const raw = dateStr?.split(",")[0] ?? "";
|
|
1141
|
-
if (raw.length < 12) return "";
|
|
1142
|
-
const yy = parseInt(raw.substring(0, 2), 10);
|
|
1143
|
-
const year = yy >= 70 ? 1900 + yy : 2e3 + yy;
|
|
1144
|
-
const month = raw.substring(2, 4);
|
|
1145
|
-
const day = raw.substring(4, 6);
|
|
1146
|
-
const hour = raw.substring(6, 8);
|
|
1147
|
-
const min = raw.substring(8, 10);
|
|
1148
|
-
const sec = raw.substring(10, 12);
|
|
1149
|
-
return `${year}-${month}-${day}T${hour}:${min}:${sec}Z`;
|
|
1150
|
-
}
|
|
1151
|
-
function validateRevocationUrl(url, fieldName) {
|
|
1152
|
-
if (url === void 0) {
|
|
1153
|
-
return void 0;
|
|
1154
|
-
}
|
|
1155
|
-
if (url === "") {
|
|
1156
|
-
throw new Error(`${fieldName} must not be empty \u2014 pass undefined to disable the extension`);
|
|
1157
|
-
}
|
|
1158
|
-
let parsed;
|
|
1159
|
-
try {
|
|
1160
|
-
parsed = new URL(url);
|
|
1161
|
-
} catch {
|
|
1162
|
-
throw new Error(`${fieldName} is not a valid URL: ${url}`);
|
|
1163
|
-
}
|
|
1164
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
1165
|
-
throw new Error(`${fieldName} must use http: or https: (got ${parsed.protocol} in ${url})`);
|
|
1166
|
-
}
|
|
1167
|
-
if (!parsed.pathname || parsed.pathname === "/") {
|
|
1168
|
-
throw new Error(`${fieldName} must include a path component (got ${url})`);
|
|
1169
|
-
}
|
|
1170
|
-
const isLoopback = parsed.hostname === "localhost" || parsed.hostname === "::1" || parsed.hostname.startsWith("127.");
|
|
1171
|
-
if (isLoopback) {
|
|
1172
|
-
console.warn(
|
|
1173
|
-
`[node-opcua-pki] ${fieldName} points at loopback (${url}) \u2014 certificates issued with this URL will be unreachable from any other host.`
|
|
1174
|
-
);
|
|
1175
|
-
}
|
|
1176
|
-
return url;
|
|
1177
|
-
}
|
|
1178
|
-
var CertificateAuthority = class {
|
|
1179
|
-
/** RSA key size used when generating the CA private key. */
|
|
1180
|
-
keySize;
|
|
1181
|
-
/** Root filesystem path of the CA directory structure. */
|
|
1182
|
-
location;
|
|
1183
|
-
/** X.500 subject of the CA certificate. */
|
|
1184
|
-
subject;
|
|
1185
|
-
/** @internal Parent CA (undefined for root CAs). */
|
|
1186
|
-
_issuerCA;
|
|
1187
|
-
/** @internal Configured CDP / AIA URLs (US-202). */
|
|
1188
|
-
_crlDistributionUrl;
|
|
1189
|
-
_ocspResponderUrl;
|
|
1190
|
-
_caIssuersUrl;
|
|
1191
|
-
#privateKeyPassphrase;
|
|
1192
|
-
/** resolved once (see `privateKeyPassphrase`); `#passphraseResolved` distinguishes "none" from "not yet" */
|
|
1193
|
-
#resolvedPassphrase;
|
|
1194
|
-
#passphraseResolved = false;
|
|
1195
|
-
constructor(options) {
|
|
1196
|
-
assert7(Object.prototype.hasOwnProperty.call(options, "location"));
|
|
1197
|
-
assert7(Object.prototype.hasOwnProperty.call(options, "keySize"));
|
|
1198
|
-
this.location = options.location;
|
|
1199
|
-
this.keySize = options.keySize || 2048;
|
|
1200
|
-
this.subject = new Subject2(options.subject || defaultSubject);
|
|
1201
|
-
this._issuerCA = options.issuerCA;
|
|
1202
|
-
this.#privateKeyPassphrase = options.privateKeyPassphrase;
|
|
1203
|
-
if (options.crlDistributionUrl !== void 0) {
|
|
1204
|
-
this.setCrlDistributionUrl(options.crlDistributionUrl);
|
|
1205
|
-
}
|
|
1206
|
-
if (options.ocspResponderUrl !== void 0) {
|
|
1207
|
-
this.setOcspResponderUrl(options.ocspResponderUrl);
|
|
1208
|
-
}
|
|
1209
|
-
if (options.caIssuersUrl !== void 0) {
|
|
1210
|
-
this.setCaIssuersUrl(options.caIssuersUrl);
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
/**
|
|
1214
|
-
* Public URL where the CRL produced by this CA is reachable, or
|
|
1215
|
-
* `undefined` if no CDP extension should be emitted on issued certs.
|
|
1216
|
-
*/
|
|
1217
|
-
get crlDistributionUrl() {
|
|
1218
|
-
return this._crlDistributionUrl;
|
|
1219
|
-
}
|
|
1220
|
-
/**
|
|
1221
|
-
* Public URL of the OCSP responder, or `undefined` if no AIA OCSP
|
|
1222
|
-
* leg should be emitted on issued certs.
|
|
1223
|
-
*/
|
|
1224
|
-
get ocspResponderUrl() {
|
|
1225
|
-
return this._ocspResponderUrl;
|
|
1226
|
-
}
|
|
1227
|
-
/**
|
|
1228
|
-
* Public URL where the issuer's certificate can be fetched, or
|
|
1229
|
-
* `undefined` if no AIA caIssuers leg should be emitted.
|
|
1230
|
-
*/
|
|
1231
|
-
get caIssuersUrl() {
|
|
1232
|
-
return this._caIssuersUrl;
|
|
1233
|
-
}
|
|
1234
|
-
/**
|
|
1235
|
-
* Configure the URL embedded as `crlDistributionPoints` in every
|
|
1236
|
-
* subsequently-issued certificate. Pass `undefined` to disable
|
|
1237
|
-
* the extension entirely. Validated synchronously — throws on
|
|
1238
|
-
* empty string, non-http(s) protocol, missing path. Warns (does
|
|
1239
|
-
* not throw) when the URL points at loopback.
|
|
1240
|
-
*
|
|
1241
|
-
* @see US-202
|
|
1242
|
-
*/
|
|
1243
|
-
setCrlDistributionUrl(url) {
|
|
1244
|
-
this._crlDistributionUrl = validateRevocationUrl(url, "crlDistributionUrl");
|
|
1245
|
-
}
|
|
1246
|
-
/**
|
|
1247
|
-
* Configure the OCSP responder URL embedded as the `OCSP` leg of
|
|
1248
|
-
* the `authorityInfoAccess` extension on every subsequently-issued
|
|
1249
|
-
* certificate. Pass `undefined` to disable.
|
|
1250
|
-
*
|
|
1251
|
-
* @see US-202
|
|
1252
|
-
*/
|
|
1253
|
-
setOcspResponderUrl(url) {
|
|
1254
|
-
this._ocspResponderUrl = validateRevocationUrl(url, "ocspResponderUrl");
|
|
1255
|
-
}
|
|
1256
|
-
/**
|
|
1257
|
-
* Configure the caIssuers URL embedded as the `caIssuers` leg of
|
|
1258
|
-
* the `authorityInfoAccess` extension on every subsequently-issued
|
|
1259
|
-
* certificate. Pass `undefined` to disable.
|
|
1260
|
-
*
|
|
1261
|
-
* @see US-202
|
|
1262
|
-
*/
|
|
1263
|
-
setCaIssuersUrl(url) {
|
|
1264
|
-
this._caIssuersUrl = validateRevocationUrl(url, "caIssuersUrl");
|
|
1265
|
-
}
|
|
1266
|
-
/**
|
|
1267
|
-
* @internal
|
|
1268
|
-
* Populate the OpenSSL config substitution env vars (`CDP_URL` and
|
|
1269
|
-
* `AIA_VALUE`) from the configured URLs, or unset them so the
|
|
1270
|
-
* matching `{{#KEY}}...{{/KEY}}` blocks in the templates are
|
|
1271
|
-
* stripped. MUST be called before every `generateStaticConfig`
|
|
1272
|
-
* invocation that signs a certificate.
|
|
1273
|
-
*/
|
|
1274
|
-
_wireRevocationEnvVars() {
|
|
1275
|
-
unsetEnv("CDP_URL");
|
|
1276
|
-
unsetEnv("AIA_VALUE");
|
|
1277
|
-
if (this._crlDistributionUrl) {
|
|
1278
|
-
setEnv("CDP_URL", this._crlDistributionUrl);
|
|
1279
|
-
}
|
|
1280
|
-
const aiaLegs = [];
|
|
1281
|
-
if (this._ocspResponderUrl) {
|
|
1282
|
-
aiaLegs.push(`OCSP;URI:${this._ocspResponderUrl}`);
|
|
1283
|
-
}
|
|
1284
|
-
if (this._caIssuersUrl) {
|
|
1285
|
-
aiaLegs.push(`caIssuers;URI:${this._caIssuersUrl}`);
|
|
1286
|
-
}
|
|
1287
|
-
if (aiaLegs.length > 0) {
|
|
1288
|
-
setEnv("AIA_VALUE", aiaLegs.join(","));
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
/** Absolute path to the CA root directory (alias for {@link location}). */
|
|
1292
|
-
get rootDir() {
|
|
1293
|
-
return this.location;
|
|
1294
|
-
}
|
|
1295
|
-
/** Path to the OpenSSL configuration file (`conf/caconfig.cnf`). */
|
|
1296
|
-
get configFile() {
|
|
1297
|
-
return path5.normalize(path5.join(this.rootDir, "./conf/caconfig.cnf"));
|
|
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
|
-
}
|
|
1362
|
-
/** Path to the CA certificate in PEM format (`public/cacert.pem`). */
|
|
1363
|
-
get caCertificate() {
|
|
1364
|
-
return makePath(this.rootDir, "./public/cacert.pem");
|
|
1365
|
-
}
|
|
1366
|
-
/**
|
|
1367
|
-
* Path to the issuer certificate chain (`public/issuer_chain.pem`).
|
|
1368
|
-
*
|
|
1369
|
-
* This file is created by {@link installCACertificate} when the
|
|
1370
|
-
* provided cert file contains additional issuer certificates
|
|
1371
|
-
* (e.g. intermediate + root). It is appended to signed certs
|
|
1372
|
-
* by {@link constructCertificateChain} to produce a full chain
|
|
1373
|
-
* per OPC UA Part 6 §6.2.6.
|
|
1374
|
-
*/
|
|
1375
|
-
get issuerCertificateChain() {
|
|
1376
|
-
return makePath(this.rootDir, "./public/issuer_chain.pem");
|
|
1377
|
-
}
|
|
1378
|
-
/**
|
|
1379
|
-
* Path to the current Certificate Revocation List in DER format.
|
|
1380
|
-
* (`crl/revocation_list.der`)
|
|
1381
|
-
*/
|
|
1382
|
-
get revocationListDER() {
|
|
1383
|
-
return makePath(this.rootDir, "./crl/revocation_list.der");
|
|
1384
|
-
}
|
|
1385
|
-
/**
|
|
1386
|
-
* Path to the current Certificate Revocation List in PEM format.
|
|
1387
|
-
* (`crl/revocation_list.crl`)
|
|
1388
|
-
*/
|
|
1389
|
-
get revocationList() {
|
|
1390
|
-
return makePath(this.rootDir, "./crl/revocation_list.crl");
|
|
1391
|
-
}
|
|
1392
|
-
/**
|
|
1393
|
-
* Path to the concatenated CA certificate + CRL file.
|
|
1394
|
-
* Used by OpenSSL for CRL-based verification.
|
|
1395
|
-
*/
|
|
1396
|
-
get caCertificateWithCrl() {
|
|
1397
|
-
return makePath(this.rootDir, "./public/cacertificate_with_crl.pem");
|
|
1398
|
-
}
|
|
1399
|
-
// ---------------------------------------------------------------
|
|
1400
|
-
// Buffer-based accessors (US-059)
|
|
1401
|
-
// ---------------------------------------------------------------
|
|
1402
|
-
/**
|
|
1403
|
-
* Return the CA certificate as a DER-encoded buffer.
|
|
1404
|
-
*
|
|
1405
|
-
* @throws if the CA certificate file does not exist
|
|
1406
|
-
* (call {@link initialize} first).
|
|
1407
|
-
*/
|
|
1408
|
-
getCACertificateDER() {
|
|
1409
|
-
const pem = readCertificatePEM(this.caCertificate);
|
|
1410
|
-
return convertPEMtoDER(pem);
|
|
1411
|
-
}
|
|
1412
|
-
/**
|
|
1413
|
-
* Return the CA certificate as a PEM-encoded string.
|
|
1414
|
-
*
|
|
1415
|
-
* @throws if the CA certificate file does not exist
|
|
1416
|
-
* (call {@link initialize} first).
|
|
1417
|
-
*/
|
|
1418
|
-
getCACertificatePEM() {
|
|
1419
|
-
const raw = readCertificatePEM(this.caCertificate);
|
|
1420
|
-
const beginMarker = "-----BEGIN CERTIFICATE-----";
|
|
1421
|
-
const idx = raw.indexOf(beginMarker);
|
|
1422
|
-
if (idx > 0) {
|
|
1423
|
-
return raw.substring(idx);
|
|
1424
|
-
}
|
|
1425
|
-
return raw;
|
|
1426
|
-
}
|
|
1427
|
-
/**
|
|
1428
|
-
* Return the current Certificate Revocation List as a
|
|
1429
|
-
* DER-encoded buffer.
|
|
1430
|
-
*
|
|
1431
|
-
* Returns an empty buffer if no CRL has been generated yet.
|
|
1432
|
-
*/
|
|
1433
|
-
getCRLDER() {
|
|
1434
|
-
const crlPath = this.revocationListDER;
|
|
1435
|
-
if (!fs7.existsSync(crlPath)) {
|
|
1436
|
-
return Buffer.alloc(0);
|
|
1437
|
-
}
|
|
1438
|
-
return fs7.readFileSync(crlPath);
|
|
1439
|
-
}
|
|
1440
|
-
/**
|
|
1441
|
-
* Return the current Certificate Revocation List as a
|
|
1442
|
-
* PEM-encoded string.
|
|
1443
|
-
*
|
|
1444
|
-
* Returns an empty string if no CRL has been generated yet.
|
|
1445
|
-
*/
|
|
1446
|
-
getCRLPEM() {
|
|
1447
|
-
const crlPath = this.revocationList;
|
|
1448
|
-
if (!fs7.existsSync(crlPath)) {
|
|
1449
|
-
return "";
|
|
1450
|
-
}
|
|
1451
|
-
const raw = fs7.readFileSync(crlPath, "utf-8");
|
|
1452
|
-
const beginMarker = "-----BEGIN X509 CRL-----";
|
|
1453
|
-
const idx = raw.indexOf(beginMarker);
|
|
1454
|
-
if (idx > 0) {
|
|
1455
|
-
return raw.substring(idx);
|
|
1456
|
-
}
|
|
1457
|
-
return raw;
|
|
1458
|
-
}
|
|
1459
|
-
// ---------------------------------------------------------------
|
|
1460
|
-
// Certificate database API (US-057)
|
|
1461
|
-
// ---------------------------------------------------------------
|
|
1462
|
-
/**
|
|
1463
|
-
* Return a list of all issued certificates recorded in the
|
|
1464
|
-
* OpenSSL `index.txt` database.
|
|
1465
|
-
*
|
|
1466
|
-
* Each entry includes the serial number, subject, status,
|
|
1467
|
-
* expiry date, and (for revoked certs) the revocation date.
|
|
1468
|
-
*/
|
|
1469
|
-
getIssuedCertificates() {
|
|
1470
|
-
return this._parseIndexTxt();
|
|
1471
|
-
}
|
|
1472
|
-
/**
|
|
1473
|
-
* Return the total number of certificates recorded in
|
|
1474
|
-
* `index.txt`.
|
|
1475
|
-
*/
|
|
1476
|
-
getIssuedCertificateCount() {
|
|
1477
|
-
return this._parseIndexTxt().length;
|
|
1478
|
-
}
|
|
1479
|
-
/**
|
|
1480
|
-
* Return the status of a certificate by its serial number.
|
|
1481
|
-
*
|
|
1482
|
-
* @param serial - hex-encoded serial number (e.g. `"1000"`)
|
|
1483
|
-
* @returns `"valid"`, `"revoked"`, `"expired"`, or
|
|
1484
|
-
* `undefined` if not found
|
|
1485
|
-
*/
|
|
1486
|
-
getCertificateStatus(serial) {
|
|
1487
|
-
const upper = serial.toUpperCase();
|
|
1488
|
-
const record = this._parseIndexTxt().find((r) => r.serial.toUpperCase() === upper);
|
|
1489
|
-
return record?.status;
|
|
1490
|
-
}
|
|
1491
|
-
/**
|
|
1492
|
-
* Read a specific issued certificate by serial number and
|
|
1493
|
-
* return its content as a DER-encoded buffer.
|
|
1494
|
-
*
|
|
1495
|
-
* OpenSSL stores signed certificates in the `certs/`
|
|
1496
|
-
* directory using the naming convention `<SERIAL>.pem`.
|
|
1497
|
-
*
|
|
1498
|
-
* @param serial - hex-encoded serial number (e.g. `"1000"`)
|
|
1499
|
-
* @returns the DER buffer, or `undefined` if not found
|
|
1500
|
-
*/
|
|
1501
|
-
getCertificateBySerial(serial) {
|
|
1502
|
-
const upper = serial.toUpperCase();
|
|
1503
|
-
const certFile = path5.join(this.rootDir, "certs", `${upper}.pem`);
|
|
1504
|
-
if (!fs7.existsSync(certFile)) {
|
|
1505
|
-
return void 0;
|
|
1506
|
-
}
|
|
1507
|
-
const pem = readCertificatePEM(certFile);
|
|
1508
|
-
return convertPEMtoDER(pem);
|
|
1509
|
-
}
|
|
1510
|
-
/**
|
|
1511
|
-
* Path to the OpenSSL certificate database file.
|
|
1512
|
-
*/
|
|
1513
|
-
get indexFile() {
|
|
1514
|
-
return path5.join(this.rootDir, "index.txt");
|
|
1515
|
-
}
|
|
1516
|
-
/**
|
|
1517
|
-
* Parse the OpenSSL `index.txt` certificate database.
|
|
1518
|
-
*
|
|
1519
|
-
* Each line has tab-separated fields:
|
|
1520
|
-
* ```
|
|
1521
|
-
* status expiry [revocationDate] serial unknown subject
|
|
1522
|
-
* ```
|
|
1523
|
-
*
|
|
1524
|
-
* - status: `V` (valid), `R` (revoked), `E` (expired)
|
|
1525
|
-
* - expiry: `YYMMDDHHmmssZ`
|
|
1526
|
-
* - revocationDate: present only for revoked certs
|
|
1527
|
-
* - serial: hex string
|
|
1528
|
-
* - unknown: always `"unknown"`
|
|
1529
|
-
* - subject: X.500 slash-delimited string
|
|
1530
|
-
*/
|
|
1531
|
-
_parseIndexTxt() {
|
|
1532
|
-
const indexPath = this.indexFile;
|
|
1533
|
-
if (!fs7.existsSync(indexPath)) {
|
|
1534
|
-
return [];
|
|
1535
|
-
}
|
|
1536
|
-
const content = fs7.readFileSync(indexPath, "utf-8");
|
|
1537
|
-
const lines = content.split("\n").filter((l) => l.trim().length > 0);
|
|
1538
|
-
const records = [];
|
|
1539
|
-
for (const line of lines) {
|
|
1540
|
-
const fields = line.split(" ");
|
|
1541
|
-
if (fields.length < 4) continue;
|
|
1542
|
-
const statusChar = fields[0];
|
|
1543
|
-
const expiryStr = fields[1];
|
|
1544
|
-
let serial;
|
|
1545
|
-
let subject;
|
|
1546
|
-
let revocationDate;
|
|
1547
|
-
if (statusChar === "R") {
|
|
1548
|
-
revocationDate = fields[2];
|
|
1549
|
-
serial = fields[3];
|
|
1550
|
-
subject = fields.length >= 6 ? fields[5] : "";
|
|
1551
|
-
} else {
|
|
1552
|
-
serial = fields[3];
|
|
1553
|
-
subject = fields.length >= 6 ? fields[5] : "";
|
|
1554
|
-
}
|
|
1555
|
-
let status;
|
|
1556
|
-
switch (statusChar) {
|
|
1557
|
-
case "V":
|
|
1558
|
-
status = "valid";
|
|
1559
|
-
break;
|
|
1560
|
-
case "R":
|
|
1561
|
-
status = "revoked";
|
|
1562
|
-
break;
|
|
1563
|
-
case "E":
|
|
1564
|
-
status = "expired";
|
|
1565
|
-
break;
|
|
1566
|
-
default:
|
|
1567
|
-
continue;
|
|
1568
|
-
}
|
|
1569
|
-
records.push({
|
|
1570
|
-
serial,
|
|
1571
|
-
status,
|
|
1572
|
-
subject,
|
|
1573
|
-
expiryDate: parseOpenSSLDate(expiryStr),
|
|
1574
|
-
revocationDate: revocationDate ? parseOpenSSLDate(revocationDate) : void 0
|
|
1575
|
-
});
|
|
1576
|
-
}
|
|
1577
|
-
return records;
|
|
1578
|
-
}
|
|
1579
|
-
// ---------------------------------------------------------------
|
|
1580
|
-
// Buffer-based CA operations (US-058)
|
|
1581
|
-
// ---------------------------------------------------------------
|
|
1582
|
-
/**
|
|
1583
|
-
* Sign a DER-encoded Certificate Signing Request and return
|
|
1584
|
-
* the signed certificate as a DER buffer.
|
|
1585
|
-
*
|
|
1586
|
-
* This method handles temp-file creation and cleanup
|
|
1587
|
-
* internally so that callers can work with in-memory
|
|
1588
|
-
* buffers only.
|
|
1589
|
-
*
|
|
1590
|
-
* The CA can override fields from the CSR by passing
|
|
1591
|
-
* `options.dns`, `options.ip`, `options.applicationUri`,
|
|
1592
|
-
* `options.startDate`, or `options.subject`.
|
|
1593
|
-
*
|
|
1594
|
-
* @param csrDer - the CSR as a DER-encoded buffer
|
|
1595
|
-
* @param options - signing options and CA overrides
|
|
1596
|
-
* @returns the signed certificate as a DER-encoded buffer
|
|
1597
|
-
*/
|
|
1598
|
-
async signCertificateRequestFromDER(csrDer, options) {
|
|
1599
|
-
const tmpDir = await fs7.promises.mkdtemp(path5.join(os3.tmpdir(), "pki-sign-"));
|
|
1600
|
-
try {
|
|
1601
|
-
const csrFile = path5.join(tmpDir, "request.csr");
|
|
1602
|
-
const certFile = path5.join(tmpDir, "certificate.pem");
|
|
1603
|
-
const csrPem = toPem(csrDer, "CERTIFICATE REQUEST");
|
|
1604
|
-
await fs7.promises.writeFile(csrFile, csrPem, "utf-8");
|
|
1605
|
-
const signingParams = {};
|
|
1606
|
-
if (options?.validityMs !== void 0) signingParams.validityMs = options.validityMs;
|
|
1607
|
-
else signingParams.validity = options?.validity ?? 365;
|
|
1608
|
-
if (options?.startDate) signingParams.startDate = options.startDate;
|
|
1609
|
-
if (options?.dns) signingParams.dns = options.dns;
|
|
1610
|
-
if (options?.ip) signingParams.ip = options.ip;
|
|
1611
|
-
if (options?.applicationUri) signingParams.applicationUri = options.applicationUri;
|
|
1612
|
-
if (options?.subject) signingParams.subject = options.subject;
|
|
1613
|
-
await this.signCertificateRequest(certFile, csrFile, signingParams);
|
|
1614
|
-
const certPem = readCertificatePEM(certFile);
|
|
1615
|
-
return convertPEMtoDER(certPem);
|
|
1616
|
-
} finally {
|
|
1617
|
-
await fs7.promises.rm(tmpDir, {
|
|
1618
|
-
recursive: true,
|
|
1619
|
-
force: true
|
|
1620
|
-
});
|
|
1621
|
-
}
|
|
1622
|
-
}
|
|
1623
|
-
/**
|
|
1624
|
-
* Advertise the validity limits this CA can honor.
|
|
1625
|
-
*
|
|
1626
|
-
* Consumers (notably the GDS server in [`cert_auth.ts`](https://github.com/sterfive/node-opcua-gds))
|
|
1627
|
-
* clamp a requested validity against these bounds before calling
|
|
1628
|
-
* {@link signCertificateRequestFromDER}, so a misconfigured
|
|
1629
|
-
* `defaultCertValidity` cannot ask the CA for something it cannot
|
|
1630
|
-
* produce.
|
|
1631
|
-
*
|
|
1632
|
-
* Defaults match the OpenSSL-backed implementation:
|
|
1633
|
-
* - `minValidityMs = 60_000` (1 minute) — practical floor; the
|
|
1634
|
-
* X.509 spec floor is 1 second but very short certs are rarely
|
|
1635
|
-
* useful and pathological for any real deployment.
|
|
1636
|
-
* - `maxValidityMs = 10 * 365 * 86_400_000` (≈ 10 years) — long
|
|
1637
|
-
* enough for root CAs.
|
|
1638
|
-
* - `validityGranularityMs = 1_000` (1 second) — RFC 5280 §4.1.2.5
|
|
1639
|
-
* floor on `notBefore` / `notAfter`.
|
|
1640
|
-
* - `nativeUnit = "second"` — what `x509Date()` actually encodes.
|
|
1641
|
-
*
|
|
1642
|
-
* @see US-208 — the consumer-side capability story.
|
|
1643
|
-
*/
|
|
1644
|
-
getCapabilities() {
|
|
1645
|
-
return {
|
|
1646
|
-
minValidityMs: 6e4,
|
|
1647
|
-
maxValidityMs: 10 * 365 * 864e5,
|
|
1648
|
-
validityGranularityMs: 1e3,
|
|
1649
|
-
nativeUnit: "second"
|
|
1650
|
-
};
|
|
1651
|
-
}
|
|
1652
|
-
/**
|
|
1653
|
-
* Generate a new RSA key pair, create an internal CSR, sign it
|
|
1654
|
-
* with this CA, and return both the certificate and private key
|
|
1655
|
-
* as DER-encoded buffers.
|
|
1656
|
-
*
|
|
1657
|
-
* The private key is **never stored** by the CA — it exists only
|
|
1658
|
-
* in a temporary directory that is cleaned up after the operation.
|
|
1659
|
-
*
|
|
1660
|
-
* This is used by `StartNewKeyPairRequest` (OPC UA Part 12) for
|
|
1661
|
-
* constrained devices that cannot generate their own keys.
|
|
1662
|
-
*
|
|
1663
|
-
* @param options - key generation and certificate parameters
|
|
1664
|
-
* @returns `{ certificateDer, privateKey }` — certificate as DER,
|
|
1665
|
-
* private key as a branded `PrivateKey` buffer
|
|
1666
|
-
*/
|
|
1667
|
-
async generateKeyPairAndSignDER(options) {
|
|
1668
|
-
const keySize = options.keySize ?? 2048;
|
|
1669
|
-
const startDate = options.startDate ?? /* @__PURE__ */ new Date();
|
|
1670
|
-
const tmpDir = await fs7.promises.mkdtemp(path5.join(os3.tmpdir(), "pki-keygen-"));
|
|
1671
|
-
try {
|
|
1672
|
-
const privateKeyFile = path5.join(tmpDir, "private_key.pem");
|
|
1673
|
-
await generatePrivateKeyFile(privateKeyFile, keySize);
|
|
1674
|
-
const configFile = path5.join(tmpDir, "openssl.cnf");
|
|
1675
|
-
await fs7.promises.writeFile(configFile, configurationFileSimpleTemplate, "utf-8");
|
|
1676
|
-
const csrFile = path5.join(tmpDir, "request.csr");
|
|
1677
|
-
await createCertificateSigningRequestWithOpenSSL(csrFile, {
|
|
1678
|
-
rootDir: tmpDir,
|
|
1679
|
-
configFile,
|
|
1680
|
-
privateKey: privateKeyFile,
|
|
1681
|
-
applicationUri: options.applicationUri,
|
|
1682
|
-
subject: options.subject,
|
|
1683
|
-
dns: options.dns ?? [],
|
|
1684
|
-
ip: options.ip ?? [],
|
|
1685
|
-
purpose: CertificatePurpose.ForApplication
|
|
1686
|
-
});
|
|
1687
|
-
const certFile = path5.join(tmpDir, "certificate.pem");
|
|
1688
|
-
const signingParams = {
|
|
1689
|
-
applicationUri: options.applicationUri,
|
|
1690
|
-
dns: options.dns,
|
|
1691
|
-
ip: options.ip,
|
|
1692
|
-
startDate
|
|
1693
|
-
};
|
|
1694
|
-
if (options.validityMs !== void 0) signingParams.validityMs = options.validityMs;
|
|
1695
|
-
else signingParams.validity = options.validity ?? 365;
|
|
1696
|
-
await this.signCertificateRequest(certFile, csrFile, signingParams);
|
|
1697
|
-
const certPem = readCertificatePEM(certFile);
|
|
1698
|
-
const certificateDer = convertPEMtoDER(certPem);
|
|
1699
|
-
const privateKey = readPrivateKey(privateKeyFile);
|
|
1700
|
-
return { certificateDer, privateKey };
|
|
1701
|
-
} finally {
|
|
1702
|
-
await fs7.promises.rm(tmpDir, {
|
|
1703
|
-
recursive: true,
|
|
1704
|
-
force: true
|
|
1705
|
-
});
|
|
1706
|
-
}
|
|
1707
|
-
}
|
|
1708
|
-
/**
|
|
1709
|
-
* Generate a new RSA key pair, create an internal CSR, sign it
|
|
1710
|
-
* with this CA, and return the result as a PKCS#12 (PFX)
|
|
1711
|
-
* buffer bundling the certificate, private key, and CA chain.
|
|
1712
|
-
*
|
|
1713
|
-
* The private key is **never stored** by the CA — it exists only
|
|
1714
|
-
* in a temporary directory that is cleaned up after the operation.
|
|
1715
|
-
*
|
|
1716
|
-
* @param options - key generation, certificate, and PFX options
|
|
1717
|
-
* @returns the PFX as a `Buffer`
|
|
1718
|
-
*/
|
|
1719
|
-
async generateKeyPairAndSignPFX(options) {
|
|
1720
|
-
const keySize = options.keySize ?? 2048;
|
|
1721
|
-
const startDate = options.startDate ?? /* @__PURE__ */ new Date();
|
|
1722
|
-
const passphrase = options.passphrase ?? "";
|
|
1723
|
-
const tmpDir = await fs7.promises.mkdtemp(path5.join(os3.tmpdir(), "pki-keygen-pfx-"));
|
|
1724
|
-
try {
|
|
1725
|
-
const privateKeyFile = path5.join(tmpDir, "private_key.pem");
|
|
1726
|
-
await generatePrivateKeyFile(privateKeyFile, keySize);
|
|
1727
|
-
const configFile = path5.join(tmpDir, "openssl.cnf");
|
|
1728
|
-
await fs7.promises.writeFile(configFile, configurationFileSimpleTemplate, "utf-8");
|
|
1729
|
-
const csrFile = path5.join(tmpDir, "request.csr");
|
|
1730
|
-
await createCertificateSigningRequestWithOpenSSL(csrFile, {
|
|
1731
|
-
rootDir: tmpDir,
|
|
1732
|
-
configFile,
|
|
1733
|
-
privateKey: privateKeyFile,
|
|
1734
|
-
applicationUri: options.applicationUri,
|
|
1735
|
-
subject: options.subject,
|
|
1736
|
-
dns: options.dns ?? [],
|
|
1737
|
-
ip: options.ip ?? [],
|
|
1738
|
-
purpose: CertificatePurpose.ForApplication
|
|
1739
|
-
});
|
|
1740
|
-
const certFile = path5.join(tmpDir, "certificate.pem");
|
|
1741
|
-
const signingParams = {
|
|
1742
|
-
applicationUri: options.applicationUri,
|
|
1743
|
-
dns: options.dns,
|
|
1744
|
-
ip: options.ip,
|
|
1745
|
-
startDate
|
|
1746
|
-
};
|
|
1747
|
-
if (options.validityMs !== void 0) signingParams.validityMs = options.validityMs;
|
|
1748
|
-
else signingParams.validity = options.validity ?? 365;
|
|
1749
|
-
await this.signCertificateRequest(certFile, csrFile, signingParams);
|
|
1750
|
-
const pfxFile = path5.join(tmpDir, "bundle.pfx");
|
|
1751
|
-
await createPFX({
|
|
1752
|
-
certificateFile: certFile,
|
|
1753
|
-
privateKeyFile,
|
|
1754
|
-
outputFile: pfxFile,
|
|
1755
|
-
passphrase,
|
|
1756
|
-
caCertificateFiles: [this.caCertificate]
|
|
1757
|
-
});
|
|
1758
|
-
return await fs7.promises.readFile(pfxFile);
|
|
1759
|
-
} finally {
|
|
1760
|
-
await fs7.promises.rm(tmpDir, {
|
|
1761
|
-
recursive: true,
|
|
1762
|
-
force: true
|
|
1763
|
-
});
|
|
1764
|
-
}
|
|
1765
|
-
}
|
|
1766
|
-
/**
|
|
1767
|
-
* Revoke a DER-encoded certificate and regenerate the CRL.
|
|
1768
|
-
*
|
|
1769
|
-
* Extracts the serial number from the certificate, then
|
|
1770
|
-
* uses the stored cert file at `certs/<serial>.pem` for
|
|
1771
|
-
* revocation — avoiding temp-file PEM format mismatches.
|
|
1772
|
-
*
|
|
1773
|
-
* @param certDer - the certificate as a DER-encoded buffer
|
|
1774
|
-
* @param reason - CRL reason code
|
|
1775
|
-
* (default: `"keyCompromise"`)
|
|
1776
|
-
* @throws if the certificate's serial is not found in the CA
|
|
1777
|
-
*/
|
|
1778
|
-
async revokeCertificateDER(certDer, reason) {
|
|
1779
|
-
const info = exploreCertificate(certDer);
|
|
1780
|
-
const serial = info.tbsCertificate.serialNumber.replace(/:/g, "").toUpperCase();
|
|
1781
|
-
const storedCertFile = path5.join(this.rootDir, "certs", `${serial}.pem`);
|
|
1782
|
-
if (!fs7.existsSync(storedCertFile)) {
|
|
1783
|
-
throw new Error(`Cannot revoke: no stored certificate found for serial ${serial} at ${storedCertFile}`);
|
|
1784
|
-
}
|
|
1785
|
-
await this.revokeCertificate(storedCertFile, {
|
|
1786
|
-
reason: reason ?? "keyCompromise"
|
|
1787
|
-
});
|
|
592
|
+
// packages/node-opcua-pki/lib/ca/backends/openssl_ca_backend.ts
|
|
593
|
+
var n2 = makePath;
|
|
594
|
+
function caAltName(ca) {
|
|
595
|
+
return `URI:urn:${ca.subject.commonName || "NodeOPCUA-CA"}`;
|
|
596
|
+
}
|
|
597
|
+
function caConfigEnvOverrides(ca, altName = caAltName(ca)) {
|
|
598
|
+
const aiaLegs = [];
|
|
599
|
+
if (ca.ocspResponderUrl) {
|
|
600
|
+
aiaLegs.push(`OCSP;URI:${ca.ocspResponderUrl}`);
|
|
1788
601
|
}
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
* private key and self-signed certificate if they do not
|
|
1792
|
-
* already exist.
|
|
1793
|
-
*/
|
|
1794
|
-
async initialize() {
|
|
1795
|
-
await construct_CertificateAuthority(this);
|
|
602
|
+
if (ca.caIssuersUrl) {
|
|
603
|
+
aiaLegs.push(`caIssuers;URI:${ca.caIssuersUrl}`);
|
|
1796
604
|
}
|
|
605
|
+
return {
|
|
606
|
+
ALTNAME: altName,
|
|
607
|
+
CDP_URL: ca.crlDistributionUrl ?? "",
|
|
608
|
+
AIA_VALUE: aiaLegs.join(",")
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
var OpenSslCaBackend = class {
|
|
612
|
+
/** This backend is the `openssl` executable, so it has to be there. */
|
|
1797
613
|
/**
|
|
1798
|
-
*
|
|
1799
|
-
*
|
|
1800
|
-
*
|
|
1801
|
-
*
|
|
1802
|
-
* external (third-party) root CA. After receiving the signed
|
|
1803
|
-
* certificate, call {@link installCACertificate} to complete
|
|
1804
|
-
* the setup.
|
|
1805
|
-
*
|
|
1806
|
-
* **Idempotent / restart-safe:**
|
|
1807
|
-
* - If the CA certificate exists and is valid → `{ status: "ready" }`
|
|
1808
|
-
* - If the CA certificate has expired → `{ status: "expired", csrPath, expiryDate }`
|
|
1809
|
-
* (a new CSR is generated, preserving the existing private key)
|
|
1810
|
-
* - If key + CSR exist but no cert (restart before install) →
|
|
1811
|
-
* `{ status: "pending", csrPath }` without regenerating
|
|
1812
|
-
* - Otherwise → generates key + CSR → `{ status: "created", csrPath }`
|
|
1813
|
-
*
|
|
1814
|
-
* @returns an {@link InitializeCSRResult} describing the CA state
|
|
614
|
+
* `-passin env:` argv and env for an openssl call that has to load this
|
|
615
|
+
* CA's key. Built here rather than on the CA: the passphrase is the
|
|
616
|
+
* CA's business, but expressing it as an openssl argument is this
|
|
617
|
+
* backend's, and the core has no reason to know the flag exists.
|
|
1815
618
|
*/
|
|
1816
|
-
async
|
|
1817
|
-
|
|
1818
|
-
mkdirRecursiveSync(caRootDir);
|
|
1819
|
-
for (const dir of ["public", "certs", "crl", "conf"]) {
|
|
1820
|
-
mkdirRecursiveSync(path5.join(caRootDir, dir));
|
|
1821
|
-
}
|
|
1822
|
-
ensurePrivateDirectory(path5.join(caRootDir, "private"));
|
|
1823
|
-
const caCertFile = this.caCertificate;
|
|
1824
|
-
const privateKeyFile = path5.join(caRootDir, "private/cakey.pem");
|
|
1825
|
-
const csrFile = path5.join(caRootDir, "private/cakey.csr");
|
|
1826
|
-
if (fs7.existsSync(privateKeyFile)) {
|
|
1827
|
-
restrictPrivateFilePermissions(privateKeyFile, 384);
|
|
1828
|
-
await this._ensurePrivateKeyProtection();
|
|
1829
|
-
}
|
|
1830
|
-
if (fs7.existsSync(caCertFile)) {
|
|
1831
|
-
const certDer = convertPEMtoDER(readCertificatePEM(caCertFile));
|
|
1832
|
-
const certInfo = exploreCertificate(certDer);
|
|
1833
|
-
const notAfter = certInfo.tbsCertificate.validity.notAfter;
|
|
1834
|
-
if (notAfter.getTime() < Date.now()) {
|
|
1835
|
-
debugLog("CA certificate has expired \u2014 generating renewal CSR");
|
|
1836
|
-
await this._generateCSR(caRootDir, privateKeyFile, csrFile);
|
|
1837
|
-
return { status: "expired", csrPath: csrFile, expiryDate: notAfter };
|
|
1838
|
-
}
|
|
1839
|
-
debugLog("CA certificate already exists and is valid \u2014 ready");
|
|
1840
|
-
return { status: "ready" };
|
|
1841
|
-
}
|
|
1842
|
-
if (fs7.existsSync(privateKeyFile) && fs7.existsSync(csrFile)) {
|
|
1843
|
-
debugLog("CA key + CSR already exist \u2014 pending external signing");
|
|
1844
|
-
return { status: "pending", csrPath: csrFile };
|
|
1845
|
-
}
|
|
1846
|
-
const serial = path5.join(caRootDir, "serial");
|
|
1847
|
-
if (!fs7.existsSync(serial)) {
|
|
1848
|
-
await fs7.promises.writeFile(serial, "1000");
|
|
1849
|
-
}
|
|
1850
|
-
const crlNumber = path5.join(caRootDir, "crlnumber");
|
|
1851
|
-
if (!fs7.existsSync(crlNumber)) {
|
|
1852
|
-
await fs7.promises.writeFile(crlNumber, "1000");
|
|
1853
|
-
}
|
|
1854
|
-
const indexFile = path5.join(caRootDir, "index.txt");
|
|
1855
|
-
if (!fs7.existsSync(indexFile)) {
|
|
1856
|
-
await fs7.promises.writeFile(indexFile, "");
|
|
1857
|
-
}
|
|
1858
|
-
const indexFileAttr = path5.join(caRootDir, "index.txt.attr");
|
|
1859
|
-
if (!fs7.existsSync(indexFileAttr)) {
|
|
1860
|
-
await fs7.promises.writeFile(indexFileAttr, "unique_subject = no");
|
|
1861
|
-
}
|
|
1862
|
-
const caConfigFile = this.configFile;
|
|
1863
|
-
await fs7.promises.writeFile(caConfigFile, renderCaConfig(caRootDir));
|
|
1864
|
-
if (!fs7.existsSync(privateKeyFile)) {
|
|
1865
|
-
await generatePrivateKeyFile(privateKeyFile, this.keySize, { passphrase: await this._privateKeyPassphrase() });
|
|
1866
|
-
restrictPrivateFilePermissions(privateKeyFile, 384);
|
|
1867
|
-
}
|
|
1868
|
-
await this._generateCSR(caRootDir, privateKeyFile, csrFile);
|
|
1869
|
-
return { status: "created", csrPath: csrFile };
|
|
619
|
+
async #passin(ca) {
|
|
620
|
+
return passinArg(await ca._privateKeyPassphrase());
|
|
1870
621
|
}
|
|
1871
|
-
/**
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
* Use this while the CA is running to detect upcoming expiry
|
|
1876
|
-
* **before** it actually expires. The existing private key is
|
|
1877
|
-
* preserved so previously issued certs remain valid.
|
|
1878
|
-
*
|
|
1879
|
-
* @param thresholdDays - number of days before expiry at which
|
|
1880
|
-
* to trigger renewal (default: 30)
|
|
1881
|
-
* @returns an {@link InitializeCSRResult} — `"expired"` if
|
|
1882
|
-
* renewal is needed, `"ready"` if the cert is still valid
|
|
1883
|
-
*/
|
|
1884
|
-
async renewCSR(thresholdDays = 30) {
|
|
1885
|
-
const caRootDir = path5.resolve(this.rootDir);
|
|
1886
|
-
const caCertFile = this.caCertificate;
|
|
1887
|
-
const privateKeyFile = path5.join(caRootDir, "private/cakey.pem");
|
|
1888
|
-
const csrFile = path5.join(caRootDir, "private/cakey.csr");
|
|
1889
|
-
if (!fs7.existsSync(caCertFile)) {
|
|
1890
|
-
return this.initializeCSR();
|
|
1891
|
-
}
|
|
1892
|
-
const certDer = convertPEMtoDER(readCertificatePEM(caCertFile));
|
|
1893
|
-
const certInfo = exploreCertificate(certDer);
|
|
1894
|
-
const notAfter = certInfo.tbsCertificate.validity.notAfter;
|
|
1895
|
-
const thresholdMs = thresholdDays * 24 * 60 * 60 * 1e3;
|
|
1896
|
-
if (notAfter.getTime() - Date.now() < thresholdMs) {
|
|
1897
|
-
debugLog(`CA certificate expires within ${thresholdDays} days \u2014 generating renewal CSR`);
|
|
1898
|
-
await this._generateCSR(caRootDir, privateKeyFile, csrFile);
|
|
1899
|
-
return { status: "expired", csrPath: csrFile, expiryDate: notAfter };
|
|
1900
|
-
}
|
|
1901
|
-
return { status: "ready" };
|
|
622
|
+
/** The openssl CLI loads its key from a file, so it cannot call out to an HSM. */
|
|
623
|
+
supportsExternalSigner = false;
|
|
624
|
+
async preflight() {
|
|
625
|
+
await ensure_openssl_installed();
|
|
1902
626
|
}
|
|
1903
627
|
/**
|
|
1904
|
-
*
|
|
1905
|
-
*
|
|
628
|
+
* Render `conf/caconfig.cnf` once with explicit overrides, run `fn`
|
|
629
|
+
* with the rendered path, and always remove the temp file. Structural
|
|
630
|
+
* guarantee that (a) every render carries the three required env
|
|
631
|
+
* values and (b) no rendered `<name>.<pid>-<n>.tmp` file is ever left
|
|
632
|
+
* behind — each was previously a per-method discipline.
|
|
1906
633
|
*/
|
|
1907
|
-
async
|
|
1908
|
-
|
|
634
|
+
async #withConfig(ca, altName, fn) {
|
|
635
|
+
const caRootDir = path3.resolve(ca.rootDir);
|
|
1909
636
|
const options = { cwd: caRootDir };
|
|
1910
|
-
const configFile = generateStaticConfig("conf/caconfig.cnf", options);
|
|
1911
|
-
|
|
637
|
+
const configFile = generateStaticConfig("conf/caconfig.cnf", options, caConfigEnvOverrides(ca, altName));
|
|
638
|
+
try {
|
|
639
|
+
return await fn(configFile, options);
|
|
640
|
+
} finally {
|
|
641
|
+
await cleanupStaticConfig(configFile, options);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
async #generateCaCsrWith(ca, configFile, options, privateKeyFile, csrFile) {
|
|
645
|
+
const passin = await this.#passin(ca);
|
|
646
|
+
displayTitle("Generate a certificate request for the CA key");
|
|
1912
647
|
await execute_openssl(
|
|
1913
648
|
[
|
|
1914
649
|
"req",
|
|
1915
650
|
"-new",
|
|
1916
651
|
"-sha256",
|
|
1917
|
-
"-text",
|
|
1918
652
|
"-extensions",
|
|
1919
653
|
"v3_ca_req",
|
|
1920
654
|
"-config",
|
|
1921
|
-
|
|
655
|
+
n2(configFile),
|
|
1922
656
|
"-key",
|
|
1923
|
-
|
|
657
|
+
n2(privateKeyFile),
|
|
1924
658
|
"-out",
|
|
1925
|
-
|
|
659
|
+
n2(csrFile),
|
|
1926
660
|
"-subj",
|
|
1927
|
-
|
|
661
|
+
ca.subject.toString(),
|
|
1928
662
|
...passin.args
|
|
1929
663
|
],
|
|
1930
664
|
{ ...options, env: passin.env }
|
|
1931
665
|
);
|
|
1932
666
|
}
|
|
1933
667
|
/**
|
|
1934
|
-
*
|
|
1935
|
-
*
|
|
1936
|
-
*
|
|
1937
|
-
* Call this after {@link initializeCSR} once the external
|
|
1938
|
-
* root CA has signed the CSR.
|
|
1939
|
-
*
|
|
1940
|
-
* **Safety checks:**
|
|
1941
|
-
* - Verifies that the certificate's public key matches the
|
|
1942
|
-
* CA private key before installing.
|
|
1943
|
-
*
|
|
1944
|
-
* @param signedCertFile - path to the PEM-encoded signed
|
|
1945
|
-
* CA certificate (issued by the external root CA)
|
|
1946
|
-
* @returns an {@link InstallCACertificateResult} with
|
|
1947
|
-
* `status: "success"` or `status: "error"` and a `reason`
|
|
1948
|
-
*/
|
|
1949
|
-
async installCACertificate(signedCertFile) {
|
|
1950
|
-
const caRootDir = path5.resolve(this.rootDir);
|
|
1951
|
-
const caCertFile = this.caCertificate;
|
|
1952
|
-
const fullPem = await fs7.promises.readFile(signedCertFile, "utf8");
|
|
1953
|
-
const pemBlocks = fullPem.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g);
|
|
1954
|
-
if (!pemBlocks || pemBlocks.length === 0) {
|
|
1955
|
-
return {
|
|
1956
|
-
status: "error",
|
|
1957
|
-
reason: "no_certificate_found",
|
|
1958
|
-
message: "The provided file does not contain any PEM-encoded certificate."
|
|
1959
|
-
};
|
|
1960
|
-
}
|
|
1961
|
-
const certDer = convertPEMtoDER(pemBlocks[0]);
|
|
1962
|
-
const privateKey = await this.getPrivateKey();
|
|
1963
|
-
if (!certificateMatchesPrivateKey(certDer, privateKey)) {
|
|
1964
|
-
return {
|
|
1965
|
-
status: "error",
|
|
1966
|
-
reason: "certificate_key_mismatch",
|
|
1967
|
-
message: "The provided certificate does not match the CA private key. Ensure the certificate was signed from the CSR generated by initializeCSR()."
|
|
1968
|
-
};
|
|
1969
|
-
}
|
|
1970
|
-
await fs7.promises.writeFile(caCertFile, `${pemBlocks[0]}
|
|
1971
|
-
`);
|
|
1972
|
-
const issuerChainFile = this.issuerCertificateChain;
|
|
1973
|
-
if (pemBlocks.length > 1) {
|
|
1974
|
-
const issuerPem = `${pemBlocks.slice(1).join("\n")}
|
|
1975
|
-
`;
|
|
1976
|
-
await fs7.promises.writeFile(issuerChainFile, issuerPem);
|
|
1977
|
-
debugLog(`Stored ${pemBlocks.length - 1} issuer certificate(s) in issuer_chain.pem`);
|
|
1978
|
-
} else {
|
|
1979
|
-
if (fs7.existsSync(issuerChainFile)) {
|
|
1980
|
-
await fs7.promises.unlink(issuerChainFile);
|
|
1981
|
-
}
|
|
1982
|
-
}
|
|
1983
|
-
const options = { cwd: caRootDir };
|
|
1984
|
-
const configFile = generateStaticConfig("conf/caconfig.cnf", options);
|
|
1985
|
-
await regenerateCrl(this.revocationList, ["-config", n4(configFile)], options, await this._opensslPassin());
|
|
1986
|
-
return { status: "success" };
|
|
1987
|
-
}
|
|
1988
|
-
/**
|
|
1989
|
-
* Sign a CSR with CA extensions (`v3_ca`), producing a
|
|
1990
|
-
* subordinate CA certificate.
|
|
1991
|
-
*
|
|
1992
|
-
* Unlike {@link signCertificateRequest} which signs with
|
|
1993
|
-
* end-entity extensions (SANs, etc.), this method signs
|
|
1994
|
-
* with `basicConstraints = CA:TRUE` and `keyUsage =
|
|
1995
|
-
* keyCertSign, cRLSign`.
|
|
1996
|
-
*
|
|
1997
|
-
* @param certFile - output path for the signed CA cert (PEM)
|
|
1998
|
-
* @param csrFile - path to the subordinate CA's CSR
|
|
1999
|
-
* @param params - signing parameters
|
|
668
|
+
* `openssl ca -gencrl` signs the CRL with the CA key it finds through the
|
|
669
|
+
* config file (`private_key = $dir/private/cakey.pem`), so it needs the
|
|
670
|
+
* CA passphrase like every other `openssl ca` invocation.
|
|
2000
671
|
*/
|
|
2001
|
-
async
|
|
2002
|
-
const
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
672
|
+
async #regenerateCrlWith(ca, configFile, options) {
|
|
673
|
+
const passin = await this.#passin(ca);
|
|
674
|
+
displaySubtitle("regenerate CRL (Certificate Revocation List)");
|
|
675
|
+
await execute_openssl(["ca", "-gencrl", "-config", n2(configFile), "-out", "crl/revocation_list.crl", ...passin.args], {
|
|
676
|
+
...options,
|
|
677
|
+
env: passin.env
|
|
678
|
+
});
|
|
2008
679
|
await execute_openssl(
|
|
2009
|
-
[
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
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 }
|
|
680
|
+
["crl", "-in", "crl/revocation_list.crl", "-out", "crl/revocation_list.der", "-outform", "der"],
|
|
681
|
+
options
|
|
682
|
+
);
|
|
683
|
+
displaySubtitle("Display (Certificate Revocation List)");
|
|
684
|
+
await execute_openssl(["crl", "-in", n2(ca.revocationList), "-text", "-noout"], options);
|
|
685
|
+
}
|
|
686
|
+
async generateCaCsr(ca, _caRootDir, privateKeyFile, csrFile) {
|
|
687
|
+
await this.#withConfig(
|
|
688
|
+
ca,
|
|
689
|
+
caAltName(ca),
|
|
690
|
+
(configFile, options) => this.#generateCaCsrWith(ca, configFile, options, privateKeyFile, csrFile)
|
|
2033
691
|
);
|
|
2034
|
-
await this.constructCertificateChain(certFile);
|
|
2035
692
|
}
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
693
|
+
async bootstrap(ca) {
|
|
694
|
+
const caRootDir = path3.resolve(ca.rootDir);
|
|
695
|
+
const privateKeyFilename = path3.join(caRootDir, "private/cakey.pem");
|
|
696
|
+
const csrFilename = path3.join(caRootDir, "private/cakey.csr");
|
|
697
|
+
await this.#withConfig(ca, caAltName(ca), async (configFile, options) => {
|
|
698
|
+
await this.#generateCaCsrWith(ca, configFile, options, privateKeyFilename, csrFilename);
|
|
699
|
+
const issuerCA = ca._issuerCA;
|
|
700
|
+
if (issuerCA) {
|
|
701
|
+
displayTitle("Generate CA Certificate (signed by issuer CA)");
|
|
702
|
+
const issuerCert = path3.resolve(issuerCA.caCertificate);
|
|
703
|
+
const issuerKey = path3.resolve(issuerCA.rootDir, "private/cakey.pem");
|
|
704
|
+
const issuerSerial = path3.resolve(issuerCA.rootDir, "serial");
|
|
705
|
+
const issuerPassin = await this.#passin(issuerCA);
|
|
706
|
+
const signWithIssuer = async () => {
|
|
707
|
+
await execute_openssl(
|
|
708
|
+
[
|
|
709
|
+
"x509",
|
|
710
|
+
"-sha256",
|
|
711
|
+
"-req",
|
|
712
|
+
"-days",
|
|
713
|
+
"3650",
|
|
714
|
+
"-extensions",
|
|
715
|
+
"v3_ca",
|
|
716
|
+
"-extfile",
|
|
717
|
+
n2(configFile),
|
|
718
|
+
"-in",
|
|
719
|
+
"private/cakey.csr",
|
|
720
|
+
"-CA",
|
|
721
|
+
n2(issuerCert),
|
|
722
|
+
"-CAkey",
|
|
723
|
+
n2(issuerKey),
|
|
724
|
+
"-CAserial",
|
|
725
|
+
n2(issuerSerial),
|
|
726
|
+
"-out",
|
|
727
|
+
"public/cacert.pem",
|
|
728
|
+
...issuerPassin.args
|
|
729
|
+
],
|
|
730
|
+
{ ...options, env: issuerPassin.env }
|
|
731
|
+
);
|
|
732
|
+
};
|
|
733
|
+
if (path3.resolve(issuerCA.rootDir) === caRootDir) {
|
|
734
|
+
await signWithIssuer();
|
|
735
|
+
} else {
|
|
736
|
+
await issuerCA._withCaDirectoryLock(signWithIssuer);
|
|
737
|
+
}
|
|
738
|
+
} else {
|
|
739
|
+
displayTitle("Generate CA Certificate (self-signed)");
|
|
740
|
+
const passin = await this.#passin(ca);
|
|
741
|
+
await execute_openssl(
|
|
742
|
+
[
|
|
743
|
+
"x509",
|
|
744
|
+
"-sha256",
|
|
745
|
+
"-req",
|
|
746
|
+
"-days",
|
|
747
|
+
"3650",
|
|
748
|
+
"-extensions",
|
|
749
|
+
"v3_ca",
|
|
750
|
+
"-extfile",
|
|
751
|
+
n2(configFile),
|
|
752
|
+
"-in",
|
|
753
|
+
"private/cakey.csr",
|
|
754
|
+
"-signkey",
|
|
755
|
+
n2(privateKeyFilename),
|
|
756
|
+
"-out",
|
|
757
|
+
"public/cacert.pem",
|
|
758
|
+
...passin.args
|
|
759
|
+
],
|
|
760
|
+
{ ...options, env: passin.env }
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
displaySubtitle("generate initial CRL (Certificate Revocation List)");
|
|
764
|
+
await this.#regenerateCrlWith(ca, configFile, options);
|
|
765
|
+
});
|
|
766
|
+
displayTitle("Create Certificate Authority (CA) ---> DONE");
|
|
767
|
+
}
|
|
768
|
+
async regenerateCrl(ca) {
|
|
769
|
+
await this.#withConfig(ca, caAltName(ca), (configFile, options) => this.#regenerateCrlWith(ca, configFile, options));
|
|
770
|
+
}
|
|
771
|
+
async signSubordinateCsr(ca, csrFile, certFile, validityDays) {
|
|
772
|
+
await this.#withConfig(ca, caAltName(ca), async (configFile, options) => {
|
|
773
|
+
const caRootDir = options.cwd;
|
|
774
|
+
const passin = await this.#passin(ca);
|
|
775
|
+
await execute_openssl(
|
|
776
|
+
[
|
|
777
|
+
"x509",
|
|
778
|
+
"-sha256",
|
|
779
|
+
"-req",
|
|
780
|
+
"-days",
|
|
781
|
+
String(validityDays),
|
|
782
|
+
"-extensions",
|
|
783
|
+
"v3_ca",
|
|
784
|
+
"-extfile",
|
|
785
|
+
n2(configFile),
|
|
786
|
+
"-in",
|
|
787
|
+
n2(csrFile),
|
|
788
|
+
"-CA",
|
|
789
|
+
n2(ca.caCertificate),
|
|
790
|
+
"-CAkey",
|
|
791
|
+
n2(path3.join(caRootDir, "private/cakey.pem")),
|
|
792
|
+
"-CAserial",
|
|
793
|
+
n2(path3.join(caRootDir, "serial")),
|
|
794
|
+
"-out",
|
|
795
|
+
n2(certFile),
|
|
796
|
+
...passin.args
|
|
797
|
+
],
|
|
798
|
+
{ ...options, env: passin.env }
|
|
2049
799
|
);
|
|
2050
|
-
}
|
|
2051
|
-
await fs7.promises.writeFile(cacertWithCRL, fs7.readFileSync(this.caCertificate));
|
|
2052
|
-
}
|
|
800
|
+
});
|
|
2053
801
|
}
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
802
|
+
async signEndEntityCsr(ca, certificate, csr, params1, sanOverride) {
|
|
803
|
+
await this.#withConfig(ca, buildSubjectAltNameString(sanOverride), async (configFile, options) => {
|
|
804
|
+
displaySubtitle("- then we ask the authority to sign the certificate signing request");
|
|
805
|
+
const passin = await this.#passin(ca);
|
|
806
|
+
await execute_openssl(
|
|
807
|
+
[
|
|
808
|
+
"ca",
|
|
809
|
+
"-config",
|
|
810
|
+
configFile,
|
|
811
|
+
"-startdate",
|
|
812
|
+
x509Date(params1.startDate),
|
|
813
|
+
"-enddate",
|
|
814
|
+
x509Date(params1.endDate),
|
|
815
|
+
"-batch",
|
|
816
|
+
"-out",
|
|
817
|
+
n2(certificate),
|
|
818
|
+
"-in",
|
|
819
|
+
n2(csr),
|
|
820
|
+
...passin.args
|
|
821
|
+
],
|
|
822
|
+
{ ...options, env: passin.env }
|
|
823
|
+
);
|
|
824
|
+
displaySubtitle("- dump the certificate for a check");
|
|
825
|
+
await execute_openssl(["x509", "-in", n2(certificate), "-dates", "-fingerprint", "-purpose", "-noout"], options);
|
|
826
|
+
});
|
|
2070
827
|
}
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
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
|
-
],
|
|
2118
|
-
options
|
|
2119
|
-
);
|
|
2120
|
-
displaySubtitle("- dump the certificate for a check");
|
|
2121
|
-
await execute_openssl(["x509", "-in", n4(certificateFile), "-dates", "-fingerprint", "-purpose", "-noout"], {});
|
|
2122
|
-
displaySubtitle("- verify self-signed certificate");
|
|
2123
|
-
await execute_openssl_no_failure(["verify", "-verbose", "-CAfile", n4(certificateFile), n4(certificateFile)], options);
|
|
2124
|
-
await fs7.promises.unlink(csrFile);
|
|
828
|
+
async revoke(ca, certificate, reason) {
|
|
829
|
+
setEnv("RANDFILE", path3.join(ca.rootDir, "random.rnd"));
|
|
830
|
+
await this.#withConfig(ca, caAltName(ca), async (configFile, options) => {
|
|
831
|
+
displaySubtitle("Revoke certificate");
|
|
832
|
+
const passin = await this.#passin(ca);
|
|
833
|
+
await execute_openssl_no_failure(
|
|
834
|
+
["ca", "-verbose", "-config", n2(configFile), "-revoke", certificate, "-crl_reason", reason, ...passin.args],
|
|
835
|
+
{ ...options, env: passin.env }
|
|
836
|
+
);
|
|
837
|
+
await this.#regenerateCrlWith(ca, configFile, options);
|
|
838
|
+
displaySubtitle("Verify that certificate is revoked");
|
|
839
|
+
await execute_openssl_no_failure(
|
|
840
|
+
[
|
|
841
|
+
"verify",
|
|
842
|
+
"-verbose",
|
|
843
|
+
"-CRLfile",
|
|
844
|
+
n2(ca.revocationList),
|
|
845
|
+
"-CAfile",
|
|
846
|
+
n2(ca.caCertificate),
|
|
847
|
+
"-crl_check",
|
|
848
|
+
n2(certificate)
|
|
849
|
+
],
|
|
850
|
+
options
|
|
851
|
+
);
|
|
852
|
+
displaySubtitle("Produce CRL in DER form ");
|
|
853
|
+
await execute_openssl(
|
|
854
|
+
["crl", "-in", n2(ca.revocationList), "-out", "crl/revocation_list.der", "-outform", "der"],
|
|
855
|
+
options
|
|
856
|
+
);
|
|
857
|
+
displaySubtitle("Produce CRL in PEM form ");
|
|
858
|
+
await execute_openssl(
|
|
859
|
+
["crl", "-in", n2(ca.revocationList), "-out", "crl/revocation_list.pem", "-outform", "pem", "-text"],
|
|
860
|
+
options
|
|
861
|
+
);
|
|
862
|
+
});
|
|
2125
863
|
}
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
* @param certificate - path to the certificate file to revoke
|
|
2130
|
-
* @param params - revocation parameters
|
|
2131
|
-
* @param params.reason - CRL reason code
|
|
2132
|
-
* (default `"keyCompromise"`)
|
|
2133
|
-
*/
|
|
2134
|
-
async revokeCertificate(certificate, params) {
|
|
2135
|
-
const crlReasons = [
|
|
2136
|
-
"unspecified",
|
|
2137
|
-
"keyCompromise",
|
|
2138
|
-
"CACompromise",
|
|
2139
|
-
"affiliationChanged",
|
|
2140
|
-
"superseded",
|
|
2141
|
-
"cessationOfOperation",
|
|
2142
|
-
"certificateHold",
|
|
2143
|
-
"removeFromCRL"
|
|
2144
|
-
];
|
|
2145
|
-
const configFile = generateStaticConfig("conf/caconfig.cnf", { cwd: this.rootDir });
|
|
864
|
+
async createSelfSignedCertificate(ca, certificateFile, privateKeyFile, params) {
|
|
865
|
+
const envOverrides = caConfigEnvOverrides(ca, buildSubjectAltNameString(params));
|
|
866
|
+
const configFile = generateStaticConfig(ca.configFile, { cwd: ca.rootDir }, envOverrides);
|
|
2146
867
|
const options = {
|
|
2147
|
-
cwd:
|
|
868
|
+
cwd: ca.rootDir,
|
|
2148
869
|
openssl_conf: makePath(configFile)
|
|
2149
870
|
};
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
871
|
+
try {
|
|
872
|
+
const subject = params.subject ? new Subject2(params.subject).toString() : "";
|
|
873
|
+
const subjectOptions = subject && subject.length > 1 ? ["-subj", subject] : [];
|
|
874
|
+
const csrFile = `${certificateFile}_csr`;
|
|
875
|
+
const passin = await this.#passin(ca);
|
|
876
|
+
displaySubtitle("- the certificate signing request");
|
|
877
|
+
await execute_openssl(
|
|
878
|
+
[
|
|
879
|
+
"req",
|
|
880
|
+
"-new",
|
|
881
|
+
"-sha256",
|
|
882
|
+
...subjectOptions,
|
|
883
|
+
"-batch",
|
|
884
|
+
"-key",
|
|
885
|
+
n2(privateKeyFile),
|
|
886
|
+
"-out",
|
|
887
|
+
n2(csrFile),
|
|
888
|
+
...passin.args
|
|
889
|
+
],
|
|
890
|
+
{ ...options, env: passin.env }
|
|
891
|
+
);
|
|
892
|
+
displaySubtitle("- creating the self-signed certificate");
|
|
893
|
+
await execute_openssl(
|
|
894
|
+
[
|
|
895
|
+
"ca",
|
|
896
|
+
"-selfsign",
|
|
897
|
+
"-keyfile",
|
|
898
|
+
n2(privateKeyFile),
|
|
899
|
+
"-startdate",
|
|
900
|
+
x509Date(params.startDate),
|
|
901
|
+
"-enddate",
|
|
902
|
+
x509Date(params.endDate),
|
|
903
|
+
"-batch",
|
|
904
|
+
"-out",
|
|
905
|
+
n2(certificateFile),
|
|
906
|
+
"-in",
|
|
907
|
+
n2(csrFile),
|
|
908
|
+
...passin.args
|
|
909
|
+
],
|
|
910
|
+
{ ...options, env: passin.env }
|
|
911
|
+
);
|
|
912
|
+
displaySubtitle("- dump the certificate for a check");
|
|
913
|
+
await execute_openssl(["x509", "-in", n2(certificateFile), "-dates", "-fingerprint", "-purpose", "-noout"], {});
|
|
914
|
+
displaySubtitle("- verify self-signed certificate");
|
|
915
|
+
await execute_openssl_no_failure(["verify", "-verbose", "-CAfile", n2(certificateFile), n2(certificateFile)], options);
|
|
916
|
+
await fs4.promises.unlink(csrFile);
|
|
917
|
+
} finally {
|
|
918
|
+
await cleanupStaticConfig(configFile, { cwd: ca.rootDir });
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
// packages/node-opcua-pki/lib/ca/certificate_authority.ts
|
|
924
|
+
var CertificateAuthority = class extends CertificateAuthorityCore {
|
|
925
|
+
constructor(options) {
|
|
926
|
+
if (options.signer && options.backend === "openssl") {
|
|
927
|
+
throw new Error(
|
|
928
|
+
"CertificateAuthority: backend 'openssl' cannot be combined with 'signer' - the openssl CLI loads its key from a file and cannot call an external signer. Drop the backend option (a signer implies 'native') or drop the signer."
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
const backend = options.signer || options.backend === "native" ? new NativeCaBackend() : new OpenSslCaBackend();
|
|
932
|
+
super({ ...options, backend });
|
|
2188
933
|
}
|
|
2189
934
|
/**
|
|
2190
|
-
*
|
|
935
|
+
* @internal `-passin env:` argv + env for an openssl call that loads
|
|
936
|
+
* this CA's key (always emitted, empty when none).
|
|
2191
937
|
*
|
|
2192
|
-
* The
|
|
2193
|
-
*
|
|
2194
|
-
*
|
|
2195
|
-
*
|
|
2196
|
-
* @param certificate - output path for the signed certificate
|
|
2197
|
-
* @param certificateSigningRequestFilename - path to the CSR
|
|
2198
|
-
* @param params1 - signing parameters (validity, dates, SANs)
|
|
2199
|
-
* @returns the path to the signed certificate
|
|
938
|
+
* The openssl backend builds its own now; this remains so that external
|
|
939
|
+
* code calling it keeps working, and lives here rather than on the core
|
|
940
|
+
* because the flag means nothing to a backend that spawns nothing.
|
|
2200
941
|
*/
|
|
2201
|
-
async
|
|
2202
|
-
await
|
|
2203
|
-
assert7(fs7.existsSync(certificateSigningRequestFilename));
|
|
2204
|
-
if (!certificateFileExist(certificate)) {
|
|
2205
|
-
return "";
|
|
2206
|
-
}
|
|
2207
|
-
adjustDate(params1);
|
|
2208
|
-
adjustApplicationUri(params1);
|
|
2209
|
-
processAltNames(params1);
|
|
2210
|
-
const options = { cwd: this.rootDir };
|
|
2211
|
-
const csr = await readCertificateSigningRequest(certificateSigningRequestFilename);
|
|
2212
|
-
const csrInfo = exploreCertificateSigningRequest(csr);
|
|
2213
|
-
const applicationUri = csrInfo.extensionRequest.subjectAltName.uniformResourceIdentifier ? csrInfo.extensionRequest.subjectAltName.uniformResourceIdentifier[0] : void 0;
|
|
2214
|
-
if (typeof applicationUri !== "string") {
|
|
2215
|
-
throw new Error("Cannot find applicationUri in CSR");
|
|
2216
|
-
}
|
|
2217
|
-
const dns = csrInfo.extensionRequest.subjectAltName.dNSName || [];
|
|
2218
|
-
let ip = csrInfo.extensionRequest.subjectAltName.iPAddress || [];
|
|
2219
|
-
ip = ip.map(octetStringToIpAddress);
|
|
2220
|
-
const params = {
|
|
2221
|
-
applicationUri,
|
|
2222
|
-
dns,
|
|
2223
|
-
ip
|
|
2224
|
-
};
|
|
2225
|
-
processAltNames(params);
|
|
2226
|
-
this._wireRevocationEnvVars();
|
|
2227
|
-
const configFile = generateStaticConfig("conf/caconfig.cnf", options);
|
|
2228
|
-
displaySubtitle("- then we ask the authority to sign the certificate signing request");
|
|
2229
|
-
const passin = await this._opensslPassin();
|
|
2230
|
-
await execute_openssl(
|
|
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 }
|
|
2247
|
-
);
|
|
2248
|
-
displaySubtitle("- dump the certificate for a check");
|
|
2249
|
-
await execute_openssl(["x509", "-in", n4(certificate), "-dates", "-fingerprint", "-purpose", "-noout"], options);
|
|
2250
|
-
displaySubtitle("- construct CA certificate with CRL");
|
|
2251
|
-
await this.constructCACertificateWithCRL();
|
|
2252
|
-
displaySubtitle("- construct certificate chain");
|
|
2253
|
-
await this.constructCertificateChain(certificate);
|
|
2254
|
-
displaySubtitle("- verify certificate against the root CA");
|
|
2255
|
-
await this.verifyCertificate(certificate);
|
|
2256
|
-
return certificate;
|
|
942
|
+
async _opensslPassin() {
|
|
943
|
+
return passinArg(await this._privateKeyPassphrase());
|
|
2257
944
|
}
|
|
2258
945
|
/**
|
|
2259
|
-
*
|
|
2260
|
-
*
|
|
2261
|
-
*
|
|
946
|
+
* @internal
|
|
947
|
+
* Legacy shim: publish the `CDP_URL` / `AIA_VALUE` config substitution
|
|
948
|
+
* values to the shared env registry, or unset them so the matching
|
|
949
|
+
* `{{#KEY}}...{{/KEY}}` blocks are stripped. Nothing in this package
|
|
950
|
+
* reads the registry any more - every openssl config render receives
|
|
951
|
+
* these values explicitly, from the same {@link caConfigEnvOverrides}
|
|
952
|
+
* builder this delegates to, so the two cannot drift - kept only for
|
|
953
|
+
* external code that renders openssl config templates against the
|
|
954
|
+
* registry directly.
|
|
955
|
+
*
|
|
956
|
+
* It lives on this class rather than the core because it is meaningful
|
|
957
|
+
* only to the openssl backend.
|
|
2262
958
|
*/
|
|
2263
|
-
|
|
2264
|
-
const
|
|
2265
|
-
if (
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
959
|
+
_wireRevocationEnvVars() {
|
|
960
|
+
const overrides = caConfigEnvOverrides(this);
|
|
961
|
+
if (overrides.CDP_URL) {
|
|
962
|
+
setEnv("CDP_URL", overrides.CDP_URL);
|
|
963
|
+
} else {
|
|
964
|
+
unsetEnv("CDP_URL");
|
|
965
|
+
}
|
|
966
|
+
if (overrides.AIA_VALUE) {
|
|
967
|
+
setEnv("AIA_VALUE", overrides.AIA_VALUE);
|
|
968
|
+
} else {
|
|
969
|
+
unsetEnv("AIA_VALUE");
|
|
2273
970
|
}
|
|
2274
971
|
}
|
|
2275
972
|
};
|
|
2276
973
|
|
|
2277
974
|
// packages/node-opcua-pki/lib/pki/certificate_manager.ts
|
|
2278
975
|
import { EventEmitter } from "events";
|
|
2279
|
-
import
|
|
2280
|
-
import
|
|
976
|
+
import fs7 from "fs";
|
|
977
|
+
import path4 from "path";
|
|
2281
978
|
import { drainPendingLocks, withLock } from "@ster5/global-mutex";
|
|
2282
|
-
import
|
|
979
|
+
import chalk3 from "chalk";
|
|
2283
980
|
import chokidar from "chokidar";
|
|
2284
981
|
import {
|
|
2285
|
-
|
|
982
|
+
caSignerFromKeyOperations,
|
|
983
|
+
exploreCertificate,
|
|
2286
984
|
exploreCertificateInfo,
|
|
2287
985
|
exploreCertificateRevocationList,
|
|
2288
|
-
generatePrivateKeyFile
|
|
986
|
+
generatePrivateKeyFile,
|
|
987
|
+
keyOperationsFromPrivateKey,
|
|
2289
988
|
makeSHA1Thumbprint,
|
|
989
|
+
PrivateKeyUnavailableError,
|
|
2290
990
|
readCertificateChain,
|
|
2291
991
|
readCertificateChainAsync,
|
|
2292
992
|
readCertificateRevocationList,
|
|
2293
|
-
readPrivateKey
|
|
993
|
+
readPrivateKey,
|
|
2294
994
|
split_der,
|
|
2295
|
-
toPem
|
|
995
|
+
toPem,
|
|
2296
996
|
verifyCertificateSignature,
|
|
2297
|
-
writePrivateKeyFile
|
|
997
|
+
writePrivateKeyFile
|
|
2298
998
|
} from "node-opcua-crypto";
|
|
2299
999
|
|
|
2300
1000
|
// packages/node-opcua-pki/lib/toolbox/without_openssl/create_certificate_signing_request.ts
|
|
2301
|
-
import
|
|
2302
|
-
import
|
|
1001
|
+
import assert3 from "assert";
|
|
1002
|
+
import fs5 from "fs";
|
|
2303
1003
|
import { coercePrivateKeyPem, createCertificateSigningRequest, pemToPrivateKey, Subject as Subject3 } from "node-opcua-crypto";
|
|
2304
1004
|
async function createCertificateSigningRequestAsync(certificateSigningRequestFilename, params) {
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
1005
|
+
assert3(params);
|
|
1006
|
+
assert3(params.rootDir);
|
|
1007
|
+
assert3(params.configFile);
|
|
1008
|
+
assert3(params.privateKey);
|
|
2309
1009
|
if (typeof params.privateKey === "string") {
|
|
2310
|
-
|
|
1010
|
+
assert3(fs5.existsSync(params.privateKey), `Private key must exist${params.privateKey}`);
|
|
2311
1011
|
}
|
|
2312
|
-
|
|
2313
|
-
|
|
1012
|
+
assert3(fs5.existsSync(params.rootDir), "RootDir key must exist");
|
|
1013
|
+
assert3(typeof certificateSigningRequestFilename === "string");
|
|
2314
1014
|
const subject = params.subject ? new Subject3(params.subject).toString() : void 0;
|
|
2315
1015
|
displaySubtitle("- Creating a Certificate Signing Request with subtile");
|
|
2316
|
-
const
|
|
2317
|
-
|
|
1016
|
+
const privateKey = isOpaqueSigner(params.privateKey) ? params.privateKey : await pemToPrivateKey(
|
|
1017
|
+
typeof params.privateKey === "string" ? await fs5.promises.readFile(params.privateKey, "utf-8") : coercePrivateKeyPem(params.privateKey)
|
|
1018
|
+
);
|
|
2318
1019
|
const { csr } = await createCertificateSigningRequest({
|
|
2319
1020
|
privateKey,
|
|
2320
1021
|
dns: params.dns,
|
|
@@ -2323,42 +1024,45 @@ async function createCertificateSigningRequestAsync(certificateSigningRequestFil
|
|
|
2323
1024
|
applicationUri: params.applicationUri,
|
|
2324
1025
|
purpose: params.purpose
|
|
2325
1026
|
});
|
|
2326
|
-
await
|
|
2327
|
-
display(
|
|
1027
|
+
await fs5.promises.writeFile(certificateSigningRequestFilename, csr, "utf-8");
|
|
1028
|
+
display(
|
|
1029
|
+
`- privateKey ${typeof params.privateKey === "string" ? params.privateKey : isOpaqueSigner(params.privateKey) ? "<opaque-signer>" : "<in-memory>"}`
|
|
1030
|
+
);
|
|
2328
1031
|
display(`- certificateSigningRequestFilename ${certificateSigningRequestFilename}`);
|
|
2329
1032
|
}
|
|
2330
1033
|
|
|
2331
1034
|
// packages/node-opcua-pki/lib/toolbox/without_openssl/create_self_signed_certificate.ts
|
|
2332
|
-
import
|
|
2333
|
-
import
|
|
1035
|
+
import assert4 from "assert";
|
|
1036
|
+
import fs6 from "fs";
|
|
2334
1037
|
import {
|
|
2335
|
-
CertificatePurpose
|
|
1038
|
+
CertificatePurpose,
|
|
2336
1039
|
coercePrivateKeyPem as coercePrivateKeyPem2,
|
|
2337
1040
|
createSelfSignedCertificate as createSelfSignedCertificate1,
|
|
2338
1041
|
pemToPrivateKey as pemToPrivateKey2,
|
|
2339
1042
|
Subject as Subject4
|
|
2340
1043
|
} from "node-opcua-crypto";
|
|
2341
1044
|
async function createSelfSignedCertificateAsync(certificate, params) {
|
|
2342
|
-
params.purpose = params.purpose ||
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
1045
|
+
params.purpose = params.purpose || CertificatePurpose.ForApplication;
|
|
1046
|
+
assert4(params.purpose, "Please provide a Certificate Purpose");
|
|
1047
|
+
assert4(fs6.existsSync(params.configFile));
|
|
1048
|
+
assert4(fs6.existsSync(params.rootDir));
|
|
2346
1049
|
if (typeof params.privateKey === "string") {
|
|
2347
|
-
|
|
1050
|
+
assert4(fs6.existsSync(params.privateKey));
|
|
2348
1051
|
}
|
|
2349
1052
|
if (!params.subject) {
|
|
2350
1053
|
throw Error("Missing subject");
|
|
2351
1054
|
}
|
|
2352
|
-
|
|
2353
|
-
|
|
1055
|
+
assert4(typeof params.applicationUri === "string");
|
|
1056
|
+
assert4(Array.isArray(params.dns));
|
|
2354
1057
|
adjustDate(params);
|
|
2355
|
-
|
|
1058
|
+
assert4(Object.prototype.hasOwnProperty.call(params, "validity"));
|
|
2356
1059
|
let subject = new Subject4(params.subject);
|
|
2357
1060
|
subject = subject.toString();
|
|
2358
1061
|
const purpose = params.purpose;
|
|
2359
1062
|
displayTitle("Generate a certificate request");
|
|
2360
|
-
const
|
|
2361
|
-
|
|
1063
|
+
const privateKey = isOpaqueSigner(params.privateKey) ? params.privateKey : await pemToPrivateKey2(
|
|
1064
|
+
typeof params.privateKey === "string" ? await fs6.promises.readFile(params.privateKey, "utf-8") : coercePrivateKeyPem2(params.privateKey)
|
|
1065
|
+
);
|
|
2362
1066
|
const { cert } = await createSelfSignedCertificate1({
|
|
2363
1067
|
privateKey,
|
|
2364
1068
|
notBefore: params.startDate,
|
|
@@ -2370,18 +1074,22 @@ async function createSelfSignedCertificateAsync(certificate, params) {
|
|
|
2370
1074
|
applicationUri: params.applicationUri,
|
|
2371
1075
|
purpose
|
|
2372
1076
|
});
|
|
2373
|
-
await
|
|
1077
|
+
await fs6.promises.writeFile(certificate, cert, "utf-8");
|
|
2374
1078
|
}
|
|
2375
1079
|
async function createSelfSignedCertificate(certificate, params) {
|
|
2376
1080
|
await createSelfSignedCertificateAsync(certificate, params);
|
|
2377
1081
|
}
|
|
2378
1082
|
|
|
1083
|
+
// packages/node-opcua-pki/lib/pki/templates/simple_config_template.cnf.ts
|
|
1084
|
+
var config = '##################################################################################################\n## SIMPLE OPENSSL CONFIG FILE FOR SELF-SIGNED CERTIFICATE GENERATION\n################################################################################################################\n\ndistinguished_name = req_distinguished_name\ndefault_md = sha1\n\ndefault_md = sha256 # The default digest algorithm\n\n[ v3_ca ]\nsubjectKeyIdentifier = hash\nauthorityKeyIdentifier = keyid:always,issuer:always\n\n# authorityKeyIdentifier = keyid\nbasicConstraints = CA:TRUE\nkeyUsage = critical, cRLSign, keyCertSign\nnsComment = "Self-signed Certificate for CA generated by Node-OPCUA Certificate utility"\n#nsCertType = sslCA, emailCA\n#subjectAltName = email:copy\n#issuerAltName = issuer:copy\n#obj = DER:02:03\n# crlDistributionPoints = @crl_info\n# [ crl_info ]\n# URI.0 = http://localhost:8900/crl.pem\nsubjectAltName = $ENV::ALTNAME\n\n[ req ]\ndays = 390\nreq_extensions = v3_req\nx509_extensions = v3_ca\n\n[v3_req]\nbasicConstraints = CA:false\nkeyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment\nsubjectAltName = $ENV::ALTNAME\n\n[ v3_ca_signed]\nsubjectKeyIdentifier = hash\nauthorityKeyIdentifier = keyid,issuer\nbasicConstraints = critical, CA:FALSE\nkeyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment\nextendedKeyUsage = clientAuth,serverAuth \nnsComment = "certificate generated by Node-OPCUA Certificate utility and signed by a CA"\nsubjectAltName = $ENV::ALTNAME\n[ v3_selfsigned]\nsubjectKeyIdentifier = hash\nauthorityKeyIdentifier = keyid,issuer\nbasicConstraints = critical, CA:FALSE\nkeyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment\nextendedKeyUsage = clientAuth,serverAuth \nnsComment = "Self-signed certificate generated by Node-OPCUA Certificate utility"\nsubjectAltName = $ENV::ALTNAME\n[ req_distinguished_name ]\ncountryName = Country Name (2 letter code)\ncountryName_default = FR\ncountryName_min = 2\ncountryName_max = 2\n# stateOrProvinceName = State or Province Name (full name)\n# stateOrProvinceName_default = Ile de France\n# localityName = Locality Name (city, district)\n# localityName_default = Paris\norganizationName = Organization Name (company)\norganizationName_default = NodeOPCUA\n# organizationalUnitName = Organizational Unit Name (department, division)\n# organizationalUnitName_default = R&D\ncommonName = Common Name (hostname, FQDN, IP, or your name)\ncommonName_max = 256\ncommonName_default = NodeOPCUA\n# emailAddress = Email Address\n# emailAddress_max = 40\n# emailAddress_default = node-opcua (at) node-opcua (dot) com\nsubjectAltName = $ENV::ALTNAME';
|
|
1085
|
+
var simple_config_template_cnf_default = config;
|
|
1086
|
+
|
|
2379
1087
|
// packages/node-opcua-pki/lib/pki/certificate_manager.ts
|
|
2380
|
-
var
|
|
2381
|
-
var fsWriteFile =
|
|
1088
|
+
var configurationFileSimpleTemplate = simple_config_template_cnf_default;
|
|
1089
|
+
var fsWriteFile = fs7.promises.writeFile;
|
|
2382
1090
|
function getOrComputeInfo(entry) {
|
|
2383
1091
|
if (!entry.info) {
|
|
2384
|
-
entry.info =
|
|
1092
|
+
entry.info = exploreCertificate(entry.certificate);
|
|
2385
1093
|
}
|
|
2386
1094
|
return entry.info;
|
|
2387
1095
|
}
|
|
@@ -2425,7 +1133,7 @@ function buildIdealCertificateName(certificate) {
|
|
|
2425
1133
|
const chain = coerceCertificateChain(certificate);
|
|
2426
1134
|
const fingerprint = makeFingerprint(chain);
|
|
2427
1135
|
try {
|
|
2428
|
-
const commonName =
|
|
1136
|
+
const commonName = exploreCertificate(chain[0]).tbsCertificate.subject.commonName || "";
|
|
2429
1137
|
const sanitizedCommonName = commonName.replace(forbiddenChars, "_");
|
|
2430
1138
|
return `${sanitizedCommonName}[${fingerprint}]`;
|
|
2431
1139
|
} catch (_err) {
|
|
@@ -2442,7 +1150,7 @@ function isSelfSigned2(info) {
|
|
|
2442
1150
|
return info.tbsCertificate.extensions?.subjectKeyIdentifier === info.tbsCertificate.extensions?.authorityKeyIdentifier?.keyIdentifier;
|
|
2443
1151
|
}
|
|
2444
1152
|
function isSelfSigned3(certificate) {
|
|
2445
|
-
const info =
|
|
1153
|
+
const info = exploreCertificate(certificate);
|
|
2446
1154
|
return isSelfSigned2(info);
|
|
2447
1155
|
}
|
|
2448
1156
|
function _isIssuerInfo(info) {
|
|
@@ -2458,7 +1166,7 @@ function _isIssuerInfo(info) {
|
|
|
2458
1166
|
}
|
|
2459
1167
|
function isIssuer(certificate) {
|
|
2460
1168
|
try {
|
|
2461
|
-
const info =
|
|
1169
|
+
const info = exploreCertificate(certificate);
|
|
2462
1170
|
return _isIssuerInfo(info);
|
|
2463
1171
|
} catch (_err) {
|
|
2464
1172
|
return false;
|
|
@@ -2466,7 +1174,7 @@ function isIssuer(certificate) {
|
|
|
2466
1174
|
}
|
|
2467
1175
|
function isIntermediateIssuer(certificate) {
|
|
2468
1176
|
try {
|
|
2469
|
-
const info =
|
|
1177
|
+
const info = exploreCertificate(certificate);
|
|
2470
1178
|
if (!_isIssuerInfo(info)) {
|
|
2471
1179
|
return false;
|
|
2472
1180
|
}
|
|
@@ -2477,7 +1185,7 @@ function isIntermediateIssuer(certificate) {
|
|
|
2477
1185
|
}
|
|
2478
1186
|
function isRootIssuer(certificate) {
|
|
2479
1187
|
try {
|
|
2480
|
-
const info =
|
|
1188
|
+
const info = exploreCertificate(certificate);
|
|
2481
1189
|
if (!_isIssuerInfo(info)) {
|
|
2482
1190
|
return false;
|
|
2483
1191
|
}
|
|
@@ -2492,7 +1200,7 @@ function findIssuerCertificateInChain(certificate, chain) {
|
|
|
2492
1200
|
if (!firstCertificate) {
|
|
2493
1201
|
return null;
|
|
2494
1202
|
}
|
|
2495
|
-
const certInfo =
|
|
1203
|
+
const certInfo = exploreCertificate(firstCertificate);
|
|
2496
1204
|
if (isSelfSigned2(certInfo)) {
|
|
2497
1205
|
return firstCertificate;
|
|
2498
1206
|
}
|
|
@@ -2503,7 +1211,7 @@ function findIssuerCertificateInChain(certificate, chain) {
|
|
|
2503
1211
|
}
|
|
2504
1212
|
const coercedChain = coerceCertificateChain(chain);
|
|
2505
1213
|
const potentialIssuers = coercedChain.filter((c) => {
|
|
2506
|
-
const info =
|
|
1214
|
+
const info = exploreCertificate(c);
|
|
2507
1215
|
return info.tbsCertificate.extensions && info.tbsCertificate.extensions.subjectKeyIdentifier === wantedIssuerKey;
|
|
2508
1216
|
});
|
|
2509
1217
|
if (potentialIssuers.length === 1) {
|
|
@@ -2538,7 +1246,31 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
2538
1246
|
// even if the consumer forgets to call dispose().
|
|
2539
1247
|
static #activeInstances = /* @__PURE__ */ new Set();
|
|
2540
1248
|
static #cleanupInstalled = false;
|
|
2541
|
-
static #
|
|
1249
|
+
static #exitHandler;
|
|
1250
|
+
/**
|
|
1251
|
+
* Install a best-effort `exit` hook that closes any watcher
|
|
1252
|
+
* still open when the process terminates.
|
|
1253
|
+
*
|
|
1254
|
+
* **This library never terminates the host process.** No
|
|
1255
|
+
* SIGINT/SIGTERM handler is installed: deciding how (and
|
|
1256
|
+
* whether) to shut down on a signal is the application's
|
|
1257
|
+
* responsibility, and a listener registered here would both
|
|
1258
|
+
* pre-empt the application's own graceful shutdown and
|
|
1259
|
+
* silently suppress Node's default signal behaviour.
|
|
1260
|
+
*
|
|
1261
|
+
* Nothing here is load-bearing for process exit. The native
|
|
1262
|
+
* `fs.watch` handles are `unref()`'d when the watchers are
|
|
1263
|
+
* created (see `#readCertificates`), so an undisposed
|
|
1264
|
+
* CertificateManager never keeps the event loop alive. This
|
|
1265
|
+
* hook is only tidiness on the way out.
|
|
1266
|
+
*
|
|
1267
|
+
* `exit` rather than `beforeExit`: `beforeExit` fires when
|
|
1268
|
+
* the loop merely drains and the loop can subsequently be
|
|
1269
|
+
* resurrected, which would leave a still-in-use instance
|
|
1270
|
+
* marked Disposed. `exit` is terminal, synchronous-only and
|
|
1271
|
+
* cannot alter the exit code.
|
|
1272
|
+
*/
|
|
1273
|
+
static #installExitCleanup() {
|
|
2542
1274
|
if (_CertificateManager.#cleanupInstalled) return;
|
|
2543
1275
|
_CertificateManager.#cleanupInstalled = true;
|
|
2544
1276
|
const closeDanglingWatchers = () => {
|
|
@@ -2554,13 +1286,22 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
2554
1286
|
}
|
|
2555
1287
|
_CertificateManager.#activeInstances.clear();
|
|
2556
1288
|
};
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
1289
|
+
_CertificateManager.#exitHandler = closeDanglingWatchers;
|
|
1290
|
+
process.on("exit", closeDanglingWatchers);
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* Remove the `exit` hook once the last instance is disposed,
|
|
1294
|
+
* so a library that is initialized and disposed repeatedly
|
|
1295
|
+
* does not accumulate process listeners. A later
|
|
1296
|
+
* `initialize()` re-arms it.
|
|
1297
|
+
*/
|
|
1298
|
+
static #uninstallExitCleanupIfIdle() {
|
|
1299
|
+
if (_CertificateManager.#activeInstances.size > 0) return;
|
|
1300
|
+
if (_CertificateManager.#exitHandler) {
|
|
1301
|
+
process.removeListener("exit", _CertificateManager.#exitHandler);
|
|
1302
|
+
_CertificateManager.#exitHandler = void 0;
|
|
2563
1303
|
}
|
|
1304
|
+
_CertificateManager.#cleanupInstalled = false;
|
|
2564
1305
|
}
|
|
2565
1306
|
/**
|
|
2566
1307
|
* Dispose **all** active CertificateManager instances,
|
|
@@ -2626,6 +1367,9 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
2626
1367
|
#disableFileWatchers;
|
|
2627
1368
|
#privateKeyPassphrase;
|
|
2628
1369
|
#privateKeyProvider;
|
|
1370
|
+
#keyOperations;
|
|
1371
|
+
/** The stable lazy wrap handed out by {@link getKeyOperations} for non-opaque configurations. */
|
|
1372
|
+
#lazyLocalKeyOperations;
|
|
2629
1373
|
/**
|
|
2630
1374
|
* The on-disk key, decrypted once and kept for the instance's lifetime,
|
|
2631
1375
|
* so the passphrase (or its resolver function) is consulted at most
|
|
@@ -2669,14 +1413,25 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
2669
1413
|
this.#disableFileWatchers = options.disableFileWatchers ?? process.env.OPCUA_PKI_DISABLE_FILE_WATCHERS === "true";
|
|
2670
1414
|
this.#privateKeyPassphrase = options.privateKeyPassphrase;
|
|
2671
1415
|
this.#privateKeyProvider = options.privateKeyProvider;
|
|
1416
|
+
this.#keyOperations = options.keyOperations;
|
|
1417
|
+
if (this.#keyOperations && this.#privateKeyProvider) {
|
|
1418
|
+
throw new Error(
|
|
1419
|
+
"CertificateManager: 'keyOperations' and 'privateKeyProvider' are mutually exclusive: one hides the key, the other sources its material"
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
if (this.#keyOperations && this.#privateKeyPassphrase) {
|
|
1423
|
+
throw new Error(
|
|
1424
|
+
"CertificateManager: 'privateKeyPassphrase' is meaningless with 'keyOperations': there is no key material for a passphrase to protect"
|
|
1425
|
+
);
|
|
1426
|
+
}
|
|
2672
1427
|
mkdirRecursiveSync(options.location);
|
|
2673
|
-
if (!
|
|
1428
|
+
if (!fs7.existsSync(this.#location)) {
|
|
2674
1429
|
throw new Error(`CertificateManager cannot access location ${this.#location}`);
|
|
2675
1430
|
}
|
|
2676
1431
|
}
|
|
2677
1432
|
/** Path to the OpenSSL configuration file. */
|
|
2678
1433
|
get configFile() {
|
|
2679
|
-
return
|
|
1434
|
+
return path4.join(this.rootDir, "own/openssl.cnf");
|
|
2680
1435
|
}
|
|
2681
1436
|
/** Root directory of the PKI store. */
|
|
2682
1437
|
get rootDir() {
|
|
@@ -2692,7 +1447,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
2692
1447
|
* be no meaningful file in that case).
|
|
2693
1448
|
*/
|
|
2694
1449
|
get privateKey() {
|
|
2695
|
-
return
|
|
1450
|
+
return path4.join(this.rootDir, "own/private/private_key.pem");
|
|
2696
1451
|
}
|
|
2697
1452
|
/**
|
|
2698
1453
|
* Resolve the private key: from `privateKeyProvider` if configured,
|
|
@@ -2710,6 +1465,11 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
2710
1465
|
* authority on what the current key is.
|
|
2711
1466
|
*/
|
|
2712
1467
|
async getPrivateKey() {
|
|
1468
|
+
if (this.#keyOperations) {
|
|
1469
|
+
throw new PrivateKeyUnavailableError(
|
|
1470
|
+
"CertificateManager.getPrivateKey is not available when keyOperations is configured: the private key is held by the key-operations provider (HSM/KMS) and cannot be read \u2014 use getKeyOperations() instead"
|
|
1471
|
+
);
|
|
1472
|
+
}
|
|
2713
1473
|
if (this.#privateKeyProvider) {
|
|
2714
1474
|
return await this.#privateKeyProvider.getPrivateKey();
|
|
2715
1475
|
}
|
|
@@ -2719,7 +1479,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
2719
1479
|
if (!this.#privateKeyPromise) {
|
|
2720
1480
|
this.#privateKeyPromise = (async () => {
|
|
2721
1481
|
const passphrase = await resolvePrivateKeyPassphrase(this.#privateKeyPassphrase);
|
|
2722
|
-
return
|
|
1482
|
+
return readPrivateKey(this.privateKey, passphrase);
|
|
2723
1483
|
})().then(
|
|
2724
1484
|
(key) => {
|
|
2725
1485
|
this.#cachedPrivateKey = key;
|
|
@@ -2735,6 +1495,75 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
2735
1495
|
}
|
|
2736
1496
|
/** In-flight first read of the on-disk key, so concurrent callers share one passphrase resolution. */
|
|
2737
1497
|
#privateKeyPromise;
|
|
1498
|
+
/**
|
|
1499
|
+
* True when this manager's key is opaque — configured through
|
|
1500
|
+
* `keyOperations`, held by an HSM/KMS, never obtainable as material.
|
|
1501
|
+
* When true, {@link getPrivateKey} throws `PrivateKeyUnavailableError`
|
|
1502
|
+
* and {@link getKeyOperations} is the only way to use the key.
|
|
1503
|
+
*/
|
|
1504
|
+
isPrivateKeyOpaque() {
|
|
1505
|
+
return !!this.#keyOperations;
|
|
1506
|
+
}
|
|
1507
|
+
/**
|
|
1508
|
+
* The key as an opaque {@link IKeyOperations} — the recommended way to
|
|
1509
|
+
* *use* the private key regardless of where it lives.
|
|
1510
|
+
*
|
|
1511
|
+
* Returns the configured `keyOperations` object when the key is opaque.
|
|
1512
|
+
* Otherwise returns a stable lazy wrap over {@link getPrivateKey}: its
|
|
1513
|
+
* methods resolve the key on first use (disk read, passphrase,
|
|
1514
|
+
* `privateKeyProvider` — all async), so the wrap offers no synchronous
|
|
1515
|
+
* fast path; callers that need one resolve the key themselves and build
|
|
1516
|
+
* a `LocalKeyOperations` over it. The wrap follows key rotation: a
|
|
1517
|
+
* `privateKeyProvider` that starts returning a different key gets a
|
|
1518
|
+
* fresh underlying `LocalKeyOperations`.
|
|
1519
|
+
*/
|
|
1520
|
+
getKeyOperations() {
|
|
1521
|
+
if (this.#keyOperations) {
|
|
1522
|
+
return this.#keyOperations;
|
|
1523
|
+
}
|
|
1524
|
+
if (!this.#lazyLocalKeyOperations) {
|
|
1525
|
+
let cached;
|
|
1526
|
+
const resolve = async () => {
|
|
1527
|
+
const key = await this.getPrivateKey();
|
|
1528
|
+
if (!cached || cached.key !== key) {
|
|
1529
|
+
cached = { key, ops: keyOperationsFromPrivateKey(key) };
|
|
1530
|
+
}
|
|
1531
|
+
return cached.ops;
|
|
1532
|
+
};
|
|
1533
|
+
this.#lazyLocalKeyOperations = {
|
|
1534
|
+
sign: async (data, params) => (await resolve()).sign(data, params),
|
|
1535
|
+
decryptBlock: async (block, params) => (await resolve()).decryptBlock(block, params),
|
|
1536
|
+
getKeyMetadata: async () => (await resolve()).getKeyMetadata(),
|
|
1537
|
+
getPublicKey: async () => (await resolve()).getPublicKey()
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1540
|
+
return this.#lazyLocalKeyOperations;
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Fail closed at `initialize()` time, not on the first certificate
|
|
1544
|
+
* operation, if the key cannot actually be used: wrong/missing
|
|
1545
|
+
* passphrase on an encrypted key, a broken `privateKeyProvider`, or an
|
|
1546
|
+
* unreachable `keyOperations` provider (probed via `getKeyMetadata`).
|
|
1547
|
+
*/
|
|
1548
|
+
async #probePrivateKey() {
|
|
1549
|
+
if (this.#keyOperations) {
|
|
1550
|
+
await this.#keyOperations.getKeyMetadata();
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
await this.getPrivateKey();
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* The key to hand to a certificate-issuance primitive: the raw
|
|
1557
|
+
* {@link PrivateKey} for a local configuration, or a {@link CaSigner}
|
|
1558
|
+
* adapted from `keyOperations` when the key is opaque — which requires
|
|
1559
|
+
* the provider to implement `getPublicKey` (the adapter says so if not).
|
|
1560
|
+
*/
|
|
1561
|
+
async #resolveSigningKey() {
|
|
1562
|
+
if (this.#keyOperations) {
|
|
1563
|
+
return caSignerFromKeyOperations(this.#keyOperations);
|
|
1564
|
+
}
|
|
1565
|
+
return await this.getPrivateKey();
|
|
1566
|
+
}
|
|
2738
1567
|
/**
|
|
2739
1568
|
* Enable, disable, or rotate the passphrase protecting the on-disk
|
|
2740
1569
|
* private key: decrypt with `oldPassphrase` (omit if the key is
|
|
@@ -2756,13 +1585,18 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
2756
1585
|
* disk file for this method to rewrite).
|
|
2757
1586
|
*/
|
|
2758
1587
|
async reencryptPrivateKey(oldPassphrase, newPassphrase) {
|
|
1588
|
+
if (this.#keyOperations) {
|
|
1589
|
+
throw new PrivateKeyUnavailableError(
|
|
1590
|
+
"reencryptPrivateKey: not supported when keyOperations is configured \u2014 there is no key material to rewrite"
|
|
1591
|
+
);
|
|
1592
|
+
}
|
|
2759
1593
|
if (this.#privateKeyProvider) {
|
|
2760
1594
|
throw new Error("reencryptPrivateKey: not supported when a privateKeyProvider is configured");
|
|
2761
1595
|
}
|
|
2762
1596
|
const oldPass = await resolvePrivateKeyPassphrase(oldPassphrase);
|
|
2763
1597
|
const newPass = await resolvePrivateKeyPassphrase(newPassphrase);
|
|
2764
1598
|
await this.withLock2(async () => {
|
|
2765
|
-
const privateKey =
|
|
1599
|
+
const privateKey = readPrivateKey(this.privateKey, oldPass);
|
|
2766
1600
|
await this.#rewritePrivateKeyFile(privateKey, newPass);
|
|
2767
1601
|
});
|
|
2768
1602
|
this.#cachedPrivateKey = void 0;
|
|
@@ -2778,15 +1612,15 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
2778
1612
|
async #rewritePrivateKeyFile(privateKey, passphrase) {
|
|
2779
1613
|
const tmpFilename = `${this.privateKey}.${process.pid}-${Date.now()}.tmp`;
|
|
2780
1614
|
try {
|
|
2781
|
-
await
|
|
2782
|
-
await
|
|
1615
|
+
await writePrivateKeyFile(tmpFilename, privateKey, { passphrase });
|
|
1616
|
+
await fs7.promises.rename(tmpFilename, this.privateKey);
|
|
2783
1617
|
} finally {
|
|
2784
|
-
await
|
|
1618
|
+
await fs7.promises.rm(tmpFilename, { force: true });
|
|
2785
1619
|
}
|
|
2786
1620
|
}
|
|
2787
1621
|
/** Path to the OpenSSL random seed file. */
|
|
2788
1622
|
get randomFile() {
|
|
2789
|
-
return
|
|
1623
|
+
return path4.join(this.rootDir, "./random.rnd");
|
|
2790
1624
|
}
|
|
2791
1625
|
/**
|
|
2792
1626
|
* Move a certificate to the rejected store.
|
|
@@ -2823,30 +1657,30 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
2823
1657
|
}
|
|
2824
1658
|
/** Path to the rejected certificates folder. */
|
|
2825
1659
|
get rejectedFolder() {
|
|
2826
|
-
return
|
|
1660
|
+
return path4.join(this.rootDir, "rejected");
|
|
2827
1661
|
}
|
|
2828
1662
|
/** Path to the trusted certificates folder. */
|
|
2829
1663
|
get trustedFolder() {
|
|
2830
|
-
return
|
|
1664
|
+
return path4.join(this.rootDir, "trusted/certs");
|
|
2831
1665
|
}
|
|
2832
1666
|
/** Path to the trusted CRL folder. */
|
|
2833
1667
|
get crlFolder() {
|
|
2834
|
-
return
|
|
1668
|
+
return path4.join(this.rootDir, "trusted/crl");
|
|
2835
1669
|
}
|
|
2836
1670
|
/** Path to the issuer (CA) certificates folder. */
|
|
2837
1671
|
get issuersCertFolder() {
|
|
2838
|
-
return
|
|
1672
|
+
return path4.join(this.rootDir, "issuers/certs");
|
|
2839
1673
|
}
|
|
2840
1674
|
/** Path to the issuer CRL folder. */
|
|
2841
1675
|
get issuersCrlFolder() {
|
|
2842
|
-
return
|
|
1676
|
+
return path4.join(this.rootDir, "issuers/crl");
|
|
2843
1677
|
}
|
|
2844
1678
|
/** Path to the own certificate folder. */
|
|
2845
1679
|
get ownCertFolder() {
|
|
2846
|
-
return
|
|
1680
|
+
return path4.join(this.rootDir, "own/certs");
|
|
2847
1681
|
}
|
|
2848
1682
|
get ownPrivateFolder() {
|
|
2849
|
-
return
|
|
1683
|
+
return path4.join(this.rootDir, "own/private");
|
|
2850
1684
|
}
|
|
2851
1685
|
/**
|
|
2852
1686
|
* Check if a certificate is in the trusted store.
|
|
@@ -2881,9 +1715,9 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
2881
1715
|
} catch (_err) {
|
|
2882
1716
|
return "BadCertificateInvalid";
|
|
2883
1717
|
}
|
|
2884
|
-
const filename =
|
|
1718
|
+
const filename = path4.join(this.rejectedFolder, `${buildIdealCertificateName(leafCertificate)}.pem`);
|
|
2885
1719
|
debugLog("certificate has never been seen before and is now rejected (untrusted) ", filename);
|
|
2886
|
-
await fsWriteFile(filename,
|
|
1720
|
+
await fsWriteFile(filename, toPem(chain, "CERTIFICATE"));
|
|
2887
1721
|
this.#thumbs.rejected.set(fingerprint, { certificate: leafCertificate, filename });
|
|
2888
1722
|
}
|
|
2889
1723
|
return "BadCertificateUntrusted";
|
|
@@ -2897,7 +1731,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
2897
1731
|
}
|
|
2898
1732
|
const chain = coerceCertificateChain(certificateOrChain);
|
|
2899
1733
|
debugLog("NB CERTIFICATE IN CHAIN = ", chain.length);
|
|
2900
|
-
const info =
|
|
1734
|
+
const info = exploreCertificate(chain[0]);
|
|
2901
1735
|
let hasValidIssuer = false;
|
|
2902
1736
|
let hasTrustedIssuer = false;
|
|
2903
1737
|
const hasIssuerKey = info.tbsCertificate.extensions?.authorityKeyIdentifier?.keyIdentifier;
|
|
@@ -2985,23 +1819,19 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
2985
1819
|
return "BadCertificateUntrusted" /* BadCertificateUntrusted */;
|
|
2986
1820
|
}
|
|
2987
1821
|
}
|
|
2988
|
-
const
|
|
2989
|
-
debugLog("chain[1] info=", _c2);
|
|
2990
|
-
const certificateInfo = exploreCertificateInfo(chain[0]);
|
|
1822
|
+
const { validity } = exploreCertificate(chain[0]).tbsCertificate;
|
|
2991
1823
|
const now = /* @__PURE__ */ new Date();
|
|
2992
1824
|
let isTimeInvalid = false;
|
|
2993
|
-
if (
|
|
1825
|
+
if (validity.notBefore.getTime() > now.getTime()) {
|
|
2994
1826
|
debugLog(
|
|
2995
|
-
`${
|
|
1827
|
+
`${chalk3.red("certificate is invalid : certificate is not active yet !")} not before date =${validity.notBefore}`
|
|
2996
1828
|
);
|
|
2997
1829
|
if (!options.acceptPendingCertificate) {
|
|
2998
1830
|
isTimeInvalid = true;
|
|
2999
1831
|
}
|
|
3000
1832
|
}
|
|
3001
|
-
if (
|
|
3002
|
-
debugLog(
|
|
3003
|
-
`${chalk6.red("certificate is invalid : certificate has expired !")} not after date =${certificateInfo.notAfter}`
|
|
3004
|
-
);
|
|
1833
|
+
if (validity.notAfter.getTime() <= now.getTime()) {
|
|
1834
|
+
debugLog(`${chalk3.red("certificate is invalid : certificate has expired !")} not after date =${validity.notAfter}`);
|
|
3005
1835
|
if (!options.acceptOutdatedCertificate) {
|
|
3006
1836
|
isTimeInvalid = true;
|
|
3007
1837
|
}
|
|
@@ -3040,7 +1870,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
3040
1870
|
const chain = coerceCertificateChain(certificate);
|
|
3041
1871
|
for (const element of chain) {
|
|
3042
1872
|
try {
|
|
3043
|
-
|
|
1873
|
+
exploreCertificate(element);
|
|
3044
1874
|
} catch (_err) {
|
|
3045
1875
|
return "BadCertificateInvalid" /* BadCertificateInvalid */;
|
|
3046
1876
|
}
|
|
@@ -3093,55 +1923,55 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
3093
1923
|
this.#initializingPromise = void 0;
|
|
3094
1924
|
this.state = 2 /* Initialized */;
|
|
3095
1925
|
_CertificateManager.#activeInstances.add(this);
|
|
3096
|
-
_CertificateManager.#
|
|
1926
|
+
_CertificateManager.#installExitCleanup();
|
|
3097
1927
|
}
|
|
3098
1928
|
async #initialize() {
|
|
3099
1929
|
const pkiDir = this.#location;
|
|
3100
1930
|
mkdirRecursiveSync(pkiDir);
|
|
3101
|
-
mkdirRecursiveSync(
|
|
3102
|
-
mkdirRecursiveSync(
|
|
3103
|
-
ensurePrivateDirectory(
|
|
3104
|
-
mkdirRecursiveSync(
|
|
3105
|
-
mkdirRecursiveSync(
|
|
3106
|
-
mkdirRecursiveSync(
|
|
3107
|
-
mkdirRecursiveSync(
|
|
3108
|
-
mkdirRecursiveSync(
|
|
3109
|
-
mkdirRecursiveSync(
|
|
3110
|
-
mkdirRecursiveSync(
|
|
3111
|
-
const ownsDiskKey = !this.#privateKeyProvider;
|
|
3112
|
-
const needsKeyGeneration = ownsDiskKey && !
|
|
1931
|
+
mkdirRecursiveSync(path4.join(pkiDir, "own"));
|
|
1932
|
+
mkdirRecursiveSync(path4.join(pkiDir, "own/certs"));
|
|
1933
|
+
ensurePrivateDirectory(path4.join(pkiDir, "own/private"));
|
|
1934
|
+
mkdirRecursiveSync(path4.join(pkiDir, "rejected"));
|
|
1935
|
+
mkdirRecursiveSync(path4.join(pkiDir, "trusted"));
|
|
1936
|
+
mkdirRecursiveSync(path4.join(pkiDir, "trusted/certs"));
|
|
1937
|
+
mkdirRecursiveSync(path4.join(pkiDir, "trusted/crl"));
|
|
1938
|
+
mkdirRecursiveSync(path4.join(pkiDir, "issuers"));
|
|
1939
|
+
mkdirRecursiveSync(path4.join(pkiDir, "issuers/certs"));
|
|
1940
|
+
mkdirRecursiveSync(path4.join(pkiDir, "issuers/crl"));
|
|
1941
|
+
const ownsDiskKey = !this.#privateKeyProvider && !this.#keyOperations;
|
|
1942
|
+
const needsKeyGeneration = ownsDiskKey && !fs7.existsSync(this.privateKey);
|
|
3113
1943
|
const needsKeyEncryption = ownsDiskKey && !needsKeyGeneration && this.#privateKeyPassphrase !== void 0 && !isEncryptedPrivateKeyFile(this.privateKey);
|
|
3114
|
-
if (!
|
|
1944
|
+
if (!fs7.existsSync(this.configFile) || needsKeyGeneration || needsKeyEncryption) {
|
|
3115
1945
|
return await this.withLock2(async () => {
|
|
3116
1946
|
if (this.state === 3 /* Disposing */ || this.state === 4 /* Disposed */) {
|
|
3117
1947
|
return;
|
|
3118
1948
|
}
|
|
3119
|
-
if (!
|
|
3120
|
-
|
|
1949
|
+
if (!fs7.existsSync(this.configFile)) {
|
|
1950
|
+
fs7.writeFileSync(this.configFile, configurationFileSimpleTemplate);
|
|
3121
1951
|
}
|
|
3122
|
-
if (ownsDiskKey && !
|
|
1952
|
+
if (ownsDiskKey && !fs7.existsSync(this.privateKey)) {
|
|
3123
1953
|
debugLog("generating private key ...");
|
|
3124
1954
|
const passphrase = await resolvePrivateKeyPassphrase(this.#privateKeyPassphrase);
|
|
3125
|
-
await
|
|
3126
|
-
this.#cachedPrivateKey =
|
|
1955
|
+
await generatePrivateKeyFile(this.privateKey, this.keySize, { passphrase });
|
|
1956
|
+
this.#cachedPrivateKey = readPrivateKey(this.privateKey, passphrase);
|
|
3127
1957
|
} else if (ownsDiskKey && this.#privateKeyPassphrase !== void 0 && !isEncryptedPrivateKeyFile(this.privateKey)) {
|
|
3128
1958
|
warningLog("initialize: private key is plaintext but a passphrase is configured; encrypting it in place");
|
|
3129
1959
|
const passphrase = await resolvePrivateKeyPassphrase(this.#privateKeyPassphrase);
|
|
3130
|
-
const plaintextKey =
|
|
1960
|
+
const plaintextKey = readPrivateKey(this.privateKey);
|
|
3131
1961
|
await this.#rewritePrivateKeyFile(plaintextKey, passphrase);
|
|
3132
1962
|
this.#cachedPrivateKey = plaintextKey;
|
|
3133
1963
|
}
|
|
3134
1964
|
if (ownsDiskKey) {
|
|
3135
1965
|
restrictPrivateFilePermissions(this.privateKey, 384);
|
|
3136
1966
|
}
|
|
3137
|
-
await this
|
|
1967
|
+
await this.#probePrivateKey();
|
|
3138
1968
|
await this.#readCertificates();
|
|
3139
1969
|
});
|
|
3140
1970
|
} else {
|
|
3141
1971
|
if (ownsDiskKey) {
|
|
3142
1972
|
restrictPrivateFilePermissions(this.privateKey, 384);
|
|
3143
1973
|
}
|
|
3144
|
-
await this
|
|
1974
|
+
await this.#probePrivateKey();
|
|
3145
1975
|
await this.#readCertificates();
|
|
3146
1976
|
}
|
|
3147
1977
|
}
|
|
@@ -3180,6 +2010,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
3180
2010
|
this.#cachedPrivateKey = void 0;
|
|
3181
2011
|
this.#privateKeyPromise = void 0;
|
|
3182
2012
|
_CertificateManager.#activeInstances.delete(this);
|
|
2013
|
+
_CertificateManager.#uninstallExitCleanupIfIdle();
|
|
3183
2014
|
}
|
|
3184
2015
|
}
|
|
3185
2016
|
/**
|
|
@@ -3207,7 +2038,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
3207
2038
|
await this.#readCertificates();
|
|
3208
2039
|
}
|
|
3209
2040
|
async withLock2(action) {
|
|
3210
|
-
const lockFileName =
|
|
2041
|
+
const lockFileName = path4.join(this.rootDir, "mutex");
|
|
3211
2042
|
return withLock({ fileToLock: lockFileName }, async () => {
|
|
3212
2043
|
return await action();
|
|
3213
2044
|
});
|
|
@@ -3225,16 +2056,16 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
3225
2056
|
if (typeof params.applicationUri !== "string") {
|
|
3226
2057
|
throw new Error("createSelfSignedCertificate: expecting applicationUri to be a string");
|
|
3227
2058
|
}
|
|
3228
|
-
if (!this.#privateKeyProvider && !
|
|
2059
|
+
if (!this.#privateKeyProvider && !this.#keyOperations && !fs7.existsSync(this.privateKey)) {
|
|
3229
2060
|
throw new Error(`Cannot find private key ${this.privateKey}`);
|
|
3230
2061
|
}
|
|
3231
|
-
let certificateFilename =
|
|
2062
|
+
let certificateFilename = path4.join(this.rootDir, "own/certs/self_signed_certificate.pem");
|
|
3232
2063
|
certificateFilename = params.outputFile || certificateFilename;
|
|
3233
2064
|
const _params = {
|
|
3234
2065
|
...params,
|
|
3235
2066
|
rootDir: this.rootDir,
|
|
3236
2067
|
configFile: this.configFile,
|
|
3237
|
-
privateKey: await this
|
|
2068
|
+
privateKey: await this.#resolveSigningKey(),
|
|
3238
2069
|
subject: params.subject || "CN=FIXME"
|
|
3239
2070
|
};
|
|
3240
2071
|
await this.withLock2(async () => {
|
|
@@ -3260,14 +2091,14 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
3260
2091
|
}
|
|
3261
2092
|
const _params = {
|
|
3262
2093
|
...params,
|
|
3263
|
-
rootDir:
|
|
3264
|
-
configFile:
|
|
3265
|
-
privateKey: await this
|
|
2094
|
+
rootDir: path4.resolve(this.rootDir),
|
|
2095
|
+
configFile: path4.resolve(this.configFile),
|
|
2096
|
+
privateKey: await this.#resolveSigningKey()
|
|
3266
2097
|
};
|
|
3267
2098
|
return await this.withLock2(async () => {
|
|
3268
2099
|
const now = /* @__PURE__ */ new Date();
|
|
3269
2100
|
const today = `${now.toISOString().slice(0, 10)}_${now.getTime()}`;
|
|
3270
|
-
const certificateSigningRequestFilename =
|
|
2101
|
+
const certificateSigningRequestFilename = path4.join(this.rootDir, "own/certs", `certificate_${today}.csr`);
|
|
3271
2102
|
await createCertificateSigningRequestAsync(certificateSigningRequestFilename, _params);
|
|
3272
2103
|
return certificateSigningRequestFilename;
|
|
3273
2104
|
});
|
|
@@ -3287,13 +2118,13 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
3287
2118
|
return status;
|
|
3288
2119
|
}
|
|
3289
2120
|
}
|
|
3290
|
-
const pemCertificate =
|
|
2121
|
+
const pemCertificate = toPem(certificate, "CERTIFICATE");
|
|
3291
2122
|
const fingerprint = makeFingerprint(certificate);
|
|
3292
2123
|
if (this.#thumbs.issuers.certs.has(fingerprint)) {
|
|
3293
2124
|
return "Good" /* Good */;
|
|
3294
2125
|
}
|
|
3295
|
-
const filename =
|
|
3296
|
-
await
|
|
2126
|
+
const filename = path4.join(this.issuersCertFolder, `issuer_${buildIdealCertificateName(certificate)}.pem`);
|
|
2127
|
+
await fs7.promises.writeFile(filename, pemCertificate, "ascii");
|
|
3297
2128
|
this.#thumbs.issuers.certs.set(fingerprint, { certificate, filename });
|
|
3298
2129
|
if (addInTrustList) {
|
|
3299
2130
|
await this.trustCertificate(certificate);
|
|
@@ -3332,10 +2163,10 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
3332
2163
|
if (!index.has(key)) {
|
|
3333
2164
|
index.set(key, { crls: [], serialNumbers: {} });
|
|
3334
2165
|
}
|
|
3335
|
-
const pemCertificate =
|
|
2166
|
+
const pemCertificate = toPem(crl, "X509 CRL");
|
|
3336
2167
|
const sanitizedKey = key.replace(/:/g, "");
|
|
3337
|
-
const filename =
|
|
3338
|
-
await
|
|
2168
|
+
const filename = path4.join(folder, `crl_[${sanitizedKey}].pem`);
|
|
2169
|
+
await fs7.promises.writeFile(filename, pemCertificate, "ascii");
|
|
3339
2170
|
await this.#onCrlFileAdded(index, filename);
|
|
3340
2171
|
await this.#waitAndCheckCRLProcessingStatus();
|
|
3341
2172
|
return "Good" /* Good */;
|
|
@@ -3354,11 +2185,11 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
3354
2185
|
async clearRevocationLists(target) {
|
|
3355
2186
|
const clearFolder = async (folder, index) => {
|
|
3356
2187
|
try {
|
|
3357
|
-
const files = await
|
|
2188
|
+
const files = await fs7.promises.readdir(folder);
|
|
3358
2189
|
for (const file of files) {
|
|
3359
|
-
const ext =
|
|
2190
|
+
const ext = path4.extname(file).toLowerCase();
|
|
3360
2191
|
if (ext === ".crl" || ext === ".pem" || ext === ".der") {
|
|
3361
|
-
await
|
|
2192
|
+
await fs7.promises.unlink(path4.join(folder, file));
|
|
3362
2193
|
}
|
|
3363
2194
|
}
|
|
3364
2195
|
} catch (err) {
|
|
@@ -3400,7 +2231,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
3400
2231
|
return null;
|
|
3401
2232
|
}
|
|
3402
2233
|
try {
|
|
3403
|
-
await
|
|
2234
|
+
await fs7.promises.unlink(entry.filename);
|
|
3404
2235
|
} catch (err) {
|
|
3405
2236
|
if (err.code !== "ENOENT") {
|
|
3406
2237
|
throw err;
|
|
@@ -3424,7 +2255,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
3424
2255
|
return null;
|
|
3425
2256
|
}
|
|
3426
2257
|
try {
|
|
3427
|
-
await
|
|
2258
|
+
await fs7.promises.unlink(entry.filename);
|
|
3428
2259
|
} catch (err) {
|
|
3429
2260
|
if (err.code !== "ENOENT") {
|
|
3430
2261
|
throw err;
|
|
@@ -3440,14 +2271,14 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
3440
2271
|
* @param target - "issuers", "trusted", or "all" (default "all")
|
|
3441
2272
|
*/
|
|
3442
2273
|
async removeRevocationListsForIssuer(issuerCertificate, target = "all") {
|
|
3443
|
-
const issuerInfo =
|
|
2274
|
+
const issuerInfo = exploreCertificate(issuerCertificate);
|
|
3444
2275
|
const issuerFingerprint = issuerInfo.tbsCertificate.subjectFingerPrint;
|
|
3445
2276
|
const processIndex = async (index) => {
|
|
3446
2277
|
const crlData = index.get(issuerFingerprint);
|
|
3447
2278
|
if (!crlData) return;
|
|
3448
2279
|
for (const crlEntry of crlData.crls) {
|
|
3449
2280
|
try {
|
|
3450
|
-
await
|
|
2281
|
+
await fs7.promises.unlink(crlEntry.filename);
|
|
3451
2282
|
} catch (err) {
|
|
3452
2283
|
if (err.code !== "ENOENT") {
|
|
3453
2284
|
throw err;
|
|
@@ -3514,7 +2345,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
3514
2345
|
const opts = this.#addCertValidation;
|
|
3515
2346
|
let leafInfo;
|
|
3516
2347
|
try {
|
|
3517
|
-
leafInfo =
|
|
2348
|
+
leafInfo = exploreCertificate(leafCertificate);
|
|
3518
2349
|
} catch (_err) {
|
|
3519
2350
|
return "BadCertificateInvalid" /* BadCertificateInvalid */;
|
|
3520
2351
|
}
|
|
@@ -3528,17 +2359,12 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
3528
2359
|
return "BadSecurityChecksFailed" /* BadSecurityChecksFailed */;
|
|
3529
2360
|
}
|
|
3530
2361
|
if (!opts.acceptExpiredCertificate) {
|
|
3531
|
-
|
|
3532
|
-
try {
|
|
3533
|
-
certDetails = exploreCertificateInfo(currentCert);
|
|
3534
|
-
} catch (_err) {
|
|
3535
|
-
return "BadCertificateInvalid" /* BadCertificateInvalid */;
|
|
3536
|
-
}
|
|
2362
|
+
const { validity } = currentInfo.tbsCertificate;
|
|
3537
2363
|
const now = /* @__PURE__ */ new Date();
|
|
3538
|
-
if (
|
|
2364
|
+
if (validity.notBefore.getTime() > now.getTime()) {
|
|
3539
2365
|
return "BadCertificateTimeInvalid" /* BadCertificateTimeInvalid */;
|
|
3540
2366
|
}
|
|
3541
|
-
if (
|
|
2367
|
+
if (validity.notAfter.getTime() <= now.getTime()) {
|
|
3542
2368
|
return depth === 1 ? "BadCertificateTimeInvalid" /* BadCertificateTimeInvalid */ : "BadCertificateIssuerTimeInvalid" /* BadCertificateIssuerTimeInvalid */;
|
|
3543
2369
|
}
|
|
3544
2370
|
}
|
|
@@ -3582,7 +2408,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
3582
2408
|
}
|
|
3583
2409
|
currentCert = issuerCert;
|
|
3584
2410
|
try {
|
|
3585
|
-
currentInfo =
|
|
2411
|
+
currentInfo = exploreCertificate(currentCert);
|
|
3586
2412
|
} catch (_err) {
|
|
3587
2413
|
return "BadCertificateInvalid" /* BadCertificateInvalid */;
|
|
3588
2414
|
}
|
|
@@ -3631,7 +2457,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
3631
2457
|
*/
|
|
3632
2458
|
async findIssuerCertificate(certificate) {
|
|
3633
2459
|
const firstCertificate = coerceCertificateChain(certificate)[0];
|
|
3634
|
-
const certInfo =
|
|
2460
|
+
const certInfo = exploreCertificate(firstCertificate);
|
|
3635
2461
|
if (isSelfSigned2(certInfo)) {
|
|
3636
2462
|
return firstCertificate;
|
|
3637
2463
|
}
|
|
@@ -3689,7 +2515,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
3689
2515
|
let depth = 0;
|
|
3690
2516
|
while (depth < maxDepth) {
|
|
3691
2517
|
const lastCert = result[result.length - 1];
|
|
3692
|
-
const lastInfo =
|
|
2518
|
+
const lastInfo = exploreCertificate(lastCert);
|
|
3693
2519
|
if (isSelfSigned2(lastInfo)) {
|
|
3694
2520
|
const wasExtended = result.length > chain.length;
|
|
3695
2521
|
return {
|
|
@@ -3716,7 +2542,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
3716
2542
|
return {
|
|
3717
2543
|
chain: result,
|
|
3718
2544
|
status: "AlreadyComplete" /* AlreadyComplete */,
|
|
3719
|
-
message: `Chain ends at root "${
|
|
2545
|
+
message: `Chain ends at root "${exploreCertificate(issuerCert).tbsCertificate.subject.commonName}" (already present in chain).`
|
|
3720
2546
|
};
|
|
3721
2547
|
}
|
|
3722
2548
|
result.push(issuerCert);
|
|
@@ -3755,9 +2581,9 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
3755
2581
|
const fingerprint = makeFingerprint(certificate);
|
|
3756
2582
|
let status = await this.#checkRejectedOrTrusted(certificate);
|
|
3757
2583
|
if (status === "unknown") {
|
|
3758
|
-
const pem =
|
|
3759
|
-
const filename =
|
|
3760
|
-
await
|
|
2584
|
+
const pem = toPem(chain, "CERTIFICATE");
|
|
2585
|
+
const filename = path4.join(this.rejectedFolder, `${buildIdealCertificateName(certificate)}.pem`);
|
|
2586
|
+
await fs7.promises.writeFile(filename, pem);
|
|
3761
2587
|
this.#thumbs.rejected.set(fingerprint, { certificate, filename });
|
|
3762
2588
|
status = "rejected";
|
|
3763
2589
|
}
|
|
@@ -3773,10 +2599,10 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
3773
2599
|
throw new Error(`#moveCertificate: certificate ${fingerprint.substring(0, 10)} not found in ${status} index`);
|
|
3774
2600
|
}
|
|
3775
2601
|
const destFolder = newStatus === "trusted" ? this.trustedFolder : this.rejectedFolder;
|
|
3776
|
-
const certificateDest =
|
|
2602
|
+
const certificateDest = path4.join(destFolder, path4.basename(srcEntry.filename));
|
|
3777
2603
|
debugLog("#moveCertificate", fingerprint.substring(0, 10), "old name", srcEntry.filename);
|
|
3778
2604
|
debugLog("#moveCertificate", fingerprint.substring(0, 10), "new name", certificateDest);
|
|
3779
|
-
await
|
|
2605
|
+
await fs7.promises.rename(srcEntry.filename, certificateDest);
|
|
3780
2606
|
indexSrc.delete(fingerprint);
|
|
3781
2607
|
const indexDest = newStatus === "trusted" ? this.#thumbs.trusted : this.#thumbs.rejected;
|
|
3782
2608
|
indexDest.set(fingerprint, { certificate, filename: certificateDest });
|
|
@@ -3784,7 +2610,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
3784
2610
|
});
|
|
3785
2611
|
}
|
|
3786
2612
|
#findAssociatedCRLs(issuerCertificate) {
|
|
3787
|
-
const issuerCertificateInfo =
|
|
2613
|
+
const issuerCertificateInfo = exploreCertificate(issuerCertificate);
|
|
3788
2614
|
const key = issuerCertificateInfo.tbsCertificate.subjectFingerPrint;
|
|
3789
2615
|
return this.#thumbs.issuersCrl.get(key) ?? this.#thumbs.crl.get(key) ?? null;
|
|
3790
2616
|
}
|
|
@@ -3823,7 +2649,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
3823
2649
|
if (!crls) {
|
|
3824
2650
|
return "BadCertificateRevocationUnknown" /* BadCertificateRevocationUnknown */;
|
|
3825
2651
|
}
|
|
3826
|
-
const certInfo =
|
|
2652
|
+
const certInfo = exploreCertificate(firstCertificate);
|
|
3827
2653
|
const serialNumber = certInfo.tbsCertificate.serialNumber || certInfo.tbsCertificate.extensions?.authorityKeyIdentifier?.serial || "";
|
|
3828
2654
|
const key = certInfo.tbsCertificate.extensions?.authorityKeyIdentifier?.authorityCertIssuerFingerPrint || "<unknown>";
|
|
3829
2655
|
const crl2 = this.#thumbs.crl.get(key) ?? null;
|
|
@@ -3849,7 +2675,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
3849
2675
|
const { index, filename } = nextCRL;
|
|
3850
2676
|
const crl = await readCertificateRevocationList(filename);
|
|
3851
2677
|
const crlInfo = exploreCertificateRevocationList(crl);
|
|
3852
|
-
debugLog(
|
|
2678
|
+
debugLog(chalk3.cyan("add CRL in folder "), filename);
|
|
3853
2679
|
const fingerprint = crlInfo.tbsCertList.issuerFingerprint;
|
|
3854
2680
|
if (!index.has(fingerprint)) {
|
|
3855
2681
|
index.set(fingerprint, { crls: [], serialNumbers: {} });
|
|
@@ -3862,7 +2688,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
3862
2688
|
data.serialNumbers[serialNumber] = revokedCertificate.revocationDate;
|
|
3863
2689
|
}
|
|
3864
2690
|
}
|
|
3865
|
-
debugLog(
|
|
2691
|
+
debugLog(chalk3.cyan("CRL"), fingerprint, "serial numbers = ", Object.keys(data.serialNumbers));
|
|
3866
2692
|
} catch (err) {
|
|
3867
2693
|
debugLog("CRL filename error =");
|
|
3868
2694
|
debugLog(err);
|
|
@@ -3888,14 +2714,15 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
3888
2714
|
const chokidarOptions = {
|
|
3889
2715
|
usePolling,
|
|
3890
2716
|
...usePolling ? { interval: pollingInterval } : {},
|
|
2717
|
+
depth: 0,
|
|
3891
2718
|
persistent: false
|
|
3892
2719
|
};
|
|
3893
2720
|
const allCapturedHandles = [];
|
|
3894
|
-
const origWatch =
|
|
2721
|
+
const origWatch = fs7.watch;
|
|
3895
2722
|
let watcherReadyCount = 0;
|
|
3896
2723
|
const totalWatchers = 5;
|
|
3897
|
-
|
|
3898
|
-
const handle = origWatch.apply(
|
|
2724
|
+
fs7.watch = ((...args) => {
|
|
2725
|
+
const handle = origWatch.apply(fs7, args);
|
|
3899
2726
|
handle.setMaxListeners(handle.getMaxListeners() + 1);
|
|
3900
2727
|
handle.on("error", () => {
|
|
3901
2728
|
});
|
|
@@ -3911,7 +2738,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
3911
2738
|
}
|
|
3912
2739
|
watcherReadyCount++;
|
|
3913
2740
|
if (watcherReadyCount >= totalWatchers) {
|
|
3914
|
-
|
|
2741
|
+
fs7.watch = origWatch;
|
|
3915
2742
|
}
|
|
3916
2743
|
};
|
|
3917
2744
|
return { w, capturedHandles: allCapturedHandles.slice(startIdx), unreffAll };
|
|
@@ -3924,7 +2751,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
3924
2751
|
this.#scanCrlFolder(this.issuersCrlFolder, this.#thumbs.issuersCrl)
|
|
3925
2752
|
]);
|
|
3926
2753
|
if (this.#disableFileWatchers) {
|
|
3927
|
-
|
|
2754
|
+
fs7.watch = origWatch;
|
|
3928
2755
|
} else {
|
|
3929
2756
|
this.#startWatcher(this.trustedFolder, this.#thumbs.trusted, createUnreffedWatcher, "trusted");
|
|
3930
2757
|
this.#startWatcher(this.issuersCertFolder, this.#thumbs.issuers.certs, createUnreffedWatcher, "issuersCerts");
|
|
@@ -3939,19 +2766,19 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
3939
2766
|
* file reads, preventing main-loop stalls with large folders.
|
|
3940
2767
|
*/
|
|
3941
2768
|
async #scanCertFolder(folder, index) {
|
|
3942
|
-
if (!
|
|
3943
|
-
const files = await
|
|
2769
|
+
if (!fs7.existsSync(folder)) return;
|
|
2770
|
+
const files = await fs7.promises.readdir(folder);
|
|
3944
2771
|
for (const file of files) {
|
|
3945
|
-
const filename =
|
|
2772
|
+
const filename = path4.join(folder, file);
|
|
3946
2773
|
try {
|
|
3947
|
-
const stat = await
|
|
2774
|
+
const stat = await fs7.promises.stat(filename);
|
|
3948
2775
|
if (!stat.isFile()) continue;
|
|
3949
2776
|
const certs = await readCertificateChainAsync(filename);
|
|
3950
2777
|
if (certs.length === 0) continue;
|
|
3951
2778
|
const certificate = certs[0];
|
|
3952
2779
|
if (certs.length > 1) {
|
|
3953
2780
|
try {
|
|
3954
|
-
await
|
|
2781
|
+
await fs7.promises.writeFile(filename, toPem(certs, "CERTIFICATE"), "ascii");
|
|
3955
2782
|
} catch (writeErr) {
|
|
3956
2783
|
debugLog(`scanCertFolder: could not rewrite legacy PEM ${filename} (read-only fs?)`, writeErr);
|
|
3957
2784
|
}
|
|
@@ -3965,7 +2792,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
3965
2792
|
}
|
|
3966
2793
|
}
|
|
3967
2794
|
}
|
|
3968
|
-
const info =
|
|
2795
|
+
const info = exploreCertificate(certificate);
|
|
3969
2796
|
const fingerprint = makeFingerprint(certificate);
|
|
3970
2797
|
index.set(fingerprint, { certificate, filename, info });
|
|
3971
2798
|
this.#filenameToHash.set(filename, fingerprint);
|
|
@@ -3978,12 +2805,12 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
3978
2805
|
* Scan a CRL folder and populate the in-memory CRL index.
|
|
3979
2806
|
*/
|
|
3980
2807
|
async #scanCrlFolder(folder, index) {
|
|
3981
|
-
if (!
|
|
3982
|
-
const files = await
|
|
2808
|
+
if (!fs7.existsSync(folder)) return;
|
|
2809
|
+
const files = await fs7.promises.readdir(folder);
|
|
3983
2810
|
for (const file of files) {
|
|
3984
|
-
const filename =
|
|
2811
|
+
const filename = path4.join(folder, file);
|
|
3985
2812
|
try {
|
|
3986
|
-
const stat = await
|
|
2813
|
+
const stat = await fs7.promises.stat(filename);
|
|
3987
2814
|
if (!stat.isFile()) continue;
|
|
3988
2815
|
this.#onCrlFileAdded(index, filename);
|
|
3989
2816
|
} catch (err) {
|
|
@@ -4041,7 +2868,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
4041
2868
|
});
|
|
4042
2869
|
let ready = false;
|
|
4043
2870
|
w.on("unlink", (filename) => {
|
|
4044
|
-
debugLog(
|
|
2871
|
+
debugLog(chalk3.cyan(`unlink in folder ${folder}`), filename);
|
|
4045
2872
|
const h = this.#filenameToHash.get(filename);
|
|
4046
2873
|
if (h && index.has(h)) {
|
|
4047
2874
|
index.delete(h);
|
|
@@ -4049,16 +2876,16 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
4049
2876
|
}
|
|
4050
2877
|
});
|
|
4051
2878
|
w.on("add", (filename) => {
|
|
4052
|
-
debugLog(
|
|
2879
|
+
debugLog(chalk3.cyan(`add in folder ${folder}`), filename);
|
|
4053
2880
|
try {
|
|
4054
2881
|
const certificate = readCertificateChain(filename)[0];
|
|
4055
|
-
const info =
|
|
2882
|
+
const info = exploreCertificate(certificate);
|
|
4056
2883
|
const fingerprint = makeFingerprint(certificate);
|
|
4057
2884
|
const isNew = !index.has(fingerprint);
|
|
4058
2885
|
index.set(fingerprint, { certificate, filename, info });
|
|
4059
2886
|
this.#filenameToHash.set(filename, fingerprint);
|
|
4060
2887
|
debugLog(
|
|
4061
|
-
|
|
2888
|
+
chalk3.magenta("CERT"),
|
|
4062
2889
|
info.tbsCertificate.subjectFingerPrint,
|
|
4063
2890
|
info.tbsCertificate.serialNumber,
|
|
4064
2891
|
info.tbsCertificate.extensions?.authorityKeyIdentifier?.authorityCertIssuerFingerPrint
|
|
@@ -4072,7 +2899,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
4072
2899
|
}
|
|
4073
2900
|
});
|
|
4074
2901
|
w.on("change", (changedPath) => {
|
|
4075
|
-
debugLog(
|
|
2902
|
+
debugLog(chalk3.cyan(`change in folder ${folder}`), changedPath);
|
|
4076
2903
|
try {
|
|
4077
2904
|
const certificate = readCertificateChain(changedPath)[0];
|
|
4078
2905
|
const newFingerprint = makeFingerprint(certificate);
|
|
@@ -4080,7 +2907,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
4080
2907
|
if (oldHash && oldHash !== newFingerprint) {
|
|
4081
2908
|
index.delete(oldHash);
|
|
4082
2909
|
}
|
|
4083
|
-
index.set(newFingerprint, { certificate, filename: changedPath, info:
|
|
2910
|
+
index.set(newFingerprint, { certificate, filename: changedPath, info: exploreCertificate(certificate) });
|
|
4084
2911
|
this.#filenameToHash.set(changedPath, newFingerprint);
|
|
4085
2912
|
this.emit("certificateChange", { store, certificate, fingerprint: newFingerprint, filename: changedPath });
|
|
4086
2913
|
} catch (err) {
|
|
@@ -4108,12 +2935,152 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
|
4108
2935
|
});
|
|
4109
2936
|
}
|
|
4110
2937
|
};
|
|
2938
|
+
|
|
2939
|
+
// packages/node-opcua-pki/lib/pki/toolbox_pfx.ts
|
|
2940
|
+
import assert5 from "assert";
|
|
2941
|
+
import fs8 from "fs";
|
|
2942
|
+
import {
|
|
2943
|
+
certificateMatchesPrivateKey,
|
|
2944
|
+
certificatesToPem,
|
|
2945
|
+
coercePrivateKeyPem as coercePrivateKeyPem3,
|
|
2946
|
+
createPfx,
|
|
2947
|
+
makeSHA1Thumbprint as makeSHA1Thumbprint2,
|
|
2948
|
+
Pkcs12UnsupportedAlgorithmError,
|
|
2949
|
+
parsePfx,
|
|
2950
|
+
readCertificateChain as readCertificateChain2,
|
|
2951
|
+
readPrivateKey as readPrivateKey2,
|
|
2952
|
+
toPem as toPem2,
|
|
2953
|
+
x509
|
|
2954
|
+
} from "node-opcua-crypto";
|
|
2955
|
+
async function readPfxFile(options) {
|
|
2956
|
+
const { pfxFile, passphrase = "" } = options;
|
|
2957
|
+
assert5(fs8.existsSync(pfxFile), `PFX file does not exist: ${pfxFile}`);
|
|
2958
|
+
try {
|
|
2959
|
+
return await parsePfx(await fs8.promises.readFile(pfxFile), passphrase);
|
|
2960
|
+
} catch (err) {
|
|
2961
|
+
if (err instanceof Pkcs12UnsupportedAlgorithmError) {
|
|
2962
|
+
const explained = new Error(
|
|
2963
|
+
[
|
|
2964
|
+
`${pfxFile} uses an encryption algorithm that is not supported (${err.message}).`,
|
|
2965
|
+
"Bundles written by openssl 1.x default to 40-bit RC2, which openssl 3 itself",
|
|
2966
|
+
"rejects unless given -legacy. Convert the file once with:",
|
|
2967
|
+
" openssl pkcs12 -legacy -in old.pfx -nodes -out tmp.pem",
|
|
2968
|
+
" openssl pkcs12 -export -in tmp.pem -out new.pfx"
|
|
2969
|
+
].join("\n")
|
|
2970
|
+
);
|
|
2971
|
+
explained.cause = err;
|
|
2972
|
+
throw explained;
|
|
2973
|
+
}
|
|
2974
|
+
throw err;
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
async function createPFX(options) {
|
|
2978
|
+
const { certificateFile, privateKeyFile, privateKeyPassphrase, outputFile, passphrase = "", caCertificateFiles } = options;
|
|
2979
|
+
assert5(fs8.existsSync(certificateFile), `Certificate file does not exist: ${certificateFile}`);
|
|
2980
|
+
assert5(fs8.existsSync(privateKeyFile), `Private key file does not exist: ${privateKeyFile}`);
|
|
2981
|
+
const [certificate, ...bundledIssuers] = readCertificateChain2(certificateFile);
|
|
2982
|
+
if (!certificate) {
|
|
2983
|
+
throw new Error(`Certificate file holds no certificate: ${certificateFile}`);
|
|
2984
|
+
}
|
|
2985
|
+
const certificateChain = [...bundledIssuers];
|
|
2986
|
+
for (const caFile of caCertificateFiles ?? []) {
|
|
2987
|
+
assert5(fs8.existsSync(caFile), `CA certificate file does not exist: ${caFile}`);
|
|
2988
|
+
certificateChain.push(...readCertificateChain2(caFile));
|
|
2989
|
+
}
|
|
2990
|
+
const privateKey = readPrivateKey2(privateKeyFile, privateKeyPassphrase);
|
|
2991
|
+
if (!certificateMatchesPrivateKey(certificate, privateKey)) {
|
|
2992
|
+
throw new Error(`The private key ${privateKeyFile} does not match the certificate ${certificateFile}`);
|
|
2993
|
+
}
|
|
2994
|
+
const pfx = await createPfx({
|
|
2995
|
+
certificate,
|
|
2996
|
+
certificateChain,
|
|
2997
|
+
privateKey,
|
|
2998
|
+
password: passphrase,
|
|
2999
|
+
friendlyName: options.friendlyName
|
|
3000
|
+
});
|
|
3001
|
+
await fs8.promises.writeFile(outputFile, pfx);
|
|
3002
|
+
}
|
|
3003
|
+
async function extractCertificateFromPFX(options) {
|
|
3004
|
+
const { certificate } = await readPfxFile(options);
|
|
3005
|
+
return toPem2(certificate, "CERTIFICATE");
|
|
3006
|
+
}
|
|
3007
|
+
async function extractPrivateKeyFromPFX(options) {
|
|
3008
|
+
const { privateKey } = await readPfxFile(options);
|
|
3009
|
+
return coercePrivateKeyPem3(privateKey);
|
|
3010
|
+
}
|
|
3011
|
+
async function extractCACertificatesFromPFX(options) {
|
|
3012
|
+
const { certificateChain } = await readPfxFile(options);
|
|
3013
|
+
return certificateChain.length === 0 ? "" : certificatesToPem(certificateChain);
|
|
3014
|
+
}
|
|
3015
|
+
async function extractAllFromPFX(options) {
|
|
3016
|
+
const { certificate, certificateChain, privateKey, friendlyName } = await readPfxFile(options);
|
|
3017
|
+
return {
|
|
3018
|
+
certificate: toPem2(certificate, "CERTIFICATE"),
|
|
3019
|
+
privateKey: coercePrivateKeyPem3(privateKey),
|
|
3020
|
+
caCertificates: certificateChain.length === 0 ? "" : certificatesToPem(certificateChain),
|
|
3021
|
+
friendlyName
|
|
3022
|
+
};
|
|
3023
|
+
}
|
|
3024
|
+
async function convertPFXtoPEM(pfxFile, pemFile, passphrase = "") {
|
|
3025
|
+
const { certificate, certificateChain, privateKey } = await readPfxFile({ pfxFile, passphrase });
|
|
3026
|
+
const parts = [coercePrivateKeyPem3(privateKey), toPem2(certificate, "CERTIFICATE")];
|
|
3027
|
+
if (certificateChain.length > 0) {
|
|
3028
|
+
parts.push(certificatesToPem(certificateChain));
|
|
3029
|
+
}
|
|
3030
|
+
await fs8.promises.writeFile(pemFile, `${parts.map((part) => part.trimEnd()).join("\n")}
|
|
3031
|
+
`, "utf-8");
|
|
3032
|
+
}
|
|
3033
|
+
function formatThumbprint(certificate) {
|
|
3034
|
+
return (makeSHA1Thumbprint2(certificate).toString("hex").toUpperCase().match(/../g) ?? []).join(":");
|
|
3035
|
+
}
|
|
3036
|
+
function describePublicKey(certificate) {
|
|
3037
|
+
const algorithm = certificate.publicKey.algorithm;
|
|
3038
|
+
if (algorithm.modulusLength) {
|
|
3039
|
+
return `${algorithm.name} ${algorithm.modulusLength} bits`;
|
|
3040
|
+
}
|
|
3041
|
+
if (algorithm.namedCurve) {
|
|
3042
|
+
return `${algorithm.name} ${algorithm.namedCurve}`;
|
|
3043
|
+
}
|
|
3044
|
+
return algorithm.name;
|
|
3045
|
+
}
|
|
3046
|
+
function describeCertificate(der, indent) {
|
|
3047
|
+
const certificate = new x509.X509Certificate(der);
|
|
3048
|
+
return [
|
|
3049
|
+
`${indent}subject : ${certificate.subject}`,
|
|
3050
|
+
`${indent}issuer : ${certificate.issuer}`,
|
|
3051
|
+
`${indent}serial number : ${certificate.serialNumber}`,
|
|
3052
|
+
`${indent}not before : ${certificate.notBefore.toISOString()}`,
|
|
3053
|
+
`${indent}not after : ${certificate.notAfter.toISOString()}`,
|
|
3054
|
+
`${indent}public key : ${describePublicKey(certificate)}`,
|
|
3055
|
+
`${indent}signature : ${certificate.signatureAlgorithm.name}`,
|
|
3056
|
+
`${indent}SHA-1 thumbprint : ${formatThumbprint(der)}`
|
|
3057
|
+
];
|
|
3058
|
+
}
|
|
3059
|
+
async function dumpPFX(pfxFile, passphrase = "") {
|
|
3060
|
+
const { certificate, certificateChain, privateKey, friendlyName } = await readPfxFile({ pfxFile, passphrase });
|
|
3061
|
+
const lines = [`PFX bundle : ${pfxFile}`];
|
|
3062
|
+
if (friendlyName) {
|
|
3063
|
+
lines.push(`friendly name : ${friendlyName}`);
|
|
3064
|
+
}
|
|
3065
|
+
const keyVerdict = certificateMatchesPrivateKey(certificate, privateKey) ? "matches this certificate" : "DOES NOT match this certificate";
|
|
3066
|
+
lines.push("", "certificate", ...describeCertificate(certificate, " "), ` private key : ${keyVerdict}`);
|
|
3067
|
+
lines.push("", `issuer chain : ${certificateChain.length} certificate(s)`);
|
|
3068
|
+
for (const [index, issuer] of certificateChain.entries()) {
|
|
3069
|
+
lines.push(` [${index}]`, ...describeCertificate(issuer, " "));
|
|
3070
|
+
}
|
|
3071
|
+
return `${lines.join("\n")}
|
|
3072
|
+
`;
|
|
3073
|
+
}
|
|
4111
3074
|
export {
|
|
4112
3075
|
CertificateAuthority,
|
|
3076
|
+
CertificateAuthorityCore,
|
|
4113
3077
|
CertificateManager,
|
|
4114
3078
|
CertificateManagerState,
|
|
4115
3079
|
ChainCompletionStatus,
|
|
3080
|
+
NativeCaBackend,
|
|
3081
|
+
OpenSslCaBackend,
|
|
4116
3082
|
PrivateKeyPassphraseRequiredError,
|
|
3083
|
+
PrivateKeyUnavailableError2 as PrivateKeyUnavailableError,
|
|
4117
3084
|
Subject,
|
|
4118
3085
|
VerificationStatus,
|
|
4119
3086
|
adjustApplicationUri,
|
|
@@ -4130,6 +3097,7 @@ export {
|
|
|
4130
3097
|
install_prerequisite,
|
|
4131
3098
|
isIntermediateIssuer,
|
|
4132
3099
|
isIssuer,
|
|
3100
|
+
isOpaqueSigner,
|
|
4133
3101
|
isRootIssuer,
|
|
4134
3102
|
makeFingerprint,
|
|
4135
3103
|
quote,
|