node-opcua-pki 6.22.1 → 7.0.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/bin/install_prerequisite.ts +1 -1
- package/bin/pki.ts +1 -1
- package/dist/bin/install_prerequisite.d.ts +2 -0
- package/dist/bin/install_prerequisite.js +6 -0
- package/dist/bin/install_prerequisite.js.map +1 -0
- package/dist/bin/pki.d.ts +2 -0
- package/dist/bin/pki.js +4 -0
- package/dist/bin/pki.js.map +1 -0
- package/dist/lib/ca/backends/native_ca_backend.d.ts +49 -0
- package/dist/lib/ca/backends/native_ca_backend.js +365 -0
- package/dist/lib/ca/backends/native_ca_backend.js.map +1 -0
- package/dist/lib/ca/backends/openssl_ca_backend.d.ts +47 -0
- package/dist/lib/ca/backends/openssl_ca_backend.js +398 -0
- package/dist/lib/ca/backends/openssl_ca_backend.js.map +1 -0
- package/dist/lib/ca/certificate_authority.d.ts +62 -0
- package/dist/lib/ca/certificate_authority.js +91 -0
- package/dist/lib/ca/certificate_authority.js.map +1 -0
- package/dist/lib/ca/core/ca_backend.d.ts +89 -0
- package/dist/lib/ca/core/ca_backend.js +24 -0
- package/dist/lib/ca/core/ca_backend.js.map +1 -0
- package/dist/lib/ca/core/ca_database.d.ts +91 -0
- package/dist/lib/ca/core/ca_database.js +267 -0
- package/dist/lib/ca/core/ca_database.js.map +1 -0
- package/dist/{core-BwqSJPHc.d.mts → lib/ca/core/certificate_authority_core.d.ts} +45 -394
- package/dist/lib/ca/core/certificate_authority_core.js +1300 -0
- package/dist/lib/ca/core/certificate_authority_core.js.map +1 -0
- package/dist/lib/ca/core/index.d.ts +17 -0
- package/dist/lib/ca/core/index.js +25 -0
- package/dist/lib/ca/core/index.js.map +1 -0
- package/dist/lib/ca/crypto_create_CA.d.ts +1 -0
- package/dist/lib/ca/crypto_create_CA.js +821 -0
- package/dist/lib/ca/crypto_create_CA.js.map +1 -0
- package/dist/lib/ca/index.d.ts +1 -0
- package/dist/lib/ca/index.js +2 -0
- package/dist/lib/ca/index.js.map +1 -0
- package/dist/lib/ca/templates/ca_config_template.cnf.d.ts +2 -0
- package/dist/lib/ca/templates/ca_config_template.cnf.js +170 -0
- package/dist/lib/ca/templates/ca_config_template.cnf.js.map +1 -0
- package/dist/lib/core.d.ts +33 -0
- package/dist/lib/core.js +56 -0
- package/dist/lib/core.js.map +1 -0
- package/dist/lib/index.d.ts +11 -0
- package/dist/lib/index.js +37 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/misc/applicationurn.d.ts +1 -0
- package/dist/lib/misc/applicationurn.js +40 -0
- package/dist/lib/misc/applicationurn.js.map +1 -0
- package/dist/lib/misc/hostname.d.ts +8 -0
- package/dist/lib/misc/hostname.js +82 -0
- package/dist/lib/misc/hostname.js.map +1 -0
- package/dist/lib/misc/subject.d.ts +2 -0
- package/dist/lib/misc/subject.js +2 -0
- package/dist/lib/misc/subject.js.map +1 -0
- package/dist/{index.d.ts → lib/pki/certificate_manager.d.ts} +24 -248
- package/dist/lib/pki/certificate_manager.js +2302 -0
- package/dist/lib/pki/certificate_manager.js.map +1 -0
- package/dist/lib/pki/templates/simple_config_template.cnf.d.ts +2 -0
- package/dist/lib/pki/templates/simple_config_template.cnf.js +74 -0
- package/dist/lib/pki/templates/simple_config_template.cnf.js.map +1 -0
- package/dist/lib/pki/toolbox_pfx.d.ts +127 -0
- package/dist/lib/pki/toolbox_pfx.js +248 -0
- package/dist/lib/pki/toolbox_pfx.js.map +1 -0
- package/dist/lib/toolbox/common.d.ts +150 -0
- package/dist/lib/toolbox/common.js +82 -0
- package/dist/lib/toolbox/common.js.map +1 -0
- package/dist/lib/toolbox/common2.d.ts +25 -0
- package/dist/lib/toolbox/common2.js +97 -0
- package/dist/lib/toolbox/common2.js.map +1 -0
- package/dist/lib/toolbox/config.d.ts +5 -0
- package/dist/lib/toolbox/config.js +28 -0
- package/dist/lib/toolbox/config.js.map +1 -0
- package/dist/lib/toolbox/debug.d.ts +5 -0
- package/dist/lib/toolbox/debug.js +35 -0
- package/dist/lib/toolbox/debug.js.map +1 -0
- package/dist/lib/toolbox/display.d.ts +4 -0
- package/dist/lib/toolbox/display.js +56 -0
- package/dist/lib/toolbox/display.js.map +1 -0
- package/dist/lib/toolbox/index.d.ts +5 -0
- package/dist/lib/toolbox/index.js +28 -0
- package/dist/lib/toolbox/index.js.map +1 -0
- package/dist/lib/toolbox/with_openssl/_create_random_file.d.ts +4 -0
- package/dist/lib/toolbox/with_openssl/_create_random_file.js +50 -0
- package/dist/lib/toolbox/with_openssl/_create_random_file.js.map +1 -0
- package/dist/lib/toolbox/with_openssl/_env.d.ts +58 -0
- package/dist/lib/toolbox/with_openssl/_env.js +126 -0
- package/dist/lib/toolbox/with_openssl/_env.js.map +1 -0
- package/dist/lib/toolbox/with_openssl/create_certificate_signing_request.d.ts +5 -0
- package/dist/lib/toolbox/with_openssl/create_certificate_signing_request.js +72 -0
- package/dist/lib/toolbox/with_openssl/create_certificate_signing_request.js.map +1 -0
- package/dist/lib/toolbox/with_openssl/create_private_key.d.ts +5 -0
- package/dist/lib/toolbox/with_openssl/create_private_key.js +92 -0
- package/dist/lib/toolbox/with_openssl/create_private_key.js.map +1 -0
- package/dist/lib/toolbox/with_openssl/create_self_signed_certificate.d.ts +5 -0
- package/dist/lib/toolbox/with_openssl/create_self_signed_certificate.js +135 -0
- package/dist/lib/toolbox/with_openssl/create_self_signed_certificate.js.map +1 -0
- package/dist/lib/toolbox/with_openssl/execute_openssl.d.ts +67 -0
- package/dist/lib/toolbox/with_openssl/execute_openssl.js +246 -0
- package/dist/lib/toolbox/with_openssl/execute_openssl.js.map +1 -0
- package/dist/lib/toolbox/with_openssl/index.d.ts +5 -0
- package/dist/lib/toolbox/with_openssl/index.js +30 -0
- package/dist/lib/toolbox/with_openssl/index.js.map +1 -0
- package/dist/lib/toolbox/with_openssl/install_prerequisite.d.ts +7 -0
- package/dist/lib/toolbox/with_openssl/install_prerequisite.js +358 -0
- package/dist/lib/toolbox/with_openssl/install_prerequisite.js.map +1 -0
- package/dist/lib/toolbox/with_openssl/toolbox.d.ts +53 -0
- package/dist/lib/toolbox/with_openssl/toolbox.js +190 -0
- package/dist/lib/toolbox/with_openssl/toolbox.js.map +1 -0
- package/dist/lib/toolbox/without_openssl/create_certificate_signing_request.d.ts +5 -0
- package/dist/lib/toolbox/without_openssl/create_certificate_signing_request.js +69 -0
- package/dist/lib/toolbox/without_openssl/create_certificate_signing_request.js.map +1 -0
- package/dist/lib/toolbox/without_openssl/create_self_signed_certificate.d.ts +3 -0
- package/dist/lib/toolbox/without_openssl/create_self_signed_certificate.js +78 -0
- package/dist/lib/toolbox/without_openssl/create_self_signed_certificate.js.map +1 -0
- package/dist/lib/toolbox/without_openssl/index.d.ts +2 -0
- package/dist/lib/toolbox/without_openssl/index.js +25 -0
- package/dist/lib/toolbox/without_openssl/index.js.map +1 -0
- package/package.json +11 -30
- package/dist/bin/install_prerequisite.mjs +0 -18
- package/dist/bin/install_prerequisite.mjs.map +0 -1
- package/dist/bin/pki.mjs +0 -5932
- package/dist/bin/pki.mjs.map +0 -1
- package/dist/chunk-EQ4DGI4M.mjs +0 -1917
- package/dist/chunk-EQ4DGI4M.mjs.map +0 -1
- package/dist/chunk-GCHH54PS.mjs +0 -30
- package/dist/chunk-GCHH54PS.mjs.map +0 -1
- package/dist/core-BwqSJPHc.d.ts +0 -1041
- package/dist/core.d.mts +0 -2
- package/dist/core.d.ts +0 -2
- package/dist/core.js +0 -1902
- package/dist/core.js.map +0 -1
- package/dist/core.mjs +0 -21
- package/dist/core.mjs.map +0 -1
- package/dist/index.d.mts +0 -1003
- package/dist/index.js +0 -5009
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -3145
- package/dist/index.mjs.map +0 -1
package/dist/index.mjs
DELETED
|
@@ -1,3145 +0,0 @@
|
|
|
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";
|
|
24
|
-
|
|
25
|
-
// packages/node-opcua-pki/lib/index.ts
|
|
26
|
-
import {
|
|
27
|
-
PrivateKeyPassphraseRequiredError,
|
|
28
|
-
PrivateKeyUnavailableError as PrivateKeyUnavailableError2
|
|
29
|
-
} from "node-opcua-crypto";
|
|
30
|
-
|
|
31
|
-
// packages/node-opcua-pki/lib/ca/backends/openssl_ca_backend.ts
|
|
32
|
-
import fs4 from "fs";
|
|
33
|
-
import path3 from "path";
|
|
34
|
-
import { Subject as Subject2 } from "node-opcua-crypto";
|
|
35
|
-
|
|
36
|
-
// packages/node-opcua-pki/lib/toolbox/with_openssl/_env.ts
|
|
37
|
-
var SAFE_ENV_PASSTHROUGH = /* @__PURE__ */ new Set([
|
|
38
|
-
// POSIX/Windows shell and process essentials
|
|
39
|
-
"path",
|
|
40
|
-
"home",
|
|
41
|
-
"userprofile",
|
|
42
|
-
"temp",
|
|
43
|
-
"tmp",
|
|
44
|
-
"tmpdir",
|
|
45
|
-
"systemroot",
|
|
46
|
-
"windir",
|
|
47
|
-
"comspec",
|
|
48
|
-
"pathext",
|
|
49
|
-
"appdata",
|
|
50
|
-
"localappdata",
|
|
51
|
-
// dynamic loader: a custom-built or relocated openssl (e.g. under /opt,
|
|
52
|
-
// or Homebrew on macOS) may need these to find its own libcrypto/libssl
|
|
53
|
-
"ld_library_path",
|
|
54
|
-
"dyld_library_path",
|
|
55
|
-
"dyld_fallback_library_path",
|
|
56
|
-
// locale, so openssl's textual output stays parseable
|
|
57
|
-
"lang",
|
|
58
|
-
"lc_all",
|
|
59
|
-
"lc_ctype",
|
|
60
|
-
// openssl configuration this app, or the host OS/user, may rely on
|
|
61
|
-
"openssl_conf",
|
|
62
|
-
"randfile",
|
|
63
|
-
"openssl_modules",
|
|
64
|
-
"openssl_engines",
|
|
65
|
-
"ssl_cert_file",
|
|
66
|
-
"ssl_cert_dir"
|
|
67
|
-
]);
|
|
68
|
-
function buildChildEnv(extra) {
|
|
69
|
-
const env = {};
|
|
70
|
-
for (const key of Object.keys(process.env)) {
|
|
71
|
-
if (SAFE_ENV_PASSTHROUGH.has(key.toLowerCase())) {
|
|
72
|
-
env[key] = process.env[key];
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return { ...env, ...extra };
|
|
76
|
-
}
|
|
77
|
-
function redactEnvForLog(options) {
|
|
78
|
-
const { env, ...rest } = options;
|
|
79
|
-
return env ? { ...rest, env: Object.keys(env) } : rest;
|
|
80
|
-
}
|
|
81
|
-
var exportedEnvVars = {};
|
|
82
|
-
function setEnv(varName, value) {
|
|
83
|
-
if (!g_config.silent) {
|
|
84
|
-
warningLog(` set ${varName}=${value}`);
|
|
85
|
-
}
|
|
86
|
-
exportedEnvVars[varName] = value;
|
|
87
|
-
if (["OPENSSL_CONF"].indexOf(varName) >= 0) {
|
|
88
|
-
process.env[varName] = value;
|
|
89
|
-
}
|
|
90
|
-
if (["RANDFILE"].indexOf(varName) >= 0) {
|
|
91
|
-
process.env[varName] = value;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
function hasEnv(varName) {
|
|
95
|
-
return Object.prototype.hasOwnProperty.call(exportedEnvVars, varName);
|
|
96
|
-
}
|
|
97
|
-
function getEnv(varName) {
|
|
98
|
-
return exportedEnvVars[varName];
|
|
99
|
-
}
|
|
100
|
-
function unsetEnv(varName) {
|
|
101
|
-
delete exportedEnvVars[varName];
|
|
102
|
-
}
|
|
103
|
-
function getEnvironmentVarNames() {
|
|
104
|
-
return Object.keys(exportedEnvVars).map((varName) => {
|
|
105
|
-
return { key: varName, pattern: `\\$ENV\\:\\:${varName}` };
|
|
106
|
-
});
|
|
107
|
-
}
|
|
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(", ");
|
|
114
|
-
}
|
|
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
|
-
|
|
124
|
-
// packages/node-opcua-pki/lib/toolbox/with_openssl/install_prerequisite.ts
|
|
125
|
-
import child_process from "child_process";
|
|
126
|
-
import fs from "fs";
|
|
127
|
-
import os from "os";
|
|
128
|
-
import path from "path";
|
|
129
|
-
import { pipeline } from "stream/promises";
|
|
130
|
-
import byline from "byline";
|
|
131
|
-
import chalk from "chalk";
|
|
132
|
-
import yauzl from "yauzl";
|
|
133
|
-
var doDebug2 = process.env.NODEOPCUAPKIDEBUG || false;
|
|
134
|
-
async function execute(file, args, cwd) {
|
|
135
|
-
let output = "";
|
|
136
|
-
return await new Promise((resolve, reject) => {
|
|
137
|
-
const child = child_process.spawn(file, args, {
|
|
138
|
-
cwd,
|
|
139
|
-
windowsHide: true,
|
|
140
|
-
env: buildChildEnv(),
|
|
141
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
142
|
-
});
|
|
143
|
-
child.on("error", (err) => reject(err));
|
|
144
|
-
child.on("close", (code) => resolve({ exitCode: code ?? 1, output }));
|
|
145
|
-
const stream1 = byline(child.stdout);
|
|
146
|
-
stream1.on("data", (line) => {
|
|
147
|
-
output += `${line}
|
|
148
|
-
`;
|
|
149
|
-
if (doDebug2) {
|
|
150
|
-
process.stdout.write(` stdout ${chalk.yellow(line)}
|
|
151
|
-
`);
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
function is_expected_openssl_version(strVersion) {
|
|
157
|
-
return !!strVersion.match(/OpenSSL \d/);
|
|
158
|
-
}
|
|
159
|
-
async function getopensslExecPath() {
|
|
160
|
-
let result1;
|
|
161
|
-
try {
|
|
162
|
-
result1 = await execute("which", ["openssl"]);
|
|
163
|
-
} catch (err) {
|
|
164
|
-
warningLog("warning: ", err.message);
|
|
165
|
-
throw new Error("Cannot find openssl");
|
|
166
|
-
}
|
|
167
|
-
const exitCode = result1?.exitCode;
|
|
168
|
-
const output = result1?.output;
|
|
169
|
-
if (exitCode !== 0) {
|
|
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"));
|
|
172
|
-
throw new Error("Cannot find openssl");
|
|
173
|
-
}
|
|
174
|
-
const opensslExecPath = output.replace(/\n\r/g, "").trim();
|
|
175
|
-
return opensslExecPath;
|
|
176
|
-
}
|
|
177
|
-
async function check_system_openssl_version() {
|
|
178
|
-
const opensslExecPath = await getopensslExecPath();
|
|
179
|
-
if (doDebug2) {
|
|
180
|
-
warningLog(` OpenSSL found in : ${chalk.yellow(opensslExecPath)}`);
|
|
181
|
-
}
|
|
182
|
-
const result = await execute(opensslExecPath, ["version"]);
|
|
183
|
-
const exitCode = result?.exitCode;
|
|
184
|
-
const output = result?.output;
|
|
185
|
-
const version = output.trim();
|
|
186
|
-
const versionOK = exitCode === 0 && is_expected_openssl_version(version);
|
|
187
|
-
if (!versionOK) {
|
|
188
|
-
let message = chalk.whiteBright("Warning !!!!!!!!!!!! ") + "\nyour version of openssl is " + version + ". It doesn't match the expected version";
|
|
189
|
-
if (process.platform === "darwin") {
|
|
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");
|
|
191
|
-
}
|
|
192
|
-
console.log(message);
|
|
193
|
-
}
|
|
194
|
-
return output;
|
|
195
|
-
}
|
|
196
|
-
async function install_and_check_win32_openssl_version() {
|
|
197
|
-
const downloadFolder = path.join(os.tmpdir(), ".");
|
|
198
|
-
function get_openssl_folder_win32() {
|
|
199
|
-
if (process.env.LOCALAPPDATA) {
|
|
200
|
-
const userProgramFolder = path.join(process.env.LOCALAPPDATA, "Programs");
|
|
201
|
-
if (fs.existsSync(userProgramFolder)) {
|
|
202
|
-
return path.join(userProgramFolder, "openssl");
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
return path.join(process.cwd(), "openssl");
|
|
206
|
-
}
|
|
207
|
-
function get_openssl_exec_path_win32() {
|
|
208
|
-
const opensslFolder2 = get_openssl_folder_win32();
|
|
209
|
-
return path.join(opensslFolder2, "openssl.exe");
|
|
210
|
-
}
|
|
211
|
-
async function check_openssl_win32() {
|
|
212
|
-
const opensslExecPath2 = get_openssl_exec_path_win32();
|
|
213
|
-
const exists = fs.existsSync(opensslExecPath2);
|
|
214
|
-
if (!exists) {
|
|
215
|
-
warningLog("checking presence of ", opensslExecPath2);
|
|
216
|
-
warningLog(chalk.red(" cannot find file ") + opensslExecPath2);
|
|
217
|
-
return {
|
|
218
|
-
opensslOk: false,
|
|
219
|
-
version: `cannot find file ${opensslExecPath2}`
|
|
220
|
-
};
|
|
221
|
-
} else {
|
|
222
|
-
const cwd = ".";
|
|
223
|
-
const { exitCode, output } = await execute(opensslExecPath2, ["version"], cwd);
|
|
224
|
-
const version = output.trim();
|
|
225
|
-
if (doDebug2) {
|
|
226
|
-
warningLog(" Version = ", version);
|
|
227
|
-
}
|
|
228
|
-
return {
|
|
229
|
-
opensslOk: exitCode === 0 && is_expected_openssl_version(version),
|
|
230
|
-
version
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
async function find_system_openssl_win32() {
|
|
235
|
-
try {
|
|
236
|
-
const result = await execute("where", ["openssl"]);
|
|
237
|
-
if (result.exitCode !== 0) {
|
|
238
|
-
return void 0;
|
|
239
|
-
}
|
|
240
|
-
const opensslPath2 = result.output.split(/\r?\n/)[0].trim();
|
|
241
|
-
if (!opensslPath2 || !fs.existsSync(opensslPath2)) {
|
|
242
|
-
return void 0;
|
|
243
|
-
}
|
|
244
|
-
const versionResult = await execute(opensslPath2, ["version"]);
|
|
245
|
-
const version = versionResult.output.trim();
|
|
246
|
-
if (versionResult.exitCode === 0 && is_expected_openssl_version(version)) {
|
|
247
|
-
warningLog(
|
|
248
|
-
chalk.green("Using system OpenSSL: ") + chalk.cyan(version) + chalk.green(" at ") + chalk.cyan(opensslPath2)
|
|
249
|
-
);
|
|
250
|
-
return opensslPath2;
|
|
251
|
-
}
|
|
252
|
-
warningLog(chalk.yellow("System OpenSSL found but version not accepted: ") + version);
|
|
253
|
-
return void 0;
|
|
254
|
-
} catch (_err) {
|
|
255
|
-
return void 0;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
function win32or64() {
|
|
259
|
-
if (process.env.PROCESSOR_ARCHITECTURE === "x86" && process.env.PROCESSOR_ARCHITEW6432) {
|
|
260
|
-
return 64;
|
|
261
|
-
}
|
|
262
|
-
if (process.env.PROCESSOR_ARCHITECTURE === "AMD64") {
|
|
263
|
-
return 64;
|
|
264
|
-
}
|
|
265
|
-
if (process.env.CURRENT_CPU === "x64") {
|
|
266
|
-
return 64;
|
|
267
|
-
}
|
|
268
|
-
return 32;
|
|
269
|
-
}
|
|
270
|
-
async function download_openssl() {
|
|
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";
|
|
272
|
-
const outputFilename = path.join(downloadFolder, path.basename(url));
|
|
273
|
-
warningLog(`downloading ${chalk.yellow(url)} to ${outputFilename}`);
|
|
274
|
-
if (fs.existsSync(outputFilename)) {
|
|
275
|
-
return { downloadedFile: outputFilename };
|
|
276
|
-
}
|
|
277
|
-
const response = await fetch(url, { redirect: "follow" });
|
|
278
|
-
if (!response.ok || !response.body) {
|
|
279
|
-
throw new Error(`Failed to download OpenSSL from ${url}: ${response.status} ${response.statusText}`);
|
|
280
|
-
}
|
|
281
|
-
const contentLength = parseInt(response.headers.get("content-length") || "0", 10);
|
|
282
|
-
let downloaded = 0;
|
|
283
|
-
let lastPercent = -1;
|
|
284
|
-
const fileStream = fs.createWriteStream(outputFilename);
|
|
285
|
-
const body = response.body;
|
|
286
|
-
body.on("data", (chunk) => {
|
|
287
|
-
downloaded += chunk.length;
|
|
288
|
-
if (contentLength > 0) {
|
|
289
|
-
const percent = Math.floor(downloaded / contentLength * 100);
|
|
290
|
-
if (percent !== lastPercent && percent % 10 === 0) {
|
|
291
|
-
lastPercent = percent;
|
|
292
|
-
warningLog(` download progress: ${percent}%`);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
});
|
|
296
|
-
await pipeline(body, fileStream);
|
|
297
|
-
const stat = fs.statSync(outputFilename);
|
|
298
|
-
if (stat.size === 0) {
|
|
299
|
-
fs.unlinkSync(outputFilename);
|
|
300
|
-
throw new Error(`Downloaded file is empty: ${outputFilename}`);
|
|
301
|
-
}
|
|
302
|
-
warningLog(chalk.green("Download complete: ") + `${stat.size} bytes`);
|
|
303
|
-
return { downloadedFile: outputFilename };
|
|
304
|
-
}
|
|
305
|
-
async function unzip_openssl(zipFilename) {
|
|
306
|
-
const opensslFolder2 = get_openssl_folder_win32();
|
|
307
|
-
const zipFile = await new Promise((resolve, reject) => {
|
|
308
|
-
yauzl.open(zipFilename, { lazyEntries: true }, (err, zipfile) => {
|
|
309
|
-
if (err) {
|
|
310
|
-
reject(err);
|
|
311
|
-
} else {
|
|
312
|
-
if (!zipfile) {
|
|
313
|
-
reject(new Error("zipfile is null"));
|
|
314
|
-
} else {
|
|
315
|
-
resolve(zipfile);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
});
|
|
319
|
-
});
|
|
320
|
-
zipFile.readEntry();
|
|
321
|
-
await new Promise((resolve, reject) => {
|
|
322
|
-
zipFile.on("end", (err) => {
|
|
323
|
-
setImmediate(() => {
|
|
324
|
-
if (doDebug2) {
|
|
325
|
-
warningLog("unzip done");
|
|
326
|
-
}
|
|
327
|
-
if (err) {
|
|
328
|
-
reject(err);
|
|
329
|
-
} else {
|
|
330
|
-
resolve();
|
|
331
|
-
}
|
|
332
|
-
});
|
|
333
|
-
});
|
|
334
|
-
zipFile.on("entry", (entry) => {
|
|
335
|
-
zipFile.openReadStream(entry, (err, readStream) => {
|
|
336
|
-
if (err) {
|
|
337
|
-
return reject(err);
|
|
338
|
-
}
|
|
339
|
-
const file = path.join(opensslFolder2, entry.fileName);
|
|
340
|
-
if (doDebug2) {
|
|
341
|
-
warningLog(" unzipping :", file);
|
|
342
|
-
}
|
|
343
|
-
const writeStream = fs.createWriteStream(file, "binary");
|
|
344
|
-
readStream?.pipe(writeStream);
|
|
345
|
-
writeStream.on("close", () => {
|
|
346
|
-
zipFile.readEntry();
|
|
347
|
-
});
|
|
348
|
-
});
|
|
349
|
-
});
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
const systemOpenssl = await find_system_openssl_win32();
|
|
353
|
-
if (systemOpenssl) {
|
|
354
|
-
return systemOpenssl;
|
|
355
|
-
}
|
|
356
|
-
const opensslFolder = get_openssl_folder_win32();
|
|
357
|
-
const opensslExecPath = get_openssl_exec_path_win32();
|
|
358
|
-
if (!fs.existsSync(opensslFolder)) {
|
|
359
|
-
if (doDebug2) {
|
|
360
|
-
warningLog("creating openssl_folder", opensslFolder);
|
|
361
|
-
}
|
|
362
|
-
fs.mkdirSync(opensslFolder);
|
|
363
|
-
}
|
|
364
|
-
const { opensslOk, version: _version } = await check_openssl_win32();
|
|
365
|
-
if (!opensslOk) {
|
|
366
|
-
warningLog(chalk.yellow("openssl seems to be missing and need to be installed"));
|
|
367
|
-
const { downloadedFile } = await download_openssl();
|
|
368
|
-
if (doDebug2) {
|
|
369
|
-
warningLog("deflating ", chalk.yellow(downloadedFile));
|
|
370
|
-
}
|
|
371
|
-
await unzip_openssl(downloadedFile);
|
|
372
|
-
const opensslExists = !!fs.existsSync(opensslExecPath);
|
|
373
|
-
if (doDebug2) {
|
|
374
|
-
warningLog("verifying ", opensslExists, opensslExists ? chalk.green("OK ") : chalk.red(" Error"), opensslExecPath);
|
|
375
|
-
}
|
|
376
|
-
const _opensslExecPath2 = await check_openssl_win32();
|
|
377
|
-
return opensslExecPath;
|
|
378
|
-
} else {
|
|
379
|
-
if (doDebug2) {
|
|
380
|
-
warningLog(chalk.green("openssl is already installed and have the expected version."));
|
|
381
|
-
}
|
|
382
|
-
return opensslExecPath;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
async function install_prerequisite() {
|
|
386
|
-
if (process.platform !== "win32") {
|
|
387
|
-
return await check_system_openssl_version();
|
|
388
|
-
} else {
|
|
389
|
-
return await install_and_check_win32_openssl_version();
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
async function get_openssl_exec_path() {
|
|
393
|
-
if (process.platform === "win32") {
|
|
394
|
-
const opensslExecPath = await install_prerequisite();
|
|
395
|
-
if (!fs.existsSync(opensslExecPath)) {
|
|
396
|
-
throw new Error(`internal error cannot find ${opensslExecPath}`);
|
|
397
|
-
}
|
|
398
|
-
return opensslExecPath;
|
|
399
|
-
} else {
|
|
400
|
-
return "openssl";
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// packages/node-opcua-pki/lib/toolbox/with_openssl/execute_openssl.ts
|
|
405
|
-
var opensslPath;
|
|
406
|
-
var n = makePath;
|
|
407
|
-
var PASSIN_ENV_VAR = "NODE_OPCUA_PKI_OPENSSL_PASSIN";
|
|
408
|
-
function passinArg(passphrase = "") {
|
|
409
|
-
return { args: ["-passin", `env:${PASSIN_ENV_VAR}`], env: { [PASSIN_ENV_VAR]: passphrase } };
|
|
410
|
-
}
|
|
411
|
-
function renderForDisplay(file, args) {
|
|
412
|
-
return [file, ...args].map((a) => a === "" || /[\s"'`$\\]/.test(a) ? JSON.stringify(a) : a).join(" ");
|
|
413
|
-
}
|
|
414
|
-
function hasUnsafeArgChar(value) {
|
|
415
|
-
for (const ch of value) {
|
|
416
|
-
const code = ch.codePointAt(0);
|
|
417
|
-
if (code < 32 || code === 127) {
|
|
418
|
-
return true;
|
|
419
|
-
}
|
|
420
|
-
if (code < 128) {
|
|
421
|
-
continue;
|
|
422
|
-
}
|
|
423
|
-
if (/[\p{L}\p{N}\p{M}]/u.test(ch)) {
|
|
424
|
-
continue;
|
|
425
|
-
}
|
|
426
|
-
return true;
|
|
427
|
-
}
|
|
428
|
-
return false;
|
|
429
|
-
}
|
|
430
|
-
function checkArgs(file, args) {
|
|
431
|
-
for (const value of [file, ...args]) {
|
|
432
|
-
if (typeof value === "string" && hasUnsafeArgChar(value)) {
|
|
433
|
-
throw new Error(`execute_openssl: refusing argument with an unexpected character: ${JSON.stringify(value)}`);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
async function execute2(file, args, options) {
|
|
438
|
-
const from = new Error();
|
|
439
|
-
checkArgs(file, args);
|
|
440
|
-
options.cwd = options.cwd || process.cwd();
|
|
441
|
-
if (!g_config.silent) {
|
|
442
|
-
warningLog(chalk2.cyan(" CWD "), options.cwd);
|
|
443
|
-
}
|
|
444
|
-
const outputs = [];
|
|
445
|
-
const errorOutputs = [];
|
|
446
|
-
const display2 = renderForDisplay(file, args);
|
|
447
|
-
return await new Promise((resolve, reject) => {
|
|
448
|
-
const child = child_process2.spawn(file, [...args], {
|
|
449
|
-
cwd: options.cwd,
|
|
450
|
-
windowsHide: true,
|
|
451
|
-
env: buildChildEnv(options.env),
|
|
452
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
453
|
-
});
|
|
454
|
-
const fail = (message) => {
|
|
455
|
-
if (!options.hideErrorMessage && !g_config.silent) {
|
|
456
|
-
const fence = "###########################################";
|
|
457
|
-
console.error(chalk2.bgWhiteBright.redBright(`${fence} OPENSSL ERROR ${fence}`));
|
|
458
|
-
console.error(chalk2.bgWhiteBright.redBright(`CWD = ${options.cwd}`));
|
|
459
|
-
console.error(chalk2.bgWhiteBright.redBright(message));
|
|
460
|
-
console.error(chalk2.bgWhiteBright.redBright(`${fence} OPENSSL ERROR ${fence}`));
|
|
461
|
-
console.error(from.stack);
|
|
462
|
-
}
|
|
463
|
-
reject(new Error(message));
|
|
464
|
-
};
|
|
465
|
-
child.on("error", (err) => fail(`Command failed: ${display2}
|
|
466
|
-
${err.message}`));
|
|
467
|
-
child.on("close", (code, signal) => {
|
|
468
|
-
if (code === 0) {
|
|
469
|
-
resolve(outputs.join(""));
|
|
470
|
-
return;
|
|
471
|
-
}
|
|
472
|
-
const why = signal ? `signal ${signal}` : `exit code ${code}`;
|
|
473
|
-
fail(`Command failed: ${display2}
|
|
474
|
-
${errorOutputs.join("")}(${why})`);
|
|
475
|
-
});
|
|
476
|
-
if (child.stdout) {
|
|
477
|
-
const stream2 = byline2(child.stdout);
|
|
478
|
-
stream2.on("data", (line) => {
|
|
479
|
-
outputs.push(`${line}
|
|
480
|
-
`);
|
|
481
|
-
});
|
|
482
|
-
if (!g_config.silent) {
|
|
483
|
-
stream2.on("data", (line) => {
|
|
484
|
-
line = line.toString();
|
|
485
|
-
if (doDebug) {
|
|
486
|
-
process.stdout.write(`${chalk2.white(" stdout ") + chalk2.whiteBright(line)}
|
|
487
|
-
`);
|
|
488
|
-
}
|
|
489
|
-
});
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
if (child.stderr) {
|
|
493
|
-
const stream1 = byline2(child.stderr);
|
|
494
|
-
stream1.on("data", (line) => {
|
|
495
|
-
line = line.toString();
|
|
496
|
-
errorOutputs.push(`${line}
|
|
497
|
-
`);
|
|
498
|
-
if (!g_config.silent && displayError) {
|
|
499
|
-
process.stdout.write(`${chalk2.white(" stderr ") + chalk2.red(line)}
|
|
500
|
-
`);
|
|
501
|
-
}
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
});
|
|
505
|
-
}
|
|
506
|
-
async function find_openssl() {
|
|
507
|
-
return await get_openssl_exec_path();
|
|
508
|
-
}
|
|
509
|
-
async function ensure_openssl_installed() {
|
|
510
|
-
if (!opensslPath) {
|
|
511
|
-
opensslPath = await find_openssl();
|
|
512
|
-
const outputs = await execute_openssl(["version"], { cwd: "." });
|
|
513
|
-
g_config.opensslVersion = outputs.trim();
|
|
514
|
-
if (doDebug) {
|
|
515
|
-
warningLog("OpenSSL version : ", g_config.opensslVersion);
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
async function execute_openssl_no_failure(args, options) {
|
|
520
|
-
options = options || {};
|
|
521
|
-
options.hideErrorMessage = true;
|
|
522
|
-
try {
|
|
523
|
-
return await execute_openssl(args, options);
|
|
524
|
-
} catch (err) {
|
|
525
|
-
debugLog(" (ignored error = ERROR : )", err.message);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
function getTempFolder() {
|
|
529
|
-
return os2.tmpdir();
|
|
530
|
-
}
|
|
531
|
-
async function execute_openssl(args, options) {
|
|
532
|
-
debugLog("execute_openssl", args, redactEnvForLog(options));
|
|
533
|
-
const empty_config_file = n(getTempFolder(), "empty_config.cnf");
|
|
534
|
-
if (!fs2.existsSync(empty_config_file)) {
|
|
535
|
-
await fs2.promises.writeFile(empty_config_file, "# empty config file");
|
|
536
|
-
}
|
|
537
|
-
options = options || {};
|
|
538
|
-
options.openssl_conf = options.openssl_conf || empty_config_file;
|
|
539
|
-
assert(options.openssl_conf);
|
|
540
|
-
setEnv("OPENSSL_CONF", options.openssl_conf);
|
|
541
|
-
if (!g_config.silent) {
|
|
542
|
-
warningLog(chalk2.cyan(" OPENSSL_CONF"), process.env.OPENSSL_CONF);
|
|
543
|
-
warningLog(chalk2.cyan(" RANDFILE "), process.env.RANDFILE);
|
|
544
|
-
warningLog(chalk2.cyan(" CMD "), chalk2.cyanBright(renderForDisplay("openssl", args)));
|
|
545
|
-
}
|
|
546
|
-
await ensure_openssl_installed();
|
|
547
|
-
return await execute2(opensslPath, args, options);
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
// packages/node-opcua-pki/lib/toolbox/with_openssl/toolbox.ts
|
|
551
|
-
import assert2 from "assert";
|
|
552
|
-
import fs3 from "fs";
|
|
553
|
-
import path2 from "path";
|
|
554
|
-
function openssl_require2DigitYearInDate() {
|
|
555
|
-
if (!g_config.opensslVersion) {
|
|
556
|
-
throw new Error(
|
|
557
|
-
"openssl_require2DigitYearInDate : openssl version is not known: please call ensure_openssl_installed()"
|
|
558
|
-
);
|
|
559
|
-
}
|
|
560
|
-
return g_config.opensslVersion.match(/OpenSSL 0\.9/);
|
|
561
|
-
}
|
|
562
|
-
g_config.opensslVersion = "";
|
|
563
|
-
var _counter = 0;
|
|
564
|
-
function stripConditionalBlocks(template, envOverrides) {
|
|
565
|
-
return template.replace(/\{\{#([A-Z_][A-Z0-9_]*)\}\}([\s\S]*?)\{\{\/\1\}\}\r?\n?/g, (_match, key, content) => {
|
|
566
|
-
const keep = envOverrides && Object.prototype.hasOwnProperty.call(envOverrides, key) ? envOverrides[key] !== "" : hasEnv(key) && getEnv(key) !== "";
|
|
567
|
-
return keep ? content : "";
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
function generateStaticConfig(configPath, options, envOverrides) {
|
|
571
|
-
const prePath = options?.cwd || "";
|
|
572
|
-
const originalFilename = !path2.isAbsolute(configPath) ? path2.join(prePath, configPath) : configPath;
|
|
573
|
-
let staticConfig = fs3.readFileSync(originalFilename, { encoding: "utf8" });
|
|
574
|
-
staticConfig = stripConditionalBlocks(staticConfig, envOverrides);
|
|
575
|
-
for (const envVar of getEnvironmentVarNames()) {
|
|
576
|
-
if (envOverrides && Object.prototype.hasOwnProperty.call(envOverrides, envVar.key)) {
|
|
577
|
-
continue;
|
|
578
|
-
}
|
|
579
|
-
staticConfig = staticConfig.replace(new RegExp(envVar.pattern, "gi"), () => getEnv(envVar.key));
|
|
580
|
-
}
|
|
581
|
-
if (envOverrides) {
|
|
582
|
-
for (const [key, value] of Object.entries(envOverrides)) {
|
|
583
|
-
staticConfig = staticConfig.replace(new RegExp(`\\$ENV\\:\\:${key}`, "gi"), () => value);
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
const staticConfigPath = `${configPath}.${process.pid}-${_counter++}.tmp`;
|
|
587
|
-
const temporaryConfigPath = !path2.isAbsolute(configPath) ? path2.join(prePath, staticConfigPath) : staticConfigPath;
|
|
588
|
-
fs3.writeFileSync(temporaryConfigPath, staticConfig);
|
|
589
|
-
if (options?.cwd) {
|
|
590
|
-
return path2.relative(options.cwd, temporaryConfigPath);
|
|
591
|
-
} else {
|
|
592
|
-
return temporaryConfigPath;
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
async function cleanupStaticConfig(configFile, options) {
|
|
596
|
-
await fs3.promises.rm(path2.resolve(options?.cwd ?? "", configFile), { force: true });
|
|
597
|
-
}
|
|
598
|
-
function x509Date(date) {
|
|
599
|
-
date = date || /* @__PURE__ */ new Date();
|
|
600
|
-
const Y = date.getUTCFullYear();
|
|
601
|
-
const M = date.getUTCMonth() + 1;
|
|
602
|
-
const D = date.getUTCDate();
|
|
603
|
-
const h = date.getUTCHours();
|
|
604
|
-
const m = date.getUTCMinutes();
|
|
605
|
-
const s = date.getUTCSeconds();
|
|
606
|
-
function w(s2, l) {
|
|
607
|
-
return `${s2}`.padStart(l, "0");
|
|
608
|
-
}
|
|
609
|
-
if (openssl_require2DigitYearInDate()) {
|
|
610
|
-
return `${w(Y, 2) + w(M, 2) + w(D, 2) + w(h, 2) + w(m, 2) + w(s, 2)}Z`;
|
|
611
|
-
} else {
|
|
612
|
-
return `${w(Y, 4) + w(M, 2) + w(D, 2) + w(h, 2) + w(m, 2) + w(s, 2)}Z`;
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
// packages/node-opcua-pki/lib/ca/backends/openssl_ca_backend.ts
|
|
617
|
-
var n2 = makePath;
|
|
618
|
-
function caAltName(ca) {
|
|
619
|
-
return `URI:urn:${ca.subject.commonName || "NodeOPCUA-CA"}`;
|
|
620
|
-
}
|
|
621
|
-
function caConfigEnvOverrides(ca, altName = caAltName(ca)) {
|
|
622
|
-
const aiaLegs = [];
|
|
623
|
-
if (ca.ocspResponderUrl) {
|
|
624
|
-
aiaLegs.push(`OCSP;URI:${ca.ocspResponderUrl}`);
|
|
625
|
-
}
|
|
626
|
-
if (ca.caIssuersUrl) {
|
|
627
|
-
aiaLegs.push(`caIssuers;URI:${ca.caIssuersUrl}`);
|
|
628
|
-
}
|
|
629
|
-
return {
|
|
630
|
-
ALTNAME: altName,
|
|
631
|
-
CDP_URL: ca.crlDistributionUrl ?? "",
|
|
632
|
-
AIA_VALUE: aiaLegs.join(",")
|
|
633
|
-
};
|
|
634
|
-
}
|
|
635
|
-
var OpenSslCaBackend = class {
|
|
636
|
-
/** This backend is the `openssl` executable, so it has to be there. */
|
|
637
|
-
/**
|
|
638
|
-
* `-passin env:` argv and env for an openssl call that has to load this
|
|
639
|
-
* CA's key. Built here rather than on the CA: the passphrase is the
|
|
640
|
-
* CA's business, but expressing it as an openssl argument is this
|
|
641
|
-
* backend's, and the core has no reason to know the flag exists.
|
|
642
|
-
*/
|
|
643
|
-
async #passin(ca) {
|
|
644
|
-
return passinArg(await ca._privateKeyPassphrase());
|
|
645
|
-
}
|
|
646
|
-
/** The openssl CLI loads its key from a file, so it cannot call out to an HSM. */
|
|
647
|
-
supportsExternalSigner = false;
|
|
648
|
-
async preflight() {
|
|
649
|
-
await ensure_openssl_installed();
|
|
650
|
-
}
|
|
651
|
-
/**
|
|
652
|
-
* Render `conf/caconfig.cnf` once with explicit overrides, run `fn`
|
|
653
|
-
* with the rendered path, and always remove the temp file. Structural
|
|
654
|
-
* guarantee that (a) every render carries the three required env
|
|
655
|
-
* values and (b) no rendered `<name>.<pid>-<n>.tmp` file is ever left
|
|
656
|
-
* behind — each was previously a per-method discipline.
|
|
657
|
-
*/
|
|
658
|
-
async #withConfig(ca, altName, fn) {
|
|
659
|
-
const caRootDir = path3.resolve(ca.rootDir);
|
|
660
|
-
const options = { cwd: caRootDir };
|
|
661
|
-
const configFile = generateStaticConfig("conf/caconfig.cnf", options, caConfigEnvOverrides(ca, altName));
|
|
662
|
-
try {
|
|
663
|
-
return await fn(configFile, options);
|
|
664
|
-
} finally {
|
|
665
|
-
await cleanupStaticConfig(configFile, options);
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
async #generateCaCsrWith(ca, configFile, options, privateKeyFile, csrFile) {
|
|
669
|
-
const passin = await this.#passin(ca);
|
|
670
|
-
displayTitle("Generate a certificate request for the CA key");
|
|
671
|
-
await execute_openssl(
|
|
672
|
-
[
|
|
673
|
-
"req",
|
|
674
|
-
"-new",
|
|
675
|
-
"-sha256",
|
|
676
|
-
"-extensions",
|
|
677
|
-
"v3_ca_req",
|
|
678
|
-
"-config",
|
|
679
|
-
n2(configFile),
|
|
680
|
-
"-key",
|
|
681
|
-
n2(privateKeyFile),
|
|
682
|
-
"-out",
|
|
683
|
-
n2(csrFile),
|
|
684
|
-
"-subj",
|
|
685
|
-
ca.subject.toString(),
|
|
686
|
-
...passin.args
|
|
687
|
-
],
|
|
688
|
-
{ ...options, env: passin.env }
|
|
689
|
-
);
|
|
690
|
-
}
|
|
691
|
-
/**
|
|
692
|
-
* `openssl ca -gencrl` signs the CRL with the CA key it finds through the
|
|
693
|
-
* config file (`private_key = $dir/private/cakey.pem`), so it needs the
|
|
694
|
-
* CA passphrase like every other `openssl ca` invocation.
|
|
695
|
-
*/
|
|
696
|
-
async #regenerateCrlWith(ca, configFile, options) {
|
|
697
|
-
const passin = await this.#passin(ca);
|
|
698
|
-
displaySubtitle("regenerate CRL (Certificate Revocation List)");
|
|
699
|
-
await execute_openssl(["ca", "-gencrl", "-config", n2(configFile), "-out", "crl/revocation_list.crl", ...passin.args], {
|
|
700
|
-
...options,
|
|
701
|
-
env: passin.env
|
|
702
|
-
});
|
|
703
|
-
await execute_openssl(
|
|
704
|
-
["crl", "-in", "crl/revocation_list.crl", "-out", "crl/revocation_list.der", "-outform", "der"],
|
|
705
|
-
options
|
|
706
|
-
);
|
|
707
|
-
displaySubtitle("Display (Certificate Revocation List)");
|
|
708
|
-
await execute_openssl(["crl", "-in", n2(ca.revocationList), "-text", "-noout"], options);
|
|
709
|
-
}
|
|
710
|
-
async generateCaCsr(ca, _caRootDir, privateKeyFile, csrFile) {
|
|
711
|
-
await this.#withConfig(
|
|
712
|
-
ca,
|
|
713
|
-
caAltName(ca),
|
|
714
|
-
(configFile, options) => this.#generateCaCsrWith(ca, configFile, options, privateKeyFile, csrFile)
|
|
715
|
-
);
|
|
716
|
-
}
|
|
717
|
-
async bootstrap(ca) {
|
|
718
|
-
const caRootDir = path3.resolve(ca.rootDir);
|
|
719
|
-
const privateKeyFilename = path3.join(caRootDir, "private/cakey.pem");
|
|
720
|
-
const csrFilename = path3.join(caRootDir, "private/cakey.csr");
|
|
721
|
-
await this.#withConfig(ca, caAltName(ca), async (configFile, options) => {
|
|
722
|
-
await this.#generateCaCsrWith(ca, configFile, options, privateKeyFilename, csrFilename);
|
|
723
|
-
const issuerCA = ca._issuerCA;
|
|
724
|
-
if (issuerCA) {
|
|
725
|
-
displayTitle("Generate CA Certificate (signed by issuer CA)");
|
|
726
|
-
const issuerCert = path3.resolve(issuerCA.caCertificate);
|
|
727
|
-
const issuerKey = path3.resolve(issuerCA.rootDir, "private/cakey.pem");
|
|
728
|
-
const issuerSerial = path3.resolve(issuerCA.rootDir, "serial");
|
|
729
|
-
const issuerPassin = await this.#passin(issuerCA);
|
|
730
|
-
const signWithIssuer = async () => {
|
|
731
|
-
await execute_openssl(
|
|
732
|
-
[
|
|
733
|
-
"x509",
|
|
734
|
-
"-sha256",
|
|
735
|
-
"-req",
|
|
736
|
-
"-days",
|
|
737
|
-
"3650",
|
|
738
|
-
"-extensions",
|
|
739
|
-
"v3_ca",
|
|
740
|
-
"-extfile",
|
|
741
|
-
n2(configFile),
|
|
742
|
-
"-in",
|
|
743
|
-
"private/cakey.csr",
|
|
744
|
-
"-CA",
|
|
745
|
-
n2(issuerCert),
|
|
746
|
-
"-CAkey",
|
|
747
|
-
n2(issuerKey),
|
|
748
|
-
"-CAserial",
|
|
749
|
-
n2(issuerSerial),
|
|
750
|
-
"-out",
|
|
751
|
-
"public/cacert.pem",
|
|
752
|
-
...issuerPassin.args
|
|
753
|
-
],
|
|
754
|
-
{ ...options, env: issuerPassin.env }
|
|
755
|
-
);
|
|
756
|
-
};
|
|
757
|
-
if (path3.resolve(issuerCA.rootDir) === caRootDir) {
|
|
758
|
-
await signWithIssuer();
|
|
759
|
-
} else {
|
|
760
|
-
await issuerCA._withCaDirectoryLock(signWithIssuer);
|
|
761
|
-
}
|
|
762
|
-
} else {
|
|
763
|
-
displayTitle("Generate CA Certificate (self-signed)");
|
|
764
|
-
const passin = await this.#passin(ca);
|
|
765
|
-
await execute_openssl(
|
|
766
|
-
[
|
|
767
|
-
"x509",
|
|
768
|
-
"-sha256",
|
|
769
|
-
"-req",
|
|
770
|
-
"-days",
|
|
771
|
-
"3650",
|
|
772
|
-
"-extensions",
|
|
773
|
-
"v3_ca",
|
|
774
|
-
"-extfile",
|
|
775
|
-
n2(configFile),
|
|
776
|
-
"-in",
|
|
777
|
-
"private/cakey.csr",
|
|
778
|
-
"-signkey",
|
|
779
|
-
n2(privateKeyFilename),
|
|
780
|
-
"-out",
|
|
781
|
-
"public/cacert.pem",
|
|
782
|
-
...passin.args
|
|
783
|
-
],
|
|
784
|
-
{ ...options, env: passin.env }
|
|
785
|
-
);
|
|
786
|
-
}
|
|
787
|
-
displaySubtitle("generate initial CRL (Certificate Revocation List)");
|
|
788
|
-
await this.#regenerateCrlWith(ca, configFile, options);
|
|
789
|
-
});
|
|
790
|
-
displayTitle("Create Certificate Authority (CA) ---> DONE");
|
|
791
|
-
}
|
|
792
|
-
async regenerateCrl(ca) {
|
|
793
|
-
await this.#withConfig(ca, caAltName(ca), (configFile, options) => this.#regenerateCrlWith(ca, configFile, options));
|
|
794
|
-
}
|
|
795
|
-
async signSubordinateCsr(ca, csrFile, certFile, validityDays) {
|
|
796
|
-
await this.#withConfig(ca, caAltName(ca), async (configFile, options) => {
|
|
797
|
-
const caRootDir = options.cwd;
|
|
798
|
-
const passin = await this.#passin(ca);
|
|
799
|
-
await execute_openssl(
|
|
800
|
-
[
|
|
801
|
-
"x509",
|
|
802
|
-
"-sha256",
|
|
803
|
-
"-req",
|
|
804
|
-
"-days",
|
|
805
|
-
String(validityDays),
|
|
806
|
-
"-extensions",
|
|
807
|
-
"v3_ca",
|
|
808
|
-
"-extfile",
|
|
809
|
-
n2(configFile),
|
|
810
|
-
"-in",
|
|
811
|
-
n2(csrFile),
|
|
812
|
-
"-CA",
|
|
813
|
-
n2(ca.caCertificate),
|
|
814
|
-
"-CAkey",
|
|
815
|
-
n2(path3.join(caRootDir, "private/cakey.pem")),
|
|
816
|
-
"-CAserial",
|
|
817
|
-
n2(path3.join(caRootDir, "serial")),
|
|
818
|
-
"-out",
|
|
819
|
-
n2(certFile),
|
|
820
|
-
...passin.args
|
|
821
|
-
],
|
|
822
|
-
{ ...options, env: passin.env }
|
|
823
|
-
);
|
|
824
|
-
});
|
|
825
|
-
}
|
|
826
|
-
async signEndEntityCsr(ca, certificate, csr, params1, sanOverride) {
|
|
827
|
-
await this.#withConfig(ca, buildSubjectAltNameString(sanOverride), async (configFile, options) => {
|
|
828
|
-
displaySubtitle("- then we ask the authority to sign the certificate signing request");
|
|
829
|
-
const passin = await this.#passin(ca);
|
|
830
|
-
await execute_openssl(
|
|
831
|
-
[
|
|
832
|
-
"ca",
|
|
833
|
-
"-config",
|
|
834
|
-
configFile,
|
|
835
|
-
"-startdate",
|
|
836
|
-
x509Date(params1.startDate),
|
|
837
|
-
"-enddate",
|
|
838
|
-
x509Date(params1.endDate),
|
|
839
|
-
"-batch",
|
|
840
|
-
"-out",
|
|
841
|
-
n2(certificate),
|
|
842
|
-
"-in",
|
|
843
|
-
n2(csr),
|
|
844
|
-
...passin.args
|
|
845
|
-
],
|
|
846
|
-
{ ...options, env: passin.env }
|
|
847
|
-
);
|
|
848
|
-
displaySubtitle("- dump the certificate for a check");
|
|
849
|
-
await execute_openssl(["x509", "-in", n2(certificate), "-dates", "-fingerprint", "-purpose", "-noout"], options);
|
|
850
|
-
});
|
|
851
|
-
}
|
|
852
|
-
async revoke(ca, certificate, reason) {
|
|
853
|
-
setEnv("RANDFILE", path3.join(ca.rootDir, "random.rnd"));
|
|
854
|
-
await this.#withConfig(ca, caAltName(ca), async (configFile, options) => {
|
|
855
|
-
displaySubtitle("Revoke certificate");
|
|
856
|
-
const passin = await this.#passin(ca);
|
|
857
|
-
await execute_openssl_no_failure(
|
|
858
|
-
["ca", "-verbose", "-config", n2(configFile), "-revoke", certificate, "-crl_reason", reason, ...passin.args],
|
|
859
|
-
{ ...options, env: passin.env }
|
|
860
|
-
);
|
|
861
|
-
await this.#regenerateCrlWith(ca, configFile, options);
|
|
862
|
-
displaySubtitle("Verify that certificate is revoked");
|
|
863
|
-
await execute_openssl_no_failure(
|
|
864
|
-
[
|
|
865
|
-
"verify",
|
|
866
|
-
"-verbose",
|
|
867
|
-
"-CRLfile",
|
|
868
|
-
n2(ca.revocationList),
|
|
869
|
-
"-CAfile",
|
|
870
|
-
n2(ca.caCertificate),
|
|
871
|
-
"-crl_check",
|
|
872
|
-
n2(certificate)
|
|
873
|
-
],
|
|
874
|
-
options
|
|
875
|
-
);
|
|
876
|
-
displaySubtitle("Produce CRL in DER form ");
|
|
877
|
-
await execute_openssl(
|
|
878
|
-
["crl", "-in", n2(ca.revocationList), "-out", "crl/revocation_list.der", "-outform", "der"],
|
|
879
|
-
options
|
|
880
|
-
);
|
|
881
|
-
displaySubtitle("Produce CRL in PEM form ");
|
|
882
|
-
await execute_openssl(
|
|
883
|
-
["crl", "-in", n2(ca.revocationList), "-out", "crl/revocation_list.pem", "-outform", "pem", "-text"],
|
|
884
|
-
options
|
|
885
|
-
);
|
|
886
|
-
});
|
|
887
|
-
}
|
|
888
|
-
async createSelfSignedCertificate(ca, certificateFile, privateKeyFile, params) {
|
|
889
|
-
const envOverrides = caConfigEnvOverrides(ca, buildSubjectAltNameString(params));
|
|
890
|
-
const configFile = generateStaticConfig(ca.configFile, { cwd: ca.rootDir }, envOverrides);
|
|
891
|
-
const options = {
|
|
892
|
-
cwd: ca.rootDir,
|
|
893
|
-
openssl_conf: makePath(configFile)
|
|
894
|
-
};
|
|
895
|
-
try {
|
|
896
|
-
const subject = params.subject ? new Subject2(params.subject).toString() : "";
|
|
897
|
-
const subjectOptions = subject && subject.length > 1 ? ["-subj", subject] : [];
|
|
898
|
-
const csrFile = `${certificateFile}_csr`;
|
|
899
|
-
const passin = await this.#passin(ca);
|
|
900
|
-
displaySubtitle("- the certificate signing request");
|
|
901
|
-
await execute_openssl(
|
|
902
|
-
[
|
|
903
|
-
"req",
|
|
904
|
-
"-new",
|
|
905
|
-
"-sha256",
|
|
906
|
-
...subjectOptions,
|
|
907
|
-
"-batch",
|
|
908
|
-
"-key",
|
|
909
|
-
n2(privateKeyFile),
|
|
910
|
-
"-out",
|
|
911
|
-
n2(csrFile),
|
|
912
|
-
...passin.args
|
|
913
|
-
],
|
|
914
|
-
{ ...options, env: passin.env }
|
|
915
|
-
);
|
|
916
|
-
displaySubtitle("- creating the self-signed certificate");
|
|
917
|
-
await execute_openssl(
|
|
918
|
-
[
|
|
919
|
-
"ca",
|
|
920
|
-
"-selfsign",
|
|
921
|
-
"-keyfile",
|
|
922
|
-
n2(privateKeyFile),
|
|
923
|
-
"-startdate",
|
|
924
|
-
x509Date(params.startDate),
|
|
925
|
-
"-enddate",
|
|
926
|
-
x509Date(params.endDate),
|
|
927
|
-
"-batch",
|
|
928
|
-
"-out",
|
|
929
|
-
n2(certificateFile),
|
|
930
|
-
"-in",
|
|
931
|
-
n2(csrFile),
|
|
932
|
-
...passin.args
|
|
933
|
-
],
|
|
934
|
-
{ ...options, env: passin.env }
|
|
935
|
-
);
|
|
936
|
-
displaySubtitle("- dump the certificate for a check");
|
|
937
|
-
await execute_openssl(["x509", "-in", n2(certificateFile), "-dates", "-fingerprint", "-purpose", "-noout"], {});
|
|
938
|
-
displaySubtitle("- verify self-signed certificate");
|
|
939
|
-
await execute_openssl_no_failure(["verify", "-verbose", "-CAfile", n2(certificateFile), n2(certificateFile)], options);
|
|
940
|
-
await fs4.promises.unlink(csrFile);
|
|
941
|
-
} finally {
|
|
942
|
-
await cleanupStaticConfig(configFile, { cwd: ca.rootDir });
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
};
|
|
946
|
-
|
|
947
|
-
// packages/node-opcua-pki/lib/ca/certificate_authority.ts
|
|
948
|
-
var CertificateAuthority = class extends CertificateAuthorityCore {
|
|
949
|
-
constructor(options) {
|
|
950
|
-
if (options.signer && options.backend === "openssl") {
|
|
951
|
-
throw new Error(
|
|
952
|
-
"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."
|
|
953
|
-
);
|
|
954
|
-
}
|
|
955
|
-
const backend = options.signer || options.backend === "native" ? new NativeCaBackend() : new OpenSslCaBackend();
|
|
956
|
-
super({ ...options, backend });
|
|
957
|
-
}
|
|
958
|
-
/**
|
|
959
|
-
* @internal `-passin env:` argv + env for an openssl call that loads
|
|
960
|
-
* this CA's key (always emitted, empty when none).
|
|
961
|
-
*
|
|
962
|
-
* The openssl backend builds its own now; this remains so that external
|
|
963
|
-
* code calling it keeps working, and lives here rather than on the core
|
|
964
|
-
* because the flag means nothing to a backend that spawns nothing.
|
|
965
|
-
*/
|
|
966
|
-
async _opensslPassin() {
|
|
967
|
-
return passinArg(await this._privateKeyPassphrase());
|
|
968
|
-
}
|
|
969
|
-
/**
|
|
970
|
-
* @internal
|
|
971
|
-
* Legacy shim: publish the `CDP_URL` / `AIA_VALUE` config substitution
|
|
972
|
-
* values to the shared env registry, or unset them so the matching
|
|
973
|
-
* `{{#KEY}}...{{/KEY}}` blocks are stripped. Nothing in this package
|
|
974
|
-
* reads the registry any more - every openssl config render receives
|
|
975
|
-
* these values explicitly, from the same {@link caConfigEnvOverrides}
|
|
976
|
-
* builder this delegates to, so the two cannot drift - kept only for
|
|
977
|
-
* external code that renders openssl config templates against the
|
|
978
|
-
* registry directly.
|
|
979
|
-
*
|
|
980
|
-
* It lives on this class rather than the core because it is meaningful
|
|
981
|
-
* only to the openssl backend.
|
|
982
|
-
*/
|
|
983
|
-
_wireRevocationEnvVars() {
|
|
984
|
-
const overrides = caConfigEnvOverrides(this);
|
|
985
|
-
if (overrides.CDP_URL) {
|
|
986
|
-
setEnv("CDP_URL", overrides.CDP_URL);
|
|
987
|
-
} else {
|
|
988
|
-
unsetEnv("CDP_URL");
|
|
989
|
-
}
|
|
990
|
-
if (overrides.AIA_VALUE) {
|
|
991
|
-
setEnv("AIA_VALUE", overrides.AIA_VALUE);
|
|
992
|
-
} else {
|
|
993
|
-
unsetEnv("AIA_VALUE");
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
};
|
|
997
|
-
|
|
998
|
-
// packages/node-opcua-pki/lib/pki/certificate_manager.ts
|
|
999
|
-
import { EventEmitter } from "events";
|
|
1000
|
-
import fs7 from "fs";
|
|
1001
|
-
import path4 from "path";
|
|
1002
|
-
import { drainPendingLocks, withLock } from "@ster5/global-mutex";
|
|
1003
|
-
import chalk3 from "chalk";
|
|
1004
|
-
import chokidar from "chokidar";
|
|
1005
|
-
import {
|
|
1006
|
-
caSignerFromKeyOperations,
|
|
1007
|
-
exploreCertificate,
|
|
1008
|
-
exploreCertificateInfo,
|
|
1009
|
-
exploreCertificateRevocationList,
|
|
1010
|
-
generatePrivateKeyFile,
|
|
1011
|
-
keyOperationsFromPrivateKey,
|
|
1012
|
-
makeSHA1Thumbprint,
|
|
1013
|
-
PrivateKeyUnavailableError,
|
|
1014
|
-
readCertificateChain,
|
|
1015
|
-
readCertificateChainAsync,
|
|
1016
|
-
readCertificateRevocationList,
|
|
1017
|
-
readPrivateKey,
|
|
1018
|
-
split_der,
|
|
1019
|
-
toPem,
|
|
1020
|
-
verifyCertificateSignature,
|
|
1021
|
-
writePrivateKeyFile
|
|
1022
|
-
} from "node-opcua-crypto";
|
|
1023
|
-
|
|
1024
|
-
// packages/node-opcua-pki/lib/toolbox/without_openssl/create_certificate_signing_request.ts
|
|
1025
|
-
import assert3 from "assert";
|
|
1026
|
-
import fs5 from "fs";
|
|
1027
|
-
import { coercePrivateKeyPem, createCertificateSigningRequest, pemToPrivateKey, Subject as Subject3 } from "node-opcua-crypto";
|
|
1028
|
-
async function createCertificateSigningRequestAsync(certificateSigningRequestFilename, params) {
|
|
1029
|
-
assert3(params);
|
|
1030
|
-
assert3(params.rootDir);
|
|
1031
|
-
assert3(params.configFile);
|
|
1032
|
-
assert3(params.privateKey);
|
|
1033
|
-
if (typeof params.privateKey === "string") {
|
|
1034
|
-
assert3(fs5.existsSync(params.privateKey), `Private key must exist${params.privateKey}`);
|
|
1035
|
-
}
|
|
1036
|
-
assert3(fs5.existsSync(params.rootDir), "RootDir key must exist");
|
|
1037
|
-
assert3(typeof certificateSigningRequestFilename === "string");
|
|
1038
|
-
const subject = params.subject ? new Subject3(params.subject).toString() : void 0;
|
|
1039
|
-
displaySubtitle("- Creating a Certificate Signing Request with subtile");
|
|
1040
|
-
const privateKey = isOpaqueSigner(params.privateKey) ? params.privateKey : await pemToPrivateKey(
|
|
1041
|
-
typeof params.privateKey === "string" ? await fs5.promises.readFile(params.privateKey, "utf-8") : coercePrivateKeyPem(params.privateKey)
|
|
1042
|
-
);
|
|
1043
|
-
const { csr } = await createCertificateSigningRequest({
|
|
1044
|
-
privateKey,
|
|
1045
|
-
dns: params.dns,
|
|
1046
|
-
ip: params.ip,
|
|
1047
|
-
subject,
|
|
1048
|
-
applicationUri: params.applicationUri,
|
|
1049
|
-
purpose: params.purpose
|
|
1050
|
-
});
|
|
1051
|
-
await fs5.promises.writeFile(certificateSigningRequestFilename, csr, "utf-8");
|
|
1052
|
-
display(
|
|
1053
|
-
`- privateKey ${typeof params.privateKey === "string" ? params.privateKey : isOpaqueSigner(params.privateKey) ? "<opaque-signer>" : "<in-memory>"}`
|
|
1054
|
-
);
|
|
1055
|
-
display(`- certificateSigningRequestFilename ${certificateSigningRequestFilename}`);
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
// packages/node-opcua-pki/lib/toolbox/without_openssl/create_self_signed_certificate.ts
|
|
1059
|
-
import assert4 from "assert";
|
|
1060
|
-
import fs6 from "fs";
|
|
1061
|
-
import {
|
|
1062
|
-
CertificatePurpose,
|
|
1063
|
-
coercePrivateKeyPem as coercePrivateKeyPem2,
|
|
1064
|
-
createSelfSignedCertificate as createSelfSignedCertificate1,
|
|
1065
|
-
pemToPrivateKey as pemToPrivateKey2,
|
|
1066
|
-
Subject as Subject4
|
|
1067
|
-
} from "node-opcua-crypto";
|
|
1068
|
-
async function createSelfSignedCertificateAsync(certificate, params) {
|
|
1069
|
-
params.purpose = params.purpose || CertificatePurpose.ForApplication;
|
|
1070
|
-
assert4(params.purpose, "Please provide a Certificate Purpose");
|
|
1071
|
-
assert4(fs6.existsSync(params.configFile));
|
|
1072
|
-
assert4(fs6.existsSync(params.rootDir));
|
|
1073
|
-
if (typeof params.privateKey === "string") {
|
|
1074
|
-
assert4(fs6.existsSync(params.privateKey));
|
|
1075
|
-
}
|
|
1076
|
-
if (!params.subject) {
|
|
1077
|
-
throw Error("Missing subject");
|
|
1078
|
-
}
|
|
1079
|
-
assert4(typeof params.applicationUri === "string");
|
|
1080
|
-
assert4(Array.isArray(params.dns));
|
|
1081
|
-
adjustDate(params);
|
|
1082
|
-
assert4(Object.prototype.hasOwnProperty.call(params, "validity"));
|
|
1083
|
-
let subject = new Subject4(params.subject);
|
|
1084
|
-
subject = subject.toString();
|
|
1085
|
-
const purpose = params.purpose;
|
|
1086
|
-
displayTitle("Generate a certificate request");
|
|
1087
|
-
const privateKey = isOpaqueSigner(params.privateKey) ? params.privateKey : await pemToPrivateKey2(
|
|
1088
|
-
typeof params.privateKey === "string" ? await fs6.promises.readFile(params.privateKey, "utf-8") : coercePrivateKeyPem2(params.privateKey)
|
|
1089
|
-
);
|
|
1090
|
-
const { cert } = await createSelfSignedCertificate1({
|
|
1091
|
-
privateKey,
|
|
1092
|
-
notBefore: params.startDate,
|
|
1093
|
-
notAfter: params.endDate,
|
|
1094
|
-
validity: params.validity,
|
|
1095
|
-
dns: params.dns,
|
|
1096
|
-
ip: params.ip,
|
|
1097
|
-
subject,
|
|
1098
|
-
applicationUri: params.applicationUri,
|
|
1099
|
-
purpose
|
|
1100
|
-
});
|
|
1101
|
-
await fs6.promises.writeFile(certificate, cert, "utf-8");
|
|
1102
|
-
}
|
|
1103
|
-
async function createSelfSignedCertificate(certificate, params) {
|
|
1104
|
-
await createSelfSignedCertificateAsync(certificate, params);
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
// packages/node-opcua-pki/lib/pki/templates/simple_config_template.cnf.ts
|
|
1108
|
-
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';
|
|
1109
|
-
var simple_config_template_cnf_default = config;
|
|
1110
|
-
|
|
1111
|
-
// packages/node-opcua-pki/lib/pki/certificate_manager.ts
|
|
1112
|
-
var configurationFileSimpleTemplate = simple_config_template_cnf_default;
|
|
1113
|
-
var fsWriteFile = fs7.promises.writeFile;
|
|
1114
|
-
function getOrComputeInfo(entry) {
|
|
1115
|
-
if (!entry.info) {
|
|
1116
|
-
entry.info = exploreCertificate(entry.certificate);
|
|
1117
|
-
}
|
|
1118
|
-
return entry.info;
|
|
1119
|
-
}
|
|
1120
|
-
var VerificationStatus = /* @__PURE__ */ ((VerificationStatus2) => {
|
|
1121
|
-
VerificationStatus2["BadCertificateInvalid"] = "BadCertificateInvalid";
|
|
1122
|
-
VerificationStatus2["BadSecurityChecksFailed"] = "BadSecurityChecksFailed";
|
|
1123
|
-
VerificationStatus2["BadCertificatePolicyCheckFailed"] = "BadCertificatePolicyCheckFailed";
|
|
1124
|
-
VerificationStatus2["BadCertificateTimeInvalid"] = "BadCertificateTimeInvalid";
|
|
1125
|
-
VerificationStatus2["BadCertificateIssuerTimeInvalid"] = "BadCertificateIssuerTimeInvalid";
|
|
1126
|
-
VerificationStatus2["BadCertificateHostNameInvalid"] = "BadCertificateHostNameInvalid";
|
|
1127
|
-
VerificationStatus2["BadCertificateUriInvalid"] = "BadCertificateUriInvalid";
|
|
1128
|
-
VerificationStatus2["BadCertificateUseNotAllowed"] = "BadCertificateUseNotAllowed";
|
|
1129
|
-
VerificationStatus2["BadCertificateIssuerUseNotAllowed"] = "BadCertificateIssuerUseNotAllowed";
|
|
1130
|
-
VerificationStatus2["BadCertificateUntrusted"] = "BadCertificateUntrusted";
|
|
1131
|
-
VerificationStatus2["BadCertificateRevocationUnknown"] = "BadCertificateRevocationUnknown";
|
|
1132
|
-
VerificationStatus2["BadCertificateIssuerRevocationUnknown"] = "BadCertificateIssuerRevocationUnknown";
|
|
1133
|
-
VerificationStatus2["BadCertificateRevoked"] = "BadCertificateRevoked";
|
|
1134
|
-
VerificationStatus2["BadCertificateIssuerRevoked"] = "BadCertificateIssuerRevoked";
|
|
1135
|
-
VerificationStatus2["BadCertificateChainIncomplete"] = "BadCertificateChainIncomplete";
|
|
1136
|
-
VerificationStatus2["Good"] = "Good";
|
|
1137
|
-
return VerificationStatus2;
|
|
1138
|
-
})(VerificationStatus || {});
|
|
1139
|
-
function coerceCertificateChain(certificate) {
|
|
1140
|
-
if (Array.isArray(certificate)) {
|
|
1141
|
-
if (certificate.length === 0) return [];
|
|
1142
|
-
return certificate.reduce((acc, cert) => {
|
|
1143
|
-
return acc.concat(split_der(cert));
|
|
1144
|
-
}, []);
|
|
1145
|
-
}
|
|
1146
|
-
return split_der(certificate);
|
|
1147
|
-
}
|
|
1148
|
-
function makeFingerprint(certificate) {
|
|
1149
|
-
const chain = coerceCertificateChain(certificate);
|
|
1150
|
-
return makeSHA1Thumbprint(chain[0]).toString("hex");
|
|
1151
|
-
}
|
|
1152
|
-
function short(stringToShorten) {
|
|
1153
|
-
return stringToShorten.substring(0, 10);
|
|
1154
|
-
}
|
|
1155
|
-
var disallowedInLabel = /[^A-Za-z0-9._ -]/g;
|
|
1156
|
-
function toFilenameLabel(commonName) {
|
|
1157
|
-
const label = (commonName || "").normalize("NFKC").replace(disallowedInLabel, "_").replace(/[^\x20-\x7E]/g, "").slice(0, 64).trim();
|
|
1158
|
-
return label || "certificate";
|
|
1159
|
-
}
|
|
1160
|
-
function buildIdealCertificateName(certificate) {
|
|
1161
|
-
const chain = coerceCertificateChain(certificate);
|
|
1162
|
-
const fingerprint = makeFingerprint(chain);
|
|
1163
|
-
try {
|
|
1164
|
-
const commonName = exploreCertificate(chain[0]).tbsCertificate.subject.commonName || "";
|
|
1165
|
-
const sanitizedCommonName = toFilenameLabel(commonName);
|
|
1166
|
-
return `${sanitizedCommonName}[${fingerprint}]`;
|
|
1167
|
-
} catch (_err) {
|
|
1168
|
-
return `invalid_certificate_[${fingerprint}]`;
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
function safeStoreJoin(folder, filename) {
|
|
1172
|
-
const full = path4.resolve(folder, filename);
|
|
1173
|
-
const base = path4.resolve(folder);
|
|
1174
|
-
if (full !== base && !full.startsWith(base + path4.sep)) {
|
|
1175
|
-
warningLog("refusing to write certificate outside its store folder:", filename);
|
|
1176
|
-
throw new Error(`certificate filename escapes its store folder: ${filename}`);
|
|
1177
|
-
}
|
|
1178
|
-
return full;
|
|
1179
|
-
}
|
|
1180
|
-
function findMatchingIssuerKey(entries, wantedIssuerKey) {
|
|
1181
|
-
return entries.filter((entry) => {
|
|
1182
|
-
const info = getOrComputeInfo(entry);
|
|
1183
|
-
return info.tbsCertificate.extensions && info.tbsCertificate.extensions.subjectKeyIdentifier === wantedIssuerKey;
|
|
1184
|
-
});
|
|
1185
|
-
}
|
|
1186
|
-
function isSelfSigned2(info) {
|
|
1187
|
-
return info.tbsCertificate.extensions?.subjectKeyIdentifier === info.tbsCertificate.extensions?.authorityKeyIdentifier?.keyIdentifier;
|
|
1188
|
-
}
|
|
1189
|
-
function isSelfSigned3(certificate) {
|
|
1190
|
-
const info = exploreCertificate(certificate);
|
|
1191
|
-
return isSelfSigned2(info);
|
|
1192
|
-
}
|
|
1193
|
-
function _isIssuerInfo(info) {
|
|
1194
|
-
const basicConstraints = info.tbsCertificate.extensions?.basicConstraints;
|
|
1195
|
-
if (basicConstraints?.cA) {
|
|
1196
|
-
return true;
|
|
1197
|
-
}
|
|
1198
|
-
const keyUsage = info.tbsCertificate.extensions?.keyUsage;
|
|
1199
|
-
if (keyUsage?.keyCertSign) {
|
|
1200
|
-
return true;
|
|
1201
|
-
}
|
|
1202
|
-
return false;
|
|
1203
|
-
}
|
|
1204
|
-
function isIssuer(certificate) {
|
|
1205
|
-
try {
|
|
1206
|
-
const info = exploreCertificate(certificate);
|
|
1207
|
-
return _isIssuerInfo(info);
|
|
1208
|
-
} catch (_err) {
|
|
1209
|
-
return false;
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
function isIntermediateIssuer(certificate) {
|
|
1213
|
-
try {
|
|
1214
|
-
const info = exploreCertificate(certificate);
|
|
1215
|
-
if (!_isIssuerInfo(info)) {
|
|
1216
|
-
return false;
|
|
1217
|
-
}
|
|
1218
|
-
return !isSelfSigned2(info);
|
|
1219
|
-
} catch (_err) {
|
|
1220
|
-
return false;
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
function isRootIssuer(certificate) {
|
|
1224
|
-
try {
|
|
1225
|
-
const info = exploreCertificate(certificate);
|
|
1226
|
-
if (!_isIssuerInfo(info)) {
|
|
1227
|
-
return false;
|
|
1228
|
-
}
|
|
1229
|
-
return isSelfSigned2(info);
|
|
1230
|
-
} catch (_err) {
|
|
1231
|
-
return false;
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
|
-
function findIssuerCertificateInChain(certificate, chain) {
|
|
1235
|
-
const coercedCertificate = coerceCertificateChain(certificate);
|
|
1236
|
-
const firstCertificate = coercedCertificate[0];
|
|
1237
|
-
if (!firstCertificate) {
|
|
1238
|
-
return null;
|
|
1239
|
-
}
|
|
1240
|
-
const certInfo = exploreCertificate(firstCertificate);
|
|
1241
|
-
if (isSelfSigned2(certInfo)) {
|
|
1242
|
-
return firstCertificate;
|
|
1243
|
-
}
|
|
1244
|
-
const wantedIssuerKey = certInfo.tbsCertificate.extensions?.authorityKeyIdentifier?.keyIdentifier;
|
|
1245
|
-
if (!wantedIssuerKey) {
|
|
1246
|
-
debugLog("Certificate has no extension 3");
|
|
1247
|
-
return null;
|
|
1248
|
-
}
|
|
1249
|
-
const coercedChain = coerceCertificateChain(chain);
|
|
1250
|
-
const potentialIssuers = coercedChain.filter((c) => {
|
|
1251
|
-
const info = exploreCertificate(c);
|
|
1252
|
-
return info.tbsCertificate.extensions && info.tbsCertificate.extensions.subjectKeyIdentifier === wantedIssuerKey;
|
|
1253
|
-
});
|
|
1254
|
-
if (potentialIssuers.length === 1) {
|
|
1255
|
-
return potentialIssuers[0];
|
|
1256
|
-
}
|
|
1257
|
-
if (potentialIssuers.length > 1) {
|
|
1258
|
-
debugLog("findIssuerCertificateInChain: certificate is not self-signed but has several issuers");
|
|
1259
|
-
return potentialIssuers[0];
|
|
1260
|
-
}
|
|
1261
|
-
return null;
|
|
1262
|
-
}
|
|
1263
|
-
var CertificateManagerState = /* @__PURE__ */ ((CertificateManagerState2) => {
|
|
1264
|
-
CertificateManagerState2[CertificateManagerState2["Uninitialized"] = 0] = "Uninitialized";
|
|
1265
|
-
CertificateManagerState2[CertificateManagerState2["Initializing"] = 1] = "Initializing";
|
|
1266
|
-
CertificateManagerState2[CertificateManagerState2["Initialized"] = 2] = "Initialized";
|
|
1267
|
-
CertificateManagerState2[CertificateManagerState2["Disposing"] = 3] = "Disposing";
|
|
1268
|
-
CertificateManagerState2[CertificateManagerState2["Disposed"] = 4] = "Disposed";
|
|
1269
|
-
return CertificateManagerState2;
|
|
1270
|
-
})(CertificateManagerState || {});
|
|
1271
|
-
var ChainCompletionStatus = /* @__PURE__ */ ((ChainCompletionStatus2) => {
|
|
1272
|
-
ChainCompletionStatus2["AlreadyComplete"] = "AlreadyComplete";
|
|
1273
|
-
ChainCompletionStatus2["ChainCompleted"] = "ChainCompleted";
|
|
1274
|
-
ChainCompletionStatus2["IssuerNotFound"] = "IssuerNotFound";
|
|
1275
|
-
ChainCompletionStatus2["EmptyChain"] = "EmptyChain";
|
|
1276
|
-
ChainCompletionStatus2["MaxDepthReached"] = "MaxDepthReached";
|
|
1277
|
-
return ChainCompletionStatus2;
|
|
1278
|
-
})(ChainCompletionStatus || {});
|
|
1279
|
-
var CertificateManager = class _CertificateManager extends EventEmitter {
|
|
1280
|
-
// ── Global instance registry ─────────────────────────────────
|
|
1281
|
-
// Tracks all initialized CertificateManager instances so their
|
|
1282
|
-
// file watchers can be closed automatically on process exit,
|
|
1283
|
-
// even if the consumer forgets to call dispose().
|
|
1284
|
-
static #activeInstances = /* @__PURE__ */ new Set();
|
|
1285
|
-
static #cleanupInstalled = false;
|
|
1286
|
-
static #exitHandler;
|
|
1287
|
-
/**
|
|
1288
|
-
* Install a best-effort `exit` hook that closes any watcher
|
|
1289
|
-
* still open when the process terminates.
|
|
1290
|
-
*
|
|
1291
|
-
* **This library never terminates the host process.** No
|
|
1292
|
-
* SIGINT/SIGTERM handler is installed: deciding how (and
|
|
1293
|
-
* whether) to shut down on a signal is the application's
|
|
1294
|
-
* responsibility, and a listener registered here would both
|
|
1295
|
-
* pre-empt the application's own graceful shutdown and
|
|
1296
|
-
* silently suppress Node's default signal behaviour.
|
|
1297
|
-
*
|
|
1298
|
-
* Nothing here is load-bearing for process exit. The native
|
|
1299
|
-
* `fs.watch` handles are `unref()`'d when the watchers are
|
|
1300
|
-
* created (see `#readCertificates`), so an undisposed
|
|
1301
|
-
* CertificateManager never keeps the event loop alive. This
|
|
1302
|
-
* hook is only tidiness on the way out.
|
|
1303
|
-
*
|
|
1304
|
-
* `exit` rather than `beforeExit`: `beforeExit` fires when
|
|
1305
|
-
* the loop merely drains and the loop can subsequently be
|
|
1306
|
-
* resurrected, which would leave a still-in-use instance
|
|
1307
|
-
* marked Disposed. `exit` is terminal, synchronous-only and
|
|
1308
|
-
* cannot alter the exit code.
|
|
1309
|
-
*/
|
|
1310
|
-
static #installExitCleanup() {
|
|
1311
|
-
if (_CertificateManager.#cleanupInstalled) return;
|
|
1312
|
-
_CertificateManager.#cleanupInstalled = true;
|
|
1313
|
-
const closeDanglingWatchers = () => {
|
|
1314
|
-
for (const cm of _CertificateManager.#activeInstances) {
|
|
1315
|
-
for (const w of cm.#watchers) {
|
|
1316
|
-
try {
|
|
1317
|
-
w.close();
|
|
1318
|
-
} catch {
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
cm.#watchers.splice(0);
|
|
1322
|
-
cm.state = 4 /* Disposed */;
|
|
1323
|
-
}
|
|
1324
|
-
_CertificateManager.#activeInstances.clear();
|
|
1325
|
-
};
|
|
1326
|
-
_CertificateManager.#exitHandler = closeDanglingWatchers;
|
|
1327
|
-
process.on("exit", closeDanglingWatchers);
|
|
1328
|
-
}
|
|
1329
|
-
/**
|
|
1330
|
-
* Remove the `exit` hook once the last instance is disposed,
|
|
1331
|
-
* so a library that is initialized and disposed repeatedly
|
|
1332
|
-
* does not accumulate process listeners. A later
|
|
1333
|
-
* `initialize()` re-arms it.
|
|
1334
|
-
*/
|
|
1335
|
-
static #uninstallExitCleanupIfIdle() {
|
|
1336
|
-
if (_CertificateManager.#activeInstances.size > 0) return;
|
|
1337
|
-
if (_CertificateManager.#exitHandler) {
|
|
1338
|
-
process.removeListener("exit", _CertificateManager.#exitHandler);
|
|
1339
|
-
_CertificateManager.#exitHandler = void 0;
|
|
1340
|
-
}
|
|
1341
|
-
_CertificateManager.#cleanupInstalled = false;
|
|
1342
|
-
}
|
|
1343
|
-
/**
|
|
1344
|
-
* Dispose **all** active CertificateManager instances,
|
|
1345
|
-
* closing their file watchers and freeing resources.
|
|
1346
|
-
*
|
|
1347
|
-
* This is mainly useful in test tear-down to ensure the
|
|
1348
|
-
* Node.js process can exit cleanly.
|
|
1349
|
-
*/
|
|
1350
|
-
static async disposeAll() {
|
|
1351
|
-
const instances = [..._CertificateManager.#activeInstances];
|
|
1352
|
-
await Promise.all(instances.map((cm) => _CertificateManager.prototype.dispose.call(cm)));
|
|
1353
|
-
}
|
|
1354
|
-
/**
|
|
1355
|
-
* Assert that all CertificateManager instances have been
|
|
1356
|
-
* properly disposed. Throws an Error listing the locations
|
|
1357
|
-
* of any leaked instances.
|
|
1358
|
-
*
|
|
1359
|
-
* Intended for use in test `afterAll()` / `afterEach()`
|
|
1360
|
-
* hooks to catch missing `dispose()` calls early.
|
|
1361
|
-
*
|
|
1362
|
-
* @example
|
|
1363
|
-
* ```ts
|
|
1364
|
-
* after(() => {
|
|
1365
|
-
* CertificateManager.checkAllDisposed();
|
|
1366
|
-
* });
|
|
1367
|
-
* ```
|
|
1368
|
-
*/
|
|
1369
|
-
static checkAllDisposed() {
|
|
1370
|
-
if (_CertificateManager.#activeInstances.size === 0) return;
|
|
1371
|
-
const locations = [..._CertificateManager.#activeInstances].map((cm) => cm.rootDir);
|
|
1372
|
-
throw new Error(
|
|
1373
|
-
`${_CertificateManager.#activeInstances.size} CertificateManager instance(s) not disposed:
|
|
1374
|
-
- ${locations.join("\n - ")}`
|
|
1375
|
-
);
|
|
1376
|
-
}
|
|
1377
|
-
// ─────────────────────────────────────────────────────────────
|
|
1378
|
-
/**
|
|
1379
|
-
* When `true` (the default), any certificate that is not
|
|
1380
|
-
* already in the trusted or rejected store is automatically
|
|
1381
|
-
* written to the rejected folder the first time it is seen.
|
|
1382
|
-
*/
|
|
1383
|
-
untrustUnknownCertificate = true;
|
|
1384
|
-
/** Current lifecycle state of this instance. */
|
|
1385
|
-
state = 0 /* Uninitialized */;
|
|
1386
|
-
/** @deprecated Use {@link folderPollingInterval} instead (typo fix). */
|
|
1387
|
-
folderPoolingInterval = 5e3;
|
|
1388
|
-
/** Interval in milliseconds for file-system polling (when enabled). */
|
|
1389
|
-
get folderPollingInterval() {
|
|
1390
|
-
return this.folderPoolingInterval;
|
|
1391
|
-
}
|
|
1392
|
-
set folderPollingInterval(value) {
|
|
1393
|
-
this.folderPoolingInterval = value;
|
|
1394
|
-
}
|
|
1395
|
-
/** RSA key size used when generating the private key. */
|
|
1396
|
-
keySize;
|
|
1397
|
-
#location;
|
|
1398
|
-
#watchers = [];
|
|
1399
|
-
#pendingUnrefs = /* @__PURE__ */ new Set();
|
|
1400
|
-
#readCertificatesCalled = false;
|
|
1401
|
-
#filenameToHash = /* @__PURE__ */ new Map();
|
|
1402
|
-
#initializingPromise;
|
|
1403
|
-
#addCertValidation;
|
|
1404
|
-
#disableFileWatchers;
|
|
1405
|
-
#privateKeyPassphrase;
|
|
1406
|
-
#privateKeyProvider;
|
|
1407
|
-
#keyOperations;
|
|
1408
|
-
/** The stable lazy wrap handed out by {@link getKeyOperations} for non-opaque configurations. */
|
|
1409
|
-
#lazyLocalKeyOperations;
|
|
1410
|
-
/**
|
|
1411
|
-
* The on-disk key, decrypted once and kept for the instance's lifetime,
|
|
1412
|
-
* so the passphrase (or its resolver function) is consulted at most
|
|
1413
|
-
* once. Cleared by `dispose()`. Not used when a provider is configured:
|
|
1414
|
-
* the provider is the authority on the current key.
|
|
1415
|
-
*/
|
|
1416
|
-
#cachedPrivateKey;
|
|
1417
|
-
#thumbs = {
|
|
1418
|
-
rejected: /* @__PURE__ */ new Map(),
|
|
1419
|
-
trusted: /* @__PURE__ */ new Map(),
|
|
1420
|
-
issuers: {
|
|
1421
|
-
certs: /* @__PURE__ */ new Map()
|
|
1422
|
-
},
|
|
1423
|
-
crl: /* @__PURE__ */ new Map(),
|
|
1424
|
-
issuersCrl: /* @__PURE__ */ new Map()
|
|
1425
|
-
};
|
|
1426
|
-
/**
|
|
1427
|
-
* Create a new CertificateManager.
|
|
1428
|
-
*
|
|
1429
|
-
* The constructor creates the root directory if it does not
|
|
1430
|
-
* exist but does **not** initialise the PKI store — call
|
|
1431
|
-
* {@link initialize} before using any other method.
|
|
1432
|
-
*
|
|
1433
|
-
* @param options - configuration options
|
|
1434
|
-
*/
|
|
1435
|
-
constructor(options) {
|
|
1436
|
-
super();
|
|
1437
|
-
options.keySize = options.keySize || 2048;
|
|
1438
|
-
if (!options.location) {
|
|
1439
|
-
throw new Error("CertificateManager: missing 'location' option");
|
|
1440
|
-
}
|
|
1441
|
-
this.#location = makePath(options.location, "");
|
|
1442
|
-
this.keySize = options.keySize;
|
|
1443
|
-
const v = options.addCertificateValidationOptions ?? {};
|
|
1444
|
-
this.#addCertValidation = {
|
|
1445
|
-
acceptExpiredCertificate: v.acceptExpiredCertificate ?? false,
|
|
1446
|
-
acceptRevokedCertificate: v.acceptRevokedCertificate ?? false,
|
|
1447
|
-
ignoreMissingRevocationList: v.ignoreMissingRevocationList ?? false,
|
|
1448
|
-
maxChainLength: v.maxChainLength ?? 5
|
|
1449
|
-
};
|
|
1450
|
-
this.#disableFileWatchers = options.disableFileWatchers ?? process.env.OPCUA_PKI_DISABLE_FILE_WATCHERS === "true";
|
|
1451
|
-
this.#privateKeyPassphrase = options.privateKeyPassphrase;
|
|
1452
|
-
this.#privateKeyProvider = options.privateKeyProvider;
|
|
1453
|
-
this.#keyOperations = options.keyOperations;
|
|
1454
|
-
if (this.#keyOperations && this.#privateKeyProvider) {
|
|
1455
|
-
throw new Error(
|
|
1456
|
-
"CertificateManager: 'keyOperations' and 'privateKeyProvider' are mutually exclusive: one hides the key, the other sources its material"
|
|
1457
|
-
);
|
|
1458
|
-
}
|
|
1459
|
-
if (this.#keyOperations && this.#privateKeyPassphrase) {
|
|
1460
|
-
throw new Error(
|
|
1461
|
-
"CertificateManager: 'privateKeyPassphrase' is meaningless with 'keyOperations': there is no key material for a passphrase to protect"
|
|
1462
|
-
);
|
|
1463
|
-
}
|
|
1464
|
-
mkdirRecursiveSync(options.location);
|
|
1465
|
-
if (!fs7.existsSync(this.#location)) {
|
|
1466
|
-
throw new Error(`CertificateManager cannot access location ${this.#location}`);
|
|
1467
|
-
}
|
|
1468
|
-
}
|
|
1469
|
-
/** Path to the OpenSSL configuration file. */
|
|
1470
|
-
get configFile() {
|
|
1471
|
-
return path4.join(this.rootDir, "own/openssl.cnf");
|
|
1472
|
-
}
|
|
1473
|
-
/** Root directory of the PKI store. */
|
|
1474
|
-
get rootDir() {
|
|
1475
|
-
return this.#location;
|
|
1476
|
-
}
|
|
1477
|
-
/**
|
|
1478
|
-
* Path to the private key file (`own/private/private_key.pem`).
|
|
1479
|
-
*
|
|
1480
|
-
* Kept for backward compatibility with code that reads the key
|
|
1481
|
-
* directly from disk. When a passphrase or a `privateKeyProvider` is
|
|
1482
|
-
* configured, prefer {@link getPrivateKey} instead — this getter still
|
|
1483
|
-
* returns the on-disk path even if a provider is configured (there may
|
|
1484
|
-
* be no meaningful file in that case).
|
|
1485
|
-
*/
|
|
1486
|
-
get privateKey() {
|
|
1487
|
-
return path4.join(this.rootDir, "own/private/private_key.pem");
|
|
1488
|
-
}
|
|
1489
|
-
/**
|
|
1490
|
-
* Resolve the private key: from `privateKeyProvider` if configured,
|
|
1491
|
-
* otherwise from disk (decrypting with `privateKeyPassphrase` if the
|
|
1492
|
-
* key is encrypted). Fails closed — throws
|
|
1493
|
-
* `PrivateKeyPassphraseRequiredError` — if the on-disk key is encrypted
|
|
1494
|
-
* and no passphrase is configured, or if the wrong passphrase is
|
|
1495
|
-
* configured.
|
|
1496
|
-
*
|
|
1497
|
-
* The on-disk key is read and decrypted once and then cached for the
|
|
1498
|
-
* lifetime of this instance, so a `privateKeyPassphrase` function is
|
|
1499
|
-
* called at most once (concurrent first calls share the same read). A
|
|
1500
|
-
* failed read is not cached, so a caller can fix the passphrase and
|
|
1501
|
-
* retry. A `privateKeyProvider` is consulted on every call: it is the
|
|
1502
|
-
* authority on what the current key is.
|
|
1503
|
-
*/
|
|
1504
|
-
async getPrivateKey() {
|
|
1505
|
-
if (this.#keyOperations) {
|
|
1506
|
-
throw new PrivateKeyUnavailableError(
|
|
1507
|
-
"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"
|
|
1508
|
-
);
|
|
1509
|
-
}
|
|
1510
|
-
if (this.#privateKeyProvider) {
|
|
1511
|
-
return await this.#privateKeyProvider.getPrivateKey();
|
|
1512
|
-
}
|
|
1513
|
-
if (this.#cachedPrivateKey) {
|
|
1514
|
-
return this.#cachedPrivateKey;
|
|
1515
|
-
}
|
|
1516
|
-
if (!this.#privateKeyPromise) {
|
|
1517
|
-
this.#privateKeyPromise = (async () => {
|
|
1518
|
-
const passphrase = await resolvePrivateKeyPassphrase(this.#privateKeyPassphrase);
|
|
1519
|
-
return readPrivateKey(this.privateKey, passphrase);
|
|
1520
|
-
})().then(
|
|
1521
|
-
(key) => {
|
|
1522
|
-
this.#cachedPrivateKey = key;
|
|
1523
|
-
return key;
|
|
1524
|
-
},
|
|
1525
|
-
(err) => {
|
|
1526
|
-
this.#privateKeyPromise = void 0;
|
|
1527
|
-
throw err;
|
|
1528
|
-
}
|
|
1529
|
-
);
|
|
1530
|
-
}
|
|
1531
|
-
return await this.#privateKeyPromise;
|
|
1532
|
-
}
|
|
1533
|
-
/** In-flight first read of the on-disk key, so concurrent callers share one passphrase resolution. */
|
|
1534
|
-
#privateKeyPromise;
|
|
1535
|
-
/**
|
|
1536
|
-
* True when this manager's key is opaque — configured through
|
|
1537
|
-
* `keyOperations`, held by an HSM/KMS, never obtainable as material.
|
|
1538
|
-
* When true, {@link getPrivateKey} throws `PrivateKeyUnavailableError`
|
|
1539
|
-
* and {@link getKeyOperations} is the only way to use the key.
|
|
1540
|
-
*/
|
|
1541
|
-
isPrivateKeyOpaque() {
|
|
1542
|
-
return !!this.#keyOperations;
|
|
1543
|
-
}
|
|
1544
|
-
/**
|
|
1545
|
-
* The key as an opaque {@link IKeyOperations} — the recommended way to
|
|
1546
|
-
* *use* the private key regardless of where it lives.
|
|
1547
|
-
*
|
|
1548
|
-
* Returns the configured `keyOperations` object when the key is opaque.
|
|
1549
|
-
* Otherwise returns a stable lazy wrap over {@link getPrivateKey}: its
|
|
1550
|
-
* methods resolve the key on first use (disk read, passphrase,
|
|
1551
|
-
* `privateKeyProvider` — all async), so the wrap offers no synchronous
|
|
1552
|
-
* fast path; callers that need one resolve the key themselves and build
|
|
1553
|
-
* a `LocalKeyOperations` over it. The wrap follows key rotation: a
|
|
1554
|
-
* `privateKeyProvider` that starts returning a different key gets a
|
|
1555
|
-
* fresh underlying `LocalKeyOperations`.
|
|
1556
|
-
*/
|
|
1557
|
-
getKeyOperations() {
|
|
1558
|
-
if (this.#keyOperations) {
|
|
1559
|
-
return this.#keyOperations;
|
|
1560
|
-
}
|
|
1561
|
-
if (!this.#lazyLocalKeyOperations) {
|
|
1562
|
-
let cached;
|
|
1563
|
-
const resolve = async () => {
|
|
1564
|
-
const key = await this.getPrivateKey();
|
|
1565
|
-
if (!cached || cached.key !== key) {
|
|
1566
|
-
cached = { key, ops: keyOperationsFromPrivateKey(key) };
|
|
1567
|
-
}
|
|
1568
|
-
return cached.ops;
|
|
1569
|
-
};
|
|
1570
|
-
this.#lazyLocalKeyOperations = {
|
|
1571
|
-
sign: async (data, params) => (await resolve()).sign(data, params),
|
|
1572
|
-
decryptBlock: async (block, params) => (await resolve()).decryptBlock(block, params),
|
|
1573
|
-
getKeyMetadata: async () => (await resolve()).getKeyMetadata(),
|
|
1574
|
-
getPublicKey: async () => (await resolve()).getPublicKey()
|
|
1575
|
-
};
|
|
1576
|
-
}
|
|
1577
|
-
return this.#lazyLocalKeyOperations;
|
|
1578
|
-
}
|
|
1579
|
-
/**
|
|
1580
|
-
* Fail closed at `initialize()` time, not on the first certificate
|
|
1581
|
-
* operation, if the key cannot actually be used: wrong/missing
|
|
1582
|
-
* passphrase on an encrypted key, a broken `privateKeyProvider`, or an
|
|
1583
|
-
* unreachable `keyOperations` provider (probed via `getKeyMetadata`).
|
|
1584
|
-
*/
|
|
1585
|
-
async #probePrivateKey() {
|
|
1586
|
-
if (this.#keyOperations) {
|
|
1587
|
-
await this.#keyOperations.getKeyMetadata();
|
|
1588
|
-
return;
|
|
1589
|
-
}
|
|
1590
|
-
await this.getPrivateKey();
|
|
1591
|
-
}
|
|
1592
|
-
/**
|
|
1593
|
-
* The key to hand to a certificate-issuance primitive: the raw
|
|
1594
|
-
* {@link PrivateKey} for a local configuration, or a {@link CaSigner}
|
|
1595
|
-
* adapted from `keyOperations` when the key is opaque — which requires
|
|
1596
|
-
* the provider to implement `getPublicKey` (the adapter says so if not).
|
|
1597
|
-
*/
|
|
1598
|
-
async #resolveSigningKey() {
|
|
1599
|
-
if (this.#keyOperations) {
|
|
1600
|
-
return caSignerFromKeyOperations(this.#keyOperations);
|
|
1601
|
-
}
|
|
1602
|
-
return await this.getPrivateKey();
|
|
1603
|
-
}
|
|
1604
|
-
/**
|
|
1605
|
-
* Enable, disable, or rotate the passphrase protecting the on-disk
|
|
1606
|
-
* private key: decrypt with `oldPassphrase` (omit if the key is
|
|
1607
|
-
* currently unencrypted), then write back encrypted with
|
|
1608
|
-
* `newPassphrase` (omit to leave it unencrypted). The write goes to a
|
|
1609
|
-
* temporary file in the same directory and is atomically renamed into
|
|
1610
|
-
* place, so a crash mid-rotation cannot leave a partially-written key;
|
|
1611
|
-
* the temporary file is removed if anything fails, so a rotation *to*
|
|
1612
|
-
* plaintext can never leave a stray cleartext copy behind. Runs under
|
|
1613
|
-
* the same lock as `initialize()`.
|
|
1614
|
-
*
|
|
1615
|
-
* This only rewrites the on-disk file — it does not update this
|
|
1616
|
-
* instance's own `privateKeyPassphrase` (set at construction), and it
|
|
1617
|
-
* drops this instance's cached key so that disk stays the source of
|
|
1618
|
-
* truth. Construct a new `CertificateManager` with the new passphrase to
|
|
1619
|
-
* continue using it afterward.
|
|
1620
|
-
*
|
|
1621
|
-
* Not supported when a `privateKeyProvider` is configured (there is no
|
|
1622
|
-
* disk file for this method to rewrite).
|
|
1623
|
-
*/
|
|
1624
|
-
async reencryptPrivateKey(oldPassphrase, newPassphrase) {
|
|
1625
|
-
if (this.#keyOperations) {
|
|
1626
|
-
throw new PrivateKeyUnavailableError(
|
|
1627
|
-
"reencryptPrivateKey: not supported when keyOperations is configured \u2014 there is no key material to rewrite"
|
|
1628
|
-
);
|
|
1629
|
-
}
|
|
1630
|
-
if (this.#privateKeyProvider) {
|
|
1631
|
-
throw new Error("reencryptPrivateKey: not supported when a privateKeyProvider is configured");
|
|
1632
|
-
}
|
|
1633
|
-
const oldPass = await resolvePrivateKeyPassphrase(oldPassphrase);
|
|
1634
|
-
const newPass = await resolvePrivateKeyPassphrase(newPassphrase);
|
|
1635
|
-
await this.withLock2(async () => {
|
|
1636
|
-
const privateKey = readPrivateKey(this.privateKey, oldPass);
|
|
1637
|
-
await this.#rewritePrivateKeyFile(privateKey, newPass);
|
|
1638
|
-
});
|
|
1639
|
-
this.#cachedPrivateKey = void 0;
|
|
1640
|
-
this.#privateKeyPromise = void 0;
|
|
1641
|
-
}
|
|
1642
|
-
/**
|
|
1643
|
-
* Atomically replace the on-disk private key with `privateKey`, written
|
|
1644
|
-
* as PKCS#8 (encrypted with `passphrase` if given). Temp file next to the
|
|
1645
|
-
* target, `0600`, renamed into place; the temp file is unlinked on any
|
|
1646
|
-
* failure so no partial or cleartext copy can be left behind.
|
|
1647
|
-
* Caller must hold the lock.
|
|
1648
|
-
*/
|
|
1649
|
-
async #rewritePrivateKeyFile(privateKey, passphrase) {
|
|
1650
|
-
const tmpFilename = `${this.privateKey}.${process.pid}-${Date.now()}.tmp`;
|
|
1651
|
-
try {
|
|
1652
|
-
await writePrivateKeyFile(tmpFilename, privateKey, { passphrase });
|
|
1653
|
-
await fs7.promises.rename(tmpFilename, this.privateKey);
|
|
1654
|
-
} finally {
|
|
1655
|
-
await fs7.promises.rm(tmpFilename, { force: true });
|
|
1656
|
-
}
|
|
1657
|
-
}
|
|
1658
|
-
/** Path to the OpenSSL random seed file. */
|
|
1659
|
-
get randomFile() {
|
|
1660
|
-
return path4.join(this.rootDir, "./random.rnd");
|
|
1661
|
-
}
|
|
1662
|
-
/**
|
|
1663
|
-
* Move a certificate to the rejected store.
|
|
1664
|
-
* If the certificate was previously trusted, it will be removed from the trusted folder.
|
|
1665
|
-
* @param certificateOrChain - the DER-encoded certificate or certificate chain
|
|
1666
|
-
*/
|
|
1667
|
-
async rejectCertificate(certificateOrChain) {
|
|
1668
|
-
await this.#moveCertificate(certificateOrChain, "rejected");
|
|
1669
|
-
}
|
|
1670
|
-
/**
|
|
1671
|
-
* Move a certificate to the trusted store.
|
|
1672
|
-
* If the certificate was previously rejected, it will be removed from the rejected folder.
|
|
1673
|
-
* @param certificateOrChain - the DER-encoded certificate or certificate chain
|
|
1674
|
-
*/
|
|
1675
|
-
async trustCertificate(certificateOrChain) {
|
|
1676
|
-
await this.#moveCertificate(certificateOrChain, "trusted");
|
|
1677
|
-
}
|
|
1678
|
-
/**
|
|
1679
|
-
* Check whether the trusted certificate store is empty.
|
|
1680
|
-
*
|
|
1681
|
-
* This inspects the in-memory index, which is kept in
|
|
1682
|
-
* sync with the `trusted/certs/` folder by file-system
|
|
1683
|
-
* watchers after {@link initialize} has been called.
|
|
1684
|
-
*/
|
|
1685
|
-
isTrustListEmpty() {
|
|
1686
|
-
return this.#thumbs.trusted.size === 0;
|
|
1687
|
-
}
|
|
1688
|
-
/**
|
|
1689
|
-
* Return the number of certificates currently in the
|
|
1690
|
-
* trusted store.
|
|
1691
|
-
*/
|
|
1692
|
-
getTrustedCertificateCount() {
|
|
1693
|
-
return this.#thumbs.trusted.size;
|
|
1694
|
-
}
|
|
1695
|
-
/** Path to the rejected certificates folder. */
|
|
1696
|
-
get rejectedFolder() {
|
|
1697
|
-
return path4.join(this.rootDir, "rejected");
|
|
1698
|
-
}
|
|
1699
|
-
/** Path to the trusted certificates folder. */
|
|
1700
|
-
get trustedFolder() {
|
|
1701
|
-
return path4.join(this.rootDir, "trusted/certs");
|
|
1702
|
-
}
|
|
1703
|
-
/** Path to the trusted CRL folder. */
|
|
1704
|
-
get crlFolder() {
|
|
1705
|
-
return path4.join(this.rootDir, "trusted/crl");
|
|
1706
|
-
}
|
|
1707
|
-
/** Path to the issuer (CA) certificates folder. */
|
|
1708
|
-
get issuersCertFolder() {
|
|
1709
|
-
return path4.join(this.rootDir, "issuers/certs");
|
|
1710
|
-
}
|
|
1711
|
-
/** Path to the issuer CRL folder. */
|
|
1712
|
-
get issuersCrlFolder() {
|
|
1713
|
-
return path4.join(this.rootDir, "issuers/crl");
|
|
1714
|
-
}
|
|
1715
|
-
/** Path to the own certificate folder. */
|
|
1716
|
-
get ownCertFolder() {
|
|
1717
|
-
return path4.join(this.rootDir, "own/certs");
|
|
1718
|
-
}
|
|
1719
|
-
get ownPrivateFolder() {
|
|
1720
|
-
return path4.join(this.rootDir, "own/private");
|
|
1721
|
-
}
|
|
1722
|
-
/**
|
|
1723
|
-
* Check if a certificate is in the trusted store.
|
|
1724
|
-
* If the certificate is unknown and `untrustUnknownCertificate` is set,
|
|
1725
|
-
* it will be written to the rejected folder.
|
|
1726
|
-
* @param certificate - the DER-encoded certificate
|
|
1727
|
-
* @returns `"Good"` if trusted, `"BadCertificateUntrusted"` if rejected/unknown,
|
|
1728
|
-
* or `"BadCertificateInvalid"` if the certificate cannot be parsed.
|
|
1729
|
-
*/
|
|
1730
|
-
async isCertificateTrusted(certificateOrCertificateChain) {
|
|
1731
|
-
try {
|
|
1732
|
-
const chain = coerceCertificateChain(certificateOrCertificateChain);
|
|
1733
|
-
const leafCertificate = chain[0];
|
|
1734
|
-
if (chain.length < 1) {
|
|
1735
|
-
return "BadCertificateInvalid";
|
|
1736
|
-
}
|
|
1737
|
-
let fingerprint;
|
|
1738
|
-
try {
|
|
1739
|
-
fingerprint = makeFingerprint(chain[0]);
|
|
1740
|
-
} catch (_err) {
|
|
1741
|
-
return "BadCertificateInvalid";
|
|
1742
|
-
}
|
|
1743
|
-
if (this.#thumbs.trusted.has(fingerprint)) {
|
|
1744
|
-
return "Good";
|
|
1745
|
-
}
|
|
1746
|
-
if (!this.#thumbs.rejected.has(fingerprint)) {
|
|
1747
|
-
if (!this.untrustUnknownCertificate) {
|
|
1748
|
-
return "Good";
|
|
1749
|
-
}
|
|
1750
|
-
try {
|
|
1751
|
-
exploreCertificateInfo(chain[0]);
|
|
1752
|
-
} catch (_err) {
|
|
1753
|
-
return "BadCertificateInvalid";
|
|
1754
|
-
}
|
|
1755
|
-
const filename = safeStoreJoin(this.rejectedFolder, `${buildIdealCertificateName(leafCertificate)}.pem`);
|
|
1756
|
-
debugLog("certificate has never been seen before and is now rejected (untrusted) ", filename);
|
|
1757
|
-
await fsWriteFile(filename, toPem(chain, "CERTIFICATE"));
|
|
1758
|
-
this.#thumbs.rejected.set(fingerprint, { certificate: leafCertificate, filename });
|
|
1759
|
-
}
|
|
1760
|
-
return "BadCertificateUntrusted";
|
|
1761
|
-
} catch (_err) {
|
|
1762
|
-
return "BadCertificateInvalid";
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1765
|
-
async #innerVerifyCertificateAsync(certificateOrChain, _isIssuer, level, options) {
|
|
1766
|
-
if (level >= 5) {
|
|
1767
|
-
return "BadSecurityChecksFailed" /* BadSecurityChecksFailed */;
|
|
1768
|
-
}
|
|
1769
|
-
const chain = coerceCertificateChain(certificateOrChain);
|
|
1770
|
-
debugLog("NB CERTIFICATE IN CHAIN = ", chain.length);
|
|
1771
|
-
const info = exploreCertificate(chain[0]);
|
|
1772
|
-
let hasValidIssuer = false;
|
|
1773
|
-
let hasTrustedIssuer = false;
|
|
1774
|
-
const hasIssuerKey = info.tbsCertificate.extensions?.authorityKeyIdentifier?.keyIdentifier;
|
|
1775
|
-
debugLog("Certificate as an Issuer Key", hasIssuerKey);
|
|
1776
|
-
if (hasIssuerKey) {
|
|
1777
|
-
const isSelfSigned = isSelfSigned2(info);
|
|
1778
|
-
debugLog("Is the Certificate self-signed ?", isSelfSigned);
|
|
1779
|
-
if (!isSelfSigned) {
|
|
1780
|
-
debugLog(
|
|
1781
|
-
"Is issuer found in the list of know issuers ?",
|
|
1782
|
-
"\n subjectKeyIdentifier = ",
|
|
1783
|
-
info.tbsCertificate.extensions?.subjectKeyIdentifier,
|
|
1784
|
-
"\n authorityKeyIdentifier = ",
|
|
1785
|
-
info.tbsCertificate.extensions?.authorityKeyIdentifier?.keyIdentifier
|
|
1786
|
-
);
|
|
1787
|
-
let issuerCertificate = await this.findIssuerCertificate(chain[0]);
|
|
1788
|
-
if (!issuerCertificate) {
|
|
1789
|
-
issuerCertificate = findIssuerCertificateInChain(chain[0], chain);
|
|
1790
|
-
if (!issuerCertificate) {
|
|
1791
|
-
debugLog(
|
|
1792
|
-
" the issuer has not been found in the chain itself nor in the issuer.cert list => the chain is incomplete!"
|
|
1793
|
-
);
|
|
1794
|
-
return "BadCertificateChainIncomplete" /* BadCertificateChainIncomplete */;
|
|
1795
|
-
}
|
|
1796
|
-
debugLog(" the issuer certificate has been found in the chain itself ! the chain is complete !");
|
|
1797
|
-
} else {
|
|
1798
|
-
debugLog(" the issuer certificate has been found in the issuer.cert folder !");
|
|
1799
|
-
}
|
|
1800
|
-
const issuerStatus = await this.#innerVerifyCertificateAsync(issuerCertificate, true, level + 1, options);
|
|
1801
|
-
if (issuerStatus === "BadCertificateRevocationUnknown" /* BadCertificateRevocationUnknown */) {
|
|
1802
|
-
return "BadCertificateIssuerRevocationUnknown" /* BadCertificateIssuerRevocationUnknown */;
|
|
1803
|
-
}
|
|
1804
|
-
if (issuerStatus === "BadCertificateIssuerRevocationUnknown" /* BadCertificateIssuerRevocationUnknown */) {
|
|
1805
|
-
return "BadCertificateIssuerRevocationUnknown" /* BadCertificateIssuerRevocationUnknown */;
|
|
1806
|
-
}
|
|
1807
|
-
if (issuerStatus === "BadCertificateTimeInvalid" /* BadCertificateTimeInvalid */) {
|
|
1808
|
-
if (!options?.acceptOutDatedIssuerCertificate) {
|
|
1809
|
-
return "BadCertificateIssuerTimeInvalid" /* BadCertificateIssuerTimeInvalid */;
|
|
1810
|
-
}
|
|
1811
|
-
}
|
|
1812
|
-
if (issuerStatus === "BadCertificateUntrusted" /* BadCertificateUntrusted */) {
|
|
1813
|
-
debugLog("warning issuerStatus = ", issuerStatus.toString(), "the issuer certificate is not trusted");
|
|
1814
|
-
}
|
|
1815
|
-
if (issuerStatus !== "Good" /* Good */ && issuerStatus !== "BadCertificateUntrusted" /* BadCertificateUntrusted */) {
|
|
1816
|
-
return "BadSecurityChecksFailed" /* BadSecurityChecksFailed */;
|
|
1817
|
-
}
|
|
1818
|
-
const isCertificateSignatureOK = verifyCertificateSignature(chain[0], issuerCertificate);
|
|
1819
|
-
if (!isCertificateSignatureOK) {
|
|
1820
|
-
debugLog(" the certificate was not signed by the issuer as it claim to be ! Danger");
|
|
1821
|
-
return "BadSecurityChecksFailed" /* BadSecurityChecksFailed */;
|
|
1822
|
-
}
|
|
1823
|
-
hasValidIssuer = true;
|
|
1824
|
-
let revokedStatus = await this.isCertificateRevoked(chain, issuerCertificate);
|
|
1825
|
-
if (revokedStatus === "BadCertificateRevocationUnknown" /* BadCertificateRevocationUnknown */) {
|
|
1826
|
-
if (options?.ignoreMissingRevocationList) {
|
|
1827
|
-
revokedStatus = "Good" /* Good */;
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1830
|
-
if (revokedStatus !== "Good" /* Good */) {
|
|
1831
|
-
debugLog("revokedStatus", revokedStatus);
|
|
1832
|
-
return revokedStatus;
|
|
1833
|
-
}
|
|
1834
|
-
const issuerTrustedStatus = await this.#checkRejectedOrTrusted(issuerCertificate);
|
|
1835
|
-
debugLog("issuerTrustedStatus", issuerTrustedStatus);
|
|
1836
|
-
if (issuerTrustedStatus === "unknown") {
|
|
1837
|
-
hasTrustedIssuer = false;
|
|
1838
|
-
} else if (issuerTrustedStatus === "trusted") {
|
|
1839
|
-
hasTrustedIssuer = true;
|
|
1840
|
-
} else if (issuerTrustedStatus === "rejected") {
|
|
1841
|
-
return "BadSecurityChecksFailed" /* BadSecurityChecksFailed */;
|
|
1842
|
-
}
|
|
1843
|
-
} else {
|
|
1844
|
-
const isCertificateSignatureOK = verifyCertificateSignature(chain[0], chain[0]);
|
|
1845
|
-
if (!isCertificateSignatureOK) {
|
|
1846
|
-
debugLog("Self-signed Certificate signature is not valid");
|
|
1847
|
-
return "BadSecurityChecksFailed" /* BadSecurityChecksFailed */;
|
|
1848
|
-
}
|
|
1849
|
-
const revokedStatus = await this.isCertificateRevoked(chain);
|
|
1850
|
-
debugLog("revokedStatus of self signed certificate:", revokedStatus);
|
|
1851
|
-
}
|
|
1852
|
-
}
|
|
1853
|
-
const status = await this.#checkRejectedOrTrusted(chain[0]);
|
|
1854
|
-
if (status === "rejected") {
|
|
1855
|
-
if (!(options.acceptCertificateWithValidIssuerChain && hasValidIssuer && hasTrustedIssuer)) {
|
|
1856
|
-
return "BadCertificateUntrusted" /* BadCertificateUntrusted */;
|
|
1857
|
-
}
|
|
1858
|
-
}
|
|
1859
|
-
const { validity } = exploreCertificate(chain[0]).tbsCertificate;
|
|
1860
|
-
const now = /* @__PURE__ */ new Date();
|
|
1861
|
-
let isTimeInvalid = false;
|
|
1862
|
-
if (validity.notBefore.getTime() > now.getTime()) {
|
|
1863
|
-
debugLog(
|
|
1864
|
-
`${chalk3.red("certificate is invalid : certificate is not active yet !")} not before date =${validity.notBefore}`
|
|
1865
|
-
);
|
|
1866
|
-
if (!options.acceptPendingCertificate) {
|
|
1867
|
-
isTimeInvalid = true;
|
|
1868
|
-
}
|
|
1869
|
-
}
|
|
1870
|
-
if (validity.notAfter.getTime() <= now.getTime()) {
|
|
1871
|
-
debugLog(`${chalk3.red("certificate is invalid : certificate has expired !")} not after date =${validity.notAfter}`);
|
|
1872
|
-
if (!options.acceptOutdatedCertificate) {
|
|
1873
|
-
isTimeInvalid = true;
|
|
1874
|
-
}
|
|
1875
|
-
}
|
|
1876
|
-
if (status === "trusted") {
|
|
1877
|
-
return isTimeInvalid ? "BadCertificateTimeInvalid" /* BadCertificateTimeInvalid */ : "Good" /* Good */;
|
|
1878
|
-
}
|
|
1879
|
-
if (hasIssuerKey) {
|
|
1880
|
-
if (!hasTrustedIssuer) {
|
|
1881
|
-
return "BadCertificateUntrusted" /* BadCertificateUntrusted */;
|
|
1882
|
-
}
|
|
1883
|
-
if (!hasValidIssuer) {
|
|
1884
|
-
return "BadCertificateUntrusted" /* BadCertificateUntrusted */;
|
|
1885
|
-
}
|
|
1886
|
-
if (!options.acceptCertificateWithValidIssuerChain) {
|
|
1887
|
-
return "BadCertificateUntrusted" /* BadCertificateUntrusted */;
|
|
1888
|
-
}
|
|
1889
|
-
return isTimeInvalid ? "BadCertificateTimeInvalid" /* BadCertificateTimeInvalid */ : "Good" /* Good */;
|
|
1890
|
-
} else {
|
|
1891
|
-
return "BadCertificateUntrusted" /* BadCertificateUntrusted */;
|
|
1892
|
-
}
|
|
1893
|
-
}
|
|
1894
|
-
/**
|
|
1895
|
-
* Internal verification hook called by {@link verifyCertificate}.
|
|
1896
|
-
*
|
|
1897
|
-
* Subclasses can override this to inject additional validation
|
|
1898
|
-
* logic (e.g. application-level policy checks) while still
|
|
1899
|
-
* delegating to the default chain/CRL/trust verification.
|
|
1900
|
-
*
|
|
1901
|
-
* @param certificate - the DER-encoded certificate to verify
|
|
1902
|
-
* @param options - verification options forwarded from the
|
|
1903
|
-
* public API
|
|
1904
|
-
* @returns the verification status code
|
|
1905
|
-
*/
|
|
1906
|
-
async verifyCertificateAsync(certificate, options) {
|
|
1907
|
-
const chain = coerceCertificateChain(certificate);
|
|
1908
|
-
for (const element of chain) {
|
|
1909
|
-
try {
|
|
1910
|
-
exploreCertificate(element);
|
|
1911
|
-
} catch (_err) {
|
|
1912
|
-
return "BadCertificateInvalid" /* BadCertificateInvalid */;
|
|
1913
|
-
}
|
|
1914
|
-
}
|
|
1915
|
-
const status1 = await this.#innerVerifyCertificateAsync(chain, false, 0, options);
|
|
1916
|
-
return status1;
|
|
1917
|
-
}
|
|
1918
|
-
/**
|
|
1919
|
-
* Verify a certificate against the PKI trust store.
|
|
1920
|
-
*
|
|
1921
|
-
* This performs a full validation including trust status,
|
|
1922
|
-
* issuer chain, CRL revocation checks, and time validity.
|
|
1923
|
-
*
|
|
1924
|
-
* @param certificate - the DER-encoded certificate to verify
|
|
1925
|
-
* @param options - optional flags to relax validation rules
|
|
1926
|
-
* @returns the verification status code
|
|
1927
|
-
*/
|
|
1928
|
-
async verifyCertificate(certificate, options) {
|
|
1929
|
-
if (!certificate) {
|
|
1930
|
-
return "BadSecurityChecksFailed" /* BadSecurityChecksFailed */;
|
|
1931
|
-
}
|
|
1932
|
-
try {
|
|
1933
|
-
const status = await this.verifyCertificateAsync(certificate, options || {});
|
|
1934
|
-
return status;
|
|
1935
|
-
} catch (error) {
|
|
1936
|
-
warningLog(`verifyCertificate error: ${error.message}`);
|
|
1937
|
-
return "BadCertificateInvalid" /* BadCertificateInvalid */;
|
|
1938
|
-
}
|
|
1939
|
-
}
|
|
1940
|
-
/**
|
|
1941
|
-
* Initialize the PKI directory structure, generate the
|
|
1942
|
-
* private key (if missing), and start file-system watchers.
|
|
1943
|
-
*
|
|
1944
|
-
* This method is idempotent — subsequent calls are no-ops.
|
|
1945
|
-
* It must be called before any certificate operations.
|
|
1946
|
-
*/
|
|
1947
|
-
async initialize() {
|
|
1948
|
-
if (this.state !== 0 /* Uninitialized */) {
|
|
1949
|
-
return;
|
|
1950
|
-
}
|
|
1951
|
-
this.state = 1 /* Initializing */;
|
|
1952
|
-
this.#initializingPromise = this.#initialize();
|
|
1953
|
-
try {
|
|
1954
|
-
await this.#initializingPromise;
|
|
1955
|
-
} catch (err) {
|
|
1956
|
-
this.#initializingPromise = void 0;
|
|
1957
|
-
this.state = 0 /* Uninitialized */;
|
|
1958
|
-
throw err;
|
|
1959
|
-
}
|
|
1960
|
-
this.#initializingPromise = void 0;
|
|
1961
|
-
this.state = 2 /* Initialized */;
|
|
1962
|
-
_CertificateManager.#activeInstances.add(this);
|
|
1963
|
-
_CertificateManager.#installExitCleanup();
|
|
1964
|
-
}
|
|
1965
|
-
async #initialize() {
|
|
1966
|
-
const pkiDir = this.#location;
|
|
1967
|
-
mkdirRecursiveSync(pkiDir);
|
|
1968
|
-
mkdirRecursiveSync(path4.join(pkiDir, "own"));
|
|
1969
|
-
mkdirRecursiveSync(path4.join(pkiDir, "own/certs"));
|
|
1970
|
-
ensurePrivateDirectory(path4.join(pkiDir, "own/private"));
|
|
1971
|
-
mkdirRecursiveSync(path4.join(pkiDir, "rejected"));
|
|
1972
|
-
mkdirRecursiveSync(path4.join(pkiDir, "trusted"));
|
|
1973
|
-
mkdirRecursiveSync(path4.join(pkiDir, "trusted/certs"));
|
|
1974
|
-
mkdirRecursiveSync(path4.join(pkiDir, "trusted/crl"));
|
|
1975
|
-
mkdirRecursiveSync(path4.join(pkiDir, "issuers"));
|
|
1976
|
-
mkdirRecursiveSync(path4.join(pkiDir, "issuers/certs"));
|
|
1977
|
-
mkdirRecursiveSync(path4.join(pkiDir, "issuers/crl"));
|
|
1978
|
-
const ownsDiskKey = !this.#privateKeyProvider && !this.#keyOperations;
|
|
1979
|
-
const needsKeyGeneration = ownsDiskKey && !fs7.existsSync(this.privateKey);
|
|
1980
|
-
const needsKeyEncryption = ownsDiskKey && !needsKeyGeneration && this.#privateKeyPassphrase !== void 0 && !isEncryptedPrivateKeyFile(this.privateKey);
|
|
1981
|
-
if (!fs7.existsSync(this.configFile) || needsKeyGeneration || needsKeyEncryption) {
|
|
1982
|
-
return await this.withLock2(async () => {
|
|
1983
|
-
if (this.state === 3 /* Disposing */ || this.state === 4 /* Disposed */) {
|
|
1984
|
-
return;
|
|
1985
|
-
}
|
|
1986
|
-
if (!fs7.existsSync(this.configFile)) {
|
|
1987
|
-
fs7.writeFileSync(this.configFile, configurationFileSimpleTemplate);
|
|
1988
|
-
}
|
|
1989
|
-
if (ownsDiskKey && !fs7.existsSync(this.privateKey)) {
|
|
1990
|
-
debugLog("generating private key ...");
|
|
1991
|
-
const passphrase = await resolvePrivateKeyPassphrase(this.#privateKeyPassphrase);
|
|
1992
|
-
await generatePrivateKeyFile(this.privateKey, this.keySize, { passphrase });
|
|
1993
|
-
this.#cachedPrivateKey = readPrivateKey(this.privateKey, passphrase);
|
|
1994
|
-
} else if (ownsDiskKey && this.#privateKeyPassphrase !== void 0 && !isEncryptedPrivateKeyFile(this.privateKey)) {
|
|
1995
|
-
warningLog("initialize: private key is plaintext but a passphrase is configured; encrypting it in place");
|
|
1996
|
-
const passphrase = await resolvePrivateKeyPassphrase(this.#privateKeyPassphrase);
|
|
1997
|
-
const plaintextKey = readPrivateKey(this.privateKey);
|
|
1998
|
-
await this.#rewritePrivateKeyFile(plaintextKey, passphrase);
|
|
1999
|
-
this.#cachedPrivateKey = plaintextKey;
|
|
2000
|
-
}
|
|
2001
|
-
if (ownsDiskKey) {
|
|
2002
|
-
restrictPrivateFilePermissions(this.privateKey, 384);
|
|
2003
|
-
}
|
|
2004
|
-
await this.#probePrivateKey();
|
|
2005
|
-
await this.#readCertificates();
|
|
2006
|
-
});
|
|
2007
|
-
} else {
|
|
2008
|
-
if (ownsDiskKey) {
|
|
2009
|
-
restrictPrivateFilePermissions(this.privateKey, 384);
|
|
2010
|
-
}
|
|
2011
|
-
await this.#probePrivateKey();
|
|
2012
|
-
await this.#readCertificates();
|
|
2013
|
-
}
|
|
2014
|
-
}
|
|
2015
|
-
/**
|
|
2016
|
-
* Dispose of the CertificateManager, releasing file watchers
|
|
2017
|
-
* and other resources. The instance should not be used after
|
|
2018
|
-
* calling this method.
|
|
2019
|
-
*/
|
|
2020
|
-
async dispose() {
|
|
2021
|
-
if (this.state === 3 /* Disposing */) {
|
|
2022
|
-
throw new Error("Already disposing");
|
|
2023
|
-
}
|
|
2024
|
-
if (this.state === 0 /* Uninitialized */) {
|
|
2025
|
-
this.state = 4 /* Disposed */;
|
|
2026
|
-
return;
|
|
2027
|
-
}
|
|
2028
|
-
if (this.state === 1 /* Initializing */) {
|
|
2029
|
-
if (this.#initializingPromise) {
|
|
2030
|
-
await this.#initializingPromise;
|
|
2031
|
-
}
|
|
2032
|
-
}
|
|
2033
|
-
try {
|
|
2034
|
-
this.state = 3 /* Disposing */;
|
|
2035
|
-
await drainPendingLocks();
|
|
2036
|
-
for (const unreff of this.#pendingUnrefs) {
|
|
2037
|
-
unreff();
|
|
2038
|
-
}
|
|
2039
|
-
this.#pendingUnrefs.clear();
|
|
2040
|
-
await Promise.all(this.#watchers.map((w) => w.close()));
|
|
2041
|
-
this.#watchers.forEach((w) => {
|
|
2042
|
-
w.removeAllListeners();
|
|
2043
|
-
});
|
|
2044
|
-
this.#watchers.splice(0);
|
|
2045
|
-
} finally {
|
|
2046
|
-
this.state = 4 /* Disposed */;
|
|
2047
|
-
this.#cachedPrivateKey = void 0;
|
|
2048
|
-
this.#privateKeyPromise = void 0;
|
|
2049
|
-
_CertificateManager.#activeInstances.delete(this);
|
|
2050
|
-
_CertificateManager.#uninstallExitCleanupIfIdle();
|
|
2051
|
-
}
|
|
2052
|
-
}
|
|
2053
|
-
/**
|
|
2054
|
-
* Force a full re-scan of all PKI folders, rebuilding
|
|
2055
|
-
* the in-memory `_thumbs` index from scratch.
|
|
2056
|
-
*
|
|
2057
|
-
* Call this after external processes have modified the
|
|
2058
|
-
* PKI folders (e.g. via `writeTrustList` or CLI tools)
|
|
2059
|
-
* to ensure the CertificateManager sees the latest
|
|
2060
|
-
* state without waiting for file-system events.
|
|
2061
|
-
*/
|
|
2062
|
-
async reloadCertificates() {
|
|
2063
|
-
await Promise.all(this.#watchers.map((w) => w.close()));
|
|
2064
|
-
for (const w of this.#watchers) {
|
|
2065
|
-
w.removeAllListeners();
|
|
2066
|
-
}
|
|
2067
|
-
this.#watchers.splice(0);
|
|
2068
|
-
this.#thumbs.rejected.clear();
|
|
2069
|
-
this.#thumbs.trusted.clear();
|
|
2070
|
-
this.#thumbs.issuers.certs.clear();
|
|
2071
|
-
this.#thumbs.crl.clear();
|
|
2072
|
-
this.#thumbs.issuersCrl.clear();
|
|
2073
|
-
this.#filenameToHash.clear();
|
|
2074
|
-
this.#readCertificatesCalled = false;
|
|
2075
|
-
await this.#readCertificates();
|
|
2076
|
-
}
|
|
2077
|
-
async withLock2(action) {
|
|
2078
|
-
const lockFileName = path4.join(this.rootDir, "mutex");
|
|
2079
|
-
return withLock({ fileToLock: lockFileName }, async () => {
|
|
2080
|
-
return await action();
|
|
2081
|
-
});
|
|
2082
|
-
}
|
|
2083
|
-
/**
|
|
2084
|
-
* Create a self-signed certificate for this PKI's private key.
|
|
2085
|
-
*
|
|
2086
|
-
* The certificate is written to `params.outputFile` or
|
|
2087
|
-
* `own/certs/self_signed_certificate.pem` by default.
|
|
2088
|
-
*
|
|
2089
|
-
* @param params - certificate parameters (subject, SANs,
|
|
2090
|
-
* validity, etc.)
|
|
2091
|
-
*/
|
|
2092
|
-
async createSelfSignedCertificate(params) {
|
|
2093
|
-
if (typeof params.applicationUri !== "string") {
|
|
2094
|
-
throw new Error("createSelfSignedCertificate: expecting applicationUri to be a string");
|
|
2095
|
-
}
|
|
2096
|
-
if (!this.#privateKeyProvider && !this.#keyOperations && !fs7.existsSync(this.privateKey)) {
|
|
2097
|
-
throw new Error(`Cannot find private key ${this.privateKey}`);
|
|
2098
|
-
}
|
|
2099
|
-
let certificateFilename = path4.join(this.rootDir, "own/certs/self_signed_certificate.pem");
|
|
2100
|
-
certificateFilename = params.outputFile || certificateFilename;
|
|
2101
|
-
const _params = {
|
|
2102
|
-
...params,
|
|
2103
|
-
rootDir: this.rootDir,
|
|
2104
|
-
configFile: this.configFile,
|
|
2105
|
-
privateKey: await this.#resolveSigningKey(),
|
|
2106
|
-
subject: params.subject || "CN=FIXME"
|
|
2107
|
-
};
|
|
2108
|
-
await this.withLock2(async () => {
|
|
2109
|
-
await createSelfSignedCertificate(certificateFilename, _params);
|
|
2110
|
-
});
|
|
2111
|
-
}
|
|
2112
|
-
/**
|
|
2113
|
-
* Create a Certificate Signing Request (CSR) using this
|
|
2114
|
-
* PKI's private key and configuration.
|
|
2115
|
-
*
|
|
2116
|
-
* The CSR file is written to `own/certs/` with a timestamped
|
|
2117
|
-
* filename.
|
|
2118
|
-
*
|
|
2119
|
-
* @param params - CSR parameters (subject, SANs)
|
|
2120
|
-
* @returns the filesystem path to the generated CSR file
|
|
2121
|
-
*/
|
|
2122
|
-
async createCertificateRequest(params) {
|
|
2123
|
-
if (!params) {
|
|
2124
|
-
throw new Error("params is required");
|
|
2125
|
-
}
|
|
2126
|
-
if (Object.prototype.hasOwnProperty.call(params, "rootDir")) {
|
|
2127
|
-
throw new Error("rootDir should not be specified ");
|
|
2128
|
-
}
|
|
2129
|
-
const _params = {
|
|
2130
|
-
...params,
|
|
2131
|
-
rootDir: path4.resolve(this.rootDir),
|
|
2132
|
-
configFile: path4.resolve(this.configFile),
|
|
2133
|
-
privateKey: await this.#resolveSigningKey()
|
|
2134
|
-
};
|
|
2135
|
-
return await this.withLock2(async () => {
|
|
2136
|
-
const now = /* @__PURE__ */ new Date();
|
|
2137
|
-
const today = `${now.toISOString().slice(0, 10)}_${now.getTime()}`;
|
|
2138
|
-
const certificateSigningRequestFilename = path4.join(this.rootDir, "own/certs", `certificate_${today}.csr`);
|
|
2139
|
-
await createCertificateSigningRequestAsync(certificateSigningRequestFilename, _params);
|
|
2140
|
-
return certificateSigningRequestFilename;
|
|
2141
|
-
});
|
|
2142
|
-
}
|
|
2143
|
-
/**
|
|
2144
|
-
* Add a CA (issuer) certificate to the issuers store.
|
|
2145
|
-
* If the certificate is already present, this is a no-op.
|
|
2146
|
-
* @param certificate - the DER-encoded CA certificate
|
|
2147
|
-
* @param validate - if `true`, verify the certificate before adding
|
|
2148
|
-
* @param addInTrustList - if `true`, also add to the trusted store
|
|
2149
|
-
* @returns `VerificationStatus.Good` on success
|
|
2150
|
-
*/
|
|
2151
|
-
async addIssuer(certificate, validate = false, addInTrustList = false) {
|
|
2152
|
-
if (validate) {
|
|
2153
|
-
const status = await this.verifyCertificate(certificate);
|
|
2154
|
-
if (status !== "Good" /* Good */ && status !== "BadCertificateUntrusted" /* BadCertificateUntrusted */) {
|
|
2155
|
-
return status;
|
|
2156
|
-
}
|
|
2157
|
-
}
|
|
2158
|
-
const pemCertificate = toPem(certificate, "CERTIFICATE");
|
|
2159
|
-
const fingerprint = makeFingerprint(certificate);
|
|
2160
|
-
if (this.#thumbs.issuers.certs.has(fingerprint)) {
|
|
2161
|
-
return "Good" /* Good */;
|
|
2162
|
-
}
|
|
2163
|
-
const filename = safeStoreJoin(this.issuersCertFolder, `issuer_${buildIdealCertificateName(certificate)}.pem`);
|
|
2164
|
-
await fs7.promises.writeFile(filename, pemCertificate, "ascii");
|
|
2165
|
-
this.#thumbs.issuers.certs.set(fingerprint, { certificate, filename });
|
|
2166
|
-
if (addInTrustList) {
|
|
2167
|
-
await this.trustCertificate(certificate);
|
|
2168
|
-
}
|
|
2169
|
-
return "Good" /* Good */;
|
|
2170
|
-
}
|
|
2171
|
-
/**
|
|
2172
|
-
* Add multiple CA (issuer) certificates to the issuers store.
|
|
2173
|
-
* @param certificates - the DER-encoded CA certificates
|
|
2174
|
-
* @param validate - if `true`, verify each certificate before adding
|
|
2175
|
-
* @param addInTrustList - if `true`, also add each certificate to the trusted store
|
|
2176
|
-
* @returns `VerificationStatus.Good` on success
|
|
2177
|
-
*/
|
|
2178
|
-
async addIssuers(certificates, validate = false, addInTrustList = false) {
|
|
2179
|
-
for (const certificate of certificates) {
|
|
2180
|
-
if (!isIssuer(certificate)) {
|
|
2181
|
-
warningLog(`Certificate ${makeFingerprint(certificate)} is not a issuer certificate`);
|
|
2182
|
-
continue;
|
|
2183
|
-
}
|
|
2184
|
-
await this.addIssuer(certificate, validate, addInTrustList);
|
|
2185
|
-
}
|
|
2186
|
-
return "Good" /* Good */;
|
|
2187
|
-
}
|
|
2188
|
-
/**
|
|
2189
|
-
* Add a CRL to the certificate manager.
|
|
2190
|
-
* @param crl - the CRL to add
|
|
2191
|
-
* @param target - "issuers" (default) writes to issuers/crl, "trusted" writes to trusted/crl
|
|
2192
|
-
*/
|
|
2193
|
-
async addRevocationList(crl, target = "issuers") {
|
|
2194
|
-
return await this.withLock2(async () => {
|
|
2195
|
-
try {
|
|
2196
|
-
const index = target === "trusted" ? this.#thumbs.crl : this.#thumbs.issuersCrl;
|
|
2197
|
-
const folder = target === "trusted" ? this.crlFolder : this.issuersCrlFolder;
|
|
2198
|
-
const crlInfo = exploreCertificateRevocationList(crl);
|
|
2199
|
-
const key = crlInfo.tbsCertList.issuerFingerprint;
|
|
2200
|
-
if (!index.has(key)) {
|
|
2201
|
-
index.set(key, { crls: [], serialNumbers: {} });
|
|
2202
|
-
}
|
|
2203
|
-
const pemCertificate = toPem(crl, "X509 CRL");
|
|
2204
|
-
const sanitizedKey = key.replace(/:/g, "");
|
|
2205
|
-
const filename = path4.join(folder, `crl_[${sanitizedKey}].pem`);
|
|
2206
|
-
await fs7.promises.writeFile(filename, pemCertificate, "ascii");
|
|
2207
|
-
await this.#onCrlFileAdded(index, filename);
|
|
2208
|
-
await this.#waitAndCheckCRLProcessingStatus();
|
|
2209
|
-
return "Good" /* Good */;
|
|
2210
|
-
} catch (err) {
|
|
2211
|
-
debugLog(err);
|
|
2212
|
-
return "BadSecurityChecksFailed" /* BadSecurityChecksFailed */;
|
|
2213
|
-
}
|
|
2214
|
-
});
|
|
2215
|
-
}
|
|
2216
|
-
/**
|
|
2217
|
-
* Remove all CRL files from the specified folder(s) and clear the
|
|
2218
|
-
* corresponding in-memory index.
|
|
2219
|
-
* @param target - "issuers" clears issuers/crl, "trusted" clears
|
|
2220
|
-
* trusted/crl, "all" clears both.
|
|
2221
|
-
*/
|
|
2222
|
-
async clearRevocationLists(target) {
|
|
2223
|
-
const clearFolder = async (folder, index) => {
|
|
2224
|
-
try {
|
|
2225
|
-
const files = await fs7.promises.readdir(folder);
|
|
2226
|
-
for (const file of files) {
|
|
2227
|
-
const ext = path4.extname(file).toLowerCase();
|
|
2228
|
-
if (ext === ".crl" || ext === ".pem" || ext === ".der") {
|
|
2229
|
-
await fs7.promises.unlink(path4.join(folder, file));
|
|
2230
|
-
}
|
|
2231
|
-
}
|
|
2232
|
-
} catch (err) {
|
|
2233
|
-
if (err.code !== "ENOENT") {
|
|
2234
|
-
throw err;
|
|
2235
|
-
}
|
|
2236
|
-
}
|
|
2237
|
-
index.clear();
|
|
2238
|
-
};
|
|
2239
|
-
if (target === "issuers" || target === "all") {
|
|
2240
|
-
await clearFolder(this.issuersCrlFolder, this.#thumbs.issuersCrl);
|
|
2241
|
-
}
|
|
2242
|
-
if (target === "trusted" || target === "all") {
|
|
2243
|
-
await clearFolder(this.crlFolder, this.#thumbs.crl);
|
|
2244
|
-
}
|
|
2245
|
-
}
|
|
2246
|
-
/**
|
|
2247
|
-
* Check whether an issuer certificate with the given thumbprint
|
|
2248
|
-
* is already registered.
|
|
2249
|
-
* @param thumbprint - hex-encoded SHA-1 thumbprint (lowercase)
|
|
2250
|
-
*/
|
|
2251
|
-
async hasIssuer(thumbprint) {
|
|
2252
|
-
await this.#readCertificates();
|
|
2253
|
-
const normalized = thumbprint.toLowerCase();
|
|
2254
|
-
return this.#thumbs.issuers.certs.has(normalized);
|
|
2255
|
-
}
|
|
2256
|
-
/**
|
|
2257
|
-
* Remove a trusted certificate identified by its SHA-1 thumbprint.
|
|
2258
|
-
* Deletes the file on disk and removes the entry from the
|
|
2259
|
-
* in-memory index.
|
|
2260
|
-
* @param thumbprint - hex-encoded SHA-1 thumbprint (lowercase)
|
|
2261
|
-
* @returns the removed certificate buffer, or `null` if not found
|
|
2262
|
-
*/
|
|
2263
|
-
async removeTrustedCertificate(thumbprint) {
|
|
2264
|
-
await this.#readCertificates();
|
|
2265
|
-
const normalized = thumbprint.toLowerCase();
|
|
2266
|
-
const entry = this.#thumbs.trusted.get(normalized);
|
|
2267
|
-
if (!entry) {
|
|
2268
|
-
return null;
|
|
2269
|
-
}
|
|
2270
|
-
try {
|
|
2271
|
-
await fs7.promises.unlink(entry.filename);
|
|
2272
|
-
} catch (err) {
|
|
2273
|
-
if (err.code !== "ENOENT") {
|
|
2274
|
-
throw err;
|
|
2275
|
-
}
|
|
2276
|
-
}
|
|
2277
|
-
this.#thumbs.trusted.delete(normalized);
|
|
2278
|
-
return entry.certificate;
|
|
2279
|
-
}
|
|
2280
|
-
/**
|
|
2281
|
-
* Remove an issuer certificate identified by its SHA-1 thumbprint.
|
|
2282
|
-
* Deletes the file on disk and removes the entry from the
|
|
2283
|
-
* in-memory index.
|
|
2284
|
-
* @param thumbprint - hex-encoded SHA-1 thumbprint (lowercase)
|
|
2285
|
-
* @returns the removed certificate buffer, or `null` if not found
|
|
2286
|
-
*/
|
|
2287
|
-
async removeIssuer(thumbprint) {
|
|
2288
|
-
await this.#readCertificates();
|
|
2289
|
-
const normalized = thumbprint.toLowerCase();
|
|
2290
|
-
const entry = this.#thumbs.issuers.certs.get(normalized);
|
|
2291
|
-
if (!entry) {
|
|
2292
|
-
return null;
|
|
2293
|
-
}
|
|
2294
|
-
try {
|
|
2295
|
-
await fs7.promises.unlink(entry.filename);
|
|
2296
|
-
} catch (err) {
|
|
2297
|
-
if (err.code !== "ENOENT") {
|
|
2298
|
-
throw err;
|
|
2299
|
-
}
|
|
2300
|
-
}
|
|
2301
|
-
this.#thumbs.issuers.certs.delete(normalized);
|
|
2302
|
-
return entry.certificate;
|
|
2303
|
-
}
|
|
2304
|
-
/**
|
|
2305
|
-
* Remove all CRL files that were issued by the given CA certificate
|
|
2306
|
-
* from the specified folder (or both).
|
|
2307
|
-
* @param issuerCertificate - the CA certificate whose CRLs to remove
|
|
2308
|
-
* @param target - "issuers", "trusted", or "all" (default "all")
|
|
2309
|
-
*/
|
|
2310
|
-
async removeRevocationListsForIssuer(issuerCertificate, target = "all") {
|
|
2311
|
-
const issuerInfo = exploreCertificate(issuerCertificate);
|
|
2312
|
-
const issuerFingerprint = issuerInfo.tbsCertificate.subjectFingerPrint;
|
|
2313
|
-
const processIndex = async (index) => {
|
|
2314
|
-
const crlData = index.get(issuerFingerprint);
|
|
2315
|
-
if (!crlData) return;
|
|
2316
|
-
for (const crlEntry of crlData.crls) {
|
|
2317
|
-
try {
|
|
2318
|
-
await fs7.promises.unlink(crlEntry.filename);
|
|
2319
|
-
} catch (err) {
|
|
2320
|
-
if (err.code !== "ENOENT") {
|
|
2321
|
-
throw err;
|
|
2322
|
-
}
|
|
2323
|
-
}
|
|
2324
|
-
}
|
|
2325
|
-
index.delete(issuerFingerprint);
|
|
2326
|
-
};
|
|
2327
|
-
if (target === "issuers" || target === "all") {
|
|
2328
|
-
await processIndex(this.#thumbs.issuersCrl);
|
|
2329
|
-
}
|
|
2330
|
-
if (target === "trusted" || target === "all") {
|
|
2331
|
-
await processIndex(this.#thumbs.crl);
|
|
2332
|
-
}
|
|
2333
|
-
}
|
|
2334
|
-
/**
|
|
2335
|
-
* Validate a certificate (optionally with its chain) and add
|
|
2336
|
-
* the leaf certificate to the trusted store.
|
|
2337
|
-
*
|
|
2338
|
-
* Performs OPC UA Part 4, Table 100 validation:
|
|
2339
|
-
*
|
|
2340
|
-
* 1. **Certificate Structure** — parse the DER encoding.
|
|
2341
|
-
* 2. **Build Certificate Chain** — walk from the leaf to a
|
|
2342
|
-
* self-signed root CA, using the provided chain and the
|
|
2343
|
-
* issuers store.
|
|
2344
|
-
* 3. **Signature** — verify each certificate's signature
|
|
2345
|
-
* against its issuer.
|
|
2346
|
-
* 4. **Issuer Presence** — every issuer in the chain must
|
|
2347
|
-
* already be registered in the issuers store (per GDS
|
|
2348
|
-
* 7.8.2.6).
|
|
2349
|
-
* 5. **Validity Period** — each certificate must be within
|
|
2350
|
-
* its validity window (overridable via
|
|
2351
|
-
* {@link AddCertificateValidationOptions.acceptExpiredCertificate}).
|
|
2352
|
-
* 6. **Revocation Check** — each certificate is checked
|
|
2353
|
-
* against its issuer's CRL (overridable via
|
|
2354
|
-
* {@link AddCertificateValidationOptions.acceptRevokedCertificate}
|
|
2355
|
-
* and {@link AddCertificateValidationOptions.ignoreMissingRevocationList}).
|
|
2356
|
-
*
|
|
2357
|
-
* Only the leaf certificate is added to the trusted store.
|
|
2358
|
-
*
|
|
2359
|
-
* @param certificateChain - DER-encoded certificate or chain
|
|
2360
|
-
* @returns `VerificationStatus.Good` on success, or an error
|
|
2361
|
-
* status indicating why the certificate was rejected.
|
|
2362
|
-
*/
|
|
2363
|
-
async addTrustedCertificateFromChain(certificateChain) {
|
|
2364
|
-
try {
|
|
2365
|
-
return await this.#addTrustedCertificateFromChainImpl(certificateChain);
|
|
2366
|
-
} catch (_err) {
|
|
2367
|
-
warningLog("addTrustedCertificateFromChain: unexpected error", _err);
|
|
2368
|
-
return "BadCertificateInvalid" /* BadCertificateInvalid */;
|
|
2369
|
-
}
|
|
2370
|
-
}
|
|
2371
|
-
async #addTrustedCertificateFromChainImpl(certificateChain) {
|
|
2372
|
-
let certificates;
|
|
2373
|
-
try {
|
|
2374
|
-
certificates = coerceCertificateChain(certificateChain);
|
|
2375
|
-
} catch (_err) {
|
|
2376
|
-
return "BadCertificateInvalid" /* BadCertificateInvalid */;
|
|
2377
|
-
}
|
|
2378
|
-
if (certificates.length === 0) {
|
|
2379
|
-
return "BadCertificateInvalid" /* BadCertificateInvalid */;
|
|
2380
|
-
}
|
|
2381
|
-
const leafCertificate = certificates[0];
|
|
2382
|
-
const opts = this.#addCertValidation;
|
|
2383
|
-
let leafInfo;
|
|
2384
|
-
try {
|
|
2385
|
-
leafInfo = exploreCertificate(leafCertificate);
|
|
2386
|
-
} catch (_err) {
|
|
2387
|
-
return "BadCertificateInvalid" /* BadCertificateInvalid */;
|
|
2388
|
-
}
|
|
2389
|
-
await this.#scanCertFolder(this.issuersCertFolder, this.#thumbs.issuers.certs);
|
|
2390
|
-
let currentCert = leafCertificate;
|
|
2391
|
-
let currentInfo = leafInfo;
|
|
2392
|
-
let depth = 0;
|
|
2393
|
-
while (true) {
|
|
2394
|
-
depth++;
|
|
2395
|
-
if (depth > opts.maxChainLength) {
|
|
2396
|
-
return "BadSecurityChecksFailed" /* BadSecurityChecksFailed */;
|
|
2397
|
-
}
|
|
2398
|
-
if (!opts.acceptExpiredCertificate) {
|
|
2399
|
-
const { validity } = currentInfo.tbsCertificate;
|
|
2400
|
-
const now = /* @__PURE__ */ new Date();
|
|
2401
|
-
if (validity.notBefore.getTime() > now.getTime()) {
|
|
2402
|
-
return "BadCertificateTimeInvalid" /* BadCertificateTimeInvalid */;
|
|
2403
|
-
}
|
|
2404
|
-
if (validity.notAfter.getTime() <= now.getTime()) {
|
|
2405
|
-
return depth === 1 ? "BadCertificateTimeInvalid" /* BadCertificateTimeInvalid */ : "BadCertificateIssuerTimeInvalid" /* BadCertificateIssuerTimeInvalid */;
|
|
2406
|
-
}
|
|
2407
|
-
}
|
|
2408
|
-
if (isSelfSigned2(currentInfo)) {
|
|
2409
|
-
try {
|
|
2410
|
-
if (!verifyCertificateSignature(currentCert, currentCert)) {
|
|
2411
|
-
return "BadCertificateInvalid" /* BadCertificateInvalid */;
|
|
2412
|
-
}
|
|
2413
|
-
} catch (_err) {
|
|
2414
|
-
return "BadCertificateInvalid" /* BadCertificateInvalid */;
|
|
2415
|
-
}
|
|
2416
|
-
break;
|
|
2417
|
-
}
|
|
2418
|
-
let issuerCert = await this.findIssuerCertificate(currentCert);
|
|
2419
|
-
if (!issuerCert) {
|
|
2420
|
-
issuerCert = findIssuerCertificateInChain(currentCert, certificates);
|
|
2421
|
-
if (!issuerCert || issuerCert === currentCert) {
|
|
2422
|
-
return "BadCertificateChainIncomplete" /* BadCertificateChainIncomplete */;
|
|
2423
|
-
}
|
|
2424
|
-
}
|
|
2425
|
-
try {
|
|
2426
|
-
if (!verifyCertificateSignature(currentCert, issuerCert)) {
|
|
2427
|
-
return "BadCertificateInvalid" /* BadCertificateInvalid */;
|
|
2428
|
-
}
|
|
2429
|
-
} catch (_err) {
|
|
2430
|
-
return "BadCertificateInvalid" /* BadCertificateInvalid */;
|
|
2431
|
-
}
|
|
2432
|
-
const issuerThumbprint = makeFingerprint(issuerCert);
|
|
2433
|
-
if (!await this.hasIssuer(issuerThumbprint)) {
|
|
2434
|
-
return "BadCertificateChainIncomplete" /* BadCertificateChainIncomplete */;
|
|
2435
|
-
}
|
|
2436
|
-
const revokedStatus = await this.isCertificateRevoked(currentCert, issuerCert);
|
|
2437
|
-
if (revokedStatus === "BadCertificateRevoked" /* BadCertificateRevoked */) {
|
|
2438
|
-
if (!opts.acceptRevokedCertificate) {
|
|
2439
|
-
return "BadCertificateRevoked" /* BadCertificateRevoked */;
|
|
2440
|
-
}
|
|
2441
|
-
} else if (revokedStatus === "BadCertificateRevocationUnknown" /* BadCertificateRevocationUnknown */) {
|
|
2442
|
-
if (!opts.ignoreMissingRevocationList) {
|
|
2443
|
-
return "BadCertificateRevocationUnknown" /* BadCertificateRevocationUnknown */;
|
|
2444
|
-
}
|
|
2445
|
-
}
|
|
2446
|
-
currentCert = issuerCert;
|
|
2447
|
-
try {
|
|
2448
|
-
currentInfo = exploreCertificate(currentCert);
|
|
2449
|
-
} catch (_err) {
|
|
2450
|
-
return "BadCertificateInvalid" /* BadCertificateInvalid */;
|
|
2451
|
-
}
|
|
2452
|
-
}
|
|
2453
|
-
await this.trustCertificate(certificates);
|
|
2454
|
-
return "Good" /* Good */;
|
|
2455
|
-
}
|
|
2456
|
-
/**
|
|
2457
|
-
* Check whether an issuer certificate is still needed by any
|
|
2458
|
-
* certificate in the trusted store.
|
|
2459
|
-
*
|
|
2460
|
-
* This is used before removing an issuer to ensure that
|
|
2461
|
-
* doing so would not break the chain of any trusted
|
|
2462
|
-
* certificate.
|
|
2463
|
-
*
|
|
2464
|
-
* @param issuerCertificate - the CA certificate to check
|
|
2465
|
-
* @returns `true` if at least one trusted certificate was
|
|
2466
|
-
* signed by this issuer.
|
|
2467
|
-
*/
|
|
2468
|
-
async isIssuerInUseByTrustedCertificate(issuerCertificate) {
|
|
2469
|
-
await this.#readCertificates();
|
|
2470
|
-
for (const entry of this.#thumbs.trusted.values()) {
|
|
2471
|
-
if (!entry.certificate) continue;
|
|
2472
|
-
try {
|
|
2473
|
-
if (verifyCertificateSignature(entry.certificate, issuerCertificate)) {
|
|
2474
|
-
return true;
|
|
2475
|
-
}
|
|
2476
|
-
} catch (_err) {
|
|
2477
|
-
}
|
|
2478
|
-
}
|
|
2479
|
-
return false;
|
|
2480
|
-
}
|
|
2481
|
-
/**
|
|
2482
|
-
* find the issuer certificate among the trusted issuer certificates.
|
|
2483
|
-
*
|
|
2484
|
-
* The findIssuerCertificate method is an asynchronous method that attempts to find
|
|
2485
|
-
* the issuer certificate for a given certificate from the list of issuer certificate declared in the PKI
|
|
2486
|
-
*
|
|
2487
|
-
* - If the certificate is self-signed, it returns the certificate itself.
|
|
2488
|
-
*
|
|
2489
|
-
* - If the certificate has no extension 3, it is assumed to be generated by an old system, and a null value is returned.
|
|
2490
|
-
*
|
|
2491
|
-
* - the method checks both issuer and trusted certificates and returns the appropriate issuercertificate,
|
|
2492
|
-
* if found. If multiple matching certificates are found, a warning is logged to the console.
|
|
2493
|
-
*
|
|
2494
|
-
*/
|
|
2495
|
-
async findIssuerCertificate(certificate) {
|
|
2496
|
-
const firstCertificate = coerceCertificateChain(certificate)[0];
|
|
2497
|
-
const certInfo = exploreCertificate(firstCertificate);
|
|
2498
|
-
if (isSelfSigned2(certInfo)) {
|
|
2499
|
-
return firstCertificate;
|
|
2500
|
-
}
|
|
2501
|
-
const wantedIssuerKey = certInfo.tbsCertificate.extensions?.authorityKeyIdentifier?.keyIdentifier;
|
|
2502
|
-
if (!wantedIssuerKey) {
|
|
2503
|
-
debugLog("Certificate has no extension 3");
|
|
2504
|
-
return null;
|
|
2505
|
-
}
|
|
2506
|
-
const issuerCertificates = [...this.#thumbs.issuers.certs.values()];
|
|
2507
|
-
const selectedIssuerCertificates = findMatchingIssuerKey(issuerCertificates, wantedIssuerKey);
|
|
2508
|
-
if (selectedIssuerCertificates.length > 0) {
|
|
2509
|
-
if (selectedIssuerCertificates.length > 1) {
|
|
2510
|
-
warningLog("Warning more than one issuer certificate exists with subjectKeyIdentifier ", wantedIssuerKey);
|
|
2511
|
-
}
|
|
2512
|
-
return selectedIssuerCertificates[0].certificate || null;
|
|
2513
|
-
}
|
|
2514
|
-
const trustedCertificates = [...this.#thumbs.trusted.values()];
|
|
2515
|
-
const selectedTrustedCertificates = findMatchingIssuerKey(trustedCertificates, wantedIssuerKey);
|
|
2516
|
-
if (selectedTrustedCertificates.length > 1) {
|
|
2517
|
-
warningLog(
|
|
2518
|
-
"Warning more than one certificate exists with subjectKeyIdentifier in trusted certificate list ",
|
|
2519
|
-
wantedIssuerKey,
|
|
2520
|
-
selectedTrustedCertificates.length
|
|
2521
|
-
);
|
|
2522
|
-
}
|
|
2523
|
-
return selectedTrustedCertificates.length > 0 ? selectedTrustedCertificates[0].certificate : null;
|
|
2524
|
-
}
|
|
2525
|
-
/**
|
|
2526
|
-
* Outcome status for {@link CertificateManager.completeCertificateChain}.
|
|
2527
|
-
*/
|
|
2528
|
-
static ChainCompletionStatus = ChainCompletionStatus;
|
|
2529
|
-
/**
|
|
2530
|
-
* Complete a certificate chain by walking the issuer store.
|
|
2531
|
-
*
|
|
2532
|
-
* Starting from the last certificate in the provided chain, this method
|
|
2533
|
-
* repeatedly calls {@link findIssuerCertificate} to locate the parent
|
|
2534
|
-
* certificate until it reaches a self-signed root or can no longer find
|
|
2535
|
-
* an issuer.
|
|
2536
|
-
*
|
|
2537
|
-
* @param chain - the (potentially partial) certificate chain, leaf first
|
|
2538
|
-
* @param maxDepth - maximum number of issuers to append (default: 10)
|
|
2539
|
-
* @returns a {@link ChainCompletionResult} containing the (possibly completed)
|
|
2540
|
-
* chain, a status code, and an optional diagnostic message.
|
|
2541
|
-
*/
|
|
2542
|
-
async completeCertificateChain(chain, maxDepth = 10) {
|
|
2543
|
-
if (chain.length === 0) {
|
|
2544
|
-
return {
|
|
2545
|
-
chain,
|
|
2546
|
-
status: "EmptyChain" /* EmptyChain */,
|
|
2547
|
-
message: "Input chain is empty \u2014 nothing to complete."
|
|
2548
|
-
};
|
|
2549
|
-
}
|
|
2550
|
-
await this.#scanCertFolder(this.issuersCertFolder, this.#thumbs.issuers.certs);
|
|
2551
|
-
const result = [...chain];
|
|
2552
|
-
let depth = 0;
|
|
2553
|
-
while (depth < maxDepth) {
|
|
2554
|
-
const lastCert = result[result.length - 1];
|
|
2555
|
-
const lastInfo = exploreCertificate(lastCert);
|
|
2556
|
-
if (isSelfSigned2(lastInfo)) {
|
|
2557
|
-
const wasExtended = result.length > chain.length;
|
|
2558
|
-
return {
|
|
2559
|
-
chain: result,
|
|
2560
|
-
status: wasExtended ? "ChainCompleted" /* ChainCompleted */ : "AlreadyComplete" /* AlreadyComplete */,
|
|
2561
|
-
message: wasExtended ? `Chain completed: ${result.length - chain.length} issuer(s) appended, ending at self-signed root "${lastInfo.tbsCertificate.subject.commonName}".` : `Chain is already complete (self-signed root "${lastInfo.tbsCertificate.subject.commonName}").`
|
|
2562
|
-
};
|
|
2563
|
-
}
|
|
2564
|
-
const issuerCert = await this.findIssuerCertificate(lastCert);
|
|
2565
|
-
if (!issuerCert) {
|
|
2566
|
-
const cn = lastInfo.tbsCertificate.subject.commonName ?? "?";
|
|
2567
|
-
const akid = lastInfo.tbsCertificate.extensions?.authorityKeyIdentifier?.keyIdentifier ?? "?";
|
|
2568
|
-
const msg = `Cannot find issuer for "${cn}" (authorityKeyIdentifier: ${akid}). Ensure the CA certificate is present in the issuers/certs folder.`;
|
|
2569
|
-
warningLog(`completeCertificateChain: ${msg}`);
|
|
2570
|
-
return {
|
|
2571
|
-
chain: result,
|
|
2572
|
-
status: "IssuerNotFound" /* IssuerNotFound */,
|
|
2573
|
-
message: msg
|
|
2574
|
-
};
|
|
2575
|
-
}
|
|
2576
|
-
const issuerFingerprint = makeFingerprint(issuerCert);
|
|
2577
|
-
const alreadyInChain = result.some((c) => makeFingerprint(c) === issuerFingerprint);
|
|
2578
|
-
if (alreadyInChain) {
|
|
2579
|
-
return {
|
|
2580
|
-
chain: result,
|
|
2581
|
-
status: "AlreadyComplete" /* AlreadyComplete */,
|
|
2582
|
-
message: `Chain ends at root "${exploreCertificate(issuerCert).tbsCertificate.subject.commonName}" (already present in chain).`
|
|
2583
|
-
};
|
|
2584
|
-
}
|
|
2585
|
-
result.push(issuerCert);
|
|
2586
|
-
depth++;
|
|
2587
|
-
}
|
|
2588
|
-
return {
|
|
2589
|
-
chain: result,
|
|
2590
|
-
status: "MaxDepthReached" /* MaxDepthReached */,
|
|
2591
|
-
message: `Chain completion stopped after ${maxDepth} iterations \u2014 possible circular chain or very deep hierarchy.`
|
|
2592
|
-
};
|
|
2593
|
-
}
|
|
2594
|
-
/**
|
|
2595
|
-
*
|
|
2596
|
-
* check if the certificate explicitly appear in the trust list, the reject list or none.
|
|
2597
|
-
* In case of being in the reject and trusted list at the same time is consider: rejected.
|
|
2598
|
-
* @internal
|
|
2599
|
-
* @private
|
|
2600
|
-
*/
|
|
2601
|
-
async #checkRejectedOrTrusted(certificate) {
|
|
2602
|
-
const firstCertificate = coerceCertificateChain(certificate)[0];
|
|
2603
|
-
const fingerprint = makeFingerprint(firstCertificate);
|
|
2604
|
-
debugLog("#checkRejectedOrTrusted fingerprint ", short(fingerprint));
|
|
2605
|
-
await this.#readCertificates();
|
|
2606
|
-
if (this.#thumbs.rejected.has(fingerprint)) {
|
|
2607
|
-
return "rejected";
|
|
2608
|
-
}
|
|
2609
|
-
if (this.#thumbs.trusted.has(fingerprint)) {
|
|
2610
|
-
return "trusted";
|
|
2611
|
-
}
|
|
2612
|
-
return "unknown";
|
|
2613
|
-
}
|
|
2614
|
-
async #moveCertificate(certificateOrChain, newStatus) {
|
|
2615
|
-
await this.withLock2(async () => {
|
|
2616
|
-
const chain = coerceCertificateChain(certificateOrChain);
|
|
2617
|
-
const certificate = chain[0];
|
|
2618
|
-
const fingerprint = makeFingerprint(certificate);
|
|
2619
|
-
let status = await this.#checkRejectedOrTrusted(certificate);
|
|
2620
|
-
if (status === "unknown") {
|
|
2621
|
-
const pem = toPem(chain, "CERTIFICATE");
|
|
2622
|
-
const filename = safeStoreJoin(this.rejectedFolder, `${buildIdealCertificateName(certificate)}.pem`);
|
|
2623
|
-
await fs7.promises.writeFile(filename, pem);
|
|
2624
|
-
this.#thumbs.rejected.set(fingerprint, { certificate, filename });
|
|
2625
|
-
status = "rejected";
|
|
2626
|
-
}
|
|
2627
|
-
debugLog("#moveCertificate", fingerprint.substring(0, 10), "from", status, "to", newStatus);
|
|
2628
|
-
if (status !== "rejected" && status !== "trusted") {
|
|
2629
|
-
throw new Error(`#moveCertificate: unexpected status '${status}' for certificate ${fingerprint.substring(0, 10)}`);
|
|
2630
|
-
}
|
|
2631
|
-
if (status !== newStatus) {
|
|
2632
|
-
const indexSrc = status === "rejected" ? this.#thumbs.rejected : this.#thumbs.trusted;
|
|
2633
|
-
const srcEntry = indexSrc.get(fingerprint);
|
|
2634
|
-
if (!srcEntry) {
|
|
2635
|
-
debugLog(" cannot find certificate ", fingerprint.substring(0, 10), " in", status);
|
|
2636
|
-
throw new Error(`#moveCertificate: certificate ${fingerprint.substring(0, 10)} not found in ${status} index`);
|
|
2637
|
-
}
|
|
2638
|
-
const destFolder = newStatus === "trusted" ? this.trustedFolder : this.rejectedFolder;
|
|
2639
|
-
const certificateDest = safeStoreJoin(destFolder, path4.basename(srcEntry.filename));
|
|
2640
|
-
debugLog("#moveCertificate", fingerprint.substring(0, 10), "old name", srcEntry.filename);
|
|
2641
|
-
debugLog("#moveCertificate", fingerprint.substring(0, 10), "new name", certificateDest);
|
|
2642
|
-
await fs7.promises.rename(srcEntry.filename, certificateDest);
|
|
2643
|
-
indexSrc.delete(fingerprint);
|
|
2644
|
-
const indexDest = newStatus === "trusted" ? this.#thumbs.trusted : this.#thumbs.rejected;
|
|
2645
|
-
indexDest.set(fingerprint, { certificate, filename: certificateDest });
|
|
2646
|
-
}
|
|
2647
|
-
});
|
|
2648
|
-
}
|
|
2649
|
-
#findAssociatedCRLs(issuerCertificate) {
|
|
2650
|
-
const issuerCertificateInfo = exploreCertificate(issuerCertificate);
|
|
2651
|
-
const key = issuerCertificateInfo.tbsCertificate.subjectFingerPrint;
|
|
2652
|
-
return this.#thumbs.issuersCrl.get(key) ?? this.#thumbs.crl.get(key) ?? null;
|
|
2653
|
-
}
|
|
2654
|
-
/**
|
|
2655
|
-
* Check whether a certificate has been revoked by its issuer's CRL.
|
|
2656
|
-
*
|
|
2657
|
-
* - Self-signed certificates are never considered revoked.
|
|
2658
|
-
* - If no `issuerCertificate` is provided, the method attempts
|
|
2659
|
-
* to find it via {@link findIssuerCertificate}.
|
|
2660
|
-
*
|
|
2661
|
-
* @param certificate - the DER-encoded certificate to check
|
|
2662
|
-
* @param issuerCertificate - optional issuer certificate; looked
|
|
2663
|
-
* up automatically when omitted
|
|
2664
|
-
* @returns `Good` if not revoked, `BadCertificateRevoked` if the
|
|
2665
|
-
* serial number appears in a CRL,
|
|
2666
|
-
* `BadCertificateRevocationUnknown` if no CRL is available,
|
|
2667
|
-
* or `BadCertificateChainIncomplete` if the issuer cannot be
|
|
2668
|
-
* found.
|
|
2669
|
-
*/
|
|
2670
|
-
async isCertificateRevoked(certificate, issuerCertificate) {
|
|
2671
|
-
const chain = coerceCertificateChain(certificate);
|
|
2672
|
-
const firstCertificate = chain[0];
|
|
2673
|
-
if (isSelfSigned3(firstCertificate)) {
|
|
2674
|
-
return "Good" /* Good */;
|
|
2675
|
-
}
|
|
2676
|
-
if (!issuerCertificate) {
|
|
2677
|
-
issuerCertificate = await this.findIssuerCertificate(firstCertificate);
|
|
2678
|
-
}
|
|
2679
|
-
if (!issuerCertificate) {
|
|
2680
|
-
issuerCertificate = findIssuerCertificateInChain(firstCertificate, chain);
|
|
2681
|
-
}
|
|
2682
|
-
if (!issuerCertificate) {
|
|
2683
|
-
return "BadCertificateChainIncomplete" /* BadCertificateChainIncomplete */;
|
|
2684
|
-
}
|
|
2685
|
-
const crls = this.#findAssociatedCRLs(issuerCertificate);
|
|
2686
|
-
if (!crls) {
|
|
2687
|
-
return "BadCertificateRevocationUnknown" /* BadCertificateRevocationUnknown */;
|
|
2688
|
-
}
|
|
2689
|
-
const certInfo = exploreCertificate(firstCertificate);
|
|
2690
|
-
const serialNumber = certInfo.tbsCertificate.serialNumber || certInfo.tbsCertificate.extensions?.authorityKeyIdentifier?.serial || "";
|
|
2691
|
-
const key = certInfo.tbsCertificate.extensions?.authorityKeyIdentifier?.authorityCertIssuerFingerPrint || "<unknown>";
|
|
2692
|
-
const crl2 = this.#thumbs.crl.get(key) ?? null;
|
|
2693
|
-
if (crls.serialNumbers[serialNumber] || crl2?.serialNumbers[serialNumber]) {
|
|
2694
|
-
return "BadCertificateRevoked" /* BadCertificateRevoked */;
|
|
2695
|
-
}
|
|
2696
|
-
return "Good" /* Good */;
|
|
2697
|
-
}
|
|
2698
|
-
#pendingCrlToProcess = 0;
|
|
2699
|
-
#onCrlProcessWaiters = [];
|
|
2700
|
-
#queue = [];
|
|
2701
|
-
#onCrlFileAdded(index, filename) {
|
|
2702
|
-
this.#queue.push({ index, filename });
|
|
2703
|
-
this.#pendingCrlToProcess += 1;
|
|
2704
|
-
if (this.#pendingCrlToProcess === 1) {
|
|
2705
|
-
this.#processNextCrl();
|
|
2706
|
-
}
|
|
2707
|
-
}
|
|
2708
|
-
async #processNextCrl() {
|
|
2709
|
-
try {
|
|
2710
|
-
const nextCRL = this.#queue.shift();
|
|
2711
|
-
if (!nextCRL) return;
|
|
2712
|
-
const { index, filename } = nextCRL;
|
|
2713
|
-
const crl = await readCertificateRevocationList(filename);
|
|
2714
|
-
const crlInfo = exploreCertificateRevocationList(crl);
|
|
2715
|
-
debugLog(chalk3.cyan("add CRL in folder "), filename);
|
|
2716
|
-
const fingerprint = crlInfo.tbsCertList.issuerFingerprint;
|
|
2717
|
-
if (!index.has(fingerprint)) {
|
|
2718
|
-
index.set(fingerprint, { crls: [], serialNumbers: {} });
|
|
2719
|
-
}
|
|
2720
|
-
const data = index.get(fingerprint) || { crls: [], serialNumbers: {} };
|
|
2721
|
-
data.crls.push({ crlInfo, filename });
|
|
2722
|
-
for (const revokedCertificate of crlInfo.tbsCertList.revokedCertificates) {
|
|
2723
|
-
const serialNumber = revokedCertificate.userCertificate;
|
|
2724
|
-
if (!data.serialNumbers[serialNumber]) {
|
|
2725
|
-
data.serialNumbers[serialNumber] = revokedCertificate.revocationDate;
|
|
2726
|
-
}
|
|
2727
|
-
}
|
|
2728
|
-
debugLog(chalk3.cyan("CRL"), fingerprint, "serial numbers = ", Object.keys(data.serialNumbers));
|
|
2729
|
-
} catch (err) {
|
|
2730
|
-
debugLog("CRL filename error =");
|
|
2731
|
-
debugLog(err);
|
|
2732
|
-
}
|
|
2733
|
-
this.#pendingCrlToProcess -= 1;
|
|
2734
|
-
if (this.#pendingCrlToProcess === 0) {
|
|
2735
|
-
for (const waiter of this.#onCrlProcessWaiters) {
|
|
2736
|
-
waiter();
|
|
2737
|
-
}
|
|
2738
|
-
this.#onCrlProcessWaiters.length = 0;
|
|
2739
|
-
} else {
|
|
2740
|
-
this.#processNextCrl();
|
|
2741
|
-
}
|
|
2742
|
-
}
|
|
2743
|
-
async #readCertificates() {
|
|
2744
|
-
if (this.#readCertificatesCalled) {
|
|
2745
|
-
return;
|
|
2746
|
-
}
|
|
2747
|
-
this.#readCertificatesCalled = true;
|
|
2748
|
-
const usePolling = process.env.OPCUA_PKI_USE_POLLING === "true";
|
|
2749
|
-
const envInterval = process.env.OPCUA_PKI_POLLING_INTERVAL ? parseInt(process.env.OPCUA_PKI_POLLING_INTERVAL, 10) : void 0;
|
|
2750
|
-
const pollingInterval = Math.min(10 * 60 * 1e3, Math.max(100, envInterval ?? this.folderPollingInterval));
|
|
2751
|
-
const chokidarOptions = {
|
|
2752
|
-
usePolling,
|
|
2753
|
-
...usePolling ? { interval: pollingInterval } : {},
|
|
2754
|
-
depth: 0,
|
|
2755
|
-
persistent: false
|
|
2756
|
-
};
|
|
2757
|
-
const allCapturedHandles = [];
|
|
2758
|
-
const origWatch = fs7.watch;
|
|
2759
|
-
let watcherReadyCount = 0;
|
|
2760
|
-
const totalWatchers = 5;
|
|
2761
|
-
fs7.watch = ((...args) => {
|
|
2762
|
-
const handle = origWatch.apply(fs7, args);
|
|
2763
|
-
handle.setMaxListeners(handle.getMaxListeners() + 1);
|
|
2764
|
-
handle.on("error", () => {
|
|
2765
|
-
});
|
|
2766
|
-
allCapturedHandles.push(handle);
|
|
2767
|
-
return handle;
|
|
2768
|
-
});
|
|
2769
|
-
const createUnreffedWatcher = (folder) => {
|
|
2770
|
-
const startIdx = allCapturedHandles.length;
|
|
2771
|
-
const w = chokidar.watch(folder, chokidarOptions);
|
|
2772
|
-
const unreffAll = () => {
|
|
2773
|
-
for (let i = startIdx; i < allCapturedHandles.length; i++) {
|
|
2774
|
-
allCapturedHandles[i].unref();
|
|
2775
|
-
}
|
|
2776
|
-
watcherReadyCount++;
|
|
2777
|
-
if (watcherReadyCount >= totalWatchers) {
|
|
2778
|
-
fs7.watch = origWatch;
|
|
2779
|
-
}
|
|
2780
|
-
};
|
|
2781
|
-
return { w, capturedHandles: allCapturedHandles.slice(startIdx), unreffAll };
|
|
2782
|
-
};
|
|
2783
|
-
await Promise.all([
|
|
2784
|
-
this.#scanCertFolder(this.trustedFolder, this.#thumbs.trusted),
|
|
2785
|
-
this.#scanCertFolder(this.issuersCertFolder, this.#thumbs.issuers.certs),
|
|
2786
|
-
this.#scanCertFolder(this.rejectedFolder, this.#thumbs.rejected),
|
|
2787
|
-
this.#scanCrlFolder(this.crlFolder, this.#thumbs.crl),
|
|
2788
|
-
this.#scanCrlFolder(this.issuersCrlFolder, this.#thumbs.issuersCrl)
|
|
2789
|
-
]);
|
|
2790
|
-
if (this.#disableFileWatchers) {
|
|
2791
|
-
fs7.watch = origWatch;
|
|
2792
|
-
} else {
|
|
2793
|
-
this.#startWatcher(this.trustedFolder, this.#thumbs.trusted, createUnreffedWatcher, "trusted");
|
|
2794
|
-
this.#startWatcher(this.issuersCertFolder, this.#thumbs.issuers.certs, createUnreffedWatcher, "issuersCerts");
|
|
2795
|
-
this.#startWatcher(this.rejectedFolder, this.#thumbs.rejected, createUnreffedWatcher, "rejected");
|
|
2796
|
-
this.#startCrlWatcher(this.crlFolder, this.#thumbs.crl, createUnreffedWatcher, "crl");
|
|
2797
|
-
this.#startCrlWatcher(this.issuersCrlFolder, this.#thumbs.issuersCrl, createUnreffedWatcher, "issuersCrl");
|
|
2798
|
-
}
|
|
2799
|
-
}
|
|
2800
|
-
/**
|
|
2801
|
-
* Scan a certificate folder and populate the in-memory index.
|
|
2802
|
-
* Uses async readdir/stat to yield the event loop between
|
|
2803
|
-
* file reads, preventing main-loop stalls with large folders.
|
|
2804
|
-
*/
|
|
2805
|
-
async #scanCertFolder(folder, index) {
|
|
2806
|
-
if (!fs7.existsSync(folder)) return;
|
|
2807
|
-
const files = await fs7.promises.readdir(folder);
|
|
2808
|
-
for (const file of files) {
|
|
2809
|
-
const filename = path4.join(folder, file);
|
|
2810
|
-
try {
|
|
2811
|
-
const stat = await fs7.promises.stat(filename);
|
|
2812
|
-
if (!stat.isFile()) continue;
|
|
2813
|
-
const certs = await readCertificateChainAsync(filename);
|
|
2814
|
-
if (certs.length === 0) continue;
|
|
2815
|
-
const certificate = certs[0];
|
|
2816
|
-
if (certs.length > 1) {
|
|
2817
|
-
try {
|
|
2818
|
-
await fs7.promises.writeFile(filename, toPem(certs, "CERTIFICATE"), "ascii");
|
|
2819
|
-
} catch (writeErr) {
|
|
2820
|
-
debugLog(`scanCertFolder: could not rewrite legacy PEM ${filename} (read-only fs?)`, writeErr);
|
|
2821
|
-
}
|
|
2822
|
-
for (let i = 1; i < certs.length; i++) {
|
|
2823
|
-
if (isIssuer(certs[i])) {
|
|
2824
|
-
try {
|
|
2825
|
-
await this.addIssuer(certs[i]);
|
|
2826
|
-
} catch (issuerErr) {
|
|
2827
|
-
debugLog(`scanCertFolder: could not auto-register issuer from ${filename}`, issuerErr);
|
|
2828
|
-
}
|
|
2829
|
-
}
|
|
2830
|
-
}
|
|
2831
|
-
}
|
|
2832
|
-
const info = exploreCertificate(certificate);
|
|
2833
|
-
const fingerprint = makeFingerprint(certificate);
|
|
2834
|
-
index.set(fingerprint, { certificate, filename, info });
|
|
2835
|
-
this.#filenameToHash.set(filename, fingerprint);
|
|
2836
|
-
} catch (err) {
|
|
2837
|
-
debugLog(`scanCertFolder: skipping ${filename}`, err);
|
|
2838
|
-
}
|
|
2839
|
-
}
|
|
2840
|
-
}
|
|
2841
|
-
/**
|
|
2842
|
-
* Scan a CRL folder and populate the in-memory CRL index.
|
|
2843
|
-
*/
|
|
2844
|
-
async #scanCrlFolder(folder, index) {
|
|
2845
|
-
if (!fs7.existsSync(folder)) return;
|
|
2846
|
-
const files = await fs7.promises.readdir(folder);
|
|
2847
|
-
for (const file of files) {
|
|
2848
|
-
const filename = path4.join(folder, file);
|
|
2849
|
-
try {
|
|
2850
|
-
const stat = await fs7.promises.stat(filename);
|
|
2851
|
-
if (!stat.isFile()) continue;
|
|
2852
|
-
this.#onCrlFileAdded(index, filename);
|
|
2853
|
-
} catch (err) {
|
|
2854
|
-
debugLog(`scanCrlFolder: skipping ${filename}`, err);
|
|
2855
|
-
}
|
|
2856
|
-
}
|
|
2857
|
-
await this.#waitAndCheckCRLProcessingStatus();
|
|
2858
|
-
}
|
|
2859
|
-
/**
|
|
2860
|
-
* Start a chokidar watcher for a CRL folder.
|
|
2861
|
-
* Non-blocking — does NOT await "ready".
|
|
2862
|
-
*/
|
|
2863
|
-
#startCrlWatcher(folder, index, createUnreffedWatcher, store) {
|
|
2864
|
-
const { w, unreffAll } = createUnreffedWatcher(folder);
|
|
2865
|
-
w.on("error", (err) => {
|
|
2866
|
-
debugLog(`chokidar CRL watcher error on ${folder}:`, err);
|
|
2867
|
-
});
|
|
2868
|
-
let ready = false;
|
|
2869
|
-
w.on("unlink", (filename) => {
|
|
2870
|
-
for (const [key, data] of index.entries()) {
|
|
2871
|
-
data.crls = data.crls.filter((c) => c.filename !== filename);
|
|
2872
|
-
if (data.crls.length === 0) {
|
|
2873
|
-
index.delete(key);
|
|
2874
|
-
}
|
|
2875
|
-
}
|
|
2876
|
-
if (ready) {
|
|
2877
|
-
this.emit("crlRemoved", { store, filename });
|
|
2878
|
-
}
|
|
2879
|
-
});
|
|
2880
|
-
w.on("add", (filename) => {
|
|
2881
|
-
if (ready) {
|
|
2882
|
-
this.#onCrlFileAdded(index, filename);
|
|
2883
|
-
this.emit("crlAdded", { store, filename });
|
|
2884
|
-
}
|
|
2885
|
-
});
|
|
2886
|
-
w.on("change", (changedPath) => {
|
|
2887
|
-
debugLog("change in folder ", folder, changedPath);
|
|
2888
|
-
});
|
|
2889
|
-
this.#watchers.push(w);
|
|
2890
|
-
this.#pendingUnrefs.add(unreffAll);
|
|
2891
|
-
w.on("ready", () => {
|
|
2892
|
-
ready = true;
|
|
2893
|
-
this.#pendingUnrefs.delete(unreffAll);
|
|
2894
|
-
unreffAll();
|
|
2895
|
-
});
|
|
2896
|
-
}
|
|
2897
|
-
/**
|
|
2898
|
-
* Start a chokidar watcher for a certificate folder.
|
|
2899
|
-
* Non-blocking — does NOT await "ready".
|
|
2900
|
-
*/
|
|
2901
|
-
#startWatcher(folder, index, createUnreffedWatcher, store) {
|
|
2902
|
-
const { w, unreffAll } = createUnreffedWatcher(folder);
|
|
2903
|
-
w.on("error", (err) => {
|
|
2904
|
-
debugLog(`chokidar cert watcher error on ${folder}:`, err);
|
|
2905
|
-
});
|
|
2906
|
-
let ready = false;
|
|
2907
|
-
w.on("unlink", (filename) => {
|
|
2908
|
-
debugLog(chalk3.cyan(`unlink in folder ${folder}`), filename);
|
|
2909
|
-
const h = this.#filenameToHash.get(filename);
|
|
2910
|
-
if (h && index.has(h)) {
|
|
2911
|
-
index.delete(h);
|
|
2912
|
-
this.emit("certificateRemoved", { store, fingerprint: h, filename });
|
|
2913
|
-
}
|
|
2914
|
-
});
|
|
2915
|
-
w.on("add", (filename) => {
|
|
2916
|
-
debugLog(chalk3.cyan(`add in folder ${folder}`), filename);
|
|
2917
|
-
try {
|
|
2918
|
-
const certificate = readCertificateChain(filename)[0];
|
|
2919
|
-
const info = exploreCertificate(certificate);
|
|
2920
|
-
const fingerprint = makeFingerprint(certificate);
|
|
2921
|
-
const isNew = !index.has(fingerprint);
|
|
2922
|
-
index.set(fingerprint, { certificate, filename, info });
|
|
2923
|
-
this.#filenameToHash.set(filename, fingerprint);
|
|
2924
|
-
debugLog(
|
|
2925
|
-
chalk3.magenta("CERT"),
|
|
2926
|
-
info.tbsCertificate.subjectFingerPrint,
|
|
2927
|
-
info.tbsCertificate.serialNumber,
|
|
2928
|
-
info.tbsCertificate.extensions?.authorityKeyIdentifier?.authorityCertIssuerFingerPrint
|
|
2929
|
-
);
|
|
2930
|
-
if (ready || isNew) {
|
|
2931
|
-
this.emit("certificateAdded", { store, certificate, fingerprint, filename });
|
|
2932
|
-
}
|
|
2933
|
-
} catch (err) {
|
|
2934
|
-
debugLog(`Walk files in folder ${folder} with file ${filename}`);
|
|
2935
|
-
debugLog(err);
|
|
2936
|
-
}
|
|
2937
|
-
});
|
|
2938
|
-
w.on("change", (changedPath) => {
|
|
2939
|
-
debugLog(chalk3.cyan(`change in folder ${folder}`), changedPath);
|
|
2940
|
-
try {
|
|
2941
|
-
const certificate = readCertificateChain(changedPath)[0];
|
|
2942
|
-
const newFingerprint = makeFingerprint(certificate);
|
|
2943
|
-
const oldHash = this.#filenameToHash.get(changedPath);
|
|
2944
|
-
if (oldHash && oldHash !== newFingerprint) {
|
|
2945
|
-
index.delete(oldHash);
|
|
2946
|
-
}
|
|
2947
|
-
index.set(newFingerprint, { certificate, filename: changedPath, info: exploreCertificate(certificate) });
|
|
2948
|
-
this.#filenameToHash.set(changedPath, newFingerprint);
|
|
2949
|
-
this.emit("certificateChange", { store, certificate, fingerprint: newFingerprint, filename: changedPath });
|
|
2950
|
-
} catch (err) {
|
|
2951
|
-
debugLog(`change event: failed to re-read ${changedPath}`, err);
|
|
2952
|
-
}
|
|
2953
|
-
});
|
|
2954
|
-
this.#watchers.push(w);
|
|
2955
|
-
this.#pendingUnrefs.add(unreffAll);
|
|
2956
|
-
w.on("ready", () => {
|
|
2957
|
-
ready = true;
|
|
2958
|
-
this.#pendingUnrefs.delete(unreffAll);
|
|
2959
|
-
unreffAll();
|
|
2960
|
-
debugLog("ready");
|
|
2961
|
-
debugLog([...index.keys()].map((k) => k.substring(0, 10)));
|
|
2962
|
-
});
|
|
2963
|
-
}
|
|
2964
|
-
// make sure that all crls have been processed.
|
|
2965
|
-
async #waitAndCheckCRLProcessingStatus() {
|
|
2966
|
-
return new Promise((resolve, _reject) => {
|
|
2967
|
-
if (this.#pendingCrlToProcess === 0) {
|
|
2968
|
-
setImmediate(resolve);
|
|
2969
|
-
return;
|
|
2970
|
-
}
|
|
2971
|
-
this.#onCrlProcessWaiters.push(resolve);
|
|
2972
|
-
});
|
|
2973
|
-
}
|
|
2974
|
-
};
|
|
2975
|
-
|
|
2976
|
-
// packages/node-opcua-pki/lib/pki/toolbox_pfx.ts
|
|
2977
|
-
import assert5 from "assert";
|
|
2978
|
-
import fs8 from "fs";
|
|
2979
|
-
import {
|
|
2980
|
-
certificateMatchesPrivateKey,
|
|
2981
|
-
certificatesToPem,
|
|
2982
|
-
coercePrivateKeyPem as coercePrivateKeyPem3,
|
|
2983
|
-
createPfx,
|
|
2984
|
-
makeSHA1Thumbprint as makeSHA1Thumbprint2,
|
|
2985
|
-
Pkcs12UnsupportedAlgorithmError,
|
|
2986
|
-
parsePfx,
|
|
2987
|
-
readCertificateChain as readCertificateChain2,
|
|
2988
|
-
readPrivateKey as readPrivateKey2,
|
|
2989
|
-
toPem as toPem2,
|
|
2990
|
-
x509
|
|
2991
|
-
} from "node-opcua-crypto";
|
|
2992
|
-
async function readPfxFile(options) {
|
|
2993
|
-
const { pfxFile, passphrase = "" } = options;
|
|
2994
|
-
assert5(fs8.existsSync(pfxFile), `PFX file does not exist: ${pfxFile}`);
|
|
2995
|
-
try {
|
|
2996
|
-
return await parsePfx(await fs8.promises.readFile(pfxFile), passphrase);
|
|
2997
|
-
} catch (err) {
|
|
2998
|
-
if (err instanceof Pkcs12UnsupportedAlgorithmError) {
|
|
2999
|
-
const explained = new Error(
|
|
3000
|
-
[
|
|
3001
|
-
`${pfxFile} uses an encryption algorithm that is not supported (${err.message}).`,
|
|
3002
|
-
"Bundles written by openssl 1.x default to 40-bit RC2, which openssl 3 itself",
|
|
3003
|
-
"rejects unless given -legacy. Convert the file once with:",
|
|
3004
|
-
" openssl pkcs12 -legacy -in old.pfx -nodes -out tmp.pem",
|
|
3005
|
-
" openssl pkcs12 -export -in tmp.pem -out new.pfx"
|
|
3006
|
-
].join("\n")
|
|
3007
|
-
);
|
|
3008
|
-
explained.cause = err;
|
|
3009
|
-
throw explained;
|
|
3010
|
-
}
|
|
3011
|
-
throw err;
|
|
3012
|
-
}
|
|
3013
|
-
}
|
|
3014
|
-
async function createPFX(options) {
|
|
3015
|
-
const { certificateFile, privateKeyFile, privateKeyPassphrase, outputFile, passphrase = "", caCertificateFiles } = options;
|
|
3016
|
-
assert5(fs8.existsSync(certificateFile), `Certificate file does not exist: ${certificateFile}`);
|
|
3017
|
-
assert5(fs8.existsSync(privateKeyFile), `Private key file does not exist: ${privateKeyFile}`);
|
|
3018
|
-
const [certificate, ...bundledIssuers] = readCertificateChain2(certificateFile);
|
|
3019
|
-
if (!certificate) {
|
|
3020
|
-
throw new Error(`Certificate file holds no certificate: ${certificateFile}`);
|
|
3021
|
-
}
|
|
3022
|
-
const certificateChain = [...bundledIssuers];
|
|
3023
|
-
for (const caFile of caCertificateFiles ?? []) {
|
|
3024
|
-
assert5(fs8.existsSync(caFile), `CA certificate file does not exist: ${caFile}`);
|
|
3025
|
-
certificateChain.push(...readCertificateChain2(caFile));
|
|
3026
|
-
}
|
|
3027
|
-
const privateKey = readPrivateKey2(privateKeyFile, privateKeyPassphrase);
|
|
3028
|
-
if (!certificateMatchesPrivateKey(certificate, privateKey)) {
|
|
3029
|
-
throw new Error(`The private key ${privateKeyFile} does not match the certificate ${certificateFile}`);
|
|
3030
|
-
}
|
|
3031
|
-
const pfx = await createPfx({
|
|
3032
|
-
certificate,
|
|
3033
|
-
certificateChain,
|
|
3034
|
-
privateKey,
|
|
3035
|
-
password: passphrase,
|
|
3036
|
-
friendlyName: options.friendlyName
|
|
3037
|
-
});
|
|
3038
|
-
await fs8.promises.writeFile(outputFile, pfx);
|
|
3039
|
-
}
|
|
3040
|
-
async function extractCertificateFromPFX(options) {
|
|
3041
|
-
const { certificate } = await readPfxFile(options);
|
|
3042
|
-
return toPem2(certificate, "CERTIFICATE");
|
|
3043
|
-
}
|
|
3044
|
-
async function extractPrivateKeyFromPFX(options) {
|
|
3045
|
-
const { privateKey } = await readPfxFile(options);
|
|
3046
|
-
return coercePrivateKeyPem3(privateKey);
|
|
3047
|
-
}
|
|
3048
|
-
async function extractCACertificatesFromPFX(options) {
|
|
3049
|
-
const { certificateChain } = await readPfxFile(options);
|
|
3050
|
-
return certificateChain.length === 0 ? "" : certificatesToPem(certificateChain);
|
|
3051
|
-
}
|
|
3052
|
-
async function extractAllFromPFX(options) {
|
|
3053
|
-
const { certificate, certificateChain, privateKey, friendlyName } = await readPfxFile(options);
|
|
3054
|
-
return {
|
|
3055
|
-
certificate: toPem2(certificate, "CERTIFICATE"),
|
|
3056
|
-
privateKey: coercePrivateKeyPem3(privateKey),
|
|
3057
|
-
caCertificates: certificateChain.length === 0 ? "" : certificatesToPem(certificateChain),
|
|
3058
|
-
friendlyName
|
|
3059
|
-
};
|
|
3060
|
-
}
|
|
3061
|
-
async function convertPFXtoPEM(pfxFile, pemFile, passphrase = "") {
|
|
3062
|
-
const { certificate, certificateChain, privateKey } = await readPfxFile({ pfxFile, passphrase });
|
|
3063
|
-
const parts = [coercePrivateKeyPem3(privateKey), toPem2(certificate, "CERTIFICATE")];
|
|
3064
|
-
if (certificateChain.length > 0) {
|
|
3065
|
-
parts.push(certificatesToPem(certificateChain));
|
|
3066
|
-
}
|
|
3067
|
-
await fs8.promises.writeFile(pemFile, `${parts.map((part) => part.trimEnd()).join("\n")}
|
|
3068
|
-
`, "utf-8");
|
|
3069
|
-
}
|
|
3070
|
-
function formatThumbprint(certificate) {
|
|
3071
|
-
return (makeSHA1Thumbprint2(certificate).toString("hex").toUpperCase().match(/../g) ?? []).join(":");
|
|
3072
|
-
}
|
|
3073
|
-
function describePublicKey(certificate) {
|
|
3074
|
-
const algorithm = certificate.publicKey.algorithm;
|
|
3075
|
-
if (algorithm.modulusLength) {
|
|
3076
|
-
return `${algorithm.name} ${algorithm.modulusLength} bits`;
|
|
3077
|
-
}
|
|
3078
|
-
if (algorithm.namedCurve) {
|
|
3079
|
-
return `${algorithm.name} ${algorithm.namedCurve}`;
|
|
3080
|
-
}
|
|
3081
|
-
return algorithm.name;
|
|
3082
|
-
}
|
|
3083
|
-
function describeCertificate(der, indent) {
|
|
3084
|
-
const certificate = new x509.X509Certificate(der);
|
|
3085
|
-
return [
|
|
3086
|
-
`${indent}subject : ${certificate.subject}`,
|
|
3087
|
-
`${indent}issuer : ${certificate.issuer}`,
|
|
3088
|
-
`${indent}serial number : ${certificate.serialNumber}`,
|
|
3089
|
-
`${indent}not before : ${certificate.notBefore.toISOString()}`,
|
|
3090
|
-
`${indent}not after : ${certificate.notAfter.toISOString()}`,
|
|
3091
|
-
`${indent}public key : ${describePublicKey(certificate)}`,
|
|
3092
|
-
`${indent}signature : ${certificate.signatureAlgorithm.name}`,
|
|
3093
|
-
`${indent}SHA-1 thumbprint : ${formatThumbprint(der)}`
|
|
3094
|
-
];
|
|
3095
|
-
}
|
|
3096
|
-
async function dumpPFX(pfxFile, passphrase = "") {
|
|
3097
|
-
const { certificate, certificateChain, privateKey, friendlyName } = await readPfxFile({ pfxFile, passphrase });
|
|
3098
|
-
const lines = [`PFX bundle : ${pfxFile}`];
|
|
3099
|
-
if (friendlyName) {
|
|
3100
|
-
lines.push(`friendly name : ${friendlyName}`);
|
|
3101
|
-
}
|
|
3102
|
-
const keyVerdict = certificateMatchesPrivateKey(certificate, privateKey) ? "matches this certificate" : "DOES NOT match this certificate";
|
|
3103
|
-
lines.push("", "certificate", ...describeCertificate(certificate, " "), ` private key : ${keyVerdict}`);
|
|
3104
|
-
lines.push("", `issuer chain : ${certificateChain.length} certificate(s)`);
|
|
3105
|
-
for (const [index, issuer] of certificateChain.entries()) {
|
|
3106
|
-
lines.push(` [${index}]`, ...describeCertificate(issuer, " "));
|
|
3107
|
-
}
|
|
3108
|
-
return `${lines.join("\n")}
|
|
3109
|
-
`;
|
|
3110
|
-
}
|
|
3111
|
-
export {
|
|
3112
|
-
CertificateAuthority,
|
|
3113
|
-
CertificateAuthorityCore,
|
|
3114
|
-
CertificateManager,
|
|
3115
|
-
CertificateManagerState,
|
|
3116
|
-
ChainCompletionStatus,
|
|
3117
|
-
NativeCaBackend,
|
|
3118
|
-
OpenSslCaBackend,
|
|
3119
|
-
PrivateKeyPassphraseRequiredError,
|
|
3120
|
-
PrivateKeyUnavailableError2 as PrivateKeyUnavailableError,
|
|
3121
|
-
Subject,
|
|
3122
|
-
VerificationStatus,
|
|
3123
|
-
adjustApplicationUri,
|
|
3124
|
-
adjustDate,
|
|
3125
|
-
coerceCertificateChain,
|
|
3126
|
-
convertPFXtoPEM,
|
|
3127
|
-
createPFX,
|
|
3128
|
-
dumpPFX,
|
|
3129
|
-
extractAllFromPFX,
|
|
3130
|
-
extractCACertificatesFromPFX,
|
|
3131
|
-
extractCertificateFromPFX,
|
|
3132
|
-
extractPrivateKeyFromPFX,
|
|
3133
|
-
findIssuerCertificateInChain,
|
|
3134
|
-
install_prerequisite,
|
|
3135
|
-
isIntermediateIssuer,
|
|
3136
|
-
isIssuer,
|
|
3137
|
-
isOpaqueSigner,
|
|
3138
|
-
isRootIssuer,
|
|
3139
|
-
makeFingerprint,
|
|
3140
|
-
quote,
|
|
3141
|
-
resolvePrivateKeyPassphrase,
|
|
3142
|
-
safeStoreJoin,
|
|
3143
|
-
toFilenameLabel
|
|
3144
|
-
};
|
|
3145
|
-
//# sourceMappingURL=index.mjs.map
|