node-opcua-pki 6.3.0 → 6.5.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/pki.ts +1 -1
- package/dist/bin/install_prerequisite.mjs +1 -1
- package/dist/bin/pki.mjs +3425 -3
- package/dist/bin/pki.mjs.map +1 -1
- package/dist/{chunk-VXGTT7QM.mjs → chunk-GCHH54PS.mjs} +9 -1
- package/dist/{chunk-VXGTT7QM.mjs.map → chunk-GCHH54PS.mjs.map} +1 -1
- package/dist/index.d.mts +507 -68
- package/dist/index.d.ts +507 -68
- package/dist/index.js +662 -1244
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +713 -1296
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/readme.md +23 -7
package/dist/bin/pki.mjs
CHANGED
|
@@ -1,15 +1,3437 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
__commonJS,
|
|
4
|
+
__dirname,
|
|
5
|
+
__esm,
|
|
6
|
+
__filename,
|
|
4
7
|
init_esm_shims
|
|
5
|
-
} from "../chunk-
|
|
8
|
+
} from "../chunk-GCHH54PS.mjs";
|
|
9
|
+
|
|
10
|
+
// packages/node-opcua-pki/lib/misc/applicationurn.ts
|
|
11
|
+
import assert from "assert";
|
|
12
|
+
import { createHash } from "crypto";
|
|
13
|
+
function makeApplicationUrn(hostname, suffix) {
|
|
14
|
+
let hostnameHash = hostname;
|
|
15
|
+
if (hostnameHash.length + 7 + suffix.length >= 64) {
|
|
16
|
+
hostnameHash = createHash("md5").update(hostname).digest("hex").substring(0, 16);
|
|
17
|
+
}
|
|
18
|
+
const applicationUrn = `urn:${hostnameHash}:${suffix}`;
|
|
19
|
+
assert(applicationUrn.length <= 64);
|
|
20
|
+
return applicationUrn;
|
|
21
|
+
}
|
|
22
|
+
var init_applicationurn = __esm({
|
|
23
|
+
"packages/node-opcua-pki/lib/misc/applicationurn.ts"() {
|
|
24
|
+
"use strict";
|
|
25
|
+
init_esm_shims();
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// packages/node-opcua-pki/lib/misc/hostname.ts
|
|
30
|
+
import dns from "dns";
|
|
31
|
+
import os from "os";
|
|
32
|
+
import { promisify } from "util";
|
|
33
|
+
function trim(str, length) {
|
|
34
|
+
if (!length) {
|
|
35
|
+
return str;
|
|
36
|
+
}
|
|
37
|
+
return str.substring(0, Math.min(str.length, length));
|
|
38
|
+
}
|
|
39
|
+
function fqdn(callback) {
|
|
40
|
+
const uqdn = os.hostname();
|
|
41
|
+
dns.lookup(uqdn, { hints: dns.ADDRCONFIG }, (err1, ip) => {
|
|
42
|
+
if (err1) {
|
|
43
|
+
return callback(err1);
|
|
44
|
+
}
|
|
45
|
+
dns.lookupService(ip, 0, (err2, _fqdn) => {
|
|
46
|
+
if (err2) {
|
|
47
|
+
return callback(err2);
|
|
48
|
+
}
|
|
49
|
+
_fqdn = _fqdn.replace(".localdomain", "");
|
|
50
|
+
callback(null, _fqdn);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async function extractFullyQualifiedDomainName() {
|
|
55
|
+
if (_fullyQualifiedDomainNameInCache) {
|
|
56
|
+
return _fullyQualifiedDomainNameInCache;
|
|
57
|
+
}
|
|
58
|
+
if (process.platform === "win32") {
|
|
59
|
+
const env = process.env;
|
|
60
|
+
_fullyQualifiedDomainNameInCache = env.COMPUTERNAME + (env.USERDNSDOMAIN && env.USERDNSDOMAIN?.length > 0 ? `.${env.USERDNSDOMAIN}` : "");
|
|
61
|
+
} else {
|
|
62
|
+
try {
|
|
63
|
+
_fullyQualifiedDomainNameInCache = await promisify(fqdn)();
|
|
64
|
+
if (_fullyQualifiedDomainNameInCache === "localhost") {
|
|
65
|
+
throw new Error("localhost not expected");
|
|
66
|
+
}
|
|
67
|
+
if (/sethostname/.test(_fullyQualifiedDomainNameInCache)) {
|
|
68
|
+
throw new Error("Detecting fqdn on windows !!!");
|
|
69
|
+
}
|
|
70
|
+
} catch (_err) {
|
|
71
|
+
_fullyQualifiedDomainNameInCache = os.hostname();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return _fullyQualifiedDomainNameInCache;
|
|
75
|
+
}
|
|
76
|
+
async function prepareFQDN() {
|
|
77
|
+
_fullyQualifiedDomainNameInCache = await extractFullyQualifiedDomainName();
|
|
78
|
+
}
|
|
79
|
+
function getFullyQualifiedDomainName(optional_max_length) {
|
|
80
|
+
if (!_fullyQualifiedDomainNameInCache) {
|
|
81
|
+
throw new Error("FullyQualifiedDomainName computation is not completed yet");
|
|
82
|
+
}
|
|
83
|
+
return _fullyQualifiedDomainNameInCache ? trim(_fullyQualifiedDomainNameInCache, optional_max_length) : "%FQDN%";
|
|
84
|
+
}
|
|
85
|
+
var _fullyQualifiedDomainNameInCache;
|
|
86
|
+
var init_hostname = __esm({
|
|
87
|
+
"packages/node-opcua-pki/lib/misc/hostname.ts"() {
|
|
88
|
+
"use strict";
|
|
89
|
+
init_esm_shims();
|
|
90
|
+
prepareFQDN();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// packages/node-opcua-pki/lib/toolbox/config.ts
|
|
95
|
+
var g_config;
|
|
96
|
+
var init_config = __esm({
|
|
97
|
+
"packages/node-opcua-pki/lib/toolbox/config.ts"() {
|
|
98
|
+
"use strict";
|
|
99
|
+
init_esm_shims();
|
|
100
|
+
g_config = {
|
|
101
|
+
opensslVersion: "unset",
|
|
102
|
+
silent: process.env.VERBOSE ? !process.env.VERBOSE : true,
|
|
103
|
+
force: false
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// packages/node-opcua-pki/lib/toolbox/debug.ts
|
|
109
|
+
function debugLog(...args) {
|
|
110
|
+
if (displayDebug) {
|
|
111
|
+
console.log.apply(null, args);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function warningLog(...args) {
|
|
115
|
+
console.log.apply(null, args);
|
|
116
|
+
}
|
|
117
|
+
var doDebug, displayError, displayDebug;
|
|
118
|
+
var init_debug = __esm({
|
|
119
|
+
"packages/node-opcua-pki/lib/toolbox/debug.ts"() {
|
|
120
|
+
"use strict";
|
|
121
|
+
init_esm_shims();
|
|
122
|
+
doDebug = process.env.NODEOPCUAPKIDEBUG || false;
|
|
123
|
+
displayError = true;
|
|
124
|
+
displayDebug = !!process.env.NODEOPCUAPKIDEBUG || false;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// packages/node-opcua-pki/lib/toolbox/common2.ts
|
|
129
|
+
import assert2 from "assert";
|
|
130
|
+
import fs from "fs";
|
|
131
|
+
import path from "path";
|
|
132
|
+
import chalk from "chalk";
|
|
133
|
+
function certificateFileExist(certificateFile) {
|
|
134
|
+
if (fs.existsSync(certificateFile) && !g_config.force) {
|
|
135
|
+
warningLog(
|
|
136
|
+
chalk.yellow(" certificate ") + chalk.cyan(certificateFile) + chalk.yellow(" already exists => do not overwrite")
|
|
137
|
+
);
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
function mkdirRecursiveSync(folder) {
|
|
143
|
+
if (!fs.existsSync(folder)) {
|
|
144
|
+
debugLog(chalk.white(" .. constructing "), folder);
|
|
145
|
+
fs.mkdirSync(folder, { recursive: true });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function makePath(folderName, filename) {
|
|
149
|
+
let s;
|
|
150
|
+
if (filename) {
|
|
151
|
+
s = path.join(path.normalize(folderName), filename);
|
|
152
|
+
} else {
|
|
153
|
+
assert2(folderName);
|
|
154
|
+
s = folderName;
|
|
155
|
+
}
|
|
156
|
+
s = s.replace(/\\/g, "/");
|
|
157
|
+
return s;
|
|
158
|
+
}
|
|
159
|
+
var init_common2 = __esm({
|
|
160
|
+
"packages/node-opcua-pki/lib/toolbox/common2.ts"() {
|
|
161
|
+
"use strict";
|
|
162
|
+
init_esm_shims();
|
|
163
|
+
init_config();
|
|
164
|
+
init_debug();
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// packages/node-opcua-pki/lib/toolbox/display.ts
|
|
169
|
+
import chalk2 from "chalk";
|
|
170
|
+
function displayChapter(str) {
|
|
171
|
+
const l = " ";
|
|
172
|
+
warningLog(`${chalk2.bgWhite(l)} `);
|
|
173
|
+
str = ` ${str}${l}`.substring(0, l.length);
|
|
174
|
+
warningLog(chalk2.bgWhite.cyan(str));
|
|
175
|
+
warningLog(`${chalk2.bgWhite(l)} `);
|
|
176
|
+
}
|
|
177
|
+
function displayTitle(str) {
|
|
178
|
+
if (!g_config.silent) {
|
|
179
|
+
warningLog("");
|
|
180
|
+
warningLog(chalk2.yellowBright(str));
|
|
181
|
+
warningLog(chalk2.yellow(new Array(str.length + 1).join("=")), "\n");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function displaySubtitle(str) {
|
|
185
|
+
if (!g_config.silent) {
|
|
186
|
+
warningLog("");
|
|
187
|
+
warningLog(` ${chalk2.yellowBright(str)}`);
|
|
188
|
+
warningLog(` ${chalk2.white(new Array(str.length + 1).join("-"))}`, "\n");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function display(str) {
|
|
192
|
+
if (!g_config.silent) {
|
|
193
|
+
warningLog(` ${str}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
var init_display = __esm({
|
|
197
|
+
"packages/node-opcua-pki/lib/toolbox/display.ts"() {
|
|
198
|
+
"use strict";
|
|
199
|
+
init_esm_shims();
|
|
200
|
+
init_config();
|
|
201
|
+
init_debug();
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// packages/node-opcua-pki/lib/toolbox/without_openssl/create_certificate_signing_request.ts
|
|
206
|
+
import assert3 from "assert";
|
|
207
|
+
import fs2 from "fs";
|
|
208
|
+
import { createCertificateSigningRequest, pemToPrivateKey, Subject } from "node-opcua-crypto";
|
|
209
|
+
async function createCertificateSigningRequestAsync(certificateSigningRequestFilename, params) {
|
|
210
|
+
assert3(params);
|
|
211
|
+
assert3(params.rootDir);
|
|
212
|
+
assert3(params.configFile);
|
|
213
|
+
assert3(params.privateKey);
|
|
214
|
+
assert3(typeof params.privateKey === "string");
|
|
215
|
+
assert3(fs2.existsSync(params.privateKey), `Private key must exist${params.privateKey}`);
|
|
216
|
+
assert3(fs2.existsSync(params.rootDir), "RootDir key must exist");
|
|
217
|
+
assert3(typeof certificateSigningRequestFilename === "string");
|
|
218
|
+
const subject = params.subject ? new Subject(params.subject).toString() : void 0;
|
|
219
|
+
displaySubtitle("- Creating a Certificate Signing Request with subtile");
|
|
220
|
+
const privateKeyPem = await fs2.promises.readFile(params.privateKey, "utf-8");
|
|
221
|
+
const privateKey = await pemToPrivateKey(privateKeyPem);
|
|
222
|
+
const { csr } = await createCertificateSigningRequest({
|
|
223
|
+
privateKey,
|
|
224
|
+
dns: params.dns,
|
|
225
|
+
ip: params.ip,
|
|
226
|
+
subject,
|
|
227
|
+
applicationUri: params.applicationUri,
|
|
228
|
+
purpose: params.purpose
|
|
229
|
+
});
|
|
230
|
+
await fs2.promises.writeFile(certificateSigningRequestFilename, csr, "utf-8");
|
|
231
|
+
display(`- privateKey ${params.privateKey}`);
|
|
232
|
+
display(`- certificateSigningRequestFilename ${certificateSigningRequestFilename}`);
|
|
233
|
+
}
|
|
234
|
+
var init_create_certificate_signing_request = __esm({
|
|
235
|
+
"packages/node-opcua-pki/lib/toolbox/without_openssl/create_certificate_signing_request.ts"() {
|
|
236
|
+
"use strict";
|
|
237
|
+
init_esm_shims();
|
|
238
|
+
init_display();
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// packages/node-opcua-pki/lib/toolbox/common.ts
|
|
243
|
+
import assert4 from "assert";
|
|
244
|
+
function quote(str) {
|
|
245
|
+
return `"${str || ""}"`;
|
|
246
|
+
}
|
|
247
|
+
function adjustDate(params) {
|
|
248
|
+
assert4(params instanceof Object);
|
|
249
|
+
params.startDate = params.startDate || /* @__PURE__ */ new Date();
|
|
250
|
+
assert4(params.startDate instanceof Date);
|
|
251
|
+
params.validity = params.validity || 365;
|
|
252
|
+
params.endDate = new Date(params.startDate.getTime());
|
|
253
|
+
params.endDate.setDate(params.startDate.getDate() + params.validity);
|
|
254
|
+
assert4(params.endDate instanceof Date);
|
|
255
|
+
assert4(params.startDate instanceof Date);
|
|
256
|
+
}
|
|
257
|
+
function adjustApplicationUri(params) {
|
|
258
|
+
const applicationUri = params.applicationUri || "";
|
|
259
|
+
if (applicationUri.length > 200) {
|
|
260
|
+
throw new Error(`Openssl doesn't support urn with length greater than 200${applicationUri}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
var init_common = __esm({
|
|
264
|
+
"packages/node-opcua-pki/lib/toolbox/common.ts"() {
|
|
265
|
+
"use strict";
|
|
266
|
+
init_esm_shims();
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// packages/node-opcua-pki/lib/toolbox/without_openssl/create_self_signed_certificate.ts
|
|
271
|
+
import assert5 from "assert";
|
|
272
|
+
import fs3 from "fs";
|
|
273
|
+
import {
|
|
274
|
+
CertificatePurpose,
|
|
275
|
+
createSelfSignedCertificate as createSelfSignedCertificate1,
|
|
276
|
+
pemToPrivateKey as pemToPrivateKey2,
|
|
277
|
+
Subject as Subject2
|
|
278
|
+
} from "node-opcua-crypto";
|
|
279
|
+
async function createSelfSignedCertificateAsync(certificate, params) {
|
|
280
|
+
params.purpose = params.purpose || CertificatePurpose.ForApplication;
|
|
281
|
+
assert5(params.purpose, "Please provide a Certificate Purpose");
|
|
282
|
+
assert5(fs3.existsSync(params.configFile));
|
|
283
|
+
assert5(fs3.existsSync(params.rootDir));
|
|
284
|
+
assert5(fs3.existsSync(params.privateKey));
|
|
285
|
+
if (!params.subject) {
|
|
286
|
+
throw Error("Missing subject");
|
|
287
|
+
}
|
|
288
|
+
assert5(typeof params.applicationUri === "string");
|
|
289
|
+
assert5(Array.isArray(params.dns));
|
|
290
|
+
adjustDate(params);
|
|
291
|
+
assert5(Object.prototype.hasOwnProperty.call(params, "validity"));
|
|
292
|
+
let subject = new Subject2(params.subject);
|
|
293
|
+
subject = subject.toString();
|
|
294
|
+
const purpose = params.purpose;
|
|
295
|
+
displayTitle("Generate a certificate request");
|
|
296
|
+
const privateKeyPem = await fs3.promises.readFile(params.privateKey, "utf-8");
|
|
297
|
+
const privateKey = await pemToPrivateKey2(privateKeyPem);
|
|
298
|
+
const { cert } = await createSelfSignedCertificate1({
|
|
299
|
+
privateKey,
|
|
300
|
+
notBefore: params.startDate,
|
|
301
|
+
notAfter: params.endDate,
|
|
302
|
+
validity: params.validity,
|
|
303
|
+
dns: params.dns,
|
|
304
|
+
ip: params.ip,
|
|
305
|
+
subject,
|
|
306
|
+
applicationUri: params.applicationUri,
|
|
307
|
+
purpose
|
|
308
|
+
});
|
|
309
|
+
await fs3.promises.writeFile(certificate, cert, "utf-8");
|
|
310
|
+
}
|
|
311
|
+
async function createSelfSignedCertificate(certificate, params) {
|
|
312
|
+
await createSelfSignedCertificateAsync(certificate, params);
|
|
313
|
+
}
|
|
314
|
+
var init_create_self_signed_certificate = __esm({
|
|
315
|
+
"packages/node-opcua-pki/lib/toolbox/without_openssl/create_self_signed_certificate.ts"() {
|
|
316
|
+
"use strict";
|
|
317
|
+
init_esm_shims();
|
|
318
|
+
init_common();
|
|
319
|
+
init_display();
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// packages/node-opcua-pki/lib/toolbox/without_openssl/index.ts
|
|
324
|
+
var init_without_openssl = __esm({
|
|
325
|
+
"packages/node-opcua-pki/lib/toolbox/without_openssl/index.ts"() {
|
|
326
|
+
"use strict";
|
|
327
|
+
init_esm_shims();
|
|
328
|
+
init_create_certificate_signing_request();
|
|
329
|
+
init_create_self_signed_certificate();
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// packages/node-opcua-pki/lib/pki/templates/simple_config_template.cnf.ts
|
|
334
|
+
var config, simple_config_template_cnf_default;
|
|
335
|
+
var init_simple_config_template_cnf = __esm({
|
|
336
|
+
"packages/node-opcua-pki/lib/pki/templates/simple_config_template.cnf.ts"() {
|
|
337
|
+
"use strict";
|
|
338
|
+
init_esm_shims();
|
|
339
|
+
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, cRLSign, keyCertSign\nsubjectAltName = $ENV::ALTNAME\n\n[ v3_ca_signed]\nsubjectKeyIdentifier = hash\nauthorityKeyIdentifier = keyid,issuer\nbasicConstraints = critical, CA:FALSE\nkeyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment, keyCertSign\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, keyCertSign\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';
|
|
340
|
+
simple_config_template_cnf_default = config;
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// packages/node-opcua-pki/lib/pki/certificate_manager.ts
|
|
345
|
+
import fs4 from "fs";
|
|
346
|
+
import path2 from "path";
|
|
347
|
+
import { withLock } from "@ster5/global-mutex";
|
|
348
|
+
import chalk3 from "chalk";
|
|
349
|
+
import chokidar from "chokidar";
|
|
350
|
+
import {
|
|
351
|
+
exploreCertificate,
|
|
352
|
+
exploreCertificateInfo,
|
|
353
|
+
exploreCertificateRevocationList,
|
|
354
|
+
generatePrivateKeyFile,
|
|
355
|
+
makeSHA1Thumbprint,
|
|
356
|
+
readCertificate,
|
|
357
|
+
readCertificateRevocationList,
|
|
358
|
+
split_der,
|
|
359
|
+
toPem,
|
|
360
|
+
verifyCertificateChain,
|
|
361
|
+
verifyCertificateSignature
|
|
362
|
+
} from "node-opcua-crypto";
|
|
363
|
+
function getOrComputeInfo(entry) {
|
|
364
|
+
if (!entry.info) {
|
|
365
|
+
entry.info = exploreCertificate(entry.certificate);
|
|
366
|
+
}
|
|
367
|
+
return entry.info;
|
|
368
|
+
}
|
|
369
|
+
function makeFingerprint(certificate) {
|
|
370
|
+
const chain = split_der(certificate);
|
|
371
|
+
return makeSHA1Thumbprint(chain[0]).toString("hex");
|
|
372
|
+
}
|
|
373
|
+
function short(stringToShorten) {
|
|
374
|
+
return stringToShorten.substring(0, 10);
|
|
375
|
+
}
|
|
376
|
+
function buildIdealCertificateName(certificate) {
|
|
377
|
+
const fingerprint2 = makeFingerprint(certificate);
|
|
378
|
+
try {
|
|
379
|
+
const commonName = exploreCertificate(certificate).tbsCertificate.subject.commonName || "";
|
|
380
|
+
const sanitizedCommonName = commonName.replace(forbiddenChars, "_");
|
|
381
|
+
return `${sanitizedCommonName}[${fingerprint2}]`;
|
|
382
|
+
} catch (_err) {
|
|
383
|
+
return `invalid_certificate_[${fingerprint2}]`;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
function findMatchingIssuerKey(entries, wantedIssuerKey) {
|
|
387
|
+
return entries.filter((entry) => {
|
|
388
|
+
const info = getOrComputeInfo(entry);
|
|
389
|
+
return info.tbsCertificate.extensions && info.tbsCertificate.extensions.subjectKeyIdentifier === wantedIssuerKey;
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
function isSelfSigned2(info) {
|
|
393
|
+
return info.tbsCertificate.extensions?.subjectKeyIdentifier === info.tbsCertificate.extensions?.authorityKeyIdentifier?.keyIdentifier;
|
|
394
|
+
}
|
|
395
|
+
function isSelfSigned3(certificate) {
|
|
396
|
+
const info = exploreCertificate(certificate);
|
|
397
|
+
return isSelfSigned2(info);
|
|
398
|
+
}
|
|
399
|
+
function findIssuerCertificateInChain(certificate, chain) {
|
|
400
|
+
if (!certificate) {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
const certInfo = exploreCertificate(certificate);
|
|
404
|
+
if (isSelfSigned2(certInfo)) {
|
|
405
|
+
return certificate;
|
|
406
|
+
}
|
|
407
|
+
const wantedIssuerKey = certInfo.tbsCertificate.extensions?.authorityKeyIdentifier?.keyIdentifier;
|
|
408
|
+
if (!wantedIssuerKey) {
|
|
409
|
+
debugLog("Certificate has no extension 3");
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
const potentialIssuers = chain.filter((c) => {
|
|
413
|
+
const info = exploreCertificate(c);
|
|
414
|
+
return info.tbsCertificate.extensions && info.tbsCertificate.extensions.subjectKeyIdentifier === wantedIssuerKey;
|
|
415
|
+
});
|
|
416
|
+
if (potentialIssuers.length === 1) {
|
|
417
|
+
return potentialIssuers[0];
|
|
418
|
+
}
|
|
419
|
+
if (potentialIssuers.length > 1) {
|
|
420
|
+
debugLog("findIssuerCertificateInChain: certificate is not self-signed but has several issuers");
|
|
421
|
+
return potentialIssuers[0];
|
|
422
|
+
}
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
var configurationFileSimpleTemplate, fsWriteFile, forbiddenChars, CertificateManager;
|
|
426
|
+
var init_certificate_manager = __esm({
|
|
427
|
+
"packages/node-opcua-pki/lib/pki/certificate_manager.ts"() {
|
|
428
|
+
"use strict";
|
|
429
|
+
init_esm_shims();
|
|
430
|
+
init_common2();
|
|
431
|
+
init_debug();
|
|
432
|
+
init_without_openssl();
|
|
433
|
+
init_simple_config_template_cnf();
|
|
434
|
+
configurationFileSimpleTemplate = simple_config_template_cnf_default;
|
|
435
|
+
fsWriteFile = fs4.promises.writeFile;
|
|
436
|
+
forbiddenChars = /[\x00-\x1F<>:"/\\|?*]/g;
|
|
437
|
+
CertificateManager = class _CertificateManager {
|
|
438
|
+
// ── Global instance registry ─────────────────────────────────
|
|
439
|
+
// Tracks all initialized CertificateManager instances so their
|
|
440
|
+
// file watchers can be closed automatically on process exit,
|
|
441
|
+
// even if the consumer forgets to call dispose().
|
|
442
|
+
static #activeInstances = /* @__PURE__ */ new Set();
|
|
443
|
+
static #cleanupInstalled = false;
|
|
444
|
+
static #installProcessCleanup() {
|
|
445
|
+
if (_CertificateManager.#cleanupInstalled) return;
|
|
446
|
+
_CertificateManager.#cleanupInstalled = true;
|
|
447
|
+
const closeDanglingWatchers = () => {
|
|
448
|
+
for (const cm of _CertificateManager.#activeInstances) {
|
|
449
|
+
for (const w of cm.#watchers) {
|
|
450
|
+
try {
|
|
451
|
+
w.close();
|
|
452
|
+
} catch {
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
cm.#watchers.splice(0);
|
|
456
|
+
cm.state = 4 /* Disposed */;
|
|
457
|
+
}
|
|
458
|
+
_CertificateManager.#activeInstances.clear();
|
|
459
|
+
};
|
|
460
|
+
process.on("beforeExit", closeDanglingWatchers);
|
|
461
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
462
|
+
process.once(signal, () => {
|
|
463
|
+
closeDanglingWatchers();
|
|
464
|
+
process.exit();
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Dispose **all** active CertificateManager instances,
|
|
470
|
+
* closing their file watchers and freeing resources.
|
|
471
|
+
*
|
|
472
|
+
* This is mainly useful in test tear-down to ensure the
|
|
473
|
+
* Node.js process can exit cleanly.
|
|
474
|
+
*/
|
|
475
|
+
static async disposeAll() {
|
|
476
|
+
const instances = [..._CertificateManager.#activeInstances];
|
|
477
|
+
await Promise.all(instances.map((cm) => cm.dispose()));
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Assert that all CertificateManager instances have been
|
|
481
|
+
* properly disposed. Throws an Error listing the locations
|
|
482
|
+
* of any leaked instances.
|
|
483
|
+
*
|
|
484
|
+
* Intended for use in test `afterAll()` / `afterEach()`
|
|
485
|
+
* hooks to catch missing `dispose()` calls early.
|
|
486
|
+
*
|
|
487
|
+
* @example
|
|
488
|
+
* ```ts
|
|
489
|
+
* after(() => {
|
|
490
|
+
* CertificateManager.checkAllDisposed();
|
|
491
|
+
* });
|
|
492
|
+
* ```
|
|
493
|
+
*/
|
|
494
|
+
static checkAllDisposed() {
|
|
495
|
+
if (_CertificateManager.#activeInstances.size === 0) return;
|
|
496
|
+
const locations = [..._CertificateManager.#activeInstances].map((cm) => cm.rootDir);
|
|
497
|
+
throw new Error(
|
|
498
|
+
`${_CertificateManager.#activeInstances.size} CertificateManager instance(s) not disposed:
|
|
499
|
+
- ${locations.join("\n - ")}`
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
// ─────────────────────────────────────────────────────────────
|
|
503
|
+
/**
|
|
504
|
+
* When `true` (the default), any certificate that is not
|
|
505
|
+
* already in the trusted or rejected store is automatically
|
|
506
|
+
* written to the rejected folder the first time it is seen.
|
|
507
|
+
*/
|
|
508
|
+
untrustUnknownCertificate = true;
|
|
509
|
+
/** Current lifecycle state of this instance. */
|
|
510
|
+
state = 0 /* Uninitialized */;
|
|
511
|
+
/** @deprecated Use {@link folderPollingInterval} instead (typo fix). */
|
|
512
|
+
folderPoolingInterval = 5e3;
|
|
513
|
+
/** Interval in milliseconds for file-system polling (when enabled). */
|
|
514
|
+
get folderPollingInterval() {
|
|
515
|
+
return this.folderPoolingInterval;
|
|
516
|
+
}
|
|
517
|
+
set folderPollingInterval(value) {
|
|
518
|
+
this.folderPoolingInterval = value;
|
|
519
|
+
}
|
|
520
|
+
/** RSA key size used when generating the private key. */
|
|
521
|
+
keySize;
|
|
522
|
+
#location;
|
|
523
|
+
#watchers = [];
|
|
524
|
+
#readCertificatesCalled = false;
|
|
525
|
+
#filenameToHash = /* @__PURE__ */ new Map();
|
|
526
|
+
#initializingPromise;
|
|
527
|
+
#thumbs = {
|
|
528
|
+
rejected: /* @__PURE__ */ new Map(),
|
|
529
|
+
trusted: /* @__PURE__ */ new Map(),
|
|
530
|
+
issuers: {
|
|
531
|
+
certs: /* @__PURE__ */ new Map()
|
|
532
|
+
},
|
|
533
|
+
crl: /* @__PURE__ */ new Map(),
|
|
534
|
+
issuersCrl: /* @__PURE__ */ new Map()
|
|
535
|
+
};
|
|
536
|
+
/**
|
|
537
|
+
* Create a new CertificateManager.
|
|
538
|
+
*
|
|
539
|
+
* The constructor creates the root directory if it does not
|
|
540
|
+
* exist but does **not** initialise the PKI store — call
|
|
541
|
+
* {@link initialize} before using any other method.
|
|
542
|
+
*
|
|
543
|
+
* @param options - configuration options
|
|
544
|
+
*/
|
|
545
|
+
constructor(options) {
|
|
546
|
+
options.keySize = options.keySize || 2048;
|
|
547
|
+
if (!options.location) {
|
|
548
|
+
throw new Error("CertificateManager: missing 'location' option");
|
|
549
|
+
}
|
|
550
|
+
this.#location = makePath(options.location, "");
|
|
551
|
+
this.keySize = options.keySize;
|
|
552
|
+
mkdirRecursiveSync(options.location);
|
|
553
|
+
if (!fs4.existsSync(this.#location)) {
|
|
554
|
+
throw new Error(`CertificateManager cannot access location ${this.#location}`);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
/** Path to the OpenSSL configuration file. */
|
|
558
|
+
get configFile() {
|
|
559
|
+
return path2.join(this.rootDir, "own/openssl.cnf");
|
|
560
|
+
}
|
|
561
|
+
/** Root directory of the PKI store. */
|
|
562
|
+
get rootDir() {
|
|
563
|
+
return this.#location;
|
|
564
|
+
}
|
|
565
|
+
/** Path to the private key file (`own/private/private_key.pem`). */
|
|
566
|
+
get privateKey() {
|
|
567
|
+
return path2.join(this.rootDir, "own/private/private_key.pem");
|
|
568
|
+
}
|
|
569
|
+
/** Path to the OpenSSL random seed file. */
|
|
570
|
+
get randomFile() {
|
|
571
|
+
return path2.join(this.rootDir, "./random.rnd");
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Move a certificate to the rejected store.
|
|
575
|
+
* If the certificate was previously trusted, it will be removed from the trusted folder.
|
|
576
|
+
* @param certificate - the DER-encoded certificate
|
|
577
|
+
*/
|
|
578
|
+
async rejectCertificate(certificate) {
|
|
579
|
+
await this.#moveCertificate(certificate, "rejected");
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Move a certificate to the trusted store.
|
|
583
|
+
* If the certificate was previously rejected, it will be removed from the rejected folder.
|
|
584
|
+
* @param certificate - the DER-encoded certificate
|
|
585
|
+
*/
|
|
586
|
+
async trustCertificate(certificate) {
|
|
587
|
+
await this.#moveCertificate(certificate, "trusted");
|
|
588
|
+
}
|
|
589
|
+
/** Path to the rejected certificates folder. */
|
|
590
|
+
get rejectedFolder() {
|
|
591
|
+
return path2.join(this.rootDir, "rejected");
|
|
592
|
+
}
|
|
593
|
+
/** Path to the trusted certificates folder. */
|
|
594
|
+
get trustedFolder() {
|
|
595
|
+
return path2.join(this.rootDir, "trusted/certs");
|
|
596
|
+
}
|
|
597
|
+
/** Path to the trusted CRL folder. */
|
|
598
|
+
get crlFolder() {
|
|
599
|
+
return path2.join(this.rootDir, "trusted/crl");
|
|
600
|
+
}
|
|
601
|
+
/** Path to the issuer (CA) certificates folder. */
|
|
602
|
+
get issuersCertFolder() {
|
|
603
|
+
return path2.join(this.rootDir, "issuers/certs");
|
|
604
|
+
}
|
|
605
|
+
/** Path to the issuer CRL folder. */
|
|
606
|
+
get issuersCrlFolder() {
|
|
607
|
+
return path2.join(this.rootDir, "issuers/crl");
|
|
608
|
+
}
|
|
609
|
+
/** Path to the own certificate folder. */
|
|
610
|
+
get ownCertFolder() {
|
|
611
|
+
return path2.join(this.rootDir, "own/certs");
|
|
612
|
+
}
|
|
613
|
+
get ownPrivateFolder() {
|
|
614
|
+
return path2.join(this.rootDir, "own/private");
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Check if a certificate is in the trusted store.
|
|
618
|
+
* If the certificate is unknown and `untrustUnknownCertificate` is set,
|
|
619
|
+
* it will be written to the rejected folder.
|
|
620
|
+
* @param certificate - the DER-encoded certificate
|
|
621
|
+
* @returns `"Good"` if trusted, `"BadCertificateUntrusted"` if rejected/unknown,
|
|
622
|
+
* or `"BadCertificateInvalid"` if the certificate cannot be parsed.
|
|
623
|
+
*/
|
|
624
|
+
async isCertificateTrusted(certificate) {
|
|
625
|
+
const fingerprint2 = makeFingerprint(certificate);
|
|
626
|
+
if (this.#thumbs.trusted.has(fingerprint2)) {
|
|
627
|
+
return "Good";
|
|
628
|
+
}
|
|
629
|
+
if (!this.#thumbs.rejected.has(fingerprint2)) {
|
|
630
|
+
if (!this.untrustUnknownCertificate) {
|
|
631
|
+
return "Good";
|
|
632
|
+
}
|
|
633
|
+
try {
|
|
634
|
+
exploreCertificateInfo(certificate);
|
|
635
|
+
} catch (_err) {
|
|
636
|
+
return "BadCertificateInvalid";
|
|
637
|
+
}
|
|
638
|
+
const filename = path2.join(this.rejectedFolder, `${buildIdealCertificateName(certificate)}.pem`);
|
|
639
|
+
debugLog("certificate has never been seen before and is now rejected (untrusted) ", filename);
|
|
640
|
+
await fsWriteFile(filename, toPem(certificate, "CERTIFICATE"));
|
|
641
|
+
this.#thumbs.rejected.set(fingerprint2, { certificate, filename });
|
|
642
|
+
}
|
|
643
|
+
return "BadCertificateUntrusted";
|
|
644
|
+
}
|
|
645
|
+
async #innerVerifyCertificateAsync(certificate, _isIssuer, level, options) {
|
|
646
|
+
if (level >= 5) {
|
|
647
|
+
return "BadSecurityChecksFailed" /* BadSecurityChecksFailed */;
|
|
648
|
+
}
|
|
649
|
+
const chain = split_der(certificate);
|
|
650
|
+
debugLog("NB CERTIFICATE IN CHAIN = ", chain.length);
|
|
651
|
+
const info = exploreCertificate(chain[0]);
|
|
652
|
+
let hasValidIssuer = false;
|
|
653
|
+
let hasTrustedIssuer = false;
|
|
654
|
+
const hasIssuerKey = info.tbsCertificate.extensions?.authorityKeyIdentifier?.keyIdentifier;
|
|
655
|
+
debugLog("Certificate as an Issuer Key", hasIssuerKey);
|
|
656
|
+
if (hasIssuerKey) {
|
|
657
|
+
const isSelfSigned = isSelfSigned2(info);
|
|
658
|
+
debugLog("Is the Certificate self-signed ?", isSelfSigned);
|
|
659
|
+
if (!isSelfSigned) {
|
|
660
|
+
debugLog(
|
|
661
|
+
"Is issuer found in the list of know issuers ?",
|
|
662
|
+
"\n subjectKeyIdentifier = ",
|
|
663
|
+
info.tbsCertificate.extensions?.subjectKeyIdentifier,
|
|
664
|
+
"\n authorityKeyIdentifier = ",
|
|
665
|
+
info.tbsCertificate.extensions?.authorityKeyIdentifier?.keyIdentifier
|
|
666
|
+
);
|
|
667
|
+
let issuerCertificate = await this.findIssuerCertificate(chain[0]);
|
|
668
|
+
if (!issuerCertificate) {
|
|
669
|
+
issuerCertificate = findIssuerCertificateInChain(chain[0], chain);
|
|
670
|
+
if (!issuerCertificate) {
|
|
671
|
+
debugLog(
|
|
672
|
+
" the issuer has not been found in the chain itself nor in the issuer.cert list => the chain is incomplete!"
|
|
673
|
+
);
|
|
674
|
+
return "BadCertificateChainIncomplete" /* BadCertificateChainIncomplete */;
|
|
675
|
+
}
|
|
676
|
+
debugLog(" the issuer certificate has been found in the chain itself ! the chain is complete !");
|
|
677
|
+
} else {
|
|
678
|
+
debugLog(" the issuer certificate has been found in the issuer.cert folder !");
|
|
679
|
+
}
|
|
680
|
+
const issuerStatus = await this.#innerVerifyCertificateAsync(issuerCertificate, true, level + 1, options);
|
|
681
|
+
if (issuerStatus === "BadCertificateRevocationUnknown" /* BadCertificateRevocationUnknown */) {
|
|
682
|
+
return "BadCertificateIssuerRevocationUnknown" /* BadCertificateIssuerRevocationUnknown */;
|
|
683
|
+
}
|
|
684
|
+
if (issuerStatus === "BadCertificateIssuerRevocationUnknown" /* BadCertificateIssuerRevocationUnknown */) {
|
|
685
|
+
return "BadCertificateIssuerRevocationUnknown" /* BadCertificateIssuerRevocationUnknown */;
|
|
686
|
+
}
|
|
687
|
+
if (issuerStatus === "BadCertificateTimeInvalid" /* BadCertificateTimeInvalid */) {
|
|
688
|
+
if (!options || !options.acceptOutDatedIssuerCertificate) {
|
|
689
|
+
return "BadCertificateIssuerTimeInvalid" /* BadCertificateIssuerTimeInvalid */;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
if (issuerStatus === "BadCertificateUntrusted" /* BadCertificateUntrusted */) {
|
|
693
|
+
debugLog("warning issuerStatus = ", issuerStatus.toString(), "the issuer certificate is not trusted");
|
|
694
|
+
}
|
|
695
|
+
if (issuerStatus !== "Good" /* Good */ && issuerStatus !== "BadCertificateUntrusted" /* BadCertificateUntrusted */) {
|
|
696
|
+
return "BadSecurityChecksFailed" /* BadSecurityChecksFailed */;
|
|
697
|
+
}
|
|
698
|
+
const isCertificateSignatureOK = verifyCertificateSignature(certificate, issuerCertificate);
|
|
699
|
+
if (!isCertificateSignatureOK) {
|
|
700
|
+
debugLog(" the certificate was not signed by the issuer as it claim to be ! Danger");
|
|
701
|
+
return "BadSecurityChecksFailed" /* BadSecurityChecksFailed */;
|
|
702
|
+
}
|
|
703
|
+
hasValidIssuer = true;
|
|
704
|
+
let revokedStatus = await this.isCertificateRevoked(certificate);
|
|
705
|
+
if (revokedStatus === "BadCertificateRevocationUnknown" /* BadCertificateRevocationUnknown */) {
|
|
706
|
+
if (options?.ignoreMissingRevocationList) {
|
|
707
|
+
revokedStatus = "Good" /* Good */;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
if (revokedStatus !== "Good" /* Good */) {
|
|
711
|
+
debugLog("revokedStatus", revokedStatus);
|
|
712
|
+
return revokedStatus;
|
|
713
|
+
}
|
|
714
|
+
const issuerTrustedStatus = await this.#checkRejectedOrTrusted(issuerCertificate);
|
|
715
|
+
debugLog("issuerTrustedStatus", issuerTrustedStatus);
|
|
716
|
+
if (issuerTrustedStatus === "unknown") {
|
|
717
|
+
hasTrustedIssuer = false;
|
|
718
|
+
} else if (issuerTrustedStatus === "trusted") {
|
|
719
|
+
hasTrustedIssuer = true;
|
|
720
|
+
} else if (issuerTrustedStatus === "rejected") {
|
|
721
|
+
return "BadSecurityChecksFailed" /* BadSecurityChecksFailed */;
|
|
722
|
+
}
|
|
723
|
+
} else {
|
|
724
|
+
const isCertificateSignatureOK = verifyCertificateSignature(certificate, certificate);
|
|
725
|
+
if (!isCertificateSignatureOK) {
|
|
726
|
+
debugLog("Self-signed Certificate signature is not valid");
|
|
727
|
+
return "BadSecurityChecksFailed" /* BadSecurityChecksFailed */;
|
|
728
|
+
}
|
|
729
|
+
const revokedStatus = await this.isCertificateRevoked(certificate);
|
|
730
|
+
debugLog("revokedStatus of self signed certificate:", revokedStatus);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
const status = await this.#checkRejectedOrTrusted(certificate);
|
|
734
|
+
if (status === "rejected") {
|
|
735
|
+
if (!(options.acceptCertificateWithValidIssuerChain && hasValidIssuer && hasTrustedIssuer)) {
|
|
736
|
+
return "BadCertificateUntrusted" /* BadCertificateUntrusted */;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
const _c2 = chain[1] ? exploreCertificateInfo(chain[1]) : "non";
|
|
740
|
+
debugLog("chain[1] info=", _c2);
|
|
741
|
+
const certificateInfo = exploreCertificateInfo(certificate);
|
|
742
|
+
const now = /* @__PURE__ */ new Date();
|
|
743
|
+
let isTimeInvalid = false;
|
|
744
|
+
if (certificateInfo.notBefore.getTime() > now.getTime()) {
|
|
745
|
+
debugLog(
|
|
746
|
+
chalk3.red("certificate is invalid : certificate is not active yet !") + " not before date =" + certificateInfo.notBefore
|
|
747
|
+
);
|
|
748
|
+
if (!options.acceptPendingCertificate) {
|
|
749
|
+
isTimeInvalid = true;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (certificateInfo.notAfter.getTime() <= now.getTime()) {
|
|
753
|
+
debugLog(
|
|
754
|
+
`${chalk3.red("certificate is invalid : certificate has expired !")} not after date =${certificateInfo.notAfter}`
|
|
755
|
+
);
|
|
756
|
+
if (!options.acceptOutdatedCertificate) {
|
|
757
|
+
isTimeInvalid = true;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
if (status === "trusted") {
|
|
761
|
+
return isTimeInvalid ? "BadCertificateTimeInvalid" /* BadCertificateTimeInvalid */ : "Good" /* Good */;
|
|
762
|
+
}
|
|
763
|
+
if (hasIssuerKey) {
|
|
764
|
+
if (!hasTrustedIssuer) {
|
|
765
|
+
return "BadCertificateUntrusted" /* BadCertificateUntrusted */;
|
|
766
|
+
}
|
|
767
|
+
if (!hasValidIssuer) {
|
|
768
|
+
return "BadCertificateUntrusted" /* BadCertificateUntrusted */;
|
|
769
|
+
}
|
|
770
|
+
if (!options.acceptCertificateWithValidIssuerChain) {
|
|
771
|
+
return "BadCertificateUntrusted" /* BadCertificateUntrusted */;
|
|
772
|
+
}
|
|
773
|
+
return isTimeInvalid ? "BadCertificateTimeInvalid" /* BadCertificateTimeInvalid */ : "Good" /* Good */;
|
|
774
|
+
} else {
|
|
775
|
+
return "BadCertificateUntrusted" /* BadCertificateUntrusted */;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Internal verification hook called by {@link verifyCertificate}.
|
|
780
|
+
*
|
|
781
|
+
* Subclasses can override this to inject additional validation
|
|
782
|
+
* logic (e.g. application-level policy checks) while still
|
|
783
|
+
* delegating to the default chain/CRL/trust verification.
|
|
784
|
+
*
|
|
785
|
+
* @param certificate - the DER-encoded certificate to verify
|
|
786
|
+
* @param options - verification options forwarded from the
|
|
787
|
+
* public API
|
|
788
|
+
* @returns the verification status code
|
|
789
|
+
*/
|
|
790
|
+
async verifyCertificateAsync(certificate, options) {
|
|
791
|
+
const status1 = await this.#innerVerifyCertificateAsync(certificate, false, 0, options);
|
|
792
|
+
return status1;
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Verify a certificate against the PKI trust store.
|
|
796
|
+
*
|
|
797
|
+
* This performs a full validation including trust status,
|
|
798
|
+
* issuer chain, CRL revocation checks, and time validity.
|
|
799
|
+
*
|
|
800
|
+
* @param certificate - the DER-encoded certificate to verify
|
|
801
|
+
* @param options - optional flags to relax validation rules
|
|
802
|
+
* @returns the verification status code
|
|
803
|
+
*/
|
|
804
|
+
async verifyCertificate(certificate, options) {
|
|
805
|
+
if (!certificate) {
|
|
806
|
+
return "BadSecurityChecksFailed" /* BadSecurityChecksFailed */;
|
|
807
|
+
}
|
|
808
|
+
try {
|
|
809
|
+
const status = await this.verifyCertificateAsync(certificate, options || {});
|
|
810
|
+
return status;
|
|
811
|
+
} catch (error) {
|
|
812
|
+
warningLog(`verifyCertificate error: ${error.message}`);
|
|
813
|
+
return "BadCertificateInvalid" /* BadCertificateInvalid */;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Initialize the PKI directory structure, generate the
|
|
818
|
+
* private key (if missing), and start file-system watchers.
|
|
819
|
+
*
|
|
820
|
+
* This method is idempotent — subsequent calls are no-ops.
|
|
821
|
+
* It must be called before any certificate operations.
|
|
822
|
+
*/
|
|
823
|
+
async initialize() {
|
|
824
|
+
if (this.state !== 0 /* Uninitialized */) {
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
this.state = 1 /* Initializing */;
|
|
828
|
+
this.#initializingPromise = this.#initialize();
|
|
829
|
+
await this.#initializingPromise;
|
|
830
|
+
this.#initializingPromise = void 0;
|
|
831
|
+
this.state = 2 /* Initialized */;
|
|
832
|
+
_CertificateManager.#activeInstances.add(this);
|
|
833
|
+
_CertificateManager.#installProcessCleanup();
|
|
834
|
+
}
|
|
835
|
+
async #initialize() {
|
|
836
|
+
this.state = 1 /* Initializing */;
|
|
837
|
+
const pkiDir = this.#location;
|
|
838
|
+
mkdirRecursiveSync(pkiDir);
|
|
839
|
+
mkdirRecursiveSync(path2.join(pkiDir, "own"));
|
|
840
|
+
mkdirRecursiveSync(path2.join(pkiDir, "own/certs"));
|
|
841
|
+
mkdirRecursiveSync(path2.join(pkiDir, "own/private"));
|
|
842
|
+
mkdirRecursiveSync(path2.join(pkiDir, "rejected"));
|
|
843
|
+
mkdirRecursiveSync(path2.join(pkiDir, "trusted"));
|
|
844
|
+
mkdirRecursiveSync(path2.join(pkiDir, "trusted/certs"));
|
|
845
|
+
mkdirRecursiveSync(path2.join(pkiDir, "trusted/crl"));
|
|
846
|
+
mkdirRecursiveSync(path2.join(pkiDir, "issuers"));
|
|
847
|
+
mkdirRecursiveSync(path2.join(pkiDir, "issuers/certs"));
|
|
848
|
+
mkdirRecursiveSync(path2.join(pkiDir, "issuers/crl"));
|
|
849
|
+
if (!fs4.existsSync(this.configFile) || !fs4.existsSync(this.privateKey)) {
|
|
850
|
+
return await this.withLock2(async () => {
|
|
851
|
+
if (this.state === 3 /* Disposing */ || this.state === 4 /* Disposed */) {
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
if (!fs4.existsSync(this.configFile)) {
|
|
855
|
+
fs4.writeFileSync(this.configFile, configurationFileSimpleTemplate);
|
|
856
|
+
}
|
|
857
|
+
if (!fs4.existsSync(this.privateKey)) {
|
|
858
|
+
debugLog("generating private key ...");
|
|
859
|
+
await generatePrivateKeyFile(this.privateKey, this.keySize);
|
|
860
|
+
await this.#readCertificates();
|
|
861
|
+
} else {
|
|
862
|
+
await this.#readCertificates();
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
} else {
|
|
866
|
+
await this.#readCertificates();
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Dispose of the CertificateManager, releasing file watchers
|
|
871
|
+
* and other resources. The instance should not be used after
|
|
872
|
+
* calling this method.
|
|
873
|
+
*/
|
|
874
|
+
async dispose() {
|
|
875
|
+
if (this.state === 3 /* Disposing */) {
|
|
876
|
+
throw new Error("Already disposing");
|
|
877
|
+
}
|
|
878
|
+
if (this.state === 0 /* Uninitialized */) {
|
|
879
|
+
this.state = 4 /* Disposed */;
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
if (this.state === 1 /* Initializing */) {
|
|
883
|
+
if (this.#initializingPromise) {
|
|
884
|
+
await this.#initializingPromise;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
try {
|
|
888
|
+
this.state = 3 /* Disposing */;
|
|
889
|
+
await Promise.all(this.#watchers.map((w) => w.close()));
|
|
890
|
+
this.#watchers.forEach((w) => {
|
|
891
|
+
w.removeAllListeners();
|
|
892
|
+
});
|
|
893
|
+
this.#watchers.splice(0);
|
|
894
|
+
} finally {
|
|
895
|
+
this.state = 4 /* Disposed */;
|
|
896
|
+
_CertificateManager.#activeInstances.delete(this);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Force a full re-scan of all PKI folders, rebuilding
|
|
901
|
+
* the in-memory `_thumbs` index from scratch.
|
|
902
|
+
*
|
|
903
|
+
* Call this after external processes have modified the
|
|
904
|
+
* PKI folders (e.g. via `writeTrustList` or CLI tools)
|
|
905
|
+
* to ensure the CertificateManager sees the latest
|
|
906
|
+
* state without waiting for file-system events.
|
|
907
|
+
*/
|
|
908
|
+
async reloadCertificates() {
|
|
909
|
+
await Promise.all(this.#watchers.map((w) => w.close()));
|
|
910
|
+
for (const w of this.#watchers) {
|
|
911
|
+
w.removeAllListeners();
|
|
912
|
+
}
|
|
913
|
+
this.#watchers.splice(0);
|
|
914
|
+
this.#thumbs.rejected.clear();
|
|
915
|
+
this.#thumbs.trusted.clear();
|
|
916
|
+
this.#thumbs.issuers.certs.clear();
|
|
917
|
+
this.#thumbs.crl.clear();
|
|
918
|
+
this.#thumbs.issuersCrl.clear();
|
|
919
|
+
this.#filenameToHash.clear();
|
|
920
|
+
this.#readCertificatesCalled = false;
|
|
921
|
+
await this.#readCertificates();
|
|
922
|
+
}
|
|
923
|
+
async withLock2(action) {
|
|
924
|
+
const lockFileName = path2.join(this.rootDir, "mutex.lock");
|
|
925
|
+
return withLock({ fileToLock: lockFileName }, async () => {
|
|
926
|
+
return await action();
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Create a self-signed certificate for this PKI's private key.
|
|
931
|
+
*
|
|
932
|
+
* The certificate is written to `params.outputFile` or
|
|
933
|
+
* `own/certs/self_signed_certificate.pem` by default.
|
|
934
|
+
*
|
|
935
|
+
* @param params - certificate parameters (subject, SANs,
|
|
936
|
+
* validity, etc.)
|
|
937
|
+
*/
|
|
938
|
+
async createSelfSignedCertificate(params) {
|
|
939
|
+
if (typeof params.applicationUri !== "string") {
|
|
940
|
+
throw new Error("createSelfSignedCertificate: expecting applicationUri to be a string");
|
|
941
|
+
}
|
|
942
|
+
if (!fs4.existsSync(this.privateKey)) {
|
|
943
|
+
throw new Error(`Cannot find private key ${this.privateKey}`);
|
|
944
|
+
}
|
|
945
|
+
let certificateFilename = path2.join(this.rootDir, "own/certs/self_signed_certificate.pem");
|
|
946
|
+
certificateFilename = params.outputFile || certificateFilename;
|
|
947
|
+
const _params = params;
|
|
948
|
+
_params.rootDir = this.rootDir;
|
|
949
|
+
_params.configFile = this.configFile;
|
|
950
|
+
_params.privateKey = this.privateKey;
|
|
951
|
+
_params.subject = params.subject || "CN=FIXME";
|
|
952
|
+
await this.withLock2(async () => {
|
|
953
|
+
await createSelfSignedCertificate(certificateFilename, _params);
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Create a Certificate Signing Request (CSR) using this
|
|
958
|
+
* PKI's private key and configuration.
|
|
959
|
+
*
|
|
960
|
+
* The CSR file is written to `own/certs/` with a timestamped
|
|
961
|
+
* filename.
|
|
962
|
+
*
|
|
963
|
+
* @param params - CSR parameters (subject, SANs)
|
|
964
|
+
* @returns the filesystem path to the generated CSR file
|
|
965
|
+
*/
|
|
966
|
+
async createCertificateRequest(params) {
|
|
967
|
+
if (!params) {
|
|
968
|
+
throw new Error("params is required");
|
|
969
|
+
}
|
|
970
|
+
const _params = params;
|
|
971
|
+
if (Object.prototype.hasOwnProperty.call(_params, "rootDir")) {
|
|
972
|
+
throw new Error("rootDir should not be specified ");
|
|
973
|
+
}
|
|
974
|
+
_params.rootDir = path2.resolve(this.rootDir);
|
|
975
|
+
_params.configFile = path2.resolve(this.configFile);
|
|
976
|
+
_params.privateKey = path2.resolve(this.privateKey);
|
|
977
|
+
return await this.withLock2(async () => {
|
|
978
|
+
const now = /* @__PURE__ */ new Date();
|
|
979
|
+
const today2 = `${now.toISOString().slice(0, 10)}_${now.getTime()}`;
|
|
980
|
+
const certificateSigningRequestFilename = path2.join(this.rootDir, "own/certs", `certificate_${today2}.csr`);
|
|
981
|
+
await createCertificateSigningRequestAsync(certificateSigningRequestFilename, _params);
|
|
982
|
+
return certificateSigningRequestFilename;
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Add a CA (issuer) certificate to the issuers store.
|
|
987
|
+
* If the certificate is already present, this is a no-op.
|
|
988
|
+
* @param certificate - the DER-encoded CA certificate
|
|
989
|
+
* @param validate - if `true`, verify the certificate before adding
|
|
990
|
+
* @param addInTrustList - if `true`, also add to the trusted store
|
|
991
|
+
* @returns `VerificationStatus.Good` on success
|
|
992
|
+
*/
|
|
993
|
+
async addIssuer(certificate, validate = false, addInTrustList = false) {
|
|
994
|
+
if (validate) {
|
|
995
|
+
const status = await this.verifyCertificate(certificate);
|
|
996
|
+
if (status !== "Good" /* Good */ && status !== "BadCertificateUntrusted" /* BadCertificateUntrusted */) {
|
|
997
|
+
return status;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
const pemCertificate = toPem(certificate, "CERTIFICATE");
|
|
1001
|
+
const fingerprint2 = makeFingerprint(certificate);
|
|
1002
|
+
if (this.#thumbs.issuers.certs.has(fingerprint2)) {
|
|
1003
|
+
return "Good" /* Good */;
|
|
1004
|
+
}
|
|
1005
|
+
const filename = path2.join(this.issuersCertFolder, `issuer_${buildIdealCertificateName(certificate)}.pem`);
|
|
1006
|
+
await fs4.promises.writeFile(filename, pemCertificate, "ascii");
|
|
1007
|
+
this.#thumbs.issuers.certs.set(fingerprint2, { certificate, filename });
|
|
1008
|
+
if (addInTrustList) {
|
|
1009
|
+
await this.trustCertificate(certificate);
|
|
1010
|
+
}
|
|
1011
|
+
return "Good" /* Good */;
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Add a CRL to the certificate manager.
|
|
1015
|
+
* @param crl - the CRL to add
|
|
1016
|
+
* @param target - "issuers" (default) writes to issuers/crl, "trusted" writes to trusted/crl
|
|
1017
|
+
*/
|
|
1018
|
+
async addRevocationList(crl, target = "issuers") {
|
|
1019
|
+
return await this.withLock2(async () => {
|
|
1020
|
+
try {
|
|
1021
|
+
const index = target === "trusted" ? this.#thumbs.crl : this.#thumbs.issuersCrl;
|
|
1022
|
+
const folder = target === "trusted" ? this.crlFolder : this.issuersCrlFolder;
|
|
1023
|
+
const crlInfo = exploreCertificateRevocationList(crl);
|
|
1024
|
+
const key = crlInfo.tbsCertList.issuerFingerprint;
|
|
1025
|
+
if (!index.has(key)) {
|
|
1026
|
+
index.set(key, { crls: [], serialNumbers: {} });
|
|
1027
|
+
}
|
|
1028
|
+
const pemCertificate = toPem(crl, "X509 CRL");
|
|
1029
|
+
const filename = path2.join(folder, `crl_${buildIdealCertificateName(crl)}.pem`);
|
|
1030
|
+
await fs4.promises.writeFile(filename, pemCertificate, "ascii");
|
|
1031
|
+
await this.#onCrlFileAdded(index, filename);
|
|
1032
|
+
await this.#waitAndCheckCRLProcessingStatus();
|
|
1033
|
+
return "Good" /* Good */;
|
|
1034
|
+
} catch (err) {
|
|
1035
|
+
debugLog(err);
|
|
1036
|
+
return "BadSecurityChecksFailed" /* BadSecurityChecksFailed */;
|
|
1037
|
+
}
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Remove all CRL files from the specified folder(s) and clear the
|
|
1042
|
+
* corresponding in-memory index.
|
|
1043
|
+
* @param target - "issuers" clears issuers/crl, "trusted" clears
|
|
1044
|
+
* trusted/crl, "all" clears both.
|
|
1045
|
+
*/
|
|
1046
|
+
async clearRevocationLists(target) {
|
|
1047
|
+
const clearFolder = async (folder, index) => {
|
|
1048
|
+
try {
|
|
1049
|
+
const files = await fs4.promises.readdir(folder);
|
|
1050
|
+
for (const file of files) {
|
|
1051
|
+
const ext = path2.extname(file).toLowerCase();
|
|
1052
|
+
if (ext === ".crl" || ext === ".pem" || ext === ".der") {
|
|
1053
|
+
await fs4.promises.unlink(path2.join(folder, file));
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
} catch (err) {
|
|
1057
|
+
if (err.code !== "ENOENT") {
|
|
1058
|
+
throw err;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
index.clear();
|
|
1062
|
+
};
|
|
1063
|
+
if (target === "issuers" || target === "all") {
|
|
1064
|
+
await clearFolder(this.issuersCrlFolder, this.#thumbs.issuersCrl);
|
|
1065
|
+
}
|
|
1066
|
+
if (target === "trusted" || target === "all") {
|
|
1067
|
+
await clearFolder(this.crlFolder, this.#thumbs.crl);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
/**
|
|
1071
|
+
* Check whether an issuer certificate with the given thumbprint
|
|
1072
|
+
* is already registered.
|
|
1073
|
+
* @param thumbprint - hex-encoded SHA-1 thumbprint (lowercase)
|
|
1074
|
+
*/
|
|
1075
|
+
async hasIssuer(thumbprint) {
|
|
1076
|
+
await this.#readCertificates();
|
|
1077
|
+
const normalized = thumbprint.toLowerCase();
|
|
1078
|
+
return this.#thumbs.issuers.certs.has(normalized);
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Remove a trusted certificate identified by its SHA-1 thumbprint.
|
|
1082
|
+
* Deletes the file on disk and removes the entry from the
|
|
1083
|
+
* in-memory index.
|
|
1084
|
+
* @param thumbprint - hex-encoded SHA-1 thumbprint (lowercase)
|
|
1085
|
+
* @returns the removed certificate buffer, or `null` if not found
|
|
1086
|
+
*/
|
|
1087
|
+
async removeTrustedCertificate(thumbprint) {
|
|
1088
|
+
await this.#readCertificates();
|
|
1089
|
+
const normalized = thumbprint.toLowerCase();
|
|
1090
|
+
const entry = this.#thumbs.trusted.get(normalized);
|
|
1091
|
+
if (!entry) {
|
|
1092
|
+
return null;
|
|
1093
|
+
}
|
|
1094
|
+
try {
|
|
1095
|
+
await fs4.promises.unlink(entry.filename);
|
|
1096
|
+
} catch (err) {
|
|
1097
|
+
if (err.code !== "ENOENT") {
|
|
1098
|
+
throw err;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
this.#thumbs.trusted.delete(normalized);
|
|
1102
|
+
return entry.certificate;
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* Remove an issuer certificate identified by its SHA-1 thumbprint.
|
|
1106
|
+
* Deletes the file on disk and removes the entry from the
|
|
1107
|
+
* in-memory index.
|
|
1108
|
+
* @param thumbprint - hex-encoded SHA-1 thumbprint (lowercase)
|
|
1109
|
+
* @returns the removed certificate buffer, or `null` if not found
|
|
1110
|
+
*/
|
|
1111
|
+
async removeIssuer(thumbprint) {
|
|
1112
|
+
await this.#readCertificates();
|
|
1113
|
+
const normalized = thumbprint.toLowerCase();
|
|
1114
|
+
const entry = this.#thumbs.issuers.certs.get(normalized);
|
|
1115
|
+
if (!entry) {
|
|
1116
|
+
return null;
|
|
1117
|
+
}
|
|
1118
|
+
try {
|
|
1119
|
+
await fs4.promises.unlink(entry.filename);
|
|
1120
|
+
} catch (err) {
|
|
1121
|
+
if (err.code !== "ENOENT") {
|
|
1122
|
+
throw err;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
this.#thumbs.issuers.certs.delete(normalized);
|
|
1126
|
+
return entry.certificate;
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Remove all CRL files that were issued by the given CA certificate
|
|
1130
|
+
* from the specified folder (or both).
|
|
1131
|
+
* @param issuerCertificate - the CA certificate whose CRLs to remove
|
|
1132
|
+
* @param target - "issuers", "trusted", or "all" (default "all")
|
|
1133
|
+
*/
|
|
1134
|
+
async removeRevocationListsForIssuer(issuerCertificate, target = "all") {
|
|
1135
|
+
const issuerInfo = exploreCertificate(issuerCertificate);
|
|
1136
|
+
const issuerFingerprint = issuerInfo.tbsCertificate.subjectFingerPrint;
|
|
1137
|
+
const processIndex = async (index) => {
|
|
1138
|
+
const crlData = index.get(issuerFingerprint);
|
|
1139
|
+
if (!crlData) return;
|
|
1140
|
+
for (const crlEntry of crlData.crls) {
|
|
1141
|
+
try {
|
|
1142
|
+
await fs4.promises.unlink(crlEntry.filename);
|
|
1143
|
+
} catch (err) {
|
|
1144
|
+
if (err.code !== "ENOENT") {
|
|
1145
|
+
throw err;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
index.delete(issuerFingerprint);
|
|
1150
|
+
};
|
|
1151
|
+
if (target === "issuers" || target === "all") {
|
|
1152
|
+
await processIndex(this.#thumbs.issuersCrl);
|
|
1153
|
+
}
|
|
1154
|
+
if (target === "trusted" || target === "all") {
|
|
1155
|
+
await processIndex(this.#thumbs.crl);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Validate a certificate (optionally with its chain) and add
|
|
1160
|
+
* the leaf certificate to the trusted store.
|
|
1161
|
+
*
|
|
1162
|
+
* The certificate buffer may contain a single certificate or a
|
|
1163
|
+
* full chain (leaf + issuer certificates concatenated in DER).
|
|
1164
|
+
* Only the leaf certificate is added to the trusted store.
|
|
1165
|
+
*
|
|
1166
|
+
* When the chain contains issuer certificates, this method
|
|
1167
|
+
* verifies that each issuer is already registered via
|
|
1168
|
+
* {@link addIssuer} before trusting the leaf.
|
|
1169
|
+
*
|
|
1170
|
+
* @param certificateChain - DER-encoded certificate or chain
|
|
1171
|
+
* @returns `VerificationStatus.Good` on success, or an error
|
|
1172
|
+
* status indicating why the certificate was rejected.
|
|
1173
|
+
*/
|
|
1174
|
+
async addTrustedCertificateFromChain(certificateChain) {
|
|
1175
|
+
const certificates = split_der(certificateChain);
|
|
1176
|
+
const leafCertificate = certificates[0];
|
|
1177
|
+
try {
|
|
1178
|
+
exploreCertificate(leafCertificate);
|
|
1179
|
+
} catch (_err) {
|
|
1180
|
+
return "BadCertificateInvalid" /* BadCertificateInvalid */;
|
|
1181
|
+
}
|
|
1182
|
+
const result = await verifyCertificateChain([leafCertificate]);
|
|
1183
|
+
if (result.status !== "Good") {
|
|
1184
|
+
return "BadCertificateInvalid" /* BadCertificateInvalid */;
|
|
1185
|
+
}
|
|
1186
|
+
if (certificates.length > 1) {
|
|
1187
|
+
const issuerFolder = this.issuersCertFolder;
|
|
1188
|
+
const issuerThumbprints = /* @__PURE__ */ new Set();
|
|
1189
|
+
const files = await fs4.promises.readdir(issuerFolder);
|
|
1190
|
+
for (const file of files) {
|
|
1191
|
+
const ext = path2.extname(file).toLowerCase();
|
|
1192
|
+
if (ext === ".pem" || ext === ".der") {
|
|
1193
|
+
try {
|
|
1194
|
+
const issuerCert = readCertificate(path2.join(issuerFolder, file));
|
|
1195
|
+
const fp = makeFingerprint(issuerCert);
|
|
1196
|
+
issuerThumbprints.add(fp);
|
|
1197
|
+
} catch (_err) {
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
for (const issuerCert of certificates.slice(1)) {
|
|
1202
|
+
const thumbprint = makeFingerprint(issuerCert);
|
|
1203
|
+
if (!issuerThumbprints.has(thumbprint)) {
|
|
1204
|
+
return "BadCertificateChainIncomplete" /* BadCertificateChainIncomplete */;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
await this.trustCertificate(leafCertificate);
|
|
1209
|
+
return "Good" /* Good */;
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* Check whether an issuer certificate is still needed by any
|
|
1213
|
+
* certificate in the trusted store.
|
|
1214
|
+
*
|
|
1215
|
+
* This is used before removing an issuer to ensure that
|
|
1216
|
+
* doing so would not break the chain of any trusted
|
|
1217
|
+
* certificate.
|
|
1218
|
+
*
|
|
1219
|
+
* @param issuerCertificate - the CA certificate to check
|
|
1220
|
+
* @returns `true` if at least one trusted certificate was
|
|
1221
|
+
* signed by this issuer.
|
|
1222
|
+
*/
|
|
1223
|
+
async isIssuerInUseByTrustedCertificate(issuerCertificate) {
|
|
1224
|
+
await this.#readCertificates();
|
|
1225
|
+
for (const entry of this.#thumbs.trusted.values()) {
|
|
1226
|
+
if (!entry.certificate) continue;
|
|
1227
|
+
try {
|
|
1228
|
+
if (verifyCertificateSignature(entry.certificate, issuerCertificate)) {
|
|
1229
|
+
return true;
|
|
1230
|
+
}
|
|
1231
|
+
} catch (_err) {
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
return false;
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1237
|
+
* find the issuer certificate among the trusted issuer certificates.
|
|
1238
|
+
*
|
|
1239
|
+
* The findIssuerCertificate method is an asynchronous method that attempts to find
|
|
1240
|
+
* the issuer certificate for a given certificate from the list of issuer certificate declared in the PKI
|
|
1241
|
+
*
|
|
1242
|
+
* - If the certificate is self-signed, it returns the certificate itself.
|
|
1243
|
+
*
|
|
1244
|
+
* - If the certificate has no extension 3, it is assumed to be generated by an old system, and a null value is returned.
|
|
1245
|
+
*
|
|
1246
|
+
* - the method checks both issuer and trusted certificates and returns the appropriate issuercertificate,
|
|
1247
|
+
* if found. If multiple matching certificates are found, a warning is logged to the console.
|
|
1248
|
+
*
|
|
1249
|
+
*/
|
|
1250
|
+
async findIssuerCertificate(certificate) {
|
|
1251
|
+
const certInfo = exploreCertificate(certificate);
|
|
1252
|
+
if (isSelfSigned2(certInfo)) {
|
|
1253
|
+
return certificate;
|
|
1254
|
+
}
|
|
1255
|
+
const wantedIssuerKey = certInfo.tbsCertificate.extensions?.authorityKeyIdentifier?.keyIdentifier;
|
|
1256
|
+
if (!wantedIssuerKey) {
|
|
1257
|
+
debugLog("Certificate has no extension 3");
|
|
1258
|
+
return null;
|
|
1259
|
+
}
|
|
1260
|
+
const issuerCertificates = [...this.#thumbs.issuers.certs.values()];
|
|
1261
|
+
const selectedIssuerCertificates = findMatchingIssuerKey(issuerCertificates, wantedIssuerKey);
|
|
1262
|
+
if (selectedIssuerCertificates.length > 0) {
|
|
1263
|
+
if (selectedIssuerCertificates.length > 1) {
|
|
1264
|
+
warningLog("Warning more than one issuer certificate exists with subjectKeyIdentifier ", wantedIssuerKey);
|
|
1265
|
+
}
|
|
1266
|
+
return selectedIssuerCertificates[0].certificate || null;
|
|
1267
|
+
}
|
|
1268
|
+
const trustedCertificates = [...this.#thumbs.trusted.values()];
|
|
1269
|
+
const selectedTrustedCertificates = findMatchingIssuerKey(trustedCertificates, wantedIssuerKey);
|
|
1270
|
+
if (selectedTrustedCertificates.length > 1) {
|
|
1271
|
+
warningLog(
|
|
1272
|
+
"Warning more than one certificate exists with subjectKeyIdentifier in trusted certificate list ",
|
|
1273
|
+
wantedIssuerKey,
|
|
1274
|
+
selectedTrustedCertificates.length
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
return selectedTrustedCertificates.length > 0 ? selectedTrustedCertificates[0].certificate : null;
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
*
|
|
1281
|
+
* check if the certificate explicitly appear in the trust list, the reject list or none.
|
|
1282
|
+
* In case of being in the reject and trusted list at the same time is consider: rejected.
|
|
1283
|
+
* @internal
|
|
1284
|
+
* @private
|
|
1285
|
+
*/
|
|
1286
|
+
async #checkRejectedOrTrusted(certificate) {
|
|
1287
|
+
const fingerprint2 = makeFingerprint(certificate);
|
|
1288
|
+
debugLog("#checkRejectedOrTrusted fingerprint ", short(fingerprint2));
|
|
1289
|
+
await this.#readCertificates();
|
|
1290
|
+
if (this.#thumbs.rejected.has(fingerprint2)) {
|
|
1291
|
+
return "rejected";
|
|
1292
|
+
}
|
|
1293
|
+
if (this.#thumbs.trusted.has(fingerprint2)) {
|
|
1294
|
+
return "trusted";
|
|
1295
|
+
}
|
|
1296
|
+
return "unknown";
|
|
1297
|
+
}
|
|
1298
|
+
async #moveCertificate(certificate, newStatus) {
|
|
1299
|
+
await this.withLock2(async () => {
|
|
1300
|
+
const fingerprint2 = makeFingerprint(certificate);
|
|
1301
|
+
let status = await this.#checkRejectedOrTrusted(certificate);
|
|
1302
|
+
if (status === "unknown") {
|
|
1303
|
+
const pem = toPem(certificate, "CERTIFICATE");
|
|
1304
|
+
const filename = path2.join(this.rejectedFolder, `${buildIdealCertificateName(certificate)}.pem`);
|
|
1305
|
+
await fs4.promises.writeFile(filename, pem);
|
|
1306
|
+
this.#thumbs.rejected.set(fingerprint2, { certificate, filename });
|
|
1307
|
+
status = "rejected";
|
|
1308
|
+
}
|
|
1309
|
+
debugLog("#moveCertificate", fingerprint2.substring(0, 10), "from", status, "to", newStatus);
|
|
1310
|
+
if (status !== "rejected" && status !== "trusted") {
|
|
1311
|
+
throw new Error(`#moveCertificate: unexpected status '${status}' for certificate ${fingerprint2.substring(0, 10)}`);
|
|
1312
|
+
}
|
|
1313
|
+
if (status !== newStatus) {
|
|
1314
|
+
const indexSrc = status === "rejected" ? this.#thumbs.rejected : this.#thumbs.trusted;
|
|
1315
|
+
const srcEntry = indexSrc.get(fingerprint2);
|
|
1316
|
+
if (!srcEntry) {
|
|
1317
|
+
debugLog(" cannot find certificate ", fingerprint2.substring(0, 10), " in", status);
|
|
1318
|
+
throw new Error(`#moveCertificate: certificate ${fingerprint2.substring(0, 10)} not found in ${status} index`);
|
|
1319
|
+
}
|
|
1320
|
+
const destFolder = newStatus === "trusted" ? this.trustedFolder : this.rejectedFolder;
|
|
1321
|
+
const certificateDest = path2.join(destFolder, path2.basename(srcEntry.filename));
|
|
1322
|
+
debugLog("#moveCertificate", fingerprint2.substring(0, 10), "old name", srcEntry.filename);
|
|
1323
|
+
debugLog("#moveCertificate", fingerprint2.substring(0, 10), "new name", certificateDest);
|
|
1324
|
+
await fs4.promises.rename(srcEntry.filename, certificateDest);
|
|
1325
|
+
indexSrc.delete(fingerprint2);
|
|
1326
|
+
const indexDest = newStatus === "trusted" ? this.#thumbs.trusted : this.#thumbs.rejected;
|
|
1327
|
+
indexDest.set(fingerprint2, { certificate, filename: certificateDest });
|
|
1328
|
+
}
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
#findAssociatedCRLs(issuerCertificate) {
|
|
1332
|
+
const issuerCertificateInfo = exploreCertificate(issuerCertificate);
|
|
1333
|
+
const key = issuerCertificateInfo.tbsCertificate.subjectFingerPrint;
|
|
1334
|
+
return this.#thumbs.issuersCrl.get(key) ?? this.#thumbs.crl.get(key) ?? null;
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Check whether a certificate has been revoked by its issuer's CRL.
|
|
1338
|
+
*
|
|
1339
|
+
* - Self-signed certificates are never considered revoked.
|
|
1340
|
+
* - If no `issuerCertificate` is provided, the method attempts
|
|
1341
|
+
* to find it via {@link findIssuerCertificate}.
|
|
1342
|
+
*
|
|
1343
|
+
* @param certificate - the DER-encoded certificate to check
|
|
1344
|
+
* @param issuerCertificate - optional issuer certificate; looked
|
|
1345
|
+
* up automatically when omitted
|
|
1346
|
+
* @returns `Good` if not revoked, `BadCertificateRevoked` if the
|
|
1347
|
+
* serial number appears in a CRL,
|
|
1348
|
+
* `BadCertificateRevocationUnknown` if no CRL is available,
|
|
1349
|
+
* or `BadCertificateChainIncomplete` if the issuer cannot be
|
|
1350
|
+
* found.
|
|
1351
|
+
*/
|
|
1352
|
+
async isCertificateRevoked(certificate, issuerCertificate) {
|
|
1353
|
+
if (isSelfSigned3(certificate)) {
|
|
1354
|
+
return "Good" /* Good */;
|
|
1355
|
+
}
|
|
1356
|
+
if (!issuerCertificate) {
|
|
1357
|
+
issuerCertificate = await this.findIssuerCertificate(certificate);
|
|
1358
|
+
}
|
|
1359
|
+
if (!issuerCertificate) {
|
|
1360
|
+
return "BadCertificateChainIncomplete" /* BadCertificateChainIncomplete */;
|
|
1361
|
+
}
|
|
1362
|
+
const crls = this.#findAssociatedCRLs(issuerCertificate);
|
|
1363
|
+
if (!crls) {
|
|
1364
|
+
return "BadCertificateRevocationUnknown" /* BadCertificateRevocationUnknown */;
|
|
1365
|
+
}
|
|
1366
|
+
const certInfo = exploreCertificate(certificate);
|
|
1367
|
+
const serialNumber = certInfo.tbsCertificate.serialNumber || certInfo.tbsCertificate.extensions?.authorityKeyIdentifier?.serial || "";
|
|
1368
|
+
const key = certInfo.tbsCertificate.extensions?.authorityKeyIdentifier?.authorityCertIssuerFingerPrint || "<unknown>";
|
|
1369
|
+
const crl2 = this.#thumbs.crl.get(key) ?? null;
|
|
1370
|
+
if (crls.serialNumbers[serialNumber] || crl2?.serialNumbers[serialNumber]) {
|
|
1371
|
+
return "BadCertificateRevoked" /* BadCertificateRevoked */;
|
|
1372
|
+
}
|
|
1373
|
+
return "Good" /* Good */;
|
|
1374
|
+
}
|
|
1375
|
+
#pendingCrlToProcess = 0;
|
|
1376
|
+
#onCrlProcess;
|
|
1377
|
+
#queue = [];
|
|
1378
|
+
#onCrlFileAdded(index, filename) {
|
|
1379
|
+
this.#queue.push({ index, filename });
|
|
1380
|
+
this.#pendingCrlToProcess += 1;
|
|
1381
|
+
if (this.#pendingCrlToProcess === 1) {
|
|
1382
|
+
this.#processNextCrl();
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
async #processNextCrl() {
|
|
1386
|
+
try {
|
|
1387
|
+
const nextCRL = this.#queue.shift();
|
|
1388
|
+
if (!nextCRL) return;
|
|
1389
|
+
const { index, filename } = nextCRL;
|
|
1390
|
+
const crl = await readCertificateRevocationList(filename);
|
|
1391
|
+
const crlInfo = exploreCertificateRevocationList(crl);
|
|
1392
|
+
debugLog(chalk3.cyan("add CRL in folder "), filename);
|
|
1393
|
+
const fingerprint2 = crlInfo.tbsCertList.issuerFingerprint;
|
|
1394
|
+
if (!index.has(fingerprint2)) {
|
|
1395
|
+
index.set(fingerprint2, { crls: [], serialNumbers: {} });
|
|
1396
|
+
}
|
|
1397
|
+
const data = index.get(fingerprint2) || { crls: [], serialNumbers: {} };
|
|
1398
|
+
data.crls.push({ crlInfo, filename });
|
|
1399
|
+
for (const revokedCertificate of crlInfo.tbsCertList.revokedCertificates) {
|
|
1400
|
+
const serialNumber = revokedCertificate.userCertificate;
|
|
1401
|
+
if (!data.serialNumbers[serialNumber]) {
|
|
1402
|
+
data.serialNumbers[serialNumber] = revokedCertificate.revocationDate;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
debugLog(chalk3.cyan("CRL"), fingerprint2, "serial numbers = ", Object.keys(data.serialNumbers));
|
|
1406
|
+
} catch (err) {
|
|
1407
|
+
debugLog("CRL filename error =");
|
|
1408
|
+
debugLog(err);
|
|
1409
|
+
}
|
|
1410
|
+
this.#pendingCrlToProcess -= 1;
|
|
1411
|
+
if (this.#pendingCrlToProcess === 0) {
|
|
1412
|
+
if (this.#onCrlProcess) {
|
|
1413
|
+
this.#onCrlProcess();
|
|
1414
|
+
this.#onCrlProcess = void 0;
|
|
1415
|
+
}
|
|
1416
|
+
} else {
|
|
1417
|
+
this.#processNextCrl();
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
async #readCertificates() {
|
|
1421
|
+
if (this.#readCertificatesCalled) {
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
this.#readCertificatesCalled = true;
|
|
1425
|
+
const usePolling = process.env.OPCUA_PKI_USE_POLLING === "true";
|
|
1426
|
+
const chokidarOptions = {
|
|
1427
|
+
usePolling,
|
|
1428
|
+
...usePolling ? { interval: Math.min(10 * 60 * 1e3, Math.max(100, this.folderPoolingInterval)) } : {},
|
|
1429
|
+
persistent: false
|
|
1430
|
+
};
|
|
1431
|
+
const createUnreffedWatcher = (folder) => {
|
|
1432
|
+
const capturedHandles = [];
|
|
1433
|
+
const origWatch = fs4.watch;
|
|
1434
|
+
fs4.watch = ((...args) => {
|
|
1435
|
+
const handle = origWatch.apply(fs4, args);
|
|
1436
|
+
capturedHandles.push(handle);
|
|
1437
|
+
return handle;
|
|
1438
|
+
});
|
|
1439
|
+
const w = chokidar.watch(folder, chokidarOptions);
|
|
1440
|
+
const unreffAll = () => {
|
|
1441
|
+
fs4.watch = origWatch;
|
|
1442
|
+
for (const h of capturedHandles) {
|
|
1443
|
+
h.unref();
|
|
1444
|
+
}
|
|
1445
|
+
};
|
|
1446
|
+
return { w, capturedHandles, unreffAll };
|
|
1447
|
+
};
|
|
1448
|
+
const promises = [
|
|
1449
|
+
this.#walkAllFiles(this.trustedFolder, this.#thumbs.trusted, createUnreffedWatcher),
|
|
1450
|
+
this.#walkAllFiles(this.issuersCertFolder, this.#thumbs.issuers.certs, createUnreffedWatcher),
|
|
1451
|
+
this.#walkAllFiles(this.rejectedFolder, this.#thumbs.rejected, createUnreffedWatcher),
|
|
1452
|
+
this.#walkCRLFiles(this.crlFolder, this.#thumbs.crl, createUnreffedWatcher),
|
|
1453
|
+
this.#walkCRLFiles(this.issuersCrlFolder, this.#thumbs.issuersCrl, createUnreffedWatcher)
|
|
1454
|
+
];
|
|
1455
|
+
await Promise.all(promises);
|
|
1456
|
+
await this.#waitAndCheckCRLProcessingStatus();
|
|
1457
|
+
}
|
|
1458
|
+
async #walkCRLFiles(folder, index, createUnreffedWatcher) {
|
|
1459
|
+
await new Promise((resolve, _reject) => {
|
|
1460
|
+
const { w, unreffAll } = createUnreffedWatcher(folder);
|
|
1461
|
+
w.on("unlink", (filename) => {
|
|
1462
|
+
for (const [key, data] of index.entries()) {
|
|
1463
|
+
data.crls = data.crls.filter((c) => c.filename !== filename);
|
|
1464
|
+
if (data.crls.length === 0) {
|
|
1465
|
+
index.delete(key);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
});
|
|
1469
|
+
w.on("add", (filename) => {
|
|
1470
|
+
this.#onCrlFileAdded(index, filename);
|
|
1471
|
+
});
|
|
1472
|
+
w.on("change", (changedPath) => {
|
|
1473
|
+
debugLog("change in folder ", folder, changedPath);
|
|
1474
|
+
});
|
|
1475
|
+
this.#watchers.push(w);
|
|
1476
|
+
w.on("ready", () => {
|
|
1477
|
+
unreffAll();
|
|
1478
|
+
resolve();
|
|
1479
|
+
});
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
async #walkAllFiles(folder, index, createUnreffedWatcher) {
|
|
1483
|
+
const { w, unreffAll } = createUnreffedWatcher(folder);
|
|
1484
|
+
w.on("unlink", (filename) => {
|
|
1485
|
+
debugLog(chalk3.cyan(`unlink in folder ${folder}`), filename);
|
|
1486
|
+
const h = this.#filenameToHash.get(filename);
|
|
1487
|
+
if (h && index.has(h)) {
|
|
1488
|
+
index.delete(h);
|
|
1489
|
+
}
|
|
1490
|
+
});
|
|
1491
|
+
w.on("add", (filename) => {
|
|
1492
|
+
debugLog(chalk3.cyan(`add in folder ${folder}`), filename);
|
|
1493
|
+
try {
|
|
1494
|
+
const certificate = readCertificate(filename);
|
|
1495
|
+
const info = exploreCertificate(certificate);
|
|
1496
|
+
const fingerprint2 = makeFingerprint(certificate);
|
|
1497
|
+
index.set(fingerprint2, { certificate, filename, info });
|
|
1498
|
+
this.#filenameToHash.set(filename, fingerprint2);
|
|
1499
|
+
debugLog(
|
|
1500
|
+
chalk3.magenta("CERT"),
|
|
1501
|
+
info.tbsCertificate.subjectFingerPrint,
|
|
1502
|
+
info.tbsCertificate.serialNumber,
|
|
1503
|
+
info.tbsCertificate.extensions?.authorityKeyIdentifier?.authorityCertIssuerFingerPrint
|
|
1504
|
+
);
|
|
1505
|
+
} catch (err) {
|
|
1506
|
+
debugLog(`Walk files in folder ${folder} with file ${filename}`);
|
|
1507
|
+
debugLog(err);
|
|
1508
|
+
}
|
|
1509
|
+
});
|
|
1510
|
+
w.on("change", (changedPath) => {
|
|
1511
|
+
debugLog(chalk3.cyan(`change in folder ${folder}`), changedPath);
|
|
1512
|
+
try {
|
|
1513
|
+
const certificate = readCertificate(changedPath);
|
|
1514
|
+
const newFingerprint = makeFingerprint(certificate);
|
|
1515
|
+
const oldHash = this.#filenameToHash.get(changedPath);
|
|
1516
|
+
if (oldHash && oldHash !== newFingerprint) {
|
|
1517
|
+
index.delete(oldHash);
|
|
1518
|
+
}
|
|
1519
|
+
index.set(newFingerprint, { certificate, filename: changedPath, info: exploreCertificate(certificate) });
|
|
1520
|
+
this.#filenameToHash.set(changedPath, newFingerprint);
|
|
1521
|
+
} catch (err) {
|
|
1522
|
+
debugLog(`change event: failed to re-read ${changedPath}`, err);
|
|
1523
|
+
}
|
|
1524
|
+
});
|
|
1525
|
+
this.#watchers.push(w);
|
|
1526
|
+
await new Promise((resolve, _reject) => {
|
|
1527
|
+
w.on("ready", () => {
|
|
1528
|
+
unreffAll();
|
|
1529
|
+
debugLog("ready");
|
|
1530
|
+
debugLog([...index.keys()].map((k) => k.substring(0, 10)));
|
|
1531
|
+
resolve();
|
|
1532
|
+
});
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
// make sure that all crls have been processed.
|
|
1536
|
+
async #waitAndCheckCRLProcessingStatus() {
|
|
1537
|
+
return new Promise((resolve, reject) => {
|
|
1538
|
+
if (this.#pendingCrlToProcess === 0) {
|
|
1539
|
+
setImmediate(resolve);
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
if (this.#onCrlProcess) {
|
|
1543
|
+
return reject(new Error("Internal Error"));
|
|
1544
|
+
}
|
|
1545
|
+
this.#onCrlProcess = resolve;
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
});
|
|
1551
|
+
|
|
1552
|
+
// packages/node-opcua-pki/lib/toolbox/index.ts
|
|
1553
|
+
var init_toolbox = __esm({
|
|
1554
|
+
"packages/node-opcua-pki/lib/toolbox/index.ts"() {
|
|
1555
|
+
"use strict";
|
|
1556
|
+
init_esm_shims();
|
|
1557
|
+
init_common();
|
|
1558
|
+
init_common2();
|
|
1559
|
+
init_config();
|
|
1560
|
+
init_debug();
|
|
1561
|
+
init_display();
|
|
1562
|
+
}
|
|
1563
|
+
});
|
|
1564
|
+
|
|
1565
|
+
// packages/node-opcua-pki/lib/toolbox/with_openssl/_env.ts
|
|
1566
|
+
function setEnv(varName, value) {
|
|
1567
|
+
if (!g_config.silent) {
|
|
1568
|
+
warningLog(` set ${varName}=${value}`);
|
|
1569
|
+
}
|
|
1570
|
+
exportedEnvVars[varName] = value;
|
|
1571
|
+
if (["OPENSSL_CONF"].indexOf(varName) >= 0) {
|
|
1572
|
+
process.env[varName] = value;
|
|
1573
|
+
}
|
|
1574
|
+
if (["RANDFILE"].indexOf(varName) >= 0) {
|
|
1575
|
+
process.env[varName] = value;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
function getEnv(varName) {
|
|
1579
|
+
return exportedEnvVars[varName];
|
|
1580
|
+
}
|
|
1581
|
+
function getEnvironmentVarNames() {
|
|
1582
|
+
return Object.keys(exportedEnvVars).map((varName) => {
|
|
1583
|
+
return { key: varName, pattern: `\\$ENV\\:\\:${varName}` };
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
function processAltNames(params) {
|
|
1587
|
+
params.dns = params.dns || [];
|
|
1588
|
+
params.ip = params.ip || [];
|
|
1589
|
+
let subjectAltName = [];
|
|
1590
|
+
subjectAltName.push(`URI:${params.applicationUri}`);
|
|
1591
|
+
subjectAltName = [].concat(
|
|
1592
|
+
subjectAltName,
|
|
1593
|
+
params.dns.map((d) => `DNS:${d}`)
|
|
1594
|
+
);
|
|
1595
|
+
subjectAltName = [].concat(
|
|
1596
|
+
subjectAltName,
|
|
1597
|
+
params.ip.map((d) => `IP:${d}`)
|
|
1598
|
+
);
|
|
1599
|
+
const subjectAltNameString = subjectAltName.join(", ");
|
|
1600
|
+
setEnv("ALTNAME", subjectAltNameString);
|
|
1601
|
+
}
|
|
1602
|
+
var exportedEnvVars;
|
|
1603
|
+
var init_env = __esm({
|
|
1604
|
+
"packages/node-opcua-pki/lib/toolbox/with_openssl/_env.ts"() {
|
|
1605
|
+
"use strict";
|
|
1606
|
+
init_esm_shims();
|
|
1607
|
+
init_config();
|
|
1608
|
+
init_debug();
|
|
1609
|
+
exportedEnvVars = {};
|
|
1610
|
+
}
|
|
1611
|
+
});
|
|
1612
|
+
|
|
1613
|
+
// packages/node-opcua-pki/lib/misc/subject.ts
|
|
1614
|
+
import { Subject as Subject3 } from "node-opcua-crypto";
|
|
1615
|
+
var init_subject = __esm({
|
|
1616
|
+
"packages/node-opcua-pki/lib/misc/subject.ts"() {
|
|
1617
|
+
"use strict";
|
|
1618
|
+
init_esm_shims();
|
|
1619
|
+
}
|
|
1620
|
+
});
|
|
1621
|
+
|
|
1622
|
+
// packages/node-opcua-pki/lib/toolbox/with_openssl/install_prerequisite.ts
|
|
1623
|
+
import child_process from "child_process";
|
|
1624
|
+
import fs5 from "fs";
|
|
1625
|
+
import os2 from "os";
|
|
1626
|
+
import path3 from "path";
|
|
1627
|
+
import url from "url";
|
|
1628
|
+
import byline from "byline";
|
|
1629
|
+
import chalk4 from "chalk";
|
|
1630
|
+
import ProgressBar from "progress";
|
|
1631
|
+
import wget from "wget-improved-2";
|
|
1632
|
+
import yauzl from "yauzl";
|
|
1633
|
+
function makeOptions() {
|
|
1634
|
+
const proxy = process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy || void 0;
|
|
1635
|
+
if (proxy) {
|
|
1636
|
+
const a = new url.URL(proxy);
|
|
1637
|
+
const auth = a.username ? `${a.username}:${a.password}` : void 0;
|
|
1638
|
+
const options = {
|
|
1639
|
+
proxy: {
|
|
1640
|
+
port: a.port ? parseInt(a.port, 10) : 80,
|
|
1641
|
+
protocol: a.protocol.replace(":", ""),
|
|
1642
|
+
host: a.hostname ?? "",
|
|
1643
|
+
proxyAuth: auth
|
|
1644
|
+
}
|
|
1645
|
+
};
|
|
1646
|
+
warningLog(chalk4.green("- using proxy "), proxy);
|
|
1647
|
+
warningLog(options);
|
|
1648
|
+
return options;
|
|
1649
|
+
}
|
|
1650
|
+
return {};
|
|
1651
|
+
}
|
|
1652
|
+
async function execute(cmd, cwd) {
|
|
1653
|
+
let output = "";
|
|
1654
|
+
const options = {
|
|
1655
|
+
cwd,
|
|
1656
|
+
windowsHide: true
|
|
1657
|
+
};
|
|
1658
|
+
return await new Promise((resolve, reject) => {
|
|
1659
|
+
const child = child_process.exec(
|
|
1660
|
+
cmd,
|
|
1661
|
+
options,
|
|
1662
|
+
(err) => {
|
|
1663
|
+
const exitCode = err === null ? 0 : err.code || 1;
|
|
1664
|
+
if (err) reject(err);
|
|
1665
|
+
else {
|
|
1666
|
+
resolve({ exitCode, output });
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
);
|
|
1670
|
+
const stream1 = byline(child.stdout);
|
|
1671
|
+
stream1.on("data", (line) => {
|
|
1672
|
+
output += `${line}
|
|
1673
|
+
`;
|
|
1674
|
+
if (doDebug2) {
|
|
1675
|
+
process.stdout.write(` stdout ${chalk4.yellow(line)}
|
|
1676
|
+
`);
|
|
1677
|
+
}
|
|
1678
|
+
});
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
function quote2(str) {
|
|
1682
|
+
return `"${str.replace(/\\/g, "/")}"`;
|
|
1683
|
+
}
|
|
1684
|
+
function is_expected_openssl_version(strVersion) {
|
|
1685
|
+
return !!strVersion.match(/OpenSSL 1|3/);
|
|
1686
|
+
}
|
|
1687
|
+
async function getopensslExecPath() {
|
|
1688
|
+
let result1;
|
|
1689
|
+
try {
|
|
1690
|
+
result1 = await execute("which openssl");
|
|
1691
|
+
} catch (err) {
|
|
1692
|
+
warningLog("warning: ", err.message);
|
|
1693
|
+
throw new Error("Cannot find openssl");
|
|
1694
|
+
}
|
|
1695
|
+
const exitCode = result1?.exitCode;
|
|
1696
|
+
const output = result1?.output;
|
|
1697
|
+
if (exitCode !== 0) {
|
|
1698
|
+
warningLog(chalk4.yellow(" it seems that ") + chalk4.cyan("openssl") + chalk4.yellow(" is not installed on your computer "));
|
|
1699
|
+
warningLog(chalk4.yellow("Please install it before running this programs"));
|
|
1700
|
+
throw new Error("Cannot find openssl");
|
|
1701
|
+
}
|
|
1702
|
+
const opensslExecPath = output.replace(/\n\r/g, "").trim();
|
|
1703
|
+
return opensslExecPath;
|
|
1704
|
+
}
|
|
1705
|
+
async function check_system_openssl_version() {
|
|
1706
|
+
const opensslExecPath = await getopensslExecPath();
|
|
1707
|
+
const q_opensslExecPath = quote2(opensslExecPath);
|
|
1708
|
+
if (doDebug2) {
|
|
1709
|
+
warningLog(` OpenSSL found in : ${chalk4.yellow(opensslExecPath)}`);
|
|
1710
|
+
}
|
|
1711
|
+
const result = await execute(`${q_opensslExecPath} version`);
|
|
1712
|
+
const exitCode = result?.exitCode;
|
|
1713
|
+
const output = result?.output;
|
|
1714
|
+
const version = output.trim();
|
|
1715
|
+
const versionOK = exitCode === 0 && is_expected_openssl_version(version);
|
|
1716
|
+
if (!versionOK) {
|
|
1717
|
+
let message = chalk4.whiteBright("Warning !!!!!!!!!!!! ") + "\nyour version of openssl is " + version + ". It doesn't match the expected version";
|
|
1718
|
+
if (process.platform === "darwin") {
|
|
1719
|
+
message += chalk4.cyan("\nplease refer to :") + chalk4.yellow(" https://github.com/node-opcua/node-opcua/wiki/installing-node-opcua-or-node-red-on-MacOS");
|
|
1720
|
+
}
|
|
1721
|
+
console.log(message);
|
|
1722
|
+
}
|
|
1723
|
+
return output;
|
|
1724
|
+
}
|
|
1725
|
+
async function install_and_check_win32_openssl_version() {
|
|
1726
|
+
const downloadFolder = path3.join(os2.tmpdir(), ".");
|
|
1727
|
+
function get_openssl_folder_win32() {
|
|
1728
|
+
if (process.env.LOCALAPPDATA) {
|
|
1729
|
+
const userProgramFolder = path3.join(process.env.LOCALAPPDATA, "Programs");
|
|
1730
|
+
if (fs5.existsSync(userProgramFolder)) {
|
|
1731
|
+
return path3.join(userProgramFolder, "openssl");
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
return path3.join(process.cwd(), "openssl");
|
|
1735
|
+
}
|
|
1736
|
+
function get_openssl_exec_path_win32() {
|
|
1737
|
+
const opensslFolder2 = get_openssl_folder_win32();
|
|
1738
|
+
return path3.join(opensslFolder2, "openssl.exe");
|
|
1739
|
+
}
|
|
1740
|
+
async function check_openssl_win32() {
|
|
1741
|
+
const opensslExecPath2 = get_openssl_exec_path_win32();
|
|
1742
|
+
const exists = fs5.existsSync(opensslExecPath2);
|
|
1743
|
+
if (!exists) {
|
|
1744
|
+
warningLog("checking presence of ", opensslExecPath2);
|
|
1745
|
+
warningLog(chalk4.red(" cannot find file ") + opensslExecPath2);
|
|
1746
|
+
return {
|
|
1747
|
+
opensslOk: false,
|
|
1748
|
+
version: `cannot find file ${opensslExecPath2}`
|
|
1749
|
+
};
|
|
1750
|
+
} else {
|
|
1751
|
+
const q_openssl_exe_path = quote2(opensslExecPath2);
|
|
1752
|
+
const cwd = ".";
|
|
1753
|
+
const { exitCode, output } = await execute(`${q_openssl_exe_path} version`, cwd);
|
|
1754
|
+
const version = output.trim();
|
|
1755
|
+
if (doDebug2) {
|
|
1756
|
+
warningLog(" Version = ", version);
|
|
1757
|
+
}
|
|
1758
|
+
return {
|
|
1759
|
+
opensslOk: exitCode === 0 && is_expected_openssl_version(version),
|
|
1760
|
+
version
|
|
1761
|
+
};
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
function win32or64() {
|
|
1765
|
+
if (process.env.PROCESSOR_ARCHITECTURE === "x86" && process.env.PROCESSOR_ARCHITEW6432) {
|
|
1766
|
+
return 64;
|
|
1767
|
+
}
|
|
1768
|
+
if (process.env.PROCESSOR_ARCHITECTURE === "AMD64") {
|
|
1769
|
+
return 64;
|
|
1770
|
+
}
|
|
1771
|
+
if (process.env.CURRENT_CPU === "x64") {
|
|
1772
|
+
return 64;
|
|
1773
|
+
}
|
|
1774
|
+
return 32;
|
|
1775
|
+
}
|
|
1776
|
+
async function download_openssl() {
|
|
1777
|
+
const url2 = 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";
|
|
1778
|
+
const outputFilename = path3.join(downloadFolder, path3.basename(url2));
|
|
1779
|
+
warningLog(`downloading ${chalk4.yellow(url2)} to ${outputFilename}`);
|
|
1780
|
+
if (fs5.existsSync(outputFilename)) {
|
|
1781
|
+
return { downloadedFile: outputFilename };
|
|
1782
|
+
}
|
|
1783
|
+
const options = makeOptions();
|
|
1784
|
+
const bar = new ProgressBar(chalk4.cyan("[:bar]") + chalk4.cyan(" :percent ") + chalk4.white(":etas"), {
|
|
1785
|
+
complete: "=",
|
|
1786
|
+
incomplete: " ",
|
|
1787
|
+
total: 100,
|
|
1788
|
+
width: 100
|
|
1789
|
+
});
|
|
1790
|
+
return await new Promise((resolve, reject) => {
|
|
1791
|
+
const download = wget.download(url2, outputFilename, options);
|
|
1792
|
+
download.on("error", (err) => {
|
|
1793
|
+
warningLog(err);
|
|
1794
|
+
setImmediate(() => {
|
|
1795
|
+
reject(err);
|
|
1796
|
+
});
|
|
1797
|
+
});
|
|
1798
|
+
download.on("end", (output) => {
|
|
1799
|
+
if (doDebug2) {
|
|
1800
|
+
warningLog(output);
|
|
1801
|
+
}
|
|
1802
|
+
resolve({ downloadedFile: outputFilename });
|
|
1803
|
+
});
|
|
1804
|
+
download.on("progress", (progress) => {
|
|
1805
|
+
bar.update(progress);
|
|
1806
|
+
});
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
async function unzip_openssl(zipFilename) {
|
|
1810
|
+
const opensslFolder2 = get_openssl_folder_win32();
|
|
1811
|
+
const zipFile = await new Promise((resolve, reject) => {
|
|
1812
|
+
yauzl.open(zipFilename, { lazyEntries: true }, (err, zipfile) => {
|
|
1813
|
+
if (err) {
|
|
1814
|
+
reject(err);
|
|
1815
|
+
} else {
|
|
1816
|
+
if (!zipfile) {
|
|
1817
|
+
reject(new Error("zipfile is null"));
|
|
1818
|
+
} else {
|
|
1819
|
+
resolve(zipfile);
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
});
|
|
1823
|
+
});
|
|
1824
|
+
zipFile.readEntry();
|
|
1825
|
+
await new Promise((resolve, reject) => {
|
|
1826
|
+
zipFile.on("end", (err) => {
|
|
1827
|
+
setImmediate(() => {
|
|
1828
|
+
if (doDebug2) {
|
|
1829
|
+
warningLog("unzip done");
|
|
1830
|
+
}
|
|
1831
|
+
if (err) {
|
|
1832
|
+
reject(err);
|
|
1833
|
+
} else {
|
|
1834
|
+
resolve();
|
|
1835
|
+
}
|
|
1836
|
+
});
|
|
1837
|
+
});
|
|
1838
|
+
zipFile.on("entry", (entry) => {
|
|
1839
|
+
zipFile.openReadStream(entry, (err, readStream) => {
|
|
1840
|
+
if (err) {
|
|
1841
|
+
return reject(err);
|
|
1842
|
+
}
|
|
1843
|
+
const file = path3.join(opensslFolder2, entry.fileName);
|
|
1844
|
+
if (doDebug2) {
|
|
1845
|
+
warningLog(" unzipping :", file);
|
|
1846
|
+
}
|
|
1847
|
+
const writeStream = fs5.createWriteStream(file, "binary");
|
|
1848
|
+
readStream?.pipe(writeStream);
|
|
1849
|
+
writeStream.on("close", () => {
|
|
1850
|
+
zipFile.readEntry();
|
|
1851
|
+
});
|
|
1852
|
+
});
|
|
1853
|
+
});
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
const opensslFolder = get_openssl_folder_win32();
|
|
1857
|
+
const opensslExecPath = get_openssl_exec_path_win32();
|
|
1858
|
+
if (!fs5.existsSync(opensslFolder)) {
|
|
1859
|
+
if (doDebug2) {
|
|
1860
|
+
warningLog("creating openssl_folder", opensslFolder);
|
|
1861
|
+
}
|
|
1862
|
+
fs5.mkdirSync(opensslFolder);
|
|
1863
|
+
}
|
|
1864
|
+
const { opensslOk, version: _version } = await check_openssl_win32();
|
|
1865
|
+
if (!opensslOk) {
|
|
1866
|
+
warningLog(chalk4.yellow("openssl seems to be missing and need to be installed"));
|
|
1867
|
+
const { downloadedFile } = await download_openssl();
|
|
1868
|
+
if (doDebug2) {
|
|
1869
|
+
warningLog("deflating ", chalk4.yellow(downloadedFile));
|
|
1870
|
+
}
|
|
1871
|
+
await unzip_openssl(downloadedFile);
|
|
1872
|
+
const opensslExists = !!fs5.existsSync(opensslExecPath);
|
|
1873
|
+
if (doDebug2) {
|
|
1874
|
+
warningLog("verifying ", opensslExists, opensslExists ? chalk4.green("OK ") : chalk4.red(" Error"), opensslExecPath);
|
|
1875
|
+
}
|
|
1876
|
+
const _opensslExecPath2 = await check_openssl_win32();
|
|
1877
|
+
return opensslExecPath;
|
|
1878
|
+
} else {
|
|
1879
|
+
if (doDebug2) {
|
|
1880
|
+
warningLog(chalk4.green("openssl is already installed and have the expected version."));
|
|
1881
|
+
}
|
|
1882
|
+
return opensslExecPath;
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
async function install_prerequisite() {
|
|
1886
|
+
if (process.platform !== "win32") {
|
|
1887
|
+
return await check_system_openssl_version();
|
|
1888
|
+
} else {
|
|
1889
|
+
return await install_and_check_win32_openssl_version();
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
async function get_openssl_exec_path() {
|
|
1893
|
+
if (process.platform === "win32") {
|
|
1894
|
+
const opensslExecPath = await install_prerequisite();
|
|
1895
|
+
if (!fs5.existsSync(opensslExecPath)) {
|
|
1896
|
+
throw new Error(`internal error cannot find ${opensslExecPath}`);
|
|
1897
|
+
}
|
|
1898
|
+
return opensslExecPath;
|
|
1899
|
+
} else {
|
|
1900
|
+
return "openssl";
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
var doDebug2;
|
|
1904
|
+
var init_install_prerequisite = __esm({
|
|
1905
|
+
"packages/node-opcua-pki/lib/toolbox/with_openssl/install_prerequisite.ts"() {
|
|
1906
|
+
"use strict";
|
|
1907
|
+
init_esm_shims();
|
|
1908
|
+
init_debug();
|
|
1909
|
+
doDebug2 = process.env.NODEOPCUAPKIDEBUG || false;
|
|
1910
|
+
}
|
|
1911
|
+
});
|
|
1912
|
+
|
|
1913
|
+
// packages/node-opcua-pki/lib/toolbox/with_openssl/execute_openssl.ts
|
|
1914
|
+
import assert6 from "assert";
|
|
1915
|
+
import child_process2 from "child_process";
|
|
1916
|
+
import fs6 from "fs";
|
|
1917
|
+
import os3 from "os";
|
|
1918
|
+
import byline2 from "byline";
|
|
1919
|
+
import chalk5 from "chalk";
|
|
1920
|
+
async function execute2(cmd, options) {
|
|
1921
|
+
const from = new Error();
|
|
1922
|
+
options.cwd = options.cwd || process.cwd();
|
|
1923
|
+
if (!g_config.silent) {
|
|
1924
|
+
warningLog(chalk5.cyan(" CWD "), options.cwd);
|
|
1925
|
+
}
|
|
1926
|
+
const outputs = [];
|
|
1927
|
+
return await new Promise((resolve, reject) => {
|
|
1928
|
+
const child = child_process2.exec(
|
|
1929
|
+
cmd,
|
|
1930
|
+
{
|
|
1931
|
+
cwd: options.cwd,
|
|
1932
|
+
windowsHide: true
|
|
1933
|
+
},
|
|
1934
|
+
(err) => {
|
|
1935
|
+
if (err) {
|
|
1936
|
+
if (!options.hideErrorMessage) {
|
|
1937
|
+
const fence = "###########################################";
|
|
1938
|
+
console.error(chalk5.bgWhiteBright.redBright(`${fence} OPENSSL ERROR ${fence}`));
|
|
1939
|
+
console.error(chalk5.bgWhiteBright.redBright(`CWD = ${options.cwd}`));
|
|
1940
|
+
console.error(chalk5.bgWhiteBright.redBright(err.message));
|
|
1941
|
+
console.error(chalk5.bgWhiteBright.redBright(`${fence} OPENSSL ERROR ${fence}`));
|
|
1942
|
+
console.error(from.stack);
|
|
1943
|
+
}
|
|
1944
|
+
reject(new Error(err.message));
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
resolve(outputs.join(""));
|
|
1948
|
+
}
|
|
1949
|
+
);
|
|
1950
|
+
if (child.stdout) {
|
|
1951
|
+
const stream2 = byline2(child.stdout);
|
|
1952
|
+
stream2.on("data", (line) => {
|
|
1953
|
+
outputs.push(`${line}
|
|
1954
|
+
`);
|
|
1955
|
+
});
|
|
1956
|
+
if (!g_config.silent) {
|
|
1957
|
+
stream2.on("data", (line) => {
|
|
1958
|
+
line = line.toString();
|
|
1959
|
+
if (doDebug) {
|
|
1960
|
+
process.stdout.write(`${chalk5.white(" stdout ") + chalk5.whiteBright(line)}
|
|
1961
|
+
`);
|
|
1962
|
+
}
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
if (!g_config.silent) {
|
|
1967
|
+
if (child.stderr) {
|
|
1968
|
+
const stream1 = byline2(child.stderr);
|
|
1969
|
+
stream1.on("data", (line) => {
|
|
1970
|
+
line = line.toString();
|
|
1971
|
+
if (displayError) {
|
|
1972
|
+
process.stdout.write(`${chalk5.white(" stderr ") + chalk5.red(line)}
|
|
1973
|
+
`);
|
|
1974
|
+
}
|
|
1975
|
+
});
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1980
|
+
async function find_openssl() {
|
|
1981
|
+
return await get_openssl_exec_path();
|
|
1982
|
+
}
|
|
1983
|
+
async function ensure_openssl_installed() {
|
|
1984
|
+
if (!opensslPath) {
|
|
1985
|
+
opensslPath = await find_openssl();
|
|
1986
|
+
const outputs = await execute_openssl("version", { cwd: "." });
|
|
1987
|
+
g_config.opensslVersion = outputs.trim();
|
|
1988
|
+
if (doDebug) {
|
|
1989
|
+
warningLog("OpenSSL version : ", g_config.opensslVersion);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
async function execute_openssl_no_failure(cmd, options) {
|
|
1994
|
+
options = options || {};
|
|
1995
|
+
options.hideErrorMessage = true;
|
|
1996
|
+
try {
|
|
1997
|
+
return await execute_openssl(cmd, options);
|
|
1998
|
+
} catch (err) {
|
|
1999
|
+
debugLog(" (ignored error = ERROR : )", err.message);
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
function getTempFolder() {
|
|
2003
|
+
return os3.tmpdir();
|
|
2004
|
+
}
|
|
2005
|
+
async function execute_openssl(cmd, options) {
|
|
2006
|
+
debugLog("execute_openssl", cmd, options);
|
|
2007
|
+
const empty_config_file = n(getTempFolder(), "empty_config.cnf");
|
|
2008
|
+
if (!fs6.existsSync(empty_config_file)) {
|
|
2009
|
+
await fs6.promises.writeFile(empty_config_file, "# empty config file");
|
|
2010
|
+
}
|
|
2011
|
+
options = options || {};
|
|
2012
|
+
options.openssl_conf = options.openssl_conf || empty_config_file;
|
|
2013
|
+
assert6(options.openssl_conf);
|
|
2014
|
+
setEnv("OPENSSL_CONF", options.openssl_conf);
|
|
2015
|
+
if (!g_config.silent) {
|
|
2016
|
+
warningLog(chalk5.cyan(" OPENSSL_CONF"), process.env.OPENSSL_CONF);
|
|
2017
|
+
warningLog(chalk5.cyan(" RANDFILE "), process.env.RANDFILE);
|
|
2018
|
+
warningLog(chalk5.cyan(" CMD openssl "), chalk5.cyanBright(cmd));
|
|
2019
|
+
}
|
|
2020
|
+
await ensure_openssl_installed();
|
|
2021
|
+
return await execute2(`${quote(opensslPath)} ${cmd}`, options);
|
|
2022
|
+
}
|
|
2023
|
+
var opensslPath, n;
|
|
2024
|
+
var init_execute_openssl = __esm({
|
|
2025
|
+
"packages/node-opcua-pki/lib/toolbox/with_openssl/execute_openssl.ts"() {
|
|
2026
|
+
"use strict";
|
|
2027
|
+
init_esm_shims();
|
|
2028
|
+
init_common();
|
|
2029
|
+
init_common2();
|
|
2030
|
+
init_config();
|
|
2031
|
+
init_debug();
|
|
2032
|
+
init_env();
|
|
2033
|
+
init_install_prerequisite();
|
|
2034
|
+
n = makePath;
|
|
2035
|
+
}
|
|
2036
|
+
});
|
|
2037
|
+
|
|
2038
|
+
// packages/node-opcua-pki/lib/toolbox/with_openssl/toolbox.ts
|
|
2039
|
+
import assert7 from "assert";
|
|
2040
|
+
import fs7 from "fs";
|
|
2041
|
+
import path4 from "path";
|
|
2042
|
+
function openssl_require2DigitYearInDate() {
|
|
2043
|
+
if (!g_config.opensslVersion) {
|
|
2044
|
+
throw new Error(
|
|
2045
|
+
"openssl_require2DigitYearInDate : openssl version is not known: please call ensure_openssl_installed()"
|
|
2046
|
+
);
|
|
2047
|
+
}
|
|
2048
|
+
return g_config.opensslVersion.match(/OpenSSL 0\.9/);
|
|
2049
|
+
}
|
|
2050
|
+
function generateStaticConfig(configPath, options) {
|
|
2051
|
+
const prePath = options?.cwd || "";
|
|
2052
|
+
const originalFilename = !path4.isAbsolute(configPath) ? path4.join(prePath, configPath) : configPath;
|
|
2053
|
+
let staticConfig = fs7.readFileSync(originalFilename, { encoding: "utf8" });
|
|
2054
|
+
for (const envVar of getEnvironmentVarNames()) {
|
|
2055
|
+
staticConfig = staticConfig.replace(new RegExp(envVar.pattern, "gi"), getEnv(envVar.key));
|
|
2056
|
+
}
|
|
2057
|
+
const staticConfigPath = `${configPath}.${process.pid}-${_counter++}.tmp`;
|
|
2058
|
+
const temporaryConfigPath = !path4.isAbsolute(configPath) ? path4.join(prePath, staticConfigPath) : staticConfigPath;
|
|
2059
|
+
fs7.writeFileSync(temporaryConfigPath, staticConfig);
|
|
2060
|
+
if (options?.cwd) {
|
|
2061
|
+
return path4.relative(options.cwd, temporaryConfigPath);
|
|
2062
|
+
} else {
|
|
2063
|
+
return temporaryConfigPath;
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
async function getPublicKeyFromPrivateKey(privateKeyFilename, publicKeyFilename) {
|
|
2067
|
+
assert7(fs7.existsSync(privateKeyFilename));
|
|
2068
|
+
await execute_openssl(`rsa -pubout -in ${q(n2(privateKeyFilename))} -out ${q(n2(publicKeyFilename))}`, {});
|
|
2069
|
+
}
|
|
2070
|
+
function x509Date(date) {
|
|
2071
|
+
date = date || /* @__PURE__ */ new Date();
|
|
2072
|
+
const Y = date.getUTCFullYear();
|
|
2073
|
+
const M = date.getUTCMonth() + 1;
|
|
2074
|
+
const D = date.getUTCDate();
|
|
2075
|
+
const h = date.getUTCHours();
|
|
2076
|
+
const m = date.getUTCMinutes();
|
|
2077
|
+
const s = date.getUTCSeconds();
|
|
2078
|
+
function w(s2, l) {
|
|
2079
|
+
return `${s2}`.padStart(l, "0");
|
|
2080
|
+
}
|
|
2081
|
+
if (openssl_require2DigitYearInDate()) {
|
|
2082
|
+
return `${w(Y, 2) + w(M, 2) + w(D, 2) + w(h, 2) + w(m, 2) + w(s, 2)}Z`;
|
|
2083
|
+
} else {
|
|
2084
|
+
return `${w(Y, 4) + w(M, 2) + w(D, 2) + w(h, 2) + w(m, 2) + w(s, 2)}Z`;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
async function dumpCertificate(certificate) {
|
|
2088
|
+
assert7(fs7.existsSync(certificate));
|
|
2089
|
+
return await execute_openssl(`x509 -in ${q(n2(certificate))} -text -noout`, {});
|
|
2090
|
+
}
|
|
2091
|
+
async function toDer(certificatePem) {
|
|
2092
|
+
assert7(fs7.existsSync(certificatePem));
|
|
2093
|
+
const certificateDer = certificatePem.replace(".pem", ".der");
|
|
2094
|
+
return await execute_openssl(`x509 -outform der -in ${certificatePem} -out ${certificateDer}`, {});
|
|
2095
|
+
}
|
|
2096
|
+
async function fingerprint(certificatePem) {
|
|
2097
|
+
assert7(fs7.existsSync(certificatePem));
|
|
2098
|
+
return await execute_openssl(`x509 -fingerprint -noout -in ${certificatePem}`, {});
|
|
2099
|
+
}
|
|
2100
|
+
var _counter, q, n2;
|
|
2101
|
+
var init_toolbox2 = __esm({
|
|
2102
|
+
"packages/node-opcua-pki/lib/toolbox/with_openssl/toolbox.ts"() {
|
|
2103
|
+
"use strict";
|
|
2104
|
+
init_esm_shims();
|
|
2105
|
+
init_common();
|
|
2106
|
+
init_common2();
|
|
2107
|
+
init_config();
|
|
2108
|
+
init_env();
|
|
2109
|
+
init_execute_openssl();
|
|
2110
|
+
g_config.opensslVersion = "";
|
|
2111
|
+
_counter = 0;
|
|
2112
|
+
q = quote;
|
|
2113
|
+
n2 = makePath;
|
|
2114
|
+
}
|
|
2115
|
+
});
|
|
2116
|
+
|
|
2117
|
+
// packages/node-opcua-pki/lib/toolbox/with_openssl/create_certificate_signing_request.ts
|
|
2118
|
+
import assert8 from "assert";
|
|
2119
|
+
import fs8 from "fs";
|
|
2120
|
+
import path5 from "path";
|
|
2121
|
+
async function createCertificateSigningRequestWithOpenSSL(certificateSigningRequestFilename, params) {
|
|
2122
|
+
assert8(params);
|
|
2123
|
+
assert8(params.rootDir);
|
|
2124
|
+
assert8(params.configFile);
|
|
2125
|
+
assert8(params.privateKey);
|
|
2126
|
+
assert8(typeof params.privateKey === "string");
|
|
2127
|
+
assert8(fs8.existsSync(params.configFile), `config file must exist ${params.configFile}`);
|
|
2128
|
+
assert8(fs8.existsSync(params.privateKey), `Private key must exist${params.privateKey}`);
|
|
2129
|
+
assert8(fs8.existsSync(params.rootDir), "RootDir key must exist");
|
|
2130
|
+
assert8(typeof certificateSigningRequestFilename === "string");
|
|
2131
|
+
processAltNames(params);
|
|
2132
|
+
const configFile = generateStaticConfig(params.configFile, { cwd: params.rootDir });
|
|
2133
|
+
const options = { cwd: params.rootDir, openssl_conf: path5.relative(params.rootDir, configFile) };
|
|
2134
|
+
const configOption = ` -config ${q2(n3(configFile))}`;
|
|
2135
|
+
const subject = params.subject ? new Subject3(params.subject).toString() : void 0;
|
|
2136
|
+
const subjectOptions = subject ? ` -subj "${subject}"` : "";
|
|
2137
|
+
displaySubtitle("- Creating a Certificate Signing Request with openssl");
|
|
2138
|
+
await execute_openssl(
|
|
2139
|
+
"req -new -sha256 -batch -text " + configOption + " -key " + q2(n3(params.privateKey)) + subjectOptions + " -out " + q2(n3(certificateSigningRequestFilename)),
|
|
2140
|
+
options
|
|
2141
|
+
);
|
|
2142
|
+
}
|
|
2143
|
+
var q2, n3;
|
|
2144
|
+
var init_create_certificate_signing_request2 = __esm({
|
|
2145
|
+
"packages/node-opcua-pki/lib/toolbox/with_openssl/create_certificate_signing_request.ts"() {
|
|
2146
|
+
"use strict";
|
|
2147
|
+
init_esm_shims();
|
|
2148
|
+
init_subject();
|
|
2149
|
+
init_common();
|
|
2150
|
+
init_common2();
|
|
2151
|
+
init_display();
|
|
2152
|
+
init_env();
|
|
2153
|
+
init_execute_openssl();
|
|
2154
|
+
init_toolbox2();
|
|
2155
|
+
q2 = quote;
|
|
2156
|
+
n3 = makePath;
|
|
2157
|
+
}
|
|
2158
|
+
});
|
|
2159
|
+
|
|
2160
|
+
// packages/node-opcua-pki/lib/toolbox/with_openssl/index.ts
|
|
2161
|
+
import exp from "constants";
|
|
2162
|
+
var init_with_openssl = __esm({
|
|
2163
|
+
"packages/node-opcua-pki/lib/toolbox/with_openssl/index.ts"() {
|
|
2164
|
+
"use strict";
|
|
2165
|
+
init_esm_shims();
|
|
2166
|
+
init_env();
|
|
2167
|
+
init_create_certificate_signing_request2();
|
|
2168
|
+
init_execute_openssl();
|
|
2169
|
+
init_install_prerequisite();
|
|
2170
|
+
init_toolbox2();
|
|
2171
|
+
}
|
|
2172
|
+
});
|
|
2173
|
+
|
|
2174
|
+
// packages/node-opcua-pki/lib/ca/templates/ca_config_template.cnf.ts
|
|
2175
|
+
var config2, ca_config_template_cnf_default;
|
|
2176
|
+
var init_ca_config_template_cnf = __esm({
|
|
2177
|
+
"packages/node-opcua-pki/lib/ca/templates/ca_config_template.cnf.ts"() {
|
|
2178
|
+
"use strict";
|
|
2179
|
+
init_esm_shims();
|
|
2180
|
+
config2 = `#.........DO NOT MODIFY BY HAND .........................
|
|
2181
|
+
[ ca ]
|
|
2182
|
+
default_ca = CA_default
|
|
2183
|
+
[ CA_default ]
|
|
2184
|
+
dir = %%ROOT_FOLDER%% # the main CA folder
|
|
2185
|
+
certs = $dir/certs # where to store certificates
|
|
2186
|
+
new_certs_dir = $dir/certs #
|
|
2187
|
+
database = $dir/index.txt # the certificate database
|
|
2188
|
+
serial = $dir/serial # the serial number counter
|
|
2189
|
+
certificate = $dir/public/cacert.pem # The root CA certificate
|
|
2190
|
+
private_key = $dir/private/cakey.pem # the CA private key
|
|
2191
|
+
x509_extensions = usr_cert #
|
|
2192
|
+
default_days = 3650 # default validity : 10 years
|
|
2193
|
+
|
|
2194
|
+
# default_md = sha1
|
|
2195
|
+
|
|
2196
|
+
default_md = sha256 # The default digest algorithm
|
|
2197
|
+
|
|
2198
|
+
preserve = no
|
|
2199
|
+
policy = policy_match
|
|
2200
|
+
# randfile = $dir/random.rnd
|
|
2201
|
+
# default_startdate = YYMMDDHHMMSSZ
|
|
2202
|
+
# default_enddate = YYMMDDHHMMSSZ
|
|
2203
|
+
crl_dir = $dir/crl
|
|
2204
|
+
crl_extensions = crl_ext
|
|
2205
|
+
crl = $dir/revocation_list.crl # the Revocation list
|
|
2206
|
+
crlnumber = $dir/crlnumber # CRL number file
|
|
2207
|
+
default_crl_days = 30
|
|
2208
|
+
default_crl_hours = 24
|
|
2209
|
+
#msie_hack
|
|
2210
|
+
|
|
2211
|
+
[ policy_match ]
|
|
2212
|
+
countryName = optional
|
|
2213
|
+
stateOrProvinceName = optional
|
|
2214
|
+
localityName = optional
|
|
2215
|
+
organizationName = optional
|
|
2216
|
+
organizationalUnitName = optional
|
|
2217
|
+
commonName = optional
|
|
2218
|
+
emailAddress = optional
|
|
2219
|
+
|
|
2220
|
+
[ req ]
|
|
2221
|
+
default_bits = 4096 # Size of keys
|
|
2222
|
+
default_keyfile = key.pem # name of generated keys
|
|
2223
|
+
distinguished_name = req_distinguished_name
|
|
2224
|
+
attributes = req_attributes
|
|
2225
|
+
x509_extensions = v3_ca
|
|
2226
|
+
#input_password
|
|
2227
|
+
#output_password
|
|
2228
|
+
string_mask = nombstr # permitted characters
|
|
2229
|
+
req_extensions = v3_req
|
|
2230
|
+
|
|
2231
|
+
[ req_distinguished_name ]
|
|
2232
|
+
|
|
2233
|
+
#0 countryName = Country Name (2 letter code)
|
|
2234
|
+
# countryName_default = FR
|
|
2235
|
+
# countryName_min = 2
|
|
2236
|
+
# countryName_max = 2
|
|
2237
|
+
# stateOrProvinceName = State or Province Name (full name)
|
|
2238
|
+
# stateOrProvinceName_default = Ile de France
|
|
2239
|
+
# localityName = Locality Name (city, district)
|
|
2240
|
+
# localityName_default = Paris
|
|
2241
|
+
organizationName = Organization Name (company)
|
|
2242
|
+
organizationName_default = NodeOPCUA
|
|
2243
|
+
# organizationalUnitName = Organizational Unit Name (department, division)
|
|
2244
|
+
# organizationalUnitName_default = R&D
|
|
2245
|
+
commonName = Common Name (hostname, FQDN, IP, or your name)
|
|
2246
|
+
commonName_max = 256
|
|
2247
|
+
commonName_default = NodeOPCUA
|
|
2248
|
+
# emailAddress = Email Address
|
|
2249
|
+
# emailAddress_max = 40
|
|
2250
|
+
# emailAddress_default = node-opcua (at) node-opcua (dot) com
|
|
2251
|
+
|
|
2252
|
+
[ req_attributes ]
|
|
2253
|
+
#challengePassword = A challenge password
|
|
2254
|
+
#challengePassword_min = 4
|
|
2255
|
+
#challengePassword_max = 20
|
|
2256
|
+
#unstructuredName = An optional company name
|
|
2257
|
+
[ usr_cert ]
|
|
2258
|
+
basicConstraints = critical, CA:FALSE
|
|
2259
|
+
subjectKeyIdentifier = hash
|
|
2260
|
+
authorityKeyIdentifier = keyid,issuer:always
|
|
2261
|
+
#authorityKeyIdentifier = keyid
|
|
2262
|
+
subjectAltName = $ENV::ALTNAME
|
|
2263
|
+
# issuerAltName = issuer:copy
|
|
2264
|
+
nsComment = ''OpenSSL Generated Certificate''
|
|
2265
|
+
#nsCertType = client, email, objsign for ''everything including object signing''
|
|
2266
|
+
#nsCaRevocationUrl = http://www.domain.dom/ca-crl.pem
|
|
2267
|
+
#nsBaseUrl =
|
|
2268
|
+
#nsRenewalUrl =
|
|
2269
|
+
#nsCaPolicyUrl =
|
|
2270
|
+
#nsSslServerName =
|
|
2271
|
+
keyUsage = critical, digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment, keyAgreement, keyCertSign
|
|
2272
|
+
extendedKeyUsage = critical,serverAuth ,clientAuth
|
|
2273
|
+
|
|
2274
|
+
[ v3_req ]
|
|
2275
|
+
basicConstraints = critical, CA:FALSE
|
|
2276
|
+
keyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment, keyAgreement
|
|
2277
|
+
extendedKeyUsage = critical,serverAuth ,clientAuth
|
|
2278
|
+
subjectAltName = $ENV::ALTNAME
|
|
2279
|
+
nsComment = "CA Generated by Node-OPCUA Certificate utility using openssl"
|
|
2280
|
+
[ v3_ca ]
|
|
2281
|
+
subjectKeyIdentifier = hash
|
|
2282
|
+
authorityKeyIdentifier = keyid:always,issuer:always
|
|
2283
|
+
# authorityKeyIdentifier = keyid
|
|
2284
|
+
basicConstraints = CA:TRUE
|
|
2285
|
+
keyUsage = critical, cRLSign, keyCertSign
|
|
2286
|
+
nsComment = "CA Certificate generated by Node-OPCUA Certificate utility using openssl"
|
|
2287
|
+
#nsCertType = sslCA, emailCA
|
|
2288
|
+
#subjectAltName = email:copy
|
|
2289
|
+
#issuerAltName = issuer:copy
|
|
2290
|
+
#obj = DER:02:03
|
|
2291
|
+
crlDistributionPoints = @crl_info
|
|
2292
|
+
[ crl_info ]
|
|
2293
|
+
URI.0 = http://localhost:8900/crl.pem
|
|
2294
|
+
[ v3_selfsigned]
|
|
2295
|
+
basicConstraints = critical, CA:FALSE
|
|
2296
|
+
keyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment, keyAgreement
|
|
2297
|
+
extendedKeyUsage = critical,serverAuth ,clientAuth
|
|
2298
|
+
nsComment = "Self-signed certificate, generated by NodeOPCUA"
|
|
2299
|
+
subjectAltName = $ENV::ALTNAME
|
|
2300
|
+
|
|
2301
|
+
[ crl_ext ]
|
|
2302
|
+
#issuerAltName = issuer:copy
|
|
2303
|
+
authorityKeyIdentifier = keyid:always,issuer:always
|
|
2304
|
+
#authorityInfoAccess = @issuer_info`;
|
|
2305
|
+
ca_config_template_cnf_default = config2;
|
|
2306
|
+
}
|
|
2307
|
+
});
|
|
2308
|
+
|
|
2309
|
+
// packages/node-opcua-pki/lib/ca/certificate_authority.ts
|
|
2310
|
+
import assert9 from "assert";
|
|
2311
|
+
import fs9 from "fs";
|
|
2312
|
+
import path6 from "path";
|
|
2313
|
+
import chalk6 from "chalk";
|
|
2314
|
+
import {
|
|
2315
|
+
exploreCertificateSigningRequest,
|
|
2316
|
+
generatePrivateKeyFile as generatePrivateKeyFile2,
|
|
2317
|
+
readCertificateSigningRequest,
|
|
2318
|
+
Subject as Subject4
|
|
2319
|
+
} from "node-opcua-crypto";
|
|
2320
|
+
function octetStringToIpAddress(a) {
|
|
2321
|
+
return parseInt(a.substring(0, 2), 16).toString() + "." + parseInt(a.substring(2, 4), 16).toString() + "." + parseInt(a.substring(4, 6), 16).toString() + "." + parseInt(a.substring(6, 8), 16).toString();
|
|
2322
|
+
}
|
|
2323
|
+
async function construct_CertificateAuthority(certificateAuthority) {
|
|
2324
|
+
const subject = certificateAuthority.subject;
|
|
2325
|
+
const caRootDir = path6.resolve(certificateAuthority.rootDir);
|
|
2326
|
+
async function make_folders() {
|
|
2327
|
+
mkdirRecursiveSync(caRootDir);
|
|
2328
|
+
mkdirRecursiveSync(path6.join(caRootDir, "private"));
|
|
2329
|
+
mkdirRecursiveSync(path6.join(caRootDir, "public"));
|
|
2330
|
+
mkdirRecursiveSync(path6.join(caRootDir, "certs"));
|
|
2331
|
+
mkdirRecursiveSync(path6.join(caRootDir, "crl"));
|
|
2332
|
+
mkdirRecursiveSync(path6.join(caRootDir, "conf"));
|
|
2333
|
+
}
|
|
2334
|
+
await make_folders();
|
|
2335
|
+
async function construct_default_files() {
|
|
2336
|
+
const serial = path6.join(caRootDir, "serial");
|
|
2337
|
+
if (!fs9.existsSync(serial)) {
|
|
2338
|
+
await fs9.promises.writeFile(serial, "1000");
|
|
2339
|
+
}
|
|
2340
|
+
const crlNumber = path6.join(caRootDir, "crlnumber");
|
|
2341
|
+
if (!fs9.existsSync(crlNumber)) {
|
|
2342
|
+
await fs9.promises.writeFile(crlNumber, "1000");
|
|
2343
|
+
}
|
|
2344
|
+
const indexFile = path6.join(caRootDir, "index.txt");
|
|
2345
|
+
if (!fs9.existsSync(indexFile)) {
|
|
2346
|
+
await fs9.promises.writeFile(indexFile, "");
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
await construct_default_files();
|
|
2350
|
+
if (fs9.existsSync(path6.join(caRootDir, "private/cakey.pem")) && !config3.forceCA) {
|
|
2351
|
+
debugLog("CA private key already exists ... skipping");
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2354
|
+
displayTitle("Create Certificate Authority (CA)");
|
|
2355
|
+
const indexFileAttr = path6.join(caRootDir, "index.txt.attr");
|
|
2356
|
+
if (!fs9.existsSync(indexFileAttr)) {
|
|
2357
|
+
await fs9.promises.writeFile(indexFileAttr, "unique_subject = no");
|
|
2358
|
+
}
|
|
2359
|
+
const caConfigFile = certificateAuthority.configFile;
|
|
2360
|
+
if (1) {
|
|
2361
|
+
let data = configurationFileTemplate;
|
|
2362
|
+
data = makePath(data.replace(/%%ROOT_FOLDER%%/, caRootDir));
|
|
2363
|
+
await fs9.promises.writeFile(caConfigFile, data);
|
|
2364
|
+
}
|
|
2365
|
+
const subjectOpt = ` -subj "${subject.toString()}" `;
|
|
2366
|
+
processAltNames({});
|
|
2367
|
+
const options = { cwd: caRootDir };
|
|
2368
|
+
const configFile = generateStaticConfig("conf/caconfig.cnf", options);
|
|
2369
|
+
const configOption = ` -config ${q3(n4(configFile))}`;
|
|
2370
|
+
const keySize = certificateAuthority.keySize;
|
|
2371
|
+
const privateKeyFilename = path6.join(caRootDir, "private/cakey.pem");
|
|
2372
|
+
const csrFilename = path6.join(caRootDir, "private/cakey.csr");
|
|
2373
|
+
displayTitle(`Generate the CA private Key - ${keySize}`);
|
|
2374
|
+
await generatePrivateKeyFile2(privateKeyFilename, keySize);
|
|
2375
|
+
displayTitle("Generate a certificate request for the CA key");
|
|
2376
|
+
await execute_openssl(
|
|
2377
|
+
"req -new -sha256 -text -extensions v3_ca" + configOption + " -key " + q3(n4(privateKeyFilename)) + " -out " + q3(n4(csrFilename)) + " " + subjectOpt,
|
|
2378
|
+
options
|
|
2379
|
+
);
|
|
2380
|
+
displayTitle("Generate CA Certificate (self-signed)");
|
|
2381
|
+
await execute_openssl(
|
|
2382
|
+
" x509 -sha256 -req -days 3650 -text -extensions v3_ca -extfile " + q3(n4(configFile)) + " -in private/cakey.csr -signkey " + q3(n4(privateKeyFilename)) + " -out public/cacert.pem",
|
|
2383
|
+
options
|
|
2384
|
+
);
|
|
2385
|
+
displaySubtitle("generate initial CRL (Certificate Revocation List)");
|
|
2386
|
+
await regenerateCrl(certificateAuthority.revocationList, configOption, options);
|
|
2387
|
+
displayTitle("Create Certificate Authority (CA) ---> DONE");
|
|
2388
|
+
}
|
|
2389
|
+
async function regenerateCrl(revocationList, configOption, options) {
|
|
2390
|
+
displaySubtitle("regenerate CRL (Certificate Revocation List)");
|
|
2391
|
+
await execute_openssl(`ca -gencrl ${configOption} -out crl/revocation_list.crl`, options);
|
|
2392
|
+
await execute_openssl("crl -in crl/revocation_list.crl -out crl/revocation_list.der -outform der", options);
|
|
2393
|
+
displaySubtitle("Display (Certificate Revocation List)");
|
|
2394
|
+
await execute_openssl(`crl -in ${q3(n4(revocationList))} -text -noout`, options);
|
|
2395
|
+
}
|
|
2396
|
+
var defaultSubject, configurationFileTemplate, config3, n4, q3, CertificateAuthority;
|
|
2397
|
+
var init_certificate_authority = __esm({
|
|
2398
|
+
"packages/node-opcua-pki/lib/ca/certificate_authority.ts"() {
|
|
2399
|
+
"use strict";
|
|
2400
|
+
init_esm_shims();
|
|
2401
|
+
init_toolbox();
|
|
2402
|
+
init_with_openssl();
|
|
2403
|
+
init_ca_config_template_cnf();
|
|
2404
|
+
defaultSubject = "/C=FR/ST=IDF/L=Paris/O=Local NODE-OPCUA Certificate Authority/CN=NodeOPCUA-CA";
|
|
2405
|
+
configurationFileTemplate = ca_config_template_cnf_default;
|
|
2406
|
+
config3 = {
|
|
2407
|
+
certificateDir: "INVALID",
|
|
2408
|
+
forceCA: false,
|
|
2409
|
+
pkiDir: "INVALID"
|
|
2410
|
+
};
|
|
2411
|
+
n4 = makePath;
|
|
2412
|
+
q3 = quote;
|
|
2413
|
+
assert9(octetStringToIpAddress("c07b9179") === "192.123.145.121");
|
|
2414
|
+
CertificateAuthority = class {
|
|
2415
|
+
/** RSA key size used when generating the CA private key. */
|
|
2416
|
+
keySize;
|
|
2417
|
+
/** Root filesystem path of the CA directory structure. */
|
|
2418
|
+
location;
|
|
2419
|
+
/** X.500 subject of the CA certificate. */
|
|
2420
|
+
subject;
|
|
2421
|
+
constructor(options) {
|
|
2422
|
+
assert9(Object.prototype.hasOwnProperty.call(options, "location"));
|
|
2423
|
+
assert9(Object.prototype.hasOwnProperty.call(options, "keySize"));
|
|
2424
|
+
this.location = options.location;
|
|
2425
|
+
this.keySize = options.keySize || 2048;
|
|
2426
|
+
this.subject = new Subject4(options.subject || defaultSubject);
|
|
2427
|
+
}
|
|
2428
|
+
/** Absolute path to the CA root directory (alias for {@link location}). */
|
|
2429
|
+
get rootDir() {
|
|
2430
|
+
return this.location;
|
|
2431
|
+
}
|
|
2432
|
+
/** Path to the OpenSSL configuration file (`conf/caconfig.cnf`). */
|
|
2433
|
+
get configFile() {
|
|
2434
|
+
return path6.normalize(path6.join(this.rootDir, "./conf/caconfig.cnf"));
|
|
2435
|
+
}
|
|
2436
|
+
/** Path to the CA certificate in PEM format (`public/cacert.pem`). */
|
|
2437
|
+
get caCertificate() {
|
|
2438
|
+
return makePath(this.rootDir, "./public/cacert.pem");
|
|
2439
|
+
}
|
|
2440
|
+
/**
|
|
2441
|
+
* Path to the current Certificate Revocation List in DER format.
|
|
2442
|
+
* (`crl/revocation_list.der`)
|
|
2443
|
+
*/
|
|
2444
|
+
get revocationListDER() {
|
|
2445
|
+
return makePath(this.rootDir, "./crl/revocation_list.der");
|
|
2446
|
+
}
|
|
2447
|
+
/**
|
|
2448
|
+
* Path to the current Certificate Revocation List in PEM format.
|
|
2449
|
+
* (`crl/revocation_list.crl`)
|
|
2450
|
+
*/
|
|
2451
|
+
get revocationList() {
|
|
2452
|
+
return makePath(this.rootDir, "./crl/revocation_list.crl");
|
|
2453
|
+
}
|
|
2454
|
+
/**
|
|
2455
|
+
* Path to the concatenated CA certificate + CRL file.
|
|
2456
|
+
* Used by OpenSSL for CRL-based verification.
|
|
2457
|
+
*/
|
|
2458
|
+
get caCertificateWithCrl() {
|
|
2459
|
+
return makePath(this.rootDir, "./public/cacertificate_with_crl.pem");
|
|
2460
|
+
}
|
|
2461
|
+
/**
|
|
2462
|
+
* Initialize the CA directory structure, generate the CA
|
|
2463
|
+
* private key and self-signed certificate if they do not
|
|
2464
|
+
* already exist.
|
|
2465
|
+
*/
|
|
2466
|
+
async initialize() {
|
|
2467
|
+
await construct_CertificateAuthority(this);
|
|
2468
|
+
}
|
|
2469
|
+
/**
|
|
2470
|
+
* Rebuild the combined CA certificate + CRL file.
|
|
2471
|
+
*
|
|
2472
|
+
* This concatenates the CA certificate with the current
|
|
2473
|
+
* revocation list so that OpenSSL can verify certificates
|
|
2474
|
+
* with CRL checking enabled.
|
|
2475
|
+
*/
|
|
2476
|
+
async constructCACertificateWithCRL() {
|
|
2477
|
+
const cacertWithCRL = this.caCertificateWithCrl;
|
|
2478
|
+
if (fs9.existsSync(this.revocationList)) {
|
|
2479
|
+
await fs9.promises.writeFile(
|
|
2480
|
+
cacertWithCRL,
|
|
2481
|
+
fs9.readFileSync(this.caCertificate, "utf8") + fs9.readFileSync(this.revocationList, "utf8")
|
|
2482
|
+
);
|
|
2483
|
+
} else {
|
|
2484
|
+
await fs9.promises.writeFile(cacertWithCRL, fs9.readFileSync(this.caCertificate));
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
/**
|
|
2488
|
+
* Append the CA certificate to a signed certificate file,
|
|
2489
|
+
* creating a PEM certificate chain.
|
|
2490
|
+
*
|
|
2491
|
+
* @param certificate - path to the certificate file to extend
|
|
2492
|
+
*/
|
|
2493
|
+
async constructCertificateChain(certificate) {
|
|
2494
|
+
assert9(fs9.existsSync(certificate));
|
|
2495
|
+
assert9(fs9.existsSync(this.caCertificate));
|
|
2496
|
+
debugLog(chalk6.yellow(" certificate file :"), chalk6.cyan(certificate));
|
|
2497
|
+
await fs9.promises.writeFile(
|
|
2498
|
+
certificate,
|
|
2499
|
+
await fs9.promises.readFile(certificate, "utf8") + await fs9.promises.readFile(this.caCertificate, "utf8")
|
|
2500
|
+
// + fs.readFileSync(this.revocationList)
|
|
2501
|
+
);
|
|
2502
|
+
}
|
|
2503
|
+
/**
|
|
2504
|
+
* Create a self-signed certificate using OpenSSL.
|
|
2505
|
+
*
|
|
2506
|
+
* @param certificateFile - output path for the signed certificate
|
|
2507
|
+
* @param privateKey - path to the private key file
|
|
2508
|
+
* @param params - certificate parameters (subject, validity, SANs)
|
|
2509
|
+
*/
|
|
2510
|
+
async createSelfSignedCertificate(certificateFile, privateKey, params) {
|
|
2511
|
+
assert9(typeof privateKey === "string");
|
|
2512
|
+
assert9(fs9.existsSync(privateKey));
|
|
2513
|
+
if (!certificateFileExist(certificateFile)) {
|
|
2514
|
+
return;
|
|
2515
|
+
}
|
|
2516
|
+
adjustDate(params);
|
|
2517
|
+
adjustApplicationUri(params);
|
|
2518
|
+
processAltNames(params);
|
|
2519
|
+
const csrFile = `${certificateFile}_csr`;
|
|
2520
|
+
assert9(csrFile);
|
|
2521
|
+
const configFile = generateStaticConfig(this.configFile, { cwd: this.rootDir });
|
|
2522
|
+
const options = {
|
|
2523
|
+
cwd: this.rootDir,
|
|
2524
|
+
openssl_conf: makePath(configFile)
|
|
2525
|
+
};
|
|
2526
|
+
const configOption = "";
|
|
2527
|
+
const subject = params.subject ? new Subject4(params.subject).toString() : "";
|
|
2528
|
+
const subjectOptions = subject && subject.length > 1 ? ` -subj ${subject} ` : "";
|
|
2529
|
+
displaySubtitle("- the certificate signing request");
|
|
2530
|
+
await execute_openssl(
|
|
2531
|
+
"req -new -sha256 -text " + configOption + subjectOptions + " -batch -key " + q3(n4(privateKey)) + " -out " + q3(n4(csrFile)),
|
|
2532
|
+
options
|
|
2533
|
+
);
|
|
2534
|
+
displaySubtitle("- creating the self-signed certificate");
|
|
2535
|
+
await execute_openssl(
|
|
2536
|
+
"ca -selfsign -keyfile " + q3(n4(privateKey)) + " -startdate " + x509Date(params.startDate) + " -enddate " + x509Date(params.endDate) + " -batch -out " + q3(n4(certificateFile)) + " -in " + q3(n4(csrFile)),
|
|
2537
|
+
options
|
|
2538
|
+
);
|
|
2539
|
+
displaySubtitle("- dump the certificate for a check");
|
|
2540
|
+
await execute_openssl(`x509 -in ${q3(n4(certificateFile))} -dates -fingerprint -purpose -noout`, {});
|
|
2541
|
+
displaySubtitle("- verify self-signed certificate");
|
|
2542
|
+
await execute_openssl_no_failure(`verify -verbose -CAfile ${q3(n4(certificateFile))} ${q3(n4(certificateFile))}`, options);
|
|
2543
|
+
await fs9.promises.unlink(csrFile);
|
|
2544
|
+
}
|
|
2545
|
+
/**
|
|
2546
|
+
* Revoke a certificate and regenerate the CRL.
|
|
2547
|
+
*
|
|
2548
|
+
* @param certificate - path to the certificate file to revoke
|
|
2549
|
+
* @param params - revocation parameters
|
|
2550
|
+
* @param params.reason - CRL reason code
|
|
2551
|
+
* (default `"keyCompromise"`)
|
|
2552
|
+
*/
|
|
2553
|
+
async revokeCertificate(certificate, params) {
|
|
2554
|
+
const crlReasons = [
|
|
2555
|
+
"unspecified",
|
|
2556
|
+
"keyCompromise",
|
|
2557
|
+
"CACompromise",
|
|
2558
|
+
"affiliationChanged",
|
|
2559
|
+
"superseded",
|
|
2560
|
+
"cessationOfOperation",
|
|
2561
|
+
"certificateHold",
|
|
2562
|
+
"removeFromCRL"
|
|
2563
|
+
];
|
|
2564
|
+
const configFile = generateStaticConfig("conf/caconfig.cnf", { cwd: this.rootDir });
|
|
2565
|
+
const options = {
|
|
2566
|
+
cwd: this.rootDir,
|
|
2567
|
+
openssl_conf: makePath(configFile)
|
|
2568
|
+
};
|
|
2569
|
+
setEnv("ALTNAME", "");
|
|
2570
|
+
const randomFile = path6.join(this.rootDir, "random.rnd");
|
|
2571
|
+
setEnv("RANDFILE", randomFile);
|
|
2572
|
+
const configOption = ` -config ${q3(n4(configFile))}`;
|
|
2573
|
+
const reason = params.reason || "keyCompromise";
|
|
2574
|
+
assert9(crlReasons.indexOf(reason) >= 0);
|
|
2575
|
+
displayTitle(`Revoking certificate ${certificate}`);
|
|
2576
|
+
displaySubtitle("Revoke certificate");
|
|
2577
|
+
await execute_openssl_no_failure(`ca -verbose ${configOption} -revoke ${q3(certificate)} -crl_reason ${reason}`, options);
|
|
2578
|
+
await regenerateCrl(this.revocationList, configOption, options);
|
|
2579
|
+
displaySubtitle("Verify that certificate is revoked");
|
|
2580
|
+
await execute_openssl_no_failure(
|
|
2581
|
+
"verify -verbose -CRLfile " + q3(n4(this.revocationList)) + " -CAfile " + q3(n4(this.caCertificate)) + " -crl_check " + q3(n4(certificate)),
|
|
2582
|
+
options
|
|
2583
|
+
);
|
|
2584
|
+
displaySubtitle("Produce CRL in DER form ");
|
|
2585
|
+
await execute_openssl(`crl -in ${q3(n4(this.revocationList))} -out crl/revocation_list.der -outform der`, options);
|
|
2586
|
+
displaySubtitle("Produce CRL in PEM form ");
|
|
2587
|
+
await execute_openssl(`crl -in ${q3(n4(this.revocationList))} -out crl/revocation_list.pem -outform pem -text `, options);
|
|
2588
|
+
}
|
|
2589
|
+
/**
|
|
2590
|
+
* Sign a Certificate Signing Request (CSR) with this CA.
|
|
2591
|
+
*
|
|
2592
|
+
* The signed certificate is written to `certificate`, and the
|
|
2593
|
+
* CA certificate chain plus CRL are appended to form a
|
|
2594
|
+
* complete certificate chain.
|
|
2595
|
+
*
|
|
2596
|
+
* @param certificate - output path for the signed certificate
|
|
2597
|
+
* @param certificateSigningRequestFilename - path to the CSR
|
|
2598
|
+
* @param params1 - signing parameters (validity, dates, SANs)
|
|
2599
|
+
* @returns the path to the signed certificate
|
|
2600
|
+
*/
|
|
2601
|
+
async signCertificateRequest(certificate, certificateSigningRequestFilename, params1) {
|
|
2602
|
+
await ensure_openssl_installed();
|
|
2603
|
+
assert9(fs9.existsSync(certificateSigningRequestFilename));
|
|
2604
|
+
if (!certificateFileExist(certificate)) {
|
|
2605
|
+
return "";
|
|
2606
|
+
}
|
|
2607
|
+
adjustDate(params1);
|
|
2608
|
+
adjustApplicationUri(params1);
|
|
2609
|
+
processAltNames(params1);
|
|
2610
|
+
const options = { cwd: this.rootDir };
|
|
2611
|
+
const csr = await readCertificateSigningRequest(certificateSigningRequestFilename);
|
|
2612
|
+
const csrInfo = exploreCertificateSigningRequest(csr);
|
|
2613
|
+
const applicationUri = csrInfo.extensionRequest.subjectAltName.uniformResourceIdentifier ? csrInfo.extensionRequest.subjectAltName.uniformResourceIdentifier[0] : void 0;
|
|
2614
|
+
if (typeof applicationUri !== "string") {
|
|
2615
|
+
throw new Error("Cannot find applicationUri in CSR");
|
|
2616
|
+
}
|
|
2617
|
+
const dns2 = csrInfo.extensionRequest.subjectAltName.dNSName || [];
|
|
2618
|
+
let ip = csrInfo.extensionRequest.subjectAltName.iPAddress || [];
|
|
2619
|
+
ip = ip.map(octetStringToIpAddress);
|
|
2620
|
+
const params = {
|
|
2621
|
+
applicationUri,
|
|
2622
|
+
dns: dns2,
|
|
2623
|
+
ip
|
|
2624
|
+
};
|
|
2625
|
+
processAltNames(params);
|
|
2626
|
+
const configFile = generateStaticConfig("conf/caconfig.cnf", options);
|
|
2627
|
+
displaySubtitle("- then we ask the authority to sign the certificate signing request");
|
|
2628
|
+
const configOption = ` -config ${configFile}`;
|
|
2629
|
+
await execute_openssl(
|
|
2630
|
+
"ca " + configOption + " -startdate " + x509Date(params1.startDate) + " -enddate " + x509Date(params1.endDate) + " -batch -out " + q3(n4(certificate)) + " -in " + q3(n4(certificateSigningRequestFilename)),
|
|
2631
|
+
options
|
|
2632
|
+
);
|
|
2633
|
+
displaySubtitle("- dump the certificate for a check");
|
|
2634
|
+
await execute_openssl(`x509 -in ${q3(n4(certificate))} -dates -fingerprint -purpose -noout`, options);
|
|
2635
|
+
displaySubtitle("- construct CA certificate with CRL");
|
|
2636
|
+
await this.constructCACertificateWithCRL();
|
|
2637
|
+
displaySubtitle("- construct certificate chain");
|
|
2638
|
+
await this.constructCertificateChain(certificate);
|
|
2639
|
+
displaySubtitle("- verify certificate against the root CA");
|
|
2640
|
+
await this.verifyCertificate(certificate);
|
|
2641
|
+
return certificate;
|
|
2642
|
+
}
|
|
2643
|
+
/**
|
|
2644
|
+
* Verify a certificate against this CA.
|
|
2645
|
+
*
|
|
2646
|
+
* @param certificate - path to the certificate file to verify
|
|
2647
|
+
*/
|
|
2648
|
+
async verifyCertificate(certificate) {
|
|
2649
|
+
const isImplemented = false;
|
|
2650
|
+
if (isImplemented) {
|
|
2651
|
+
const options = { cwd: this.rootDir };
|
|
2652
|
+
const configFile = generateStaticConfig("conf/caconfig.cnf", options);
|
|
2653
|
+
setEnv("OPENSSL_CONF", makePath(configFile));
|
|
2654
|
+
const _configOption = ` -config ${configFile}`;
|
|
2655
|
+
_configOption;
|
|
2656
|
+
await execute_openssl_no_failure(
|
|
2657
|
+
`verify -verbose -CAfile ${q3(n4(this.caCertificateWithCrl))} ${q3(n4(certificate))}`,
|
|
2658
|
+
options
|
|
2659
|
+
);
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
};
|
|
2663
|
+
}
|
|
2664
|
+
});
|
|
2665
|
+
|
|
2666
|
+
// packages/node-opcua-pki/lib/ca/crypto_create_CA.ts
|
|
2667
|
+
import assert10 from "assert";
|
|
2668
|
+
import fs10 from "fs";
|
|
2669
|
+
import { createRequire } from "module";
|
|
2670
|
+
import os4 from "os";
|
|
2671
|
+
import path7 from "path";
|
|
2672
|
+
import chalk7 from "chalk";
|
|
2673
|
+
import { CertificatePurpose as CertificatePurpose2, generatePrivateKeyFile as generatePrivateKeyFile3, Subject as Subject5 } from "node-opcua-crypto";
|
|
2674
|
+
import commandLineArgs from "command-line-args";
|
|
2675
|
+
import commandLineUsage from "command-line-usage";
|
|
2676
|
+
function get_offset_date(date, nbDays) {
|
|
2677
|
+
const d = new Date(date.getTime());
|
|
2678
|
+
d.setDate(d.getDate() + nbDays);
|
|
2679
|
+
return d;
|
|
2680
|
+
}
|
|
2681
|
+
async function construct_CertificateAuthority2(subject) {
|
|
2682
|
+
assert10(typeof gLocalConfig.CAFolder === "string", "expecting a CAFolder in config");
|
|
2683
|
+
assert10(typeof gLocalConfig.keySize === "number", "expecting a keySize in config");
|
|
2684
|
+
if (!g_certificateAuthority) {
|
|
2685
|
+
g_certificateAuthority = new CertificateAuthority({
|
|
2686
|
+
keySize: gLocalConfig.keySize,
|
|
2687
|
+
location: gLocalConfig.CAFolder,
|
|
2688
|
+
subject
|
|
2689
|
+
});
|
|
2690
|
+
await g_certificateAuthority.initialize();
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
async function construct_CertificateManager() {
|
|
2694
|
+
assert10(typeof gLocalConfig.PKIFolder === "string", "expecting a PKIFolder in config");
|
|
2695
|
+
if (!certificateManager) {
|
|
2696
|
+
certificateManager = new CertificateManager({
|
|
2697
|
+
keySize: gLocalConfig.keySize,
|
|
2698
|
+
location: gLocalConfig.PKIFolder
|
|
2699
|
+
});
|
|
2700
|
+
await certificateManager.initialize();
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
function default_template_content() {
|
|
2704
|
+
if (process.pkg?.entrypoint) {
|
|
2705
|
+
const a = fs10.readFileSync(path7.join(__dirname, "../../bin/pki_config.example.js"), "utf8");
|
|
2706
|
+
return a;
|
|
2707
|
+
}
|
|
2708
|
+
function find_default_config_template() {
|
|
2709
|
+
const rootFolder = find_module_root_folder();
|
|
2710
|
+
const configName = "pki_config.example.js";
|
|
2711
|
+
let default_config_template2 = path7.join(rootFolder, "bin", configName);
|
|
2712
|
+
if (!fs10.existsSync(default_config_template2)) {
|
|
2713
|
+
default_config_template2 = path7.join(__dirname, "..", configName);
|
|
2714
|
+
if (!fs10.existsSync(default_config_template2)) {
|
|
2715
|
+
default_config_template2 = path7.join(__dirname, `../bin/${configName}`);
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
return default_config_template2;
|
|
2719
|
+
}
|
|
2720
|
+
const default_config_template = find_default_config_template();
|
|
2721
|
+
assert10(fs10.existsSync(default_config_template));
|
|
2722
|
+
const default_config_template_content = fs10.readFileSync(default_config_template, "utf8");
|
|
2723
|
+
return default_config_template_content;
|
|
2724
|
+
}
|
|
2725
|
+
function find_module_root_folder() {
|
|
2726
|
+
let rootFolder = path7.join(__dirname);
|
|
2727
|
+
for (let i = 0; i < 4; i++) {
|
|
2728
|
+
if (fs10.existsSync(path7.join(rootFolder, "package.json"))) {
|
|
2729
|
+
return rootFolder;
|
|
2730
|
+
}
|
|
2731
|
+
rootFolder = path7.join(rootFolder, "..");
|
|
2732
|
+
}
|
|
2733
|
+
assert10(fs10.existsSync(path7.join(rootFolder, "package.json")), "root folder must have a package.json file");
|
|
2734
|
+
return rootFolder;
|
|
2735
|
+
}
|
|
2736
|
+
async function readConfiguration(argv) {
|
|
2737
|
+
if (argv.silent) {
|
|
2738
|
+
g_config.silent = true;
|
|
2739
|
+
} else {
|
|
2740
|
+
g_config.silent = false;
|
|
2741
|
+
}
|
|
2742
|
+
const fqdn2 = await extractFullyQualifiedDomainName();
|
|
2743
|
+
const hostname = os4.hostname();
|
|
2744
|
+
let certificateDir;
|
|
2745
|
+
function performSubstitution(str) {
|
|
2746
|
+
str = str.replace("{CWD}", process.cwd());
|
|
2747
|
+
if (certificateDir) {
|
|
2748
|
+
str = str.replace("{root}", certificateDir);
|
|
2749
|
+
}
|
|
2750
|
+
if (gLocalConfig?.PKIFolder) {
|
|
2751
|
+
str = str.replace("{PKIFolder}", gLocalConfig.PKIFolder);
|
|
2752
|
+
}
|
|
2753
|
+
str = str.replace("{hostname}", hostname);
|
|
2754
|
+
str = str.replace("%FQDN%", fqdn2);
|
|
2755
|
+
return str;
|
|
2756
|
+
}
|
|
2757
|
+
function prepare(file) {
|
|
2758
|
+
const tmp = path7.resolve(performSubstitution(file));
|
|
2759
|
+
return makePath(tmp);
|
|
2760
|
+
}
|
|
2761
|
+
certificateDir = argv.root;
|
|
2762
|
+
assert10(typeof certificateDir === "string");
|
|
2763
|
+
certificateDir = prepare(certificateDir);
|
|
2764
|
+
mkdirRecursiveSync(certificateDir);
|
|
2765
|
+
assert10(fs10.existsSync(certificateDir));
|
|
2766
|
+
const default_config = path7.join(certificateDir, "config.js");
|
|
2767
|
+
if (!fs10.existsSync(default_config)) {
|
|
2768
|
+
debugLog(chalk7.yellow(" Creating default g_config file "), chalk7.cyan(default_config));
|
|
2769
|
+
const default_config_template_content = default_template_content();
|
|
2770
|
+
fs10.writeFileSync(default_config, default_config_template_content);
|
|
2771
|
+
} else {
|
|
2772
|
+
debugLog(chalk7.yellow(" using g_config file "), chalk7.cyan(default_config));
|
|
2773
|
+
}
|
|
2774
|
+
if (!fs10.existsSync(default_config)) {
|
|
2775
|
+
debugLog(chalk7.redBright(" cannot find config file ", default_config));
|
|
2776
|
+
}
|
|
2777
|
+
const defaultRandomFile = path7.join(path7.dirname(default_config), "random.rnd");
|
|
2778
|
+
setEnv("RANDFILE", defaultRandomFile);
|
|
2779
|
+
const _require = createRequire(__filename);
|
|
2780
|
+
gLocalConfig = _require(default_config);
|
|
2781
|
+
gLocalConfig.subject = new Subject5(gLocalConfig.subject || "");
|
|
2782
|
+
if (argv.subject) {
|
|
2783
|
+
gLocalConfig.subject = new Subject5(argv.subject);
|
|
2784
|
+
}
|
|
2785
|
+
if (!gLocalConfig.subject.commonName) {
|
|
2786
|
+
throw new Error("subject must have a Common Name");
|
|
2787
|
+
}
|
|
2788
|
+
gLocalConfig.certificateDir = certificateDir;
|
|
2789
|
+
let CAFolder = argv.CAFolder || path7.join(certificateDir, "CA");
|
|
2790
|
+
CAFolder = prepare(CAFolder);
|
|
2791
|
+
gLocalConfig.CAFolder = CAFolder;
|
|
2792
|
+
gLocalConfig.PKIFolder = path7.join(gLocalConfig.certificateDir, "PKI");
|
|
2793
|
+
if (argv.PKIFolder) {
|
|
2794
|
+
gLocalConfig.PKIFolder = prepare(argv.PKIFolder);
|
|
2795
|
+
}
|
|
2796
|
+
gLocalConfig.PKIFolder = prepare(gLocalConfig.PKIFolder);
|
|
2797
|
+
if (argv.privateKey) {
|
|
2798
|
+
gLocalConfig.privateKey = prepare(argv.privateKey);
|
|
2799
|
+
}
|
|
2800
|
+
if (argv.applicationUri) {
|
|
2801
|
+
gLocalConfig.applicationUri = performSubstitution(argv.applicationUri);
|
|
2802
|
+
}
|
|
2803
|
+
if (argv.output) {
|
|
2804
|
+
gLocalConfig.outputFile = argv.output;
|
|
2805
|
+
}
|
|
2806
|
+
gLocalConfig.altNames = [];
|
|
2807
|
+
if (argv.altNames) {
|
|
2808
|
+
gLocalConfig.altNames = argv.altNames.split(";");
|
|
2809
|
+
}
|
|
2810
|
+
gLocalConfig.dns = [getFullyQualifiedDomainName()];
|
|
2811
|
+
if (argv.dns) {
|
|
2812
|
+
gLocalConfig.dns = argv.dns.split(",").map(performSubstitution);
|
|
2813
|
+
}
|
|
2814
|
+
gLocalConfig.ip = [];
|
|
2815
|
+
if (argv.ip) {
|
|
2816
|
+
gLocalConfig.ip = argv.ip.split(",");
|
|
2817
|
+
}
|
|
2818
|
+
if (argv.keySize) {
|
|
2819
|
+
const v = argv.keySize;
|
|
2820
|
+
if (v !== 1024 && v !== 2048 && v !== 3072 && v !== 4096) {
|
|
2821
|
+
throw new Error(`invalid keysize specified ${v} should be 1024,2048,3072 or 4096`);
|
|
2822
|
+
}
|
|
2823
|
+
gLocalConfig.keySize = argv.keySize;
|
|
2824
|
+
}
|
|
2825
|
+
if (argv.validity) {
|
|
2826
|
+
gLocalConfig.validity = argv.validity;
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
async function createDefaultCertificate(base_name, prefix, key_length, applicationUri, dev) {
|
|
2830
|
+
assert10(key_length === 1024 || key_length === 2048 || key_length === 3072 || key_length === 4096);
|
|
2831
|
+
const private_key_file = makePath(base_name, `${prefix}key_${key_length}.pem`);
|
|
2832
|
+
const public_key_file = makePath(base_name, `${prefix}public_key_${key_length}.pub`);
|
|
2833
|
+
const certificate_file = makePath(base_name, `${prefix}cert_${key_length}.pem`);
|
|
2834
|
+
const certificate_file_outofdate = makePath(base_name, `${prefix}cert_${key_length}_outofdate.pem`);
|
|
2835
|
+
const certificate_file_not_active_yet = makePath(base_name, `${prefix}cert_${key_length}_not_active_yet.pem`);
|
|
2836
|
+
const certificate_revoked = makePath(base_name, `${prefix}cert_${key_length}_revoked.pem`);
|
|
2837
|
+
const self_signed_certificate_file = makePath(base_name, `${prefix}selfsigned_cert_${key_length}.pem`);
|
|
2838
|
+
const fqdn2 = getFullyQualifiedDomainName();
|
|
2839
|
+
const hostname = os4.hostname();
|
|
2840
|
+
const dns2 = [
|
|
2841
|
+
// for conformance reason, localhost shall not be present in the DNS field of COP
|
|
2842
|
+
// ***FORBIDEN** "localhost",
|
|
2843
|
+
getFullyQualifiedDomainName()
|
|
2844
|
+
];
|
|
2845
|
+
if (hostname !== fqdn2) {
|
|
2846
|
+
dns2.push(hostname);
|
|
2847
|
+
}
|
|
2848
|
+
const ip = [];
|
|
2849
|
+
async function createCertificateIfNotExist(certificate, private_key, applicationUri2, startDate, validity) {
|
|
2850
|
+
if (fs10.existsSync(certificate)) {
|
|
2851
|
+
warningLog(chalk7.yellow(" certificate"), chalk7.cyan(certificate), chalk7.yellow(" already exists => skipping"));
|
|
2852
|
+
return "";
|
|
2853
|
+
} else {
|
|
2854
|
+
return await createCertificate(certificate, private_key, applicationUri2, startDate, validity);
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
async function createCertificate(certificate, privateKey, applicationUri2, startDate, validity) {
|
|
2858
|
+
const certificateSigningRequestFile = `${certificate}.csr`;
|
|
2859
|
+
const configFile = makePath(base_name, "../certificates/PKI/own/openssl.cnf");
|
|
2860
|
+
const dns3 = [os4.hostname()];
|
|
2861
|
+
const ip2 = ["127.0.0.1"];
|
|
2862
|
+
const params = {
|
|
2863
|
+
applicationUri: applicationUri2,
|
|
2864
|
+
privateKey,
|
|
2865
|
+
rootDir: ".",
|
|
2866
|
+
configFile,
|
|
2867
|
+
dns: dns3,
|
|
2868
|
+
ip: ip2,
|
|
2869
|
+
purpose: CertificatePurpose2.ForApplication
|
|
2870
|
+
};
|
|
2871
|
+
await createCertificateSigningRequestWithOpenSSL(certificateSigningRequestFile, params);
|
|
2872
|
+
return await g_certificateAuthority.signCertificateRequest(certificate, certificateSigningRequestFile, {
|
|
2873
|
+
applicationUri: applicationUri2,
|
|
2874
|
+
dns: dns3,
|
|
2875
|
+
ip: ip2,
|
|
2876
|
+
startDate,
|
|
2877
|
+
validity
|
|
2878
|
+
});
|
|
2879
|
+
}
|
|
2880
|
+
async function createSelfSignedCertificate2(certificate, private_key, applicationUri2, startDate, validity) {
|
|
2881
|
+
await g_certificateAuthority.createSelfSignedCertificate(certificate, private_key, {
|
|
2882
|
+
applicationUri: applicationUri2,
|
|
2883
|
+
dns: dns2,
|
|
2884
|
+
ip,
|
|
2885
|
+
startDate,
|
|
2886
|
+
validity
|
|
2887
|
+
});
|
|
2888
|
+
}
|
|
2889
|
+
async function revoke_certificate(certificate) {
|
|
2890
|
+
await g_certificateAuthority.revokeCertificate(certificate, {});
|
|
2891
|
+
}
|
|
2892
|
+
async function createPrivateKeyIfNotExist(privateKey, keyLength) {
|
|
2893
|
+
if (fs10.existsSync(privateKey)) {
|
|
2894
|
+
warningLog(chalk7.yellow(" privateKey"), chalk7.cyan(privateKey), chalk7.yellow(" already exists => skipping"));
|
|
2895
|
+
return;
|
|
2896
|
+
} else {
|
|
2897
|
+
await generatePrivateKeyFile3(privateKey, keyLength);
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
displaySubtitle(` create private key :${private_key_file}`);
|
|
2901
|
+
await createPrivateKeyIfNotExist(private_key_file, key_length);
|
|
2902
|
+
displaySubtitle(` extract public key ${public_key_file} from private key `);
|
|
2903
|
+
await getPublicKeyFromPrivateKey(private_key_file, public_key_file);
|
|
2904
|
+
displaySubtitle(` create Certificate ${certificate_file}`);
|
|
2905
|
+
await createCertificateIfNotExist(certificate_file, private_key_file, applicationUri, yesterday, 365);
|
|
2906
|
+
displaySubtitle(` create self signed Certificate ${self_signed_certificate_file}`);
|
|
2907
|
+
if (fs10.existsSync(self_signed_certificate_file)) {
|
|
2908
|
+
return;
|
|
2909
|
+
}
|
|
2910
|
+
await createSelfSignedCertificate2(self_signed_certificate_file, private_key_file, applicationUri, yesterday, 365);
|
|
2911
|
+
if (dev) {
|
|
2912
|
+
await createCertificateIfNotExist(certificate_file_outofdate, private_key_file, applicationUri, two_years_ago, 365);
|
|
2913
|
+
await createCertificateIfNotExist(certificate_file_not_active_yet, private_key_file, applicationUri, next_year, 365);
|
|
2914
|
+
if (!fs10.existsSync(certificate_revoked)) {
|
|
2915
|
+
const certificate = await createCertificateIfNotExist(
|
|
2916
|
+
certificate_revoked,
|
|
2917
|
+
private_key_file,
|
|
2918
|
+
`${applicationUri}Revoked`,
|
|
2919
|
+
// make sure we used a uniq URI here
|
|
2920
|
+
yesterday,
|
|
2921
|
+
365
|
|
2922
|
+
);
|
|
2923
|
+
warningLog(" certificate to revoke => ", certificate);
|
|
2924
|
+
revoke_certificate(certificate_revoked);
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
async function wrap(func) {
|
|
2929
|
+
try {
|
|
2930
|
+
await func();
|
|
2931
|
+
} catch (err) {
|
|
2932
|
+
console.log(err.message);
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
async function create_default_certificates(dev) {
|
|
2936
|
+
assert10(gLocalConfig);
|
|
2937
|
+
const base_name = gLocalConfig.certificateDir || "";
|
|
2938
|
+
assert10(fs10.existsSync(base_name));
|
|
2939
|
+
let clientURN;
|
|
2940
|
+
let serverURN;
|
|
2941
|
+
let discoveryServerURN;
|
|
2942
|
+
wrap(async () => {
|
|
2943
|
+
await extractFullyQualifiedDomainName();
|
|
2944
|
+
const hostname = os4.hostname();
|
|
2945
|
+
const fqdn2 = getFullyQualifiedDomainName();
|
|
2946
|
+
warningLog(chalk7.yellow(" hostname = "), chalk7.cyan(hostname));
|
|
2947
|
+
warningLog(chalk7.yellow(" fqdn = "), chalk7.cyan(fqdn2));
|
|
2948
|
+
clientURN = makeApplicationUrn(hostname, "NodeOPCUA-Client");
|
|
2949
|
+
serverURN = makeApplicationUrn(hostname, "NodeOPCUA-Server");
|
|
2950
|
+
discoveryServerURN = makeApplicationUrn(hostname, "NodeOPCUA-DiscoveryServer");
|
|
2951
|
+
displayTitle("Create Application Certificate for Server & its private key");
|
|
2952
|
+
await createDefaultCertificate(base_name, "client_", 1024, clientURN, dev);
|
|
2953
|
+
await createDefaultCertificate(base_name, "client_", 2048, clientURN, dev);
|
|
2954
|
+
await createDefaultCertificate(base_name, "client_", 3072, clientURN, dev);
|
|
2955
|
+
await createDefaultCertificate(base_name, "client_", 4096, clientURN, dev);
|
|
2956
|
+
displayTitle("Create Application Certificate for Client & its private key");
|
|
2957
|
+
await createDefaultCertificate(base_name, "server_", 1024, serverURN, dev);
|
|
2958
|
+
await createDefaultCertificate(base_name, "server_", 2048, serverURN, dev);
|
|
2959
|
+
await createDefaultCertificate(base_name, "server_", 3072, serverURN, dev);
|
|
2960
|
+
await createDefaultCertificate(base_name, "server_", 4096, serverURN, dev);
|
|
2961
|
+
displayTitle("Create Application Certificate for DiscoveryServer & its private key");
|
|
2962
|
+
await createDefaultCertificate(base_name, "discoveryServer_", 1024, discoveryServerURN, dev);
|
|
2963
|
+
await createDefaultCertificate(base_name, "discoveryServer_", 2048, discoveryServerURN, dev);
|
|
2964
|
+
await createDefaultCertificate(base_name, "discoveryServer_", 3072, discoveryServerURN, dev);
|
|
2965
|
+
await createDefaultCertificate(base_name, "discoveryServer_", 4096, discoveryServerURN, dev);
|
|
2966
|
+
});
|
|
2967
|
+
}
|
|
2968
|
+
async function createDefaultCertificates(dev) {
|
|
2969
|
+
await construct_CertificateAuthority2("");
|
|
2970
|
+
await construct_CertificateManager();
|
|
2971
|
+
await create_default_certificates(dev);
|
|
2972
|
+
}
|
|
2973
|
+
function getOptions(names) {
|
|
2974
|
+
return commonOptions.filter((o) => names.includes(o.name) || o.name === "help" || o.name === "silent");
|
|
2975
|
+
}
|
|
2976
|
+
function showHelp(command, description, options, usage) {
|
|
2977
|
+
const sections = [
|
|
2978
|
+
{
|
|
2979
|
+
header: `Command: ${command}`,
|
|
2980
|
+
content: description
|
|
2981
|
+
},
|
|
2982
|
+
{
|
|
2983
|
+
header: "Usage",
|
|
2984
|
+
content: usage || `$0 ${command} [options]`
|
|
2985
|
+
},
|
|
2986
|
+
{
|
|
2987
|
+
header: "Options",
|
|
2988
|
+
optionList: options
|
|
2989
|
+
}
|
|
2990
|
+
];
|
|
2991
|
+
console.log(commandLineUsage(sections));
|
|
2992
|
+
}
|
|
2993
|
+
async function main(argumentsList) {
|
|
2994
|
+
const mainDefinitions = [{ name: "command", defaultOption: true }];
|
|
2995
|
+
let mainOptions;
|
|
2996
|
+
try {
|
|
2997
|
+
mainOptions = commandLineArgs(mainDefinitions, { argv: argumentsList, stopAtFirstUnknown: true });
|
|
2998
|
+
} catch (err) {
|
|
2999
|
+
console.log(err.message);
|
|
3000
|
+
return;
|
|
3001
|
+
}
|
|
3002
|
+
const argv = mainOptions._unknown || [];
|
|
3003
|
+
const command = mainOptions.command;
|
|
3004
|
+
if (!command || command === "help") {
|
|
3005
|
+
console.log(
|
|
3006
|
+
commandLineUsage([
|
|
3007
|
+
{
|
|
3008
|
+
header: "node-opcua-pki",
|
|
3009
|
+
content: `PKI management for node-opcua
|
|
3010
|
+
|
|
3011
|
+
${epilog}`
|
|
3012
|
+
},
|
|
3013
|
+
{
|
|
3014
|
+
header: "Commands",
|
|
3015
|
+
content: [
|
|
3016
|
+
{ name: "demo", summary: "create default certificate for node-opcua demos" },
|
|
3017
|
+
{ name: "createCA", summary: "create a Certificate Authority" },
|
|
3018
|
+
{ name: "createPKI", summary: "create a Public Key Infrastructure" },
|
|
3019
|
+
{ name: "certificate", summary: "create a new certificate" },
|
|
3020
|
+
{ name: "revoke <certificateFile>", summary: "revoke a existing certificate" },
|
|
3021
|
+
{ name: "csr", summary: "create a certificate signing request" },
|
|
3022
|
+
{ name: "sign", summary: "validate a certificate signing request and generate a certificate" },
|
|
3023
|
+
{ name: "dump <certificateFile>", summary: "display a certificate" },
|
|
3024
|
+
{ name: "toder <pemCertificate>", summary: "convert a certificate to a DER format with finger print" },
|
|
3025
|
+
{ name: "fingerprint <certificateFile>", summary: "print the certificate fingerprint" },
|
|
3026
|
+
{ name: "version", summary: "display the version number" }
|
|
3027
|
+
]
|
|
3028
|
+
}
|
|
3029
|
+
])
|
|
3030
|
+
);
|
|
3031
|
+
return;
|
|
3032
|
+
}
|
|
3033
|
+
if (command === "version") {
|
|
3034
|
+
const rootFolder = find_module_root_folder();
|
|
3035
|
+
const pkg = JSON.parse(fs10.readFileSync(path7.join(rootFolder, "package.json"), "utf-8"));
|
|
3036
|
+
console.log(pkg.version);
|
|
3037
|
+
return;
|
|
3038
|
+
}
|
|
3039
|
+
if (command === "demo") {
|
|
3040
|
+
const optionsDef = [
|
|
3041
|
+
...getOptions(["root", "silent"]),
|
|
3042
|
+
{ name: "dev", type: Boolean, description: "create all sort of fancy certificates for dev testing purposes" },
|
|
3043
|
+
{ name: "clean", type: Boolean, description: "Purge existing directory [use with care!]" }
|
|
3044
|
+
];
|
|
3045
|
+
const local_argv = commandLineArgs(optionsDef, { argv });
|
|
3046
|
+
if (local_argv.help)
|
|
3047
|
+
return showHelp(
|
|
3048
|
+
"demo",
|
|
3049
|
+
"create default certificate for node-opcua demos",
|
|
3050
|
+
optionsDef,
|
|
3051
|
+
"$0 demo [--dev] [--silent] [--clean]"
|
|
3052
|
+
);
|
|
3053
|
+
await wrap(async () => {
|
|
3054
|
+
await ensure_openssl_installed();
|
|
3055
|
+
displayChapter("Create Demo certificates");
|
|
3056
|
+
displayTitle("reading configuration");
|
|
3057
|
+
await readConfiguration(local_argv);
|
|
3058
|
+
if (local_argv.clean) {
|
|
3059
|
+
displayTitle("Cleaning old certificates");
|
|
3060
|
+
assert10(gLocalConfig);
|
|
3061
|
+
const certificateDir = gLocalConfig.certificateDir || "";
|
|
3062
|
+
const files = await fs10.promises.readdir(certificateDir);
|
|
3063
|
+
for (const file of files) {
|
|
3064
|
+
if (file.includes(".pem") || file.includes(".pub")) {
|
|
3065
|
+
await fs10.promises.unlink(path7.join(certificateDir, file));
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
mkdirRecursiveSync(certificateDir);
|
|
3069
|
+
}
|
|
3070
|
+
displayTitle("create certificates");
|
|
3071
|
+
await createDefaultCertificates(local_argv.dev);
|
|
3072
|
+
displayChapter("Demo certificates CREATED");
|
|
3073
|
+
});
|
|
3074
|
+
return;
|
|
3075
|
+
}
|
|
3076
|
+
if (command === "createCA") {
|
|
3077
|
+
const optionsDef = [
|
|
3078
|
+
...getOptions(["root", "CAFolder", "keySize", "silent"]),
|
|
3079
|
+
{ name: "subject", type: String, defaultValue: defaultSubject, description: "the CA certificate subject" }
|
|
3080
|
+
];
|
|
3081
|
+
const local_argv = commandLineArgs(optionsDef, { argv });
|
|
3082
|
+
if (local_argv.help) return showHelp("createCA", "create a Certificate Authority", optionsDef);
|
|
3083
|
+
await wrap(async () => {
|
|
3084
|
+
await ensure_openssl_installed();
|
|
3085
|
+
await readConfiguration(local_argv);
|
|
3086
|
+
await construct_CertificateAuthority2(local_argv.subject);
|
|
3087
|
+
});
|
|
3088
|
+
return;
|
|
3089
|
+
}
|
|
3090
|
+
if (command === "createPKI") {
|
|
3091
|
+
const optionsDef = getOptions(["root", "PKIFolder", "keySize", "silent"]);
|
|
3092
|
+
const local_argv = commandLineArgs(optionsDef, { argv });
|
|
3093
|
+
if (local_argv.help) return showHelp("createPKI", "create a Public Key Infrastructure", optionsDef);
|
|
3094
|
+
await wrap(async () => {
|
|
3095
|
+
await readConfiguration(local_argv);
|
|
3096
|
+
await construct_CertificateManager();
|
|
3097
|
+
});
|
|
3098
|
+
return;
|
|
3099
|
+
}
|
|
3100
|
+
if (command === "certificate") {
|
|
3101
|
+
const optionsDef = [
|
|
3102
|
+
...getOptions(["root", "CAFolder", "PKIFolder", "privateKey", "silent"]),
|
|
3103
|
+
{
|
|
3104
|
+
name: "applicationUri",
|
|
3105
|
+
alias: "a",
|
|
3106
|
+
type: String,
|
|
3107
|
+
defaultValue: "urn:{hostname}:Node-OPCUA-Server",
|
|
3108
|
+
description: "the application URI"
|
|
3109
|
+
},
|
|
3110
|
+
{
|
|
3111
|
+
name: "output",
|
|
3112
|
+
alias: "o",
|
|
3113
|
+
type: String,
|
|
3114
|
+
defaultValue: "my_certificate.pem",
|
|
3115
|
+
description: "the name of the generated certificate =>"
|
|
3116
|
+
},
|
|
3117
|
+
{
|
|
3118
|
+
name: "selfSigned",
|
|
3119
|
+
alias: "s",
|
|
3120
|
+
type: Boolean,
|
|
3121
|
+
defaultValue: false,
|
|
3122
|
+
description: "if true, certificate will be self-signed"
|
|
3123
|
+
},
|
|
3124
|
+
{ name: "validity", alias: "v", type: Number, description: "the certificate validity in days" },
|
|
3125
|
+
{
|
|
3126
|
+
name: "dns",
|
|
3127
|
+
type: String,
|
|
3128
|
+
defaultValue: "{hostname}",
|
|
3129
|
+
description: "the list of valid domain name (comma separated)"
|
|
3130
|
+
},
|
|
3131
|
+
{ name: "ip", type: String, defaultValue: "", description: "the list of valid IPs (comma separated)" },
|
|
3132
|
+
{
|
|
3133
|
+
name: "subject",
|
|
3134
|
+
type: String,
|
|
3135
|
+
defaultValue: "",
|
|
3136
|
+
description: "the certificate subject ( for instance C=FR/ST=Centre/L=Orleans/O=SomeOrganization/CN=Hello )"
|
|
3137
|
+
}
|
|
3138
|
+
];
|
|
3139
|
+
const local_argv = commandLineArgs(optionsDef, { argv });
|
|
3140
|
+
if (local_argv.help || !local_argv.applicationUri || !local_argv.output)
|
|
3141
|
+
return showHelp("certificate", "create a new certificate", optionsDef);
|
|
3142
|
+
async function command_certificate(local_argv2) {
|
|
3143
|
+
const selfSigned = !!local_argv2.selfSigned;
|
|
3144
|
+
if (!selfSigned) {
|
|
3145
|
+
await command_full_certificate(local_argv2);
|
|
3146
|
+
} else {
|
|
3147
|
+
await command_selfsigned_certificate(local_argv2);
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
async function command_selfsigned_certificate(local_argv2) {
|
|
3151
|
+
const _fqdn = await extractFullyQualifiedDomainName();
|
|
3152
|
+
await readConfiguration(local_argv2);
|
|
3153
|
+
await construct_CertificateManager();
|
|
3154
|
+
displaySubtitle(` create self signed Certificate ${gLocalConfig.outputFile}`);
|
|
3155
|
+
let subject = local_argv2.subject && local_argv2.subject.length > 1 ? new Subject5(local_argv2.subject) : gLocalConfig.subject || "";
|
|
3156
|
+
subject = JSON.parse(JSON.stringify(subject));
|
|
3157
|
+
const params = {
|
|
3158
|
+
applicationUri: gLocalConfig.applicationUri || "",
|
|
3159
|
+
dns: gLocalConfig.dns || [],
|
|
3160
|
+
ip: gLocalConfig.ip || [],
|
|
3161
|
+
outputFile: gLocalConfig.outputFile || "self_signed_certificate.pem",
|
|
3162
|
+
startDate: gLocalConfig.startDate || /* @__PURE__ */ new Date(),
|
|
3163
|
+
subject,
|
|
3164
|
+
validity: gLocalConfig.validity || 365
|
|
3165
|
+
};
|
|
3166
|
+
await certificateManager.createSelfSignedCertificate(params);
|
|
3167
|
+
}
|
|
3168
|
+
async function command_full_certificate(local_argv2) {
|
|
3169
|
+
await readConfiguration(local_argv2);
|
|
3170
|
+
await construct_CertificateManager();
|
|
3171
|
+
await construct_CertificateAuthority2("");
|
|
3172
|
+
assert10(fs10.existsSync(gLocalConfig.CAFolder || ""), " CA folder must exist");
|
|
3173
|
+
gLocalConfig.privateKey = void 0;
|
|
3174
|
+
gLocalConfig.subject = local_argv2.subject && local_argv2.subject.length > 1 ? local_argv2.subject : gLocalConfig.subject;
|
|
3175
|
+
const csr_file = await certificateManager.createCertificateRequest(
|
|
3176
|
+
gLocalConfig
|
|
3177
|
+
);
|
|
3178
|
+
if (!csr_file) {
|
|
3179
|
+
return;
|
|
3180
|
+
}
|
|
3181
|
+
warningLog(" csr_file = ", csr_file);
|
|
3182
|
+
const certificate = csr_file.replace(".csr", ".pem");
|
|
3183
|
+
if (fs10.existsSync(certificate)) {
|
|
3184
|
+
throw new Error(` File ${certificate} already exist`);
|
|
3185
|
+
}
|
|
3186
|
+
await g_certificateAuthority.signCertificateRequest(
|
|
3187
|
+
certificate,
|
|
3188
|
+
csr_file,
|
|
3189
|
+
gLocalConfig
|
|
3190
|
+
);
|
|
3191
|
+
assert10(typeof gLocalConfig.outputFile === "string");
|
|
3192
|
+
fs10.writeFileSync(gLocalConfig.outputFile || "", fs10.readFileSync(certificate, "ascii"));
|
|
3193
|
+
}
|
|
3194
|
+
await wrap(async () => await command_certificate(local_argv));
|
|
3195
|
+
return;
|
|
3196
|
+
}
|
|
3197
|
+
if (command === "revoke") {
|
|
3198
|
+
const optionsDef = [{ name: "certificateFile", type: String, defaultOption: true }, ...getOptions(["root", "CAFolder"])];
|
|
3199
|
+
const local_argv = commandLineArgs(optionsDef, { argv });
|
|
3200
|
+
if (local_argv.help || !local_argv.certificateFile)
|
|
3201
|
+
return showHelp(
|
|
3202
|
+
"revoke <certificateFile>",
|
|
3203
|
+
"revoke a existing certificate",
|
|
3204
|
+
optionsDef,
|
|
3205
|
+
"$0 revoke my_certificate.pem"
|
|
3206
|
+
);
|
|
3207
|
+
async function revoke_certificate(certificate) {
|
|
3208
|
+
await g_certificateAuthority.revokeCertificate(certificate, {});
|
|
3209
|
+
}
|
|
3210
|
+
await wrap(async () => {
|
|
3211
|
+
const certificate = path7.resolve(local_argv.certificateFile);
|
|
3212
|
+
warningLog(chalk7.yellow(" Certificate to revoke : "), chalk7.cyan(certificate));
|
|
3213
|
+
if (!fs10.existsSync(certificate)) {
|
|
3214
|
+
throw new Error(`cannot find certificate to revoke ${certificate}`);
|
|
3215
|
+
}
|
|
3216
|
+
await readConfiguration(local_argv);
|
|
3217
|
+
await construct_CertificateAuthority2("");
|
|
3218
|
+
await revoke_certificate(certificate);
|
|
3219
|
+
warningLog("done ... ");
|
|
3220
|
+
warningLog(" crl = ", g_certificateAuthority.revocationList);
|
|
3221
|
+
warningLog("\nyou should now publish the new Certificate Revocation List");
|
|
3222
|
+
});
|
|
3223
|
+
return;
|
|
3224
|
+
}
|
|
3225
|
+
if (command === "csr") {
|
|
3226
|
+
const optionsDef = [
|
|
3227
|
+
...getOptions(["root", "PKIFolder", "privateKey", "silent"]),
|
|
3228
|
+
{
|
|
3229
|
+
name: "applicationUri",
|
|
3230
|
+
alias: "a",
|
|
3231
|
+
type: String,
|
|
3232
|
+
defaultValue: "urn:{hostname}:Node-OPCUA-Server",
|
|
3233
|
+
description: "the application URI"
|
|
3234
|
+
},
|
|
3235
|
+
{
|
|
3236
|
+
name: "output",
|
|
3237
|
+
alias: "o",
|
|
3238
|
+
type: String,
|
|
3239
|
+
defaultValue: "my_certificate_signing_request.csr",
|
|
3240
|
+
description: "the name of the generated signing_request"
|
|
3241
|
+
},
|
|
3242
|
+
{
|
|
3243
|
+
name: "dns",
|
|
3244
|
+
type: String,
|
|
3245
|
+
defaultValue: "{hostname}",
|
|
3246
|
+
description: "the list of valid domain name (comma separated)"
|
|
3247
|
+
},
|
|
3248
|
+
{ name: "ip", type: String, defaultValue: "", description: "the list of valid IPs (comma separated)" },
|
|
3249
|
+
{
|
|
3250
|
+
name: "subject",
|
|
3251
|
+
type: String,
|
|
3252
|
+
defaultValue: "/CN=Certificate",
|
|
3253
|
+
description: "the certificate subject ( for instance /C=FR/ST=Centre/L=Orleans/O=SomeOrganization/CN=Hello )"
|
|
3254
|
+
}
|
|
3255
|
+
];
|
|
3256
|
+
const local_argv = commandLineArgs(optionsDef, { argv });
|
|
3257
|
+
if (local_argv.help) return showHelp("csr", "create a certificate signing request", optionsDef);
|
|
3258
|
+
await wrap(async () => {
|
|
3259
|
+
await readConfiguration(local_argv);
|
|
3260
|
+
if (!fs10.existsSync(gLocalConfig.PKIFolder || "")) {
|
|
3261
|
+
warningLog("PKI folder must exist");
|
|
3262
|
+
}
|
|
3263
|
+
await construct_CertificateManager();
|
|
3264
|
+
if (!gLocalConfig.outputFile || fs10.existsSync(gLocalConfig.outputFile)) {
|
|
3265
|
+
throw new Error(` File ${gLocalConfig.outputFile} already exist`);
|
|
3266
|
+
}
|
|
3267
|
+
gLocalConfig.privateKey = void 0;
|
|
3268
|
+
gLocalConfig.subject = local_argv.subject && local_argv.subject.length > 1 ? local_argv.subject : gLocalConfig.subject;
|
|
3269
|
+
const internal_csr_file = await certificateManager.createCertificateRequest(
|
|
3270
|
+
gLocalConfig
|
|
3271
|
+
);
|
|
3272
|
+
if (!internal_csr_file) {
|
|
3273
|
+
return;
|
|
3274
|
+
}
|
|
3275
|
+
if (!gLocalConfig.outputFile) {
|
|
3276
|
+
warningLog("please specify a output file");
|
|
3277
|
+
return;
|
|
3278
|
+
}
|
|
3279
|
+
const csr = await fs10.promises.readFile(internal_csr_file, "utf-8");
|
|
3280
|
+
fs10.writeFileSync(gLocalConfig.outputFile || "", csr, "utf-8");
|
|
3281
|
+
warningLog("Subject = ", gLocalConfig.subject);
|
|
3282
|
+
warningLog("applicationUri = ", gLocalConfig.applicationUri);
|
|
3283
|
+
warningLog("altNames = ", gLocalConfig.altNames);
|
|
3284
|
+
warningLog("dns = ", gLocalConfig.dns);
|
|
3285
|
+
warningLog("ip = ", gLocalConfig.ip);
|
|
3286
|
+
warningLog("CSR file = ", gLocalConfig.outputFile);
|
|
3287
|
+
});
|
|
3288
|
+
return;
|
|
3289
|
+
}
|
|
3290
|
+
if (command === "sign") {
|
|
3291
|
+
const optionsDef = [
|
|
3292
|
+
...getOptions(["root", "CAFolder", "silent"]),
|
|
3293
|
+
{ name: "csr", alias: "i", type: String, defaultValue: "my_certificate_signing_request.csr", description: "the csr" },
|
|
3294
|
+
{
|
|
3295
|
+
name: "output",
|
|
3296
|
+
alias: "o",
|
|
3297
|
+
type: String,
|
|
3298
|
+
defaultValue: "my_certificate.pem",
|
|
3299
|
+
description: "the name of the generated certificate"
|
|
3300
|
+
},
|
|
3301
|
+
{ name: "validity", alias: "v", type: Number, defaultValue: 365, description: "the certificate validity in days" }
|
|
3302
|
+
];
|
|
3303
|
+
const local_argv = commandLineArgs(optionsDef, { argv });
|
|
3304
|
+
if (local_argv.help || !local_argv.csr || !local_argv.output)
|
|
3305
|
+
return showHelp("sign", "validate a certificate signing request and generate a certificate", optionsDef);
|
|
3306
|
+
await wrap(async () => {
|
|
3307
|
+
await readConfiguration(local_argv);
|
|
3308
|
+
if (!fs10.existsSync(gLocalConfig.CAFolder || "")) {
|
|
3309
|
+
throw new Error(`CA folder must exist:${gLocalConfig.CAFolder}`);
|
|
3310
|
+
}
|
|
3311
|
+
await construct_CertificateAuthority2("");
|
|
3312
|
+
const csr_file = path7.resolve(local_argv.csr || "");
|
|
3313
|
+
if (!fs10.existsSync(csr_file)) {
|
|
3314
|
+
throw new Error(`Certificate signing request doesn't exist: ${csr_file}`);
|
|
3315
|
+
}
|
|
3316
|
+
const certificate = path7.resolve(local_argv.output || csr_file.replace(".csr", ".pem"));
|
|
3317
|
+
if (fs10.existsSync(certificate)) {
|
|
3318
|
+
throw new Error(` File ${certificate} already exist`);
|
|
3319
|
+
}
|
|
3320
|
+
await g_certificateAuthority.signCertificateRequest(
|
|
3321
|
+
certificate,
|
|
3322
|
+
csr_file,
|
|
3323
|
+
gLocalConfig
|
|
3324
|
+
);
|
|
3325
|
+
assert10(typeof gLocalConfig.outputFile === "string");
|
|
3326
|
+
fs10.writeFileSync(gLocalConfig.outputFile || "", fs10.readFileSync(certificate, "ascii"));
|
|
3327
|
+
});
|
|
3328
|
+
return;
|
|
3329
|
+
}
|
|
3330
|
+
if (command === "dump") {
|
|
3331
|
+
const optionsDef = [
|
|
3332
|
+
{ name: "certificateFile", type: String, defaultOption: true },
|
|
3333
|
+
{ name: "help", alias: "h", type: Boolean }
|
|
3334
|
+
];
|
|
3335
|
+
const local_argv = commandLineArgs(optionsDef, { argv });
|
|
3336
|
+
if (local_argv.help || !local_argv.certificateFile)
|
|
3337
|
+
return showHelp("dump <certificateFile>", "display a certificate", optionsDef);
|
|
3338
|
+
await wrap(async () => {
|
|
3339
|
+
const data = await dumpCertificate(local_argv.certificateFile);
|
|
3340
|
+
warningLog(data);
|
|
3341
|
+
});
|
|
3342
|
+
return;
|
|
3343
|
+
}
|
|
3344
|
+
if (command === "toder") {
|
|
3345
|
+
const optionsDef = [
|
|
3346
|
+
{ name: "pemCertificate", type: String, defaultOption: true },
|
|
3347
|
+
{ name: "help", alias: "h", type: Boolean }
|
|
3348
|
+
];
|
|
3349
|
+
const local_argv = commandLineArgs(optionsDef, { argv });
|
|
3350
|
+
if (local_argv.help || !local_argv.pemCertificate)
|
|
3351
|
+
return showHelp("toder <pemCertificate>", "convert a certificate to a DER format with finger print", optionsDef);
|
|
3352
|
+
await wrap(async () => {
|
|
3353
|
+
await toDer(local_argv.pemCertificate);
|
|
3354
|
+
});
|
|
3355
|
+
return;
|
|
3356
|
+
}
|
|
3357
|
+
if (command === "fingerprint") {
|
|
3358
|
+
const optionsDef = [
|
|
3359
|
+
{ name: "certificateFile", type: String, defaultOption: true },
|
|
3360
|
+
{ name: "help", alias: "h", type: Boolean }
|
|
3361
|
+
];
|
|
3362
|
+
const local_argv = commandLineArgs(optionsDef, { argv });
|
|
3363
|
+
if (local_argv.help || !local_argv.certificateFile)
|
|
3364
|
+
return showHelp("fingerprint <certificateFile>", "print the certificate fingerprint", optionsDef);
|
|
3365
|
+
await wrap(async () => {
|
|
3366
|
+
const certificate = local_argv.certificateFile;
|
|
3367
|
+
const data = await fingerprint(certificate);
|
|
3368
|
+
if (!data) return;
|
|
3369
|
+
const s = data.split("=")[1].split(":").join("").trim();
|
|
3370
|
+
warningLog(s);
|
|
3371
|
+
});
|
|
3372
|
+
return;
|
|
3373
|
+
}
|
|
3374
|
+
console.log(`Unknown command: ${command}`);
|
|
3375
|
+
}
|
|
3376
|
+
var epilog, today, yesterday, two_years_ago, next_year, gLocalConfig, g_certificateAuthority, certificateManager, commonOptions;
|
|
3377
|
+
var init_crypto_create_CA = __esm({
|
|
3378
|
+
"packages/node-opcua-pki/lib/ca/crypto_create_CA.ts"() {
|
|
3379
|
+
"use strict";
|
|
3380
|
+
init_esm_shims();
|
|
3381
|
+
init_applicationurn();
|
|
3382
|
+
init_hostname();
|
|
3383
|
+
init_certificate_manager();
|
|
3384
|
+
init_toolbox();
|
|
3385
|
+
init_with_openssl();
|
|
3386
|
+
init_certificate_authority();
|
|
3387
|
+
epilog = "Copyright (c) sterfive - node-opcua - 2017-2026";
|
|
3388
|
+
today = /* @__PURE__ */ new Date();
|
|
3389
|
+
yesterday = get_offset_date(today, -1);
|
|
3390
|
+
two_years_ago = get_offset_date(today, -2 * 365);
|
|
3391
|
+
next_year = get_offset_date(today, 365);
|
|
3392
|
+
gLocalConfig = {};
|
|
3393
|
+
commonOptions = [
|
|
3394
|
+
{
|
|
3395
|
+
name: "root",
|
|
3396
|
+
alias: "r",
|
|
3397
|
+
type: String,
|
|
3398
|
+
defaultValue: "{CWD}/certificates",
|
|
3399
|
+
description: "the location of the Certificate folder"
|
|
3400
|
+
},
|
|
3401
|
+
{
|
|
3402
|
+
name: "CAFolder",
|
|
3403
|
+
alias: "c",
|
|
3404
|
+
type: String,
|
|
3405
|
+
defaultValue: "{root}/CA",
|
|
3406
|
+
description: "the location of the Certificate Authority folder"
|
|
3407
|
+
},
|
|
3408
|
+
{ name: "PKIFolder", type: String, defaultValue: "{root}/PKI", description: "the location of the Public Key Infrastructure" },
|
|
3409
|
+
{ name: "silent", type: Boolean, defaultValue: false, description: "minimize output" },
|
|
3410
|
+
{
|
|
3411
|
+
name: "privateKey",
|
|
3412
|
+
alias: "p",
|
|
3413
|
+
type: String,
|
|
3414
|
+
defaultValue: "{PKIFolder}/own/private_key.pem",
|
|
3415
|
+
description: "the private key to use to generate certificate"
|
|
3416
|
+
},
|
|
3417
|
+
{
|
|
3418
|
+
name: "keySize",
|
|
3419
|
+
alias: "k",
|
|
3420
|
+
type: Number,
|
|
3421
|
+
defaultValue: 2048,
|
|
3422
|
+
description: "the private key size in bits (1024|2048|3072|4096)"
|
|
3423
|
+
},
|
|
3424
|
+
{ name: "help", alias: "h", type: Boolean, description: "display this help" }
|
|
3425
|
+
];
|
|
3426
|
+
}
|
|
3427
|
+
});
|
|
6
3428
|
|
|
7
3429
|
// packages/node-opcua-pki/bin/pki.ts
|
|
8
|
-
import { pki_main } from "node-opcua-pki";
|
|
9
3430
|
var require_pki = __commonJS({
|
|
10
3431
|
"packages/node-opcua-pki/bin/pki.ts"() {
|
|
11
3432
|
init_esm_shims();
|
|
12
|
-
|
|
3433
|
+
init_crypto_create_CA();
|
|
3434
|
+
main(process.argv.splice(2));
|
|
13
3435
|
}
|
|
14
3436
|
});
|
|
15
3437
|
export default require_pki();
|