node-opcua-pki 6.19.1 → 6.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/pki.mjs +1983 -840
- package/dist/bin/pki.mjs.map +1 -1
- package/dist/chunk-2HTNH73K.mjs +1913 -0
- package/dist/chunk-2HTNH73K.mjs.map +1 -0
- package/dist/core-C6faZClF.d.mts +1032 -0
- package/dist/core-C6faZClF.d.ts +1032 -0
- package/dist/core.d.mts +2 -0
- package/dist/core.d.ts +2 -0
- package/dist/core.js +1902 -0
- package/dist/core.js.map +1 -0
- package/dist/core.mjs +21 -0
- package/dist/core.mjs.map +1 -0
- package/dist/index.d.mts +185 -746
- package/dist/index.d.ts +185 -746
- package/dist/index.js +2119 -970
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1074 -1759
- package/dist/index.mjs.map +1 -1
- package/package.json +14 -3
- package/readme.md +4 -0
package/dist/bin/pki.mjs
CHANGED
|
@@ -91,6 +91,45 @@ var init_hostname = __esm({
|
|
|
91
91
|
}
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
+
// packages/node-opcua-pki/lib/toolbox/common.ts
|
|
95
|
+
import assert2 from "assert";
|
|
96
|
+
async function resolvePrivateKeyPassphrase(passphrase) {
|
|
97
|
+
if (passphrase === void 0) {
|
|
98
|
+
return void 0;
|
|
99
|
+
}
|
|
100
|
+
return typeof passphrase === "function" ? await passphrase() : passphrase;
|
|
101
|
+
}
|
|
102
|
+
function adjustDate(params) {
|
|
103
|
+
assert2(params instanceof Object);
|
|
104
|
+
params.startDate = params.startDate || /* @__PURE__ */ new Date();
|
|
105
|
+
assert2(params.startDate instanceof Date);
|
|
106
|
+
if (params.validityMs !== void 0) {
|
|
107
|
+
if (params.validityMs <= 0) {
|
|
108
|
+
throw new RangeError(`validityMs must be > 0 (got ${params.validityMs})`);
|
|
109
|
+
}
|
|
110
|
+
params.endDate = new Date(params.startDate.getTime() + params.validityMs);
|
|
111
|
+
params.validity = Math.ceil(params.validityMs / 864e5);
|
|
112
|
+
} else {
|
|
113
|
+
params.validity = params.validity || 365;
|
|
114
|
+
params.endDate = new Date(params.startDate.getTime());
|
|
115
|
+
params.endDate.setDate(params.startDate.getDate() + params.validity);
|
|
116
|
+
}
|
|
117
|
+
assert2(params.endDate instanceof Date);
|
|
118
|
+
assert2(params.startDate instanceof Date);
|
|
119
|
+
}
|
|
120
|
+
function adjustApplicationUri(params) {
|
|
121
|
+
const applicationUri = params.applicationUri || "";
|
|
122
|
+
if (applicationUri.length > 200) {
|
|
123
|
+
throw new Error(`Openssl doesn't support urn with length greater than 200${applicationUri}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
var init_common = __esm({
|
|
127
|
+
"packages/node-opcua-pki/lib/toolbox/common.ts"() {
|
|
128
|
+
"use strict";
|
|
129
|
+
init_esm_shims();
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
94
133
|
// packages/node-opcua-pki/lib/toolbox/config.ts
|
|
95
134
|
var g_config;
|
|
96
135
|
var init_config = __esm({
|
|
@@ -126,7 +165,7 @@ var init_debug = __esm({
|
|
|
126
165
|
});
|
|
127
166
|
|
|
128
167
|
// packages/node-opcua-pki/lib/toolbox/common2.ts
|
|
129
|
-
import
|
|
168
|
+
import assert3 from "assert";
|
|
130
169
|
import fs from "fs";
|
|
131
170
|
import path from "path";
|
|
132
171
|
import chalk from "chalk";
|
|
@@ -145,12 +184,31 @@ function mkdirRecursiveSync(folder) {
|
|
|
145
184
|
fs.mkdirSync(folder, { recursive: true });
|
|
146
185
|
}
|
|
147
186
|
}
|
|
187
|
+
function restrictPrivateFilePermissions(target, mode) {
|
|
188
|
+
if (process.platform === "win32") {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
fs.chmodSync(target, mode);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
warningLog(chalk.yellow(" could not restrict permissions on "), target, err.message);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function ensurePrivateDirectory(dir) {
|
|
198
|
+
if (!fs.existsSync(dir)) {
|
|
199
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
200
|
+
}
|
|
201
|
+
restrictPrivateFilePermissions(dir, 448);
|
|
202
|
+
}
|
|
203
|
+
function isEncryptedPrivateKeyFile(filename) {
|
|
204
|
+
return fs.readFileSync(filename, "utf-8").includes("-----BEGIN ENCRYPTED PRIVATE KEY-----");
|
|
205
|
+
}
|
|
148
206
|
function makePath(folderName, filename) {
|
|
149
207
|
let s;
|
|
150
208
|
if (filename) {
|
|
151
209
|
s = path.join(path.normalize(folderName), filename);
|
|
152
210
|
} else {
|
|
153
|
-
|
|
211
|
+
assert3(folderName);
|
|
154
212
|
s = folderName;
|
|
155
213
|
}
|
|
156
214
|
s = s.replace(/\\/g, "/");
|
|
@@ -203,21 +261,22 @@ var init_display = __esm({
|
|
|
203
261
|
});
|
|
204
262
|
|
|
205
263
|
// packages/node-opcua-pki/lib/toolbox/without_openssl/create_certificate_signing_request.ts
|
|
206
|
-
import
|
|
264
|
+
import assert4 from "assert";
|
|
207
265
|
import fs2 from "fs";
|
|
208
|
-
import { createCertificateSigningRequest, pemToPrivateKey, Subject } from "node-opcua-crypto";
|
|
266
|
+
import { coercePrivateKeyPem, createCertificateSigningRequest, pemToPrivateKey, Subject } from "node-opcua-crypto";
|
|
209
267
|
async function createCertificateSigningRequestAsync(certificateSigningRequestFilename, params) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
268
|
+
assert4(params);
|
|
269
|
+
assert4(params.rootDir);
|
|
270
|
+
assert4(params.configFile);
|
|
271
|
+
assert4(params.privateKey);
|
|
272
|
+
if (typeof params.privateKey === "string") {
|
|
273
|
+
assert4(fs2.existsSync(params.privateKey), `Private key must exist${params.privateKey}`);
|
|
274
|
+
}
|
|
275
|
+
assert4(fs2.existsSync(params.rootDir), "RootDir key must exist");
|
|
276
|
+
assert4(typeof certificateSigningRequestFilename === "string");
|
|
218
277
|
const subject = params.subject ? new Subject(params.subject).toString() : void 0;
|
|
219
278
|
displaySubtitle("- Creating a Certificate Signing Request with subtile");
|
|
220
|
-
const privateKeyPem = await fs2.promises.readFile(params.privateKey, "utf-8");
|
|
279
|
+
const privateKeyPem = typeof params.privateKey === "string" ? await fs2.promises.readFile(params.privateKey, "utf-8") : coercePrivateKeyPem(params.privateKey);
|
|
221
280
|
const privateKey = await pemToPrivateKey(privateKeyPem);
|
|
222
281
|
const { csr } = await createCertificateSigningRequest({
|
|
223
282
|
privateKey,
|
|
@@ -228,7 +287,7 @@ async function createCertificateSigningRequestAsync(certificateSigningRequestFil
|
|
|
228
287
|
purpose: params.purpose
|
|
229
288
|
});
|
|
230
289
|
await fs2.promises.writeFile(certificateSigningRequestFilename, csr, "utf-8");
|
|
231
|
-
display(`- privateKey ${params.privateKey}`);
|
|
290
|
+
display(`- privateKey ${typeof params.privateKey === "string" ? params.privateKey : "<in-memory>"}`);
|
|
232
291
|
display(`- certificateSigningRequestFilename ${certificateSigningRequestFilename}`);
|
|
233
292
|
}
|
|
234
293
|
var init_create_certificate_signing_request = __esm({
|
|
@@ -239,47 +298,12 @@ var init_create_certificate_signing_request = __esm({
|
|
|
239
298
|
}
|
|
240
299
|
});
|
|
241
300
|
|
|
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
|
-
if (params.validityMs !== void 0) {
|
|
252
|
-
if (params.validityMs <= 0) {
|
|
253
|
-
throw new RangeError(`validityMs must be > 0 (got ${params.validityMs})`);
|
|
254
|
-
}
|
|
255
|
-
params.endDate = new Date(params.startDate.getTime() + params.validityMs);
|
|
256
|
-
params.validity = Math.ceil(params.validityMs / 864e5);
|
|
257
|
-
} else {
|
|
258
|
-
params.validity = params.validity || 365;
|
|
259
|
-
params.endDate = new Date(params.startDate.getTime());
|
|
260
|
-
params.endDate.setDate(params.startDate.getDate() + params.validity);
|
|
261
|
-
}
|
|
262
|
-
assert4(params.endDate instanceof Date);
|
|
263
|
-
assert4(params.startDate instanceof Date);
|
|
264
|
-
}
|
|
265
|
-
function adjustApplicationUri(params) {
|
|
266
|
-
const applicationUri = params.applicationUri || "";
|
|
267
|
-
if (applicationUri.length > 200) {
|
|
268
|
-
throw new Error(`Openssl doesn't support urn with length greater than 200${applicationUri}`);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
var init_common = __esm({
|
|
272
|
-
"packages/node-opcua-pki/lib/toolbox/common.ts"() {
|
|
273
|
-
"use strict";
|
|
274
|
-
init_esm_shims();
|
|
275
|
-
}
|
|
276
|
-
});
|
|
277
|
-
|
|
278
301
|
// packages/node-opcua-pki/lib/toolbox/without_openssl/create_self_signed_certificate.ts
|
|
279
302
|
import assert5 from "assert";
|
|
280
303
|
import fs3 from "fs";
|
|
281
304
|
import {
|
|
282
305
|
CertificatePurpose,
|
|
306
|
+
coercePrivateKeyPem as coercePrivateKeyPem2,
|
|
283
307
|
createSelfSignedCertificate as createSelfSignedCertificate1,
|
|
284
308
|
pemToPrivateKey as pemToPrivateKey2,
|
|
285
309
|
Subject as Subject2
|
|
@@ -289,7 +313,9 @@ async function createSelfSignedCertificateAsync(certificate, params) {
|
|
|
289
313
|
assert5(params.purpose, "Please provide a Certificate Purpose");
|
|
290
314
|
assert5(fs3.existsSync(params.configFile));
|
|
291
315
|
assert5(fs3.existsSync(params.rootDir));
|
|
292
|
-
|
|
316
|
+
if (typeof params.privateKey === "string") {
|
|
317
|
+
assert5(fs3.existsSync(params.privateKey));
|
|
318
|
+
}
|
|
293
319
|
if (!params.subject) {
|
|
294
320
|
throw Error("Missing subject");
|
|
295
321
|
}
|
|
@@ -301,7 +327,7 @@ async function createSelfSignedCertificateAsync(certificate, params) {
|
|
|
301
327
|
subject = subject.toString();
|
|
302
328
|
const purpose = params.purpose;
|
|
303
329
|
displayTitle("Generate a certificate request");
|
|
304
|
-
const privateKeyPem = await fs3.promises.readFile(params.privateKey, "utf-8");
|
|
330
|
+
const privateKeyPem = typeof params.privateKey === "string" ? await fs3.promises.readFile(params.privateKey, "utf-8") : coercePrivateKeyPem2(params.privateKey);
|
|
305
331
|
const privateKey = await pemToPrivateKey2(privateKeyPem);
|
|
306
332
|
const { cert } = await createSelfSignedCertificate1({
|
|
307
333
|
privateKey,
|
|
@@ -365,9 +391,11 @@ import {
|
|
|
365
391
|
readCertificateChain,
|
|
366
392
|
readCertificateChainAsync,
|
|
367
393
|
readCertificateRevocationList,
|
|
394
|
+
readPrivateKey,
|
|
368
395
|
split_der,
|
|
369
396
|
toPem,
|
|
370
|
-
verifyCertificateSignature
|
|
397
|
+
verifyCertificateSignature,
|
|
398
|
+
writePrivateKeyFile
|
|
371
399
|
} from "node-opcua-crypto";
|
|
372
400
|
function getOrComputeInfo(entry) {
|
|
373
401
|
if (!entry.info) {
|
|
@@ -468,6 +496,7 @@ var init_certificate_manager = __esm({
|
|
|
468
496
|
"packages/node-opcua-pki/lib/pki/certificate_manager.ts"() {
|
|
469
497
|
"use strict";
|
|
470
498
|
init_esm_shims();
|
|
499
|
+
init_common();
|
|
471
500
|
init_common2();
|
|
472
501
|
init_debug();
|
|
473
502
|
init_without_openssl();
|
|
@@ -490,7 +519,31 @@ var init_certificate_manager = __esm({
|
|
|
490
519
|
// even if the consumer forgets to call dispose().
|
|
491
520
|
static #activeInstances = /* @__PURE__ */ new Set();
|
|
492
521
|
static #cleanupInstalled = false;
|
|
493
|
-
static #
|
|
522
|
+
static #exitHandler;
|
|
523
|
+
/**
|
|
524
|
+
* Install a best-effort `exit` hook that closes any watcher
|
|
525
|
+
* still open when the process terminates.
|
|
526
|
+
*
|
|
527
|
+
* **This library never terminates the host process.** No
|
|
528
|
+
* SIGINT/SIGTERM handler is installed: deciding how (and
|
|
529
|
+
* whether) to shut down on a signal is the application's
|
|
530
|
+
* responsibility, and a listener registered here would both
|
|
531
|
+
* pre-empt the application's own graceful shutdown and
|
|
532
|
+
* silently suppress Node's default signal behaviour.
|
|
533
|
+
*
|
|
534
|
+
* Nothing here is load-bearing for process exit. The native
|
|
535
|
+
* `fs.watch` handles are `unref()`'d when the watchers are
|
|
536
|
+
* created (see `#readCertificates`), so an undisposed
|
|
537
|
+
* CertificateManager never keeps the event loop alive. This
|
|
538
|
+
* hook is only tidiness on the way out.
|
|
539
|
+
*
|
|
540
|
+
* `exit` rather than `beforeExit`: `beforeExit` fires when
|
|
541
|
+
* the loop merely drains and the loop can subsequently be
|
|
542
|
+
* resurrected, which would leave a still-in-use instance
|
|
543
|
+
* marked Disposed. `exit` is terminal, synchronous-only and
|
|
544
|
+
* cannot alter the exit code.
|
|
545
|
+
*/
|
|
546
|
+
static #installExitCleanup() {
|
|
494
547
|
if (_CertificateManager.#cleanupInstalled) return;
|
|
495
548
|
_CertificateManager.#cleanupInstalled = true;
|
|
496
549
|
const closeDanglingWatchers = () => {
|
|
@@ -506,13 +559,22 @@ var init_certificate_manager = __esm({
|
|
|
506
559
|
}
|
|
507
560
|
_CertificateManager.#activeInstances.clear();
|
|
508
561
|
};
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
562
|
+
_CertificateManager.#exitHandler = closeDanglingWatchers;
|
|
563
|
+
process.on("exit", closeDanglingWatchers);
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Remove the `exit` hook once the last instance is disposed,
|
|
567
|
+
* so a library that is initialized and disposed repeatedly
|
|
568
|
+
* does not accumulate process listeners. A later
|
|
569
|
+
* `initialize()` re-arms it.
|
|
570
|
+
*/
|
|
571
|
+
static #uninstallExitCleanupIfIdle() {
|
|
572
|
+
if (_CertificateManager.#activeInstances.size > 0) return;
|
|
573
|
+
if (_CertificateManager.#exitHandler) {
|
|
574
|
+
process.removeListener("exit", _CertificateManager.#exitHandler);
|
|
575
|
+
_CertificateManager.#exitHandler = void 0;
|
|
515
576
|
}
|
|
577
|
+
_CertificateManager.#cleanupInstalled = false;
|
|
516
578
|
}
|
|
517
579
|
/**
|
|
518
580
|
* Dispose **all** active CertificateManager instances,
|
|
@@ -576,6 +638,15 @@ var init_certificate_manager = __esm({
|
|
|
576
638
|
#initializingPromise;
|
|
577
639
|
#addCertValidation;
|
|
578
640
|
#disableFileWatchers;
|
|
641
|
+
#privateKeyPassphrase;
|
|
642
|
+
#privateKeyProvider;
|
|
643
|
+
/**
|
|
644
|
+
* The on-disk key, decrypted once and kept for the instance's lifetime,
|
|
645
|
+
* so the passphrase (or its resolver function) is consulted at most
|
|
646
|
+
* once. Cleared by `dispose()`. Not used when a provider is configured:
|
|
647
|
+
* the provider is the authority on the current key.
|
|
648
|
+
*/
|
|
649
|
+
#cachedPrivateKey;
|
|
579
650
|
#thumbs = {
|
|
580
651
|
rejected: /* @__PURE__ */ new Map(),
|
|
581
652
|
trusted: /* @__PURE__ */ new Map(),
|
|
@@ -610,6 +681,8 @@ var init_certificate_manager = __esm({
|
|
|
610
681
|
maxChainLength: v.maxChainLength ?? 5
|
|
611
682
|
};
|
|
612
683
|
this.#disableFileWatchers = options.disableFileWatchers ?? process.env.OPCUA_PKI_DISABLE_FILE_WATCHERS === "true";
|
|
684
|
+
this.#privateKeyPassphrase = options.privateKeyPassphrase;
|
|
685
|
+
this.#privateKeyProvider = options.privateKeyProvider;
|
|
613
686
|
mkdirRecursiveSync(options.location);
|
|
614
687
|
if (!fs4.existsSync(this.#location)) {
|
|
615
688
|
throw new Error(`CertificateManager cannot access location ${this.#location}`);
|
|
@@ -623,10 +696,108 @@ var init_certificate_manager = __esm({
|
|
|
623
696
|
get rootDir() {
|
|
624
697
|
return this.#location;
|
|
625
698
|
}
|
|
626
|
-
/**
|
|
699
|
+
/**
|
|
700
|
+
* Path to the private key file (`own/private/private_key.pem`).
|
|
701
|
+
*
|
|
702
|
+
* Kept for backward compatibility with code that reads the key
|
|
703
|
+
* directly from disk. When a passphrase or a `privateKeyProvider` is
|
|
704
|
+
* configured, prefer {@link getPrivateKey} instead — this getter still
|
|
705
|
+
* returns the on-disk path even if a provider is configured (there may
|
|
706
|
+
* be no meaningful file in that case).
|
|
707
|
+
*/
|
|
627
708
|
get privateKey() {
|
|
628
709
|
return path2.join(this.rootDir, "own/private/private_key.pem");
|
|
629
710
|
}
|
|
711
|
+
/**
|
|
712
|
+
* Resolve the private key: from `privateKeyProvider` if configured,
|
|
713
|
+
* otherwise from disk (decrypting with `privateKeyPassphrase` if the
|
|
714
|
+
* key is encrypted). Fails closed — throws
|
|
715
|
+
* `PrivateKeyPassphraseRequiredError` — if the on-disk key is encrypted
|
|
716
|
+
* and no passphrase is configured, or if the wrong passphrase is
|
|
717
|
+
* configured.
|
|
718
|
+
*
|
|
719
|
+
* The on-disk key is read and decrypted once and then cached for the
|
|
720
|
+
* lifetime of this instance, so a `privateKeyPassphrase` function is
|
|
721
|
+
* called at most once (concurrent first calls share the same read). A
|
|
722
|
+
* failed read is not cached, so a caller can fix the passphrase and
|
|
723
|
+
* retry. A `privateKeyProvider` is consulted on every call: it is the
|
|
724
|
+
* authority on what the current key is.
|
|
725
|
+
*/
|
|
726
|
+
async getPrivateKey() {
|
|
727
|
+
if (this.#privateKeyProvider) {
|
|
728
|
+
return await this.#privateKeyProvider.getPrivateKey();
|
|
729
|
+
}
|
|
730
|
+
if (this.#cachedPrivateKey) {
|
|
731
|
+
return this.#cachedPrivateKey;
|
|
732
|
+
}
|
|
733
|
+
if (!this.#privateKeyPromise) {
|
|
734
|
+
this.#privateKeyPromise = (async () => {
|
|
735
|
+
const passphrase = await resolvePrivateKeyPassphrase(this.#privateKeyPassphrase);
|
|
736
|
+
return readPrivateKey(this.privateKey, passphrase);
|
|
737
|
+
})().then(
|
|
738
|
+
(key) => {
|
|
739
|
+
this.#cachedPrivateKey = key;
|
|
740
|
+
return key;
|
|
741
|
+
},
|
|
742
|
+
(err) => {
|
|
743
|
+
this.#privateKeyPromise = void 0;
|
|
744
|
+
throw err;
|
|
745
|
+
}
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
return await this.#privateKeyPromise;
|
|
749
|
+
}
|
|
750
|
+
/** In-flight first read of the on-disk key, so concurrent callers share one passphrase resolution. */
|
|
751
|
+
#privateKeyPromise;
|
|
752
|
+
/**
|
|
753
|
+
* Enable, disable, or rotate the passphrase protecting the on-disk
|
|
754
|
+
* private key: decrypt with `oldPassphrase` (omit if the key is
|
|
755
|
+
* currently unencrypted), then write back encrypted with
|
|
756
|
+
* `newPassphrase` (omit to leave it unencrypted). The write goes to a
|
|
757
|
+
* temporary file in the same directory and is atomically renamed into
|
|
758
|
+
* place, so a crash mid-rotation cannot leave a partially-written key;
|
|
759
|
+
* the temporary file is removed if anything fails, so a rotation *to*
|
|
760
|
+
* plaintext can never leave a stray cleartext copy behind. Runs under
|
|
761
|
+
* the same lock as `initialize()`.
|
|
762
|
+
*
|
|
763
|
+
* This only rewrites the on-disk file — it does not update this
|
|
764
|
+
* instance's own `privateKeyPassphrase` (set at construction), and it
|
|
765
|
+
* drops this instance's cached key so that disk stays the source of
|
|
766
|
+
* truth. Construct a new `CertificateManager` with the new passphrase to
|
|
767
|
+
* continue using it afterward.
|
|
768
|
+
*
|
|
769
|
+
* Not supported when a `privateKeyProvider` is configured (there is no
|
|
770
|
+
* disk file for this method to rewrite).
|
|
771
|
+
*/
|
|
772
|
+
async reencryptPrivateKey(oldPassphrase, newPassphrase) {
|
|
773
|
+
if (this.#privateKeyProvider) {
|
|
774
|
+
throw new Error("reencryptPrivateKey: not supported when a privateKeyProvider is configured");
|
|
775
|
+
}
|
|
776
|
+
const oldPass = await resolvePrivateKeyPassphrase(oldPassphrase);
|
|
777
|
+
const newPass = await resolvePrivateKeyPassphrase(newPassphrase);
|
|
778
|
+
await this.withLock2(async () => {
|
|
779
|
+
const privateKey = readPrivateKey(this.privateKey, oldPass);
|
|
780
|
+
await this.#rewritePrivateKeyFile(privateKey, newPass);
|
|
781
|
+
});
|
|
782
|
+
this.#cachedPrivateKey = void 0;
|
|
783
|
+
this.#privateKeyPromise = void 0;
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Atomically replace the on-disk private key with `privateKey`, written
|
|
787
|
+
* as PKCS#8 (encrypted with `passphrase` if given). Temp file next to the
|
|
788
|
+
* target, `0600`, renamed into place; the temp file is unlinked on any
|
|
789
|
+
* failure so no partial or cleartext copy can be left behind.
|
|
790
|
+
* Caller must hold the lock.
|
|
791
|
+
*/
|
|
792
|
+
async #rewritePrivateKeyFile(privateKey, passphrase) {
|
|
793
|
+
const tmpFilename = `${this.privateKey}.${process.pid}-${Date.now()}.tmp`;
|
|
794
|
+
try {
|
|
795
|
+
await writePrivateKeyFile(tmpFilename, privateKey, { passphrase });
|
|
796
|
+
await fs4.promises.rename(tmpFilename, this.privateKey);
|
|
797
|
+
} finally {
|
|
798
|
+
await fs4.promises.rm(tmpFilename, { force: true });
|
|
799
|
+
}
|
|
800
|
+
}
|
|
630
801
|
/** Path to the OpenSSL random seed file. */
|
|
631
802
|
get randomFile() {
|
|
632
803
|
return path2.join(this.rootDir, "./random.rnd");
|
|
@@ -828,23 +999,19 @@ var init_certificate_manager = __esm({
|
|
|
828
999
|
return "BadCertificateUntrusted" /* BadCertificateUntrusted */;
|
|
829
1000
|
}
|
|
830
1001
|
}
|
|
831
|
-
const
|
|
832
|
-
debugLog("chain[1] info=", _c2);
|
|
833
|
-
const certificateInfo = exploreCertificateInfo(chain[0]);
|
|
1002
|
+
const { validity } = exploreCertificate(chain[0]).tbsCertificate;
|
|
834
1003
|
const now = /* @__PURE__ */ new Date();
|
|
835
1004
|
let isTimeInvalid = false;
|
|
836
|
-
if (
|
|
1005
|
+
if (validity.notBefore.getTime() > now.getTime()) {
|
|
837
1006
|
debugLog(
|
|
838
|
-
`${chalk3.red("certificate is invalid : certificate is not active yet !")} not before date =${
|
|
1007
|
+
`${chalk3.red("certificate is invalid : certificate is not active yet !")} not before date =${validity.notBefore}`
|
|
839
1008
|
);
|
|
840
1009
|
if (!options.acceptPendingCertificate) {
|
|
841
1010
|
isTimeInvalid = true;
|
|
842
1011
|
}
|
|
843
1012
|
}
|
|
844
|
-
if (
|
|
845
|
-
debugLog(
|
|
846
|
-
`${chalk3.red("certificate is invalid : certificate has expired !")} not after date =${certificateInfo.notAfter}`
|
|
847
|
-
);
|
|
1013
|
+
if (validity.notAfter.getTime() <= now.getTime()) {
|
|
1014
|
+
debugLog(`${chalk3.red("certificate is invalid : certificate has expired !")} not after date =${validity.notAfter}`);
|
|
848
1015
|
if (!options.acceptOutdatedCertificate) {
|
|
849
1016
|
isTimeInvalid = true;
|
|
850
1017
|
}
|
|
@@ -883,7 +1050,7 @@ var init_certificate_manager = __esm({
|
|
|
883
1050
|
const chain = coerceCertificateChain(certificate);
|
|
884
1051
|
for (const element of chain) {
|
|
885
1052
|
try {
|
|
886
|
-
|
|
1053
|
+
exploreCertificate(element);
|
|
887
1054
|
} catch (_err) {
|
|
888
1055
|
return "BadCertificateInvalid" /* BadCertificateInvalid */;
|
|
889
1056
|
}
|
|
@@ -926,19 +1093,24 @@ var init_certificate_manager = __esm({
|
|
|
926
1093
|
}
|
|
927
1094
|
this.state = 1 /* Initializing */;
|
|
928
1095
|
this.#initializingPromise = this.#initialize();
|
|
929
|
-
|
|
1096
|
+
try {
|
|
1097
|
+
await this.#initializingPromise;
|
|
1098
|
+
} catch (err) {
|
|
1099
|
+
this.#initializingPromise = void 0;
|
|
1100
|
+
this.state = 0 /* Uninitialized */;
|
|
1101
|
+
throw err;
|
|
1102
|
+
}
|
|
930
1103
|
this.#initializingPromise = void 0;
|
|
931
1104
|
this.state = 2 /* Initialized */;
|
|
932
1105
|
_CertificateManager.#activeInstances.add(this);
|
|
933
|
-
_CertificateManager.#
|
|
1106
|
+
_CertificateManager.#installExitCleanup();
|
|
934
1107
|
}
|
|
935
1108
|
async #initialize() {
|
|
936
|
-
this.state = 1 /* Initializing */;
|
|
937
1109
|
const pkiDir = this.#location;
|
|
938
1110
|
mkdirRecursiveSync(pkiDir);
|
|
939
1111
|
mkdirRecursiveSync(path2.join(pkiDir, "own"));
|
|
940
1112
|
mkdirRecursiveSync(path2.join(pkiDir, "own/certs"));
|
|
941
|
-
|
|
1113
|
+
ensurePrivateDirectory(path2.join(pkiDir, "own/private"));
|
|
942
1114
|
mkdirRecursiveSync(path2.join(pkiDir, "rejected"));
|
|
943
1115
|
mkdirRecursiveSync(path2.join(pkiDir, "trusted"));
|
|
944
1116
|
mkdirRecursiveSync(path2.join(pkiDir, "trusted/certs"));
|
|
@@ -946,7 +1118,10 @@ var init_certificate_manager = __esm({
|
|
|
946
1118
|
mkdirRecursiveSync(path2.join(pkiDir, "issuers"));
|
|
947
1119
|
mkdirRecursiveSync(path2.join(pkiDir, "issuers/certs"));
|
|
948
1120
|
mkdirRecursiveSync(path2.join(pkiDir, "issuers/crl"));
|
|
949
|
-
|
|
1121
|
+
const ownsDiskKey = !this.#privateKeyProvider;
|
|
1122
|
+
const needsKeyGeneration = ownsDiskKey && !fs4.existsSync(this.privateKey);
|
|
1123
|
+
const needsKeyEncryption = ownsDiskKey && !needsKeyGeneration && this.#privateKeyPassphrase !== void 0 && !isEncryptedPrivateKeyFile(this.privateKey);
|
|
1124
|
+
if (!fs4.existsSync(this.configFile) || needsKeyGeneration || needsKeyEncryption) {
|
|
950
1125
|
return await this.withLock2(async () => {
|
|
951
1126
|
if (this.state === 3 /* Disposing */ || this.state === 4 /* Disposed */) {
|
|
952
1127
|
return;
|
|
@@ -954,15 +1129,29 @@ var init_certificate_manager = __esm({
|
|
|
954
1129
|
if (!fs4.existsSync(this.configFile)) {
|
|
955
1130
|
fs4.writeFileSync(this.configFile, configurationFileSimpleTemplate);
|
|
956
1131
|
}
|
|
957
|
-
if (!fs4.existsSync(this.privateKey)) {
|
|
1132
|
+
if (ownsDiskKey && !fs4.existsSync(this.privateKey)) {
|
|
958
1133
|
debugLog("generating private key ...");
|
|
959
|
-
await
|
|
960
|
-
await this
|
|
961
|
-
|
|
962
|
-
|
|
1134
|
+
const passphrase = await resolvePrivateKeyPassphrase(this.#privateKeyPassphrase);
|
|
1135
|
+
await generatePrivateKeyFile(this.privateKey, this.keySize, { passphrase });
|
|
1136
|
+
this.#cachedPrivateKey = readPrivateKey(this.privateKey, passphrase);
|
|
1137
|
+
} else if (ownsDiskKey && this.#privateKeyPassphrase !== void 0 && !isEncryptedPrivateKeyFile(this.privateKey)) {
|
|
1138
|
+
warningLog("initialize: private key is plaintext but a passphrase is configured; encrypting it in place");
|
|
1139
|
+
const passphrase = await resolvePrivateKeyPassphrase(this.#privateKeyPassphrase);
|
|
1140
|
+
const plaintextKey = readPrivateKey(this.privateKey);
|
|
1141
|
+
await this.#rewritePrivateKeyFile(plaintextKey, passphrase);
|
|
1142
|
+
this.#cachedPrivateKey = plaintextKey;
|
|
963
1143
|
}
|
|
1144
|
+
if (ownsDiskKey) {
|
|
1145
|
+
restrictPrivateFilePermissions(this.privateKey, 384);
|
|
1146
|
+
}
|
|
1147
|
+
await this.getPrivateKey();
|
|
1148
|
+
await this.#readCertificates();
|
|
964
1149
|
});
|
|
965
1150
|
} else {
|
|
1151
|
+
if (ownsDiskKey) {
|
|
1152
|
+
restrictPrivateFilePermissions(this.privateKey, 384);
|
|
1153
|
+
}
|
|
1154
|
+
await this.getPrivateKey();
|
|
966
1155
|
await this.#readCertificates();
|
|
967
1156
|
}
|
|
968
1157
|
}
|
|
@@ -998,7 +1187,10 @@ var init_certificate_manager = __esm({
|
|
|
998
1187
|
this.#watchers.splice(0);
|
|
999
1188
|
} finally {
|
|
1000
1189
|
this.state = 4 /* Disposed */;
|
|
1190
|
+
this.#cachedPrivateKey = void 0;
|
|
1191
|
+
this.#privateKeyPromise = void 0;
|
|
1001
1192
|
_CertificateManager.#activeInstances.delete(this);
|
|
1193
|
+
_CertificateManager.#uninstallExitCleanupIfIdle();
|
|
1002
1194
|
}
|
|
1003
1195
|
}
|
|
1004
1196
|
/**
|
|
@@ -1044,16 +1236,18 @@ var init_certificate_manager = __esm({
|
|
|
1044
1236
|
if (typeof params.applicationUri !== "string") {
|
|
1045
1237
|
throw new Error("createSelfSignedCertificate: expecting applicationUri to be a string");
|
|
1046
1238
|
}
|
|
1047
|
-
if (!fs4.existsSync(this.privateKey)) {
|
|
1239
|
+
if (!this.#privateKeyProvider && !fs4.existsSync(this.privateKey)) {
|
|
1048
1240
|
throw new Error(`Cannot find private key ${this.privateKey}`);
|
|
1049
1241
|
}
|
|
1050
1242
|
let certificateFilename = path2.join(this.rootDir, "own/certs/self_signed_certificate.pem");
|
|
1051
1243
|
certificateFilename = params.outputFile || certificateFilename;
|
|
1052
|
-
const _params =
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1244
|
+
const _params = {
|
|
1245
|
+
...params,
|
|
1246
|
+
rootDir: this.rootDir,
|
|
1247
|
+
configFile: this.configFile,
|
|
1248
|
+
privateKey: await this.getPrivateKey(),
|
|
1249
|
+
subject: params.subject || "CN=FIXME"
|
|
1250
|
+
};
|
|
1057
1251
|
await this.withLock2(async () => {
|
|
1058
1252
|
await createSelfSignedCertificate(certificateFilename, _params);
|
|
1059
1253
|
});
|
|
@@ -1072,13 +1266,15 @@ var init_certificate_manager = __esm({
|
|
|
1072
1266
|
if (!params) {
|
|
1073
1267
|
throw new Error("params is required");
|
|
1074
1268
|
}
|
|
1075
|
-
|
|
1076
|
-
if (Object.prototype.hasOwnProperty.call(_params, "rootDir")) {
|
|
1269
|
+
if (Object.prototype.hasOwnProperty.call(params, "rootDir")) {
|
|
1077
1270
|
throw new Error("rootDir should not be specified ");
|
|
1078
1271
|
}
|
|
1079
|
-
_params
|
|
1080
|
-
|
|
1081
|
-
|
|
1272
|
+
const _params = {
|
|
1273
|
+
...params,
|
|
1274
|
+
rootDir: path2.resolve(this.rootDir),
|
|
1275
|
+
configFile: path2.resolve(this.configFile),
|
|
1276
|
+
privateKey: await this.getPrivateKey()
|
|
1277
|
+
};
|
|
1082
1278
|
return await this.withLock2(async () => {
|
|
1083
1279
|
const now = /* @__PURE__ */ new Date();
|
|
1084
1280
|
const today2 = `${now.toISOString().slice(0, 10)}_${now.getTime()}`;
|
|
@@ -1343,17 +1539,12 @@ var init_certificate_manager = __esm({
|
|
|
1343
1539
|
return "BadSecurityChecksFailed" /* BadSecurityChecksFailed */;
|
|
1344
1540
|
}
|
|
1345
1541
|
if (!opts.acceptExpiredCertificate) {
|
|
1346
|
-
|
|
1347
|
-
try {
|
|
1348
|
-
certDetails = exploreCertificateInfo(currentCert);
|
|
1349
|
-
} catch (_err) {
|
|
1350
|
-
return "BadCertificateInvalid" /* BadCertificateInvalid */;
|
|
1351
|
-
}
|
|
1542
|
+
const { validity } = currentInfo.tbsCertificate;
|
|
1352
1543
|
const now = /* @__PURE__ */ new Date();
|
|
1353
|
-
if (
|
|
1544
|
+
if (validity.notBefore.getTime() > now.getTime()) {
|
|
1354
1545
|
return "BadCertificateTimeInvalid" /* BadCertificateTimeInvalid */;
|
|
1355
1546
|
}
|
|
1356
|
-
if (
|
|
1547
|
+
if (validity.notAfter.getTime() <= now.getTime()) {
|
|
1357
1548
|
return depth === 1 ? "BadCertificateTimeInvalid" /* BadCertificateTimeInvalid */ : "BadCertificateIssuerTimeInvalid" /* BadCertificateIssuerTimeInvalid */;
|
|
1358
1549
|
}
|
|
1359
1550
|
}
|
|
@@ -1703,6 +1894,7 @@ var init_certificate_manager = __esm({
|
|
|
1703
1894
|
const chokidarOptions = {
|
|
1704
1895
|
usePolling,
|
|
1705
1896
|
...usePolling ? { interval: pollingInterval } : {},
|
|
1897
|
+
depth: 0,
|
|
1706
1898
|
persistent: false
|
|
1707
1899
|
};
|
|
1708
1900
|
const allCapturedHandles = [];
|
|
@@ -1940,6 +2132,19 @@ var init_toolbox = __esm({
|
|
|
1940
2132
|
});
|
|
1941
2133
|
|
|
1942
2134
|
// packages/node-opcua-pki/lib/toolbox/with_openssl/_env.ts
|
|
2135
|
+
function buildChildEnv(extra) {
|
|
2136
|
+
const env = {};
|
|
2137
|
+
for (const key of Object.keys(process.env)) {
|
|
2138
|
+
if (SAFE_ENV_PASSTHROUGH.has(key.toLowerCase())) {
|
|
2139
|
+
env[key] = process.env[key];
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
return { ...env, ...extra };
|
|
2143
|
+
}
|
|
2144
|
+
function redactEnvForLog(options) {
|
|
2145
|
+
const { env, ...rest } = options;
|
|
2146
|
+
return env ? { ...rest, env: Object.keys(env) } : rest;
|
|
2147
|
+
}
|
|
1943
2148
|
function setEnv(varName, value) {
|
|
1944
2149
|
if (!g_config.silent) {
|
|
1945
2150
|
warningLog(` set ${varName}=${value}`);
|
|
@@ -1966,29 +2171,56 @@ function getEnvironmentVarNames() {
|
|
|
1966
2171
|
return { key: varName, pattern: `\\$ENV\\:\\:${varName}` };
|
|
1967
2172
|
});
|
|
1968
2173
|
}
|
|
2174
|
+
function buildSubjectAltNameString(params) {
|
|
2175
|
+
return [
|
|
2176
|
+
`URI:${params.applicationUri}`,
|
|
2177
|
+
...(params.dns ?? []).map((d) => `DNS:${d}`),
|
|
2178
|
+
...(params.ip ?? []).map((d) => `IP:${d}`)
|
|
2179
|
+
].join(", ");
|
|
2180
|
+
}
|
|
1969
2181
|
function processAltNames(params) {
|
|
1970
2182
|
params.dns = params.dns || [];
|
|
1971
2183
|
params.ip = params.ip || [];
|
|
1972
|
-
|
|
1973
|
-
subjectAltName.push(`URI:${params.applicationUri}`);
|
|
1974
|
-
subjectAltName = [].concat(
|
|
1975
|
-
subjectAltName,
|
|
1976
|
-
params.dns.map((d) => `DNS:${d}`)
|
|
1977
|
-
);
|
|
1978
|
-
subjectAltName = [].concat(
|
|
1979
|
-
subjectAltName,
|
|
1980
|
-
params.ip.map((d) => `IP:${d}`)
|
|
1981
|
-
);
|
|
1982
|
-
const subjectAltNameString = subjectAltName.join(", ");
|
|
1983
|
-
setEnv("ALTNAME", subjectAltNameString);
|
|
2184
|
+
setEnv("ALTNAME", buildSubjectAltNameString(params));
|
|
1984
2185
|
}
|
|
1985
|
-
var exportedEnvVars;
|
|
2186
|
+
var SAFE_ENV_PASSTHROUGH, exportedEnvVars;
|
|
1986
2187
|
var init_env = __esm({
|
|
1987
2188
|
"packages/node-opcua-pki/lib/toolbox/with_openssl/_env.ts"() {
|
|
1988
2189
|
"use strict";
|
|
1989
2190
|
init_esm_shims();
|
|
1990
2191
|
init_config();
|
|
1991
2192
|
init_debug();
|
|
2193
|
+
SAFE_ENV_PASSTHROUGH = /* @__PURE__ */ new Set([
|
|
2194
|
+
// POSIX/Windows shell and process essentials
|
|
2195
|
+
"path",
|
|
2196
|
+
"home",
|
|
2197
|
+
"userprofile",
|
|
2198
|
+
"temp",
|
|
2199
|
+
"tmp",
|
|
2200
|
+
"tmpdir",
|
|
2201
|
+
"systemroot",
|
|
2202
|
+
"windir",
|
|
2203
|
+
"comspec",
|
|
2204
|
+
"pathext",
|
|
2205
|
+
"appdata",
|
|
2206
|
+
"localappdata",
|
|
2207
|
+
// dynamic loader: a custom-built or relocated openssl (e.g. under /opt,
|
|
2208
|
+
// or Homebrew on macOS) may need these to find its own libcrypto/libssl
|
|
2209
|
+
"ld_library_path",
|
|
2210
|
+
"dyld_library_path",
|
|
2211
|
+
"dyld_fallback_library_path",
|
|
2212
|
+
// locale, so openssl's textual output stays parseable
|
|
2213
|
+
"lang",
|
|
2214
|
+
"lc_all",
|
|
2215
|
+
"lc_ctype",
|
|
2216
|
+
// openssl configuration this app, or the host OS/user, may rely on
|
|
2217
|
+
"openssl_conf",
|
|
2218
|
+
"randfile",
|
|
2219
|
+
"openssl_modules",
|
|
2220
|
+
"openssl_engines",
|
|
2221
|
+
"ssl_cert_file",
|
|
2222
|
+
"ssl_cert_dir"
|
|
2223
|
+
]);
|
|
1992
2224
|
exportedEnvVars = {};
|
|
1993
2225
|
}
|
|
1994
2226
|
});
|
|
@@ -2011,24 +2243,17 @@ import { pipeline } from "stream/promises";
|
|
|
2011
2243
|
import byline from "byline";
|
|
2012
2244
|
import chalk4 from "chalk";
|
|
2013
2245
|
import yauzl from "yauzl";
|
|
2014
|
-
async function execute(
|
|
2246
|
+
async function execute(file, args, cwd) {
|
|
2015
2247
|
let output = "";
|
|
2016
|
-
const options = {
|
|
2017
|
-
cwd,
|
|
2018
|
-
windowsHide: true
|
|
2019
|
-
};
|
|
2020
2248
|
return await new Promise((resolve, reject) => {
|
|
2021
|
-
const child = child_process.
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
(
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
}
|
|
2030
|
-
}
|
|
2031
|
-
);
|
|
2249
|
+
const child = child_process.spawn(file, args, {
|
|
2250
|
+
cwd,
|
|
2251
|
+
windowsHide: true,
|
|
2252
|
+
env: buildChildEnv(),
|
|
2253
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2254
|
+
});
|
|
2255
|
+
child.on("error", (err) => reject(err));
|
|
2256
|
+
child.on("close", (code) => resolve({ exitCode: code ?? 1, output }));
|
|
2032
2257
|
const stream1 = byline(child.stdout);
|
|
2033
2258
|
stream1.on("data", (line) => {
|
|
2034
2259
|
output += `${line}
|
|
@@ -2040,16 +2265,13 @@ async function execute(cmd, cwd) {
|
|
|
2040
2265
|
});
|
|
2041
2266
|
});
|
|
2042
2267
|
}
|
|
2043
|
-
function quote2(str) {
|
|
2044
|
-
return `"${str.replace(/\\/g, "/")}"`;
|
|
2045
|
-
}
|
|
2046
2268
|
function is_expected_openssl_version(strVersion) {
|
|
2047
2269
|
return !!strVersion.match(/OpenSSL \d/);
|
|
2048
2270
|
}
|
|
2049
2271
|
async function getopensslExecPath() {
|
|
2050
2272
|
let result1;
|
|
2051
2273
|
try {
|
|
2052
|
-
result1 = await execute("which openssl");
|
|
2274
|
+
result1 = await execute("which", ["openssl"]);
|
|
2053
2275
|
} catch (err) {
|
|
2054
2276
|
warningLog("warning: ", err.message);
|
|
2055
2277
|
throw new Error("Cannot find openssl");
|
|
@@ -2066,11 +2288,10 @@ async function getopensslExecPath() {
|
|
|
2066
2288
|
}
|
|
2067
2289
|
async function check_system_openssl_version() {
|
|
2068
2290
|
const opensslExecPath = await getopensslExecPath();
|
|
2069
|
-
const q_opensslExecPath = quote2(opensslExecPath);
|
|
2070
2291
|
if (doDebug2) {
|
|
2071
2292
|
warningLog(` OpenSSL found in : ${chalk4.yellow(opensslExecPath)}`);
|
|
2072
2293
|
}
|
|
2073
|
-
const result = await execute(
|
|
2294
|
+
const result = await execute(opensslExecPath, ["version"]);
|
|
2074
2295
|
const exitCode = result?.exitCode;
|
|
2075
2296
|
const output = result?.output;
|
|
2076
2297
|
const version = output.trim();
|
|
@@ -2110,9 +2331,8 @@ async function install_and_check_win32_openssl_version() {
|
|
|
2110
2331
|
version: `cannot find file ${opensslExecPath2}`
|
|
2111
2332
|
};
|
|
2112
2333
|
} else {
|
|
2113
|
-
const q_openssl_exe_path = quote2(opensslExecPath2);
|
|
2114
2334
|
const cwd = ".";
|
|
2115
|
-
const { exitCode, output } = await execute(
|
|
2335
|
+
const { exitCode, output } = await execute(opensslExecPath2, ["version"], cwd);
|
|
2116
2336
|
const version = output.trim();
|
|
2117
2337
|
if (doDebug2) {
|
|
2118
2338
|
warningLog(" Version = ", version);
|
|
@@ -2125,7 +2345,7 @@ async function install_and_check_win32_openssl_version() {
|
|
|
2125
2345
|
}
|
|
2126
2346
|
async function find_system_openssl_win32() {
|
|
2127
2347
|
try {
|
|
2128
|
-
const result = await execute("where openssl");
|
|
2348
|
+
const result = await execute("where", ["openssl"]);
|
|
2129
2349
|
if (result.exitCode !== 0) {
|
|
2130
2350
|
return void 0;
|
|
2131
2351
|
}
|
|
@@ -2133,8 +2353,7 @@ async function install_and_check_win32_openssl_version() {
|
|
|
2133
2353
|
if (!opensslPath2 || !fs5.existsSync(opensslPath2)) {
|
|
2134
2354
|
return void 0;
|
|
2135
2355
|
}
|
|
2136
|
-
const
|
|
2137
|
-
const versionResult = await execute(`${q5} version`);
|
|
2356
|
+
const versionResult = await execute(opensslPath2, ["version"]);
|
|
2138
2357
|
const version = versionResult.output.trim();
|
|
2139
2358
|
if (versionResult.exitCode === 0 && is_expected_openssl_version(version)) {
|
|
2140
2359
|
warningLog(
|
|
@@ -2299,6 +2518,7 @@ var init_install_prerequisite = __esm({
|
|
|
2299
2518
|
"use strict";
|
|
2300
2519
|
init_esm_shims();
|
|
2301
2520
|
init_debug();
|
|
2521
|
+
init_env();
|
|
2302
2522
|
doDebug2 = process.env.NODEOPCUAPKIDEBUG || false;
|
|
2303
2523
|
}
|
|
2304
2524
|
});
|
|
@@ -2310,36 +2530,50 @@ import fs6 from "fs";
|
|
|
2310
2530
|
import os3 from "os";
|
|
2311
2531
|
import byline2 from "byline";
|
|
2312
2532
|
import chalk5 from "chalk";
|
|
2313
|
-
|
|
2533
|
+
function passinArg(passphrase = "") {
|
|
2534
|
+
return { args: ["-passin", `env:${PASSIN_ENV_VAR}`], env: { [PASSIN_ENV_VAR]: passphrase } };
|
|
2535
|
+
}
|
|
2536
|
+
function renderForDisplay(file, args) {
|
|
2537
|
+
return [file, ...args].map((a) => a === "" || /[\s"'`$\\]/.test(a) ? JSON.stringify(a) : a).join(" ");
|
|
2538
|
+
}
|
|
2539
|
+
async function execute2(file, args, options) {
|
|
2314
2540
|
const from = new Error();
|
|
2315
2541
|
options.cwd = options.cwd || process.cwd();
|
|
2316
2542
|
if (!g_config.silent) {
|
|
2317
2543
|
warningLog(chalk5.cyan(" CWD "), options.cwd);
|
|
2318
2544
|
}
|
|
2319
2545
|
const outputs = [];
|
|
2546
|
+
const errorOutputs = [];
|
|
2547
|
+
const display2 = renderForDisplay(file, args);
|
|
2320
2548
|
return await new Promise((resolve, reject) => {
|
|
2321
|
-
const child = child_process2.
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2549
|
+
const child = child_process2.spawn(file, [...args], {
|
|
2550
|
+
cwd: options.cwd,
|
|
2551
|
+
windowsHide: true,
|
|
2552
|
+
env: buildChildEnv(options.env),
|
|
2553
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2554
|
+
});
|
|
2555
|
+
const fail = (message) => {
|
|
2556
|
+
if (!options.hideErrorMessage && !g_config.silent) {
|
|
2557
|
+
const fence = "###########################################";
|
|
2558
|
+
console.error(chalk5.bgWhiteBright.redBright(`${fence} OPENSSL ERROR ${fence}`));
|
|
2559
|
+
console.error(chalk5.bgWhiteBright.redBright(`CWD = ${options.cwd}`));
|
|
2560
|
+
console.error(chalk5.bgWhiteBright.redBright(message));
|
|
2561
|
+
console.error(chalk5.bgWhiteBright.redBright(`${fence} OPENSSL ERROR ${fence}`));
|
|
2562
|
+
console.error(from.stack);
|
|
2563
|
+
}
|
|
2564
|
+
reject(new Error(message));
|
|
2565
|
+
};
|
|
2566
|
+
child.on("error", (err) => fail(`Command failed: ${display2}
|
|
2567
|
+
${err.message}`));
|
|
2568
|
+
child.on("close", (code, signal) => {
|
|
2569
|
+
if (code === 0) {
|
|
2340
2570
|
resolve(outputs.join(""));
|
|
2571
|
+
return;
|
|
2341
2572
|
}
|
|
2342
|
-
|
|
2573
|
+
const why = signal ? `signal ${signal}` : `exit code ${code}`;
|
|
2574
|
+
fail(`Command failed: ${display2}
|
|
2575
|
+
${errorOutputs.join("")}(${why})`);
|
|
2576
|
+
});
|
|
2343
2577
|
if (child.stdout) {
|
|
2344
2578
|
const stream2 = byline2(child.stdout);
|
|
2345
2579
|
stream2.on("data", (line) => {
|
|
@@ -2356,17 +2590,17 @@ async function execute2(cmd, options) {
|
|
|
2356
2590
|
});
|
|
2357
2591
|
}
|
|
2358
2592
|
}
|
|
2359
|
-
if (
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
if (displayError) {
|
|
2365
|
-
process.stdout.write(`${chalk5.white(" stderr ") + chalk5.red(line)}
|
|
2593
|
+
if (child.stderr) {
|
|
2594
|
+
const stream1 = byline2(child.stderr);
|
|
2595
|
+
stream1.on("data", (line) => {
|
|
2596
|
+
line = line.toString();
|
|
2597
|
+
errorOutputs.push(`${line}
|
|
2366
2598
|
`);
|
|
2367
|
-
|
|
2368
|
-
}
|
|
2369
|
-
|
|
2599
|
+
if (!g_config.silent && displayError) {
|
|
2600
|
+
process.stdout.write(`${chalk5.white(" stderr ") + chalk5.red(line)}
|
|
2601
|
+
`);
|
|
2602
|
+
}
|
|
2603
|
+
});
|
|
2370
2604
|
}
|
|
2371
2605
|
});
|
|
2372
2606
|
}
|
|
@@ -2376,18 +2610,18 @@ async function find_openssl() {
|
|
|
2376
2610
|
async function ensure_openssl_installed() {
|
|
2377
2611
|
if (!opensslPath) {
|
|
2378
2612
|
opensslPath = await find_openssl();
|
|
2379
|
-
const outputs = await execute_openssl("version", { cwd: "." });
|
|
2613
|
+
const outputs = await execute_openssl(["version"], { cwd: "." });
|
|
2380
2614
|
g_config.opensslVersion = outputs.trim();
|
|
2381
2615
|
if (doDebug) {
|
|
2382
2616
|
warningLog("OpenSSL version : ", g_config.opensslVersion);
|
|
2383
2617
|
}
|
|
2384
2618
|
}
|
|
2385
2619
|
}
|
|
2386
|
-
async function execute_openssl_no_failure(
|
|
2620
|
+
async function execute_openssl_no_failure(args, options) {
|
|
2387
2621
|
options = options || {};
|
|
2388
2622
|
options.hideErrorMessage = true;
|
|
2389
2623
|
try {
|
|
2390
|
-
return await execute_openssl(
|
|
2624
|
+
return await execute_openssl(args, options);
|
|
2391
2625
|
} catch (err) {
|
|
2392
2626
|
debugLog(" (ignored error = ERROR : )", err.message);
|
|
2393
2627
|
}
|
|
@@ -2395,8 +2629,8 @@ async function execute_openssl_no_failure(cmd, options) {
|
|
|
2395
2629
|
function getTempFolder() {
|
|
2396
2630
|
return os3.tmpdir();
|
|
2397
2631
|
}
|
|
2398
|
-
async function execute_openssl(
|
|
2399
|
-
debugLog("execute_openssl",
|
|
2632
|
+
async function execute_openssl(args, options) {
|
|
2633
|
+
debugLog("execute_openssl", args, redactEnvForLog(options));
|
|
2400
2634
|
const empty_config_file = n(getTempFolder(), "empty_config.cnf");
|
|
2401
2635
|
if (!fs6.existsSync(empty_config_file)) {
|
|
2402
2636
|
await fs6.promises.writeFile(empty_config_file, "# empty config file");
|
|
@@ -2408,23 +2642,23 @@ async function execute_openssl(cmd, options) {
|
|
|
2408
2642
|
if (!g_config.silent) {
|
|
2409
2643
|
warningLog(chalk5.cyan(" OPENSSL_CONF"), process.env.OPENSSL_CONF);
|
|
2410
2644
|
warningLog(chalk5.cyan(" RANDFILE "), process.env.RANDFILE);
|
|
2411
|
-
warningLog(chalk5.cyan(" CMD
|
|
2645
|
+
warningLog(chalk5.cyan(" CMD "), chalk5.cyanBright(renderForDisplay("openssl", args)));
|
|
2412
2646
|
}
|
|
2413
2647
|
await ensure_openssl_installed();
|
|
2414
|
-
return await execute2(
|
|
2648
|
+
return await execute2(opensslPath, args, options);
|
|
2415
2649
|
}
|
|
2416
|
-
var opensslPath, n;
|
|
2650
|
+
var opensslPath, n, PASSIN_ENV_VAR;
|
|
2417
2651
|
var init_execute_openssl = __esm({
|
|
2418
2652
|
"packages/node-opcua-pki/lib/toolbox/with_openssl/execute_openssl.ts"() {
|
|
2419
2653
|
"use strict";
|
|
2420
2654
|
init_esm_shims();
|
|
2421
|
-
init_common();
|
|
2422
2655
|
init_common2();
|
|
2423
2656
|
init_config();
|
|
2424
2657
|
init_debug();
|
|
2425
2658
|
init_env();
|
|
2426
2659
|
init_install_prerequisite();
|
|
2427
2660
|
n = makePath;
|
|
2661
|
+
PASSIN_ENV_VAR = "NODE_OPCUA_PKI_OPENSSL_PASSIN";
|
|
2428
2662
|
}
|
|
2429
2663
|
});
|
|
2430
2664
|
|
|
@@ -2440,19 +2674,27 @@ function openssl_require2DigitYearInDate() {
|
|
|
2440
2674
|
}
|
|
2441
2675
|
return g_config.opensslVersion.match(/OpenSSL 0\.9/);
|
|
2442
2676
|
}
|
|
2443
|
-
function stripConditionalBlocks(template) {
|
|
2677
|
+
function stripConditionalBlocks(template, envOverrides) {
|
|
2444
2678
|
return template.replace(/\{\{#([A-Z_][A-Z0-9_]*)\}\}([\s\S]*?)\{\{\/\1\}\}\r?\n?/g, (_match, key, content) => {
|
|
2445
|
-
const keep = hasEnv(key) && getEnv(key) !== "";
|
|
2679
|
+
const keep = envOverrides && Object.prototype.hasOwnProperty.call(envOverrides, key) ? envOverrides[key] !== "" : hasEnv(key) && getEnv(key) !== "";
|
|
2446
2680
|
return keep ? content : "";
|
|
2447
2681
|
});
|
|
2448
2682
|
}
|
|
2449
|
-
function generateStaticConfig(configPath, options) {
|
|
2683
|
+
function generateStaticConfig(configPath, options, envOverrides) {
|
|
2450
2684
|
const prePath = options?.cwd || "";
|
|
2451
2685
|
const originalFilename = !path4.isAbsolute(configPath) ? path4.join(prePath, configPath) : configPath;
|
|
2452
2686
|
let staticConfig = fs7.readFileSync(originalFilename, { encoding: "utf8" });
|
|
2453
|
-
staticConfig = stripConditionalBlocks(staticConfig);
|
|
2687
|
+
staticConfig = stripConditionalBlocks(staticConfig, envOverrides);
|
|
2454
2688
|
for (const envVar of getEnvironmentVarNames()) {
|
|
2455
|
-
|
|
2689
|
+
if (envOverrides && Object.prototype.hasOwnProperty.call(envOverrides, envVar.key)) {
|
|
2690
|
+
continue;
|
|
2691
|
+
}
|
|
2692
|
+
staticConfig = staticConfig.replace(new RegExp(envVar.pattern, "gi"), () => getEnv(envVar.key));
|
|
2693
|
+
}
|
|
2694
|
+
if (envOverrides) {
|
|
2695
|
+
for (const [key, value] of Object.entries(envOverrides)) {
|
|
2696
|
+
staticConfig = staticConfig.replace(new RegExp(`\\$ENV\\:\\:${key}`, "gi"), () => value);
|
|
2697
|
+
}
|
|
2456
2698
|
}
|
|
2457
2699
|
const staticConfigPath = `${configPath}.${process.pid}-${_counter++}.tmp`;
|
|
2458
2700
|
const temporaryConfigPath = !path4.isAbsolute(configPath) ? path4.join(prePath, staticConfigPath) : staticConfigPath;
|
|
@@ -2463,9 +2705,15 @@ function generateStaticConfig(configPath, options) {
|
|
|
2463
2705
|
return temporaryConfigPath;
|
|
2464
2706
|
}
|
|
2465
2707
|
}
|
|
2466
|
-
async function
|
|
2708
|
+
async function cleanupStaticConfig(configFile, options) {
|
|
2709
|
+
await fs7.promises.rm(path4.resolve(options?.cwd ?? "", configFile), { force: true });
|
|
2710
|
+
}
|
|
2711
|
+
async function getPublicKeyFromPrivateKey(privateKeyFilename, publicKeyFilename, passphrase) {
|
|
2467
2712
|
assert7(fs7.existsSync(privateKeyFilename));
|
|
2468
|
-
|
|
2713
|
+
const passin = passinArg(passphrase);
|
|
2714
|
+
await execute_openssl(["rsa", "-pubout", "-in", n2(privateKeyFilename), "-out", n2(publicKeyFilename), ...passin.args], {
|
|
2715
|
+
env: passin.env
|
|
2716
|
+
});
|
|
2469
2717
|
}
|
|
2470
2718
|
function x509Date(date) {
|
|
2471
2719
|
date = date || /* @__PURE__ */ new Date();
|
|
@@ -2486,30 +2734,28 @@ function x509Date(date) {
|
|
|
2486
2734
|
}
|
|
2487
2735
|
async function dumpCertificate(certificate) {
|
|
2488
2736
|
assert7(fs7.existsSync(certificate));
|
|
2489
|
-
return await execute_openssl(
|
|
2737
|
+
return await execute_openssl(["x509", "-in", n2(certificate), "-text", "-noout"], {});
|
|
2490
2738
|
}
|
|
2491
2739
|
async function toDer(certificatePem) {
|
|
2492
2740
|
assert7(fs7.existsSync(certificatePem));
|
|
2493
2741
|
const certificateDer = certificatePem.replace(".pem", ".der");
|
|
2494
|
-
return await execute_openssl(
|
|
2742
|
+
return await execute_openssl(["x509", "-outform", "der", "-in", certificatePem, "-out", certificateDer], {});
|
|
2495
2743
|
}
|
|
2496
2744
|
async function fingerprint(certificatePem) {
|
|
2497
2745
|
assert7(fs7.existsSync(certificatePem));
|
|
2498
|
-
return await execute_openssl(
|
|
2746
|
+
return await execute_openssl(["x509", "-fingerprint", "-noout", "-in", certificatePem], {});
|
|
2499
2747
|
}
|
|
2500
|
-
var _counter,
|
|
2748
|
+
var _counter, n2;
|
|
2501
2749
|
var init_toolbox2 = __esm({
|
|
2502
2750
|
"packages/node-opcua-pki/lib/toolbox/with_openssl/toolbox.ts"() {
|
|
2503
2751
|
"use strict";
|
|
2504
2752
|
init_esm_shims();
|
|
2505
|
-
init_common();
|
|
2506
2753
|
init_common2();
|
|
2507
2754
|
init_config();
|
|
2508
2755
|
init_env();
|
|
2509
2756
|
init_execute_openssl();
|
|
2510
2757
|
g_config.opensslVersion = "";
|
|
2511
2758
|
_counter = 0;
|
|
2512
|
-
q = quote;
|
|
2513
2759
|
n2 = makePath;
|
|
2514
2760
|
}
|
|
2515
2761
|
});
|
|
@@ -2530,29 +2776,42 @@ async function createCertificateSigningRequestWithOpenSSL(certificateSigningRequ
|
|
|
2530
2776
|
assert8(typeof certificateSigningRequestFilename === "string");
|
|
2531
2777
|
processAltNames(params);
|
|
2532
2778
|
const configFile = generateStaticConfig(params.configFile, { cwd: params.rootDir });
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2779
|
+
try {
|
|
2780
|
+
const options = { cwd: params.rootDir, openssl_conf: path5.relative(params.rootDir, configFile) };
|
|
2781
|
+
const subject = params.subject ? new Subject3(params.subject).toString() : void 0;
|
|
2782
|
+
displaySubtitle("- Creating a Certificate Signing Request with openssl");
|
|
2783
|
+
await execute_openssl(
|
|
2784
|
+
[
|
|
2785
|
+
"req",
|
|
2786
|
+
"-new",
|
|
2787
|
+
"-sha256",
|
|
2788
|
+
"-batch",
|
|
2789
|
+
"-text",
|
|
2790
|
+
"-config",
|
|
2791
|
+
n3(configFile),
|
|
2792
|
+
"-key",
|
|
2793
|
+
n3(params.privateKey),
|
|
2794
|
+
...subject ? ["-subj", subject] : [],
|
|
2795
|
+
"-out",
|
|
2796
|
+
n3(certificateSigningRequestFilename)
|
|
2797
|
+
],
|
|
2798
|
+
options
|
|
2799
|
+
);
|
|
2800
|
+
} finally {
|
|
2801
|
+
await cleanupStaticConfig(configFile, { cwd: params.rootDir });
|
|
2802
|
+
}
|
|
2542
2803
|
}
|
|
2543
|
-
var
|
|
2804
|
+
var n3;
|
|
2544
2805
|
var init_create_certificate_signing_request2 = __esm({
|
|
2545
2806
|
"packages/node-opcua-pki/lib/toolbox/with_openssl/create_certificate_signing_request.ts"() {
|
|
2546
2807
|
"use strict";
|
|
2547
2808
|
init_esm_shims();
|
|
2548
2809
|
init_subject();
|
|
2549
|
-
init_common();
|
|
2550
2810
|
init_common2();
|
|
2551
2811
|
init_display();
|
|
2552
2812
|
init_env();
|
|
2553
2813
|
init_execute_openssl();
|
|
2554
2814
|
init_toolbox2();
|
|
2555
|
-
q2 = quote;
|
|
2556
2815
|
n3 = makePath;
|
|
2557
2816
|
}
|
|
2558
2817
|
});
|
|
@@ -2571,94 +2830,909 @@ var init_with_openssl = __esm({
|
|
|
2571
2830
|
}
|
|
2572
2831
|
});
|
|
2573
2832
|
|
|
2574
|
-
// packages/node-opcua-pki/lib/
|
|
2575
|
-
import assert9 from "assert";
|
|
2833
|
+
// packages/node-opcua-pki/lib/ca/core/ca_database.ts
|
|
2576
2834
|
import fs9 from "fs";
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
}
|
|
2590
|
-
cmd += ` -out ${q3(n4(outputFile))}`;
|
|
2591
|
-
cmd += ` -passout pass:${passphrase}`;
|
|
2592
|
-
await execute_openssl(cmd, {});
|
|
2835
|
+
import path6 from "path";
|
|
2836
|
+
import { convertPEMtoDER, readCertificatePEM } from "node-opcua-crypto";
|
|
2837
|
+
function parseOpenSSLDate(dateStr) {
|
|
2838
|
+
const raw = dateStr?.split(",")[0] ?? "";
|
|
2839
|
+
if (raw.length < 12) return "";
|
|
2840
|
+
const yy = parseInt(raw.substring(0, 2), 10);
|
|
2841
|
+
const year = yy >= 70 ? 1900 + yy : 2e3 + yy;
|
|
2842
|
+
const month = raw.substring(2, 4);
|
|
2843
|
+
const day = raw.substring(4, 6);
|
|
2844
|
+
const hour = raw.substring(6, 8);
|
|
2845
|
+
const min = raw.substring(8, 10);
|
|
2846
|
+
const sec = raw.substring(10, 12);
|
|
2847
|
+
return `${year}-${month}-${day}T${hour}:${min}:${sec}Z`;
|
|
2593
2848
|
}
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2849
|
+
function formatOpenSSLDate(date) {
|
|
2850
|
+
const yy = String(date.getUTCFullYear() % 100).padStart(2, "0");
|
|
2851
|
+
const mm = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
2852
|
+
const dd = String(date.getUTCDate()).padStart(2, "0");
|
|
2853
|
+
const hh = String(date.getUTCHours()).padStart(2, "0");
|
|
2854
|
+
const mi = String(date.getUTCMinutes()).padStart(2, "0");
|
|
2855
|
+
const ss = String(date.getUTCSeconds()).padStart(2, "0");
|
|
2856
|
+
return `${yy}${mm}${dd}${hh}${mi}${ss}Z`;
|
|
2857
|
+
}
|
|
2858
|
+
function parseRevocationReason(dateStr) {
|
|
2859
|
+
const parts = dateStr?.split(",");
|
|
2860
|
+
return parts && parts.length > 1 ? parts[1] : void 0;
|
|
2861
|
+
}
|
|
2862
|
+
function evenLengthHex(value) {
|
|
2863
|
+
const hex = value.toString(16).toUpperCase();
|
|
2864
|
+
return hex.length % 2 === 0 ? hex : `0${hex}`;
|
|
2865
|
+
}
|
|
2866
|
+
function escapeIndexField(value) {
|
|
2867
|
+
return value.replace(/[\x00-\x1f\x7f\\]/g, (c) => `\\x${c.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`);
|
|
2868
|
+
}
|
|
2869
|
+
function assertHexSerial(serial) {
|
|
2870
|
+
if (!/^[0-9A-Fa-f]+$/.test(serial)) {
|
|
2871
|
+
throw new Error(`Invalid certificate serial number: ${JSON.stringify(serial)}`);
|
|
2604
2872
|
}
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
var
|
|
2609
|
-
|
|
2610
|
-
"packages/node-opcua-pki/lib/ca/templates/ca_config_template.cnf.ts"() {
|
|
2873
|
+
return serial.toUpperCase();
|
|
2874
|
+
}
|
|
2875
|
+
var CaDatabase;
|
|
2876
|
+
var init_ca_database = __esm({
|
|
2877
|
+
"packages/node-opcua-pki/lib/ca/core/ca_database.ts"() {
|
|
2611
2878
|
"use strict";
|
|
2612
2879
|
init_esm_shims();
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2880
|
+
CaDatabase = class {
|
|
2881
|
+
#rootDir;
|
|
2882
|
+
constructor(rootDir) {
|
|
2883
|
+
this.#rootDir = rootDir;
|
|
2884
|
+
}
|
|
2885
|
+
/** Path to the OpenSSL certificate database file (`index.txt`). */
|
|
2886
|
+
get indexFile() {
|
|
2887
|
+
return path6.join(this.#rootDir, "index.txt");
|
|
2888
|
+
}
|
|
2889
|
+
/**
|
|
2890
|
+
* Parse the OpenSSL `index.txt` certificate database.
|
|
2891
|
+
*
|
|
2892
|
+
* Each line has tab-separated fields:
|
|
2893
|
+
* ```
|
|
2894
|
+
* status expiry [revocationDate] serial unknown subject
|
|
2895
|
+
* ```
|
|
2896
|
+
*
|
|
2897
|
+
* - status: `V` (valid), `R` (revoked), `E` (expired)
|
|
2898
|
+
* - expiry: `YYMMDDHHmmssZ`
|
|
2899
|
+
* - revocationDate: present only for revoked certs
|
|
2900
|
+
* - serial: hex string
|
|
2901
|
+
* - unknown: always `"unknown"`
|
|
2902
|
+
* - subject: X.500 slash-delimited string
|
|
2903
|
+
*/
|
|
2904
|
+
readIndex() {
|
|
2905
|
+
const indexPath = this.indexFile;
|
|
2906
|
+
if (!fs9.existsSync(indexPath)) {
|
|
2907
|
+
return [];
|
|
2908
|
+
}
|
|
2909
|
+
const content = fs9.readFileSync(indexPath, "utf-8");
|
|
2910
|
+
const lines = content.split("\n").filter((l) => l.trim().length > 0);
|
|
2911
|
+
const records = [];
|
|
2912
|
+
for (const line of lines) {
|
|
2913
|
+
const fields = line.split(" ");
|
|
2914
|
+
if (fields.length < 4) continue;
|
|
2915
|
+
const statusChar = fields[0];
|
|
2916
|
+
const expiryStr = fields[1];
|
|
2917
|
+
let serial;
|
|
2918
|
+
let subject;
|
|
2919
|
+
let revocationDate;
|
|
2920
|
+
if (statusChar === "R") {
|
|
2921
|
+
revocationDate = fields[2];
|
|
2922
|
+
serial = fields[3];
|
|
2923
|
+
subject = fields.length >= 6 ? fields[5] : "";
|
|
2924
|
+
} else {
|
|
2925
|
+
serial = fields[3];
|
|
2926
|
+
subject = fields.length >= 6 ? fields[5] : "";
|
|
2927
|
+
}
|
|
2928
|
+
let status;
|
|
2929
|
+
switch (statusChar) {
|
|
2930
|
+
case "V":
|
|
2931
|
+
status = "valid";
|
|
2932
|
+
break;
|
|
2933
|
+
case "R":
|
|
2934
|
+
status = "revoked";
|
|
2935
|
+
break;
|
|
2936
|
+
case "E":
|
|
2937
|
+
status = "expired";
|
|
2938
|
+
break;
|
|
2939
|
+
default:
|
|
2940
|
+
continue;
|
|
2941
|
+
}
|
|
2942
|
+
records.push({
|
|
2943
|
+
serial,
|
|
2944
|
+
status,
|
|
2945
|
+
subject,
|
|
2946
|
+
expiryDate: parseOpenSSLDate(expiryStr),
|
|
2947
|
+
revocationDate: revocationDate ? parseOpenSSLDate(revocationDate) : void 0,
|
|
2948
|
+
reason: revocationDate ? parseRevocationReason(revocationDate) : void 0
|
|
2949
|
+
});
|
|
2950
|
+
}
|
|
2951
|
+
return records;
|
|
2952
|
+
}
|
|
2953
|
+
/** Look up one record by serial number (case-insensitive). */
|
|
2954
|
+
findBySerial(serial) {
|
|
2955
|
+
const upper = serial.toUpperCase();
|
|
2956
|
+
return this.readIndex().find((r) => r.serial.toUpperCase() === upper);
|
|
2957
|
+
}
|
|
2958
|
+
/**
|
|
2959
|
+
* Read a specific issued certificate by serial number.
|
|
2960
|
+
*
|
|
2961
|
+
* OpenSSL stores signed certificates in the `certs/` directory using
|
|
2962
|
+
* the naming convention `<SERIAL>.pem`.
|
|
2963
|
+
*
|
|
2964
|
+
* @param serial - hex-encoded serial number (e.g. `"1000"`)
|
|
2965
|
+
* @returns the DER buffer, or `undefined` if not found
|
|
2966
|
+
*/
|
|
2967
|
+
getCertificateBySerial(serial) {
|
|
2968
|
+
const upper = serial.toUpperCase();
|
|
2969
|
+
const certFile = path6.join(this.#rootDir, "certs", `${upper}.pem`);
|
|
2970
|
+
if (!fs9.existsSync(certFile)) {
|
|
2971
|
+
return void 0;
|
|
2972
|
+
}
|
|
2973
|
+
const pem = readCertificatePEM(certFile);
|
|
2974
|
+
return convertPEMtoDER(pem);
|
|
2975
|
+
}
|
|
2976
|
+
/**
|
|
2977
|
+
* Read-increment-write a hex counter file (`serial`/`crlnumber`),
|
|
2978
|
+
* returning the value to hand out — matching `openssl ca`'s own
|
|
2979
|
+
* behavior: the file holds the *next* value to assign, and is bumped
|
|
2980
|
+
* to the following one immediately after being read. A `.old` backup
|
|
2981
|
+
* of the pre-bump content is kept, as `openssl` itself does.
|
|
2982
|
+
*/
|
|
2983
|
+
#nextHexCounter(fileName) {
|
|
2984
|
+
const counterFile = path6.join(this.#rootDir, fileName);
|
|
2985
|
+
const current = BigInt(`0x${fs9.readFileSync(counterFile, "utf-8").trim()}`);
|
|
2986
|
+
fs9.copyFileSync(counterFile, `${counterFile}.old`);
|
|
2987
|
+
fs9.writeFileSync(counterFile, evenLengthHex(current + 1n));
|
|
2988
|
+
return evenLengthHex(current);
|
|
2989
|
+
}
|
|
2990
|
+
/** Hand out the next certificate serial number, bumping the `serial` file. */
|
|
2991
|
+
nextSerial() {
|
|
2992
|
+
return this.#nextHexCounter("serial");
|
|
2993
|
+
}
|
|
2994
|
+
/** Hand out the next CRL number, bumping the `crlnumber` file. */
|
|
2995
|
+
nextCrlNumber() {
|
|
2996
|
+
return this.#nextHexCounter("crlnumber");
|
|
2997
|
+
}
|
|
2998
|
+
/** Append a newly-issued certificate's `V` (valid) row to `index.txt`. */
|
|
2999
|
+
appendIssued(record) {
|
|
3000
|
+
const serial = assertHexSerial(record.serial);
|
|
3001
|
+
const subject = escapeIndexField(record.subject);
|
|
3002
|
+
const line = `V ${formatOpenSSLDate(record.expiryDate)} ${serial} unknown ${subject}
|
|
3003
|
+
`;
|
|
3004
|
+
fs9.appendFileSync(this.indexFile, line);
|
|
3005
|
+
}
|
|
3006
|
+
/**
|
|
3007
|
+
* Rewrite a certificate's `index.txt` row from `V` to `R` (revoked).
|
|
3008
|
+
* Throws if the serial is not found, or is already revoked — the same
|
|
3009
|
+
* "already revoked" case `openssl ca -revoke` itself rejects.
|
|
3010
|
+
*/
|
|
3011
|
+
markRevoked(serial, revocationDate, reason) {
|
|
3012
|
+
const upper = assertHexSerial(serial);
|
|
3013
|
+
if (!/^[A-Za-z]+$/.test(reason)) {
|
|
3014
|
+
throw new Error(`Invalid CRL reason: ${JSON.stringify(reason)}`);
|
|
3015
|
+
}
|
|
3016
|
+
const lines = fs9.readFileSync(this.indexFile, "utf-8").split("\n").filter((l) => l.trim().length > 0);
|
|
3017
|
+
let found = false;
|
|
3018
|
+
const rewritten = lines.map((line) => {
|
|
3019
|
+
const fields = line.split(" ");
|
|
3020
|
+
if (fields.length < 4 || fields[3]?.toUpperCase() !== upper) {
|
|
3021
|
+
return line;
|
|
3022
|
+
}
|
|
3023
|
+
if (fields[0] === "R") {
|
|
3024
|
+
throw new Error(`Certificate ${upper} is already revoked`);
|
|
3025
|
+
}
|
|
3026
|
+
found = true;
|
|
3027
|
+
const subject = fields.length >= 6 ? fields[5] : "";
|
|
3028
|
+
return `R ${fields[1]} ${formatOpenSSLDate(revocationDate)},${reason} ${upper} unknown ${subject}`;
|
|
3029
|
+
});
|
|
3030
|
+
if (!found) {
|
|
3031
|
+
throw new Error(`Certificate ${upper} not found in the certificate database`);
|
|
3032
|
+
}
|
|
3033
|
+
fs9.writeFileSync(this.indexFile, `${rewritten.join("\n")}
|
|
3034
|
+
`);
|
|
3035
|
+
}
|
|
3036
|
+
/** Store a signed certificate's PEM under `certs/<SERIAL>.pem`, as `openssl ca` does. */
|
|
3037
|
+
storeCertificate(serial, pem) {
|
|
3038
|
+
const certFile = path6.join(this.#rootDir, "certs", `${assertHexSerial(serial)}.pem`);
|
|
3039
|
+
fs9.writeFileSync(certFile, pem);
|
|
3040
|
+
}
|
|
3041
|
+
};
|
|
3042
|
+
}
|
|
3043
|
+
});
|
|
3044
|
+
|
|
3045
|
+
// packages/node-opcua-pki/lib/ca/backends/native_ca_backend.ts
|
|
3046
|
+
import fs10 from "fs";
|
|
3047
|
+
import path7 from "path";
|
|
3048
|
+
import {
|
|
3049
|
+
CertificatePurpose as CertificatePurpose2,
|
|
3050
|
+
createCertificateFromCsr,
|
|
3051
|
+
createCertificateSigningRequest as createCertificateSigningRequest2,
|
|
3052
|
+
createCrl,
|
|
3053
|
+
isCaSigner,
|
|
3054
|
+
privateKeyToCryptoKey,
|
|
3055
|
+
readPrivateKey as readPrivateKey2,
|
|
3056
|
+
Subject as Subject4,
|
|
3057
|
+
x509
|
|
3058
|
+
} from "node-opcua-crypto";
|
|
3059
|
+
function signingAlgorithmOf(key) {
|
|
3060
|
+
return isCaSigner(key) ? key.algorithm : RSA_SHA256;
|
|
3061
|
+
}
|
|
3062
|
+
function daysFromNow(notBefore, days) {
|
|
3063
|
+
return new Date(notBefore.getTime() + days * 24 * 60 * 60 * 1e3);
|
|
3064
|
+
}
|
|
3065
|
+
function caSanUri(ca) {
|
|
3066
|
+
return `urn:${ca.subject.commonName || "NodeOPCUA-CA"}`;
|
|
3067
|
+
}
|
|
3068
|
+
function sanFromCsr(csrPem) {
|
|
3069
|
+
const request = new x509.Pkcs10CertificateRequest(csrPem);
|
|
3070
|
+
const extension = request.getExtension("2.5.29.17");
|
|
3071
|
+
const dns2 = [];
|
|
3072
|
+
const ip = [];
|
|
3073
|
+
let applicationUri;
|
|
3074
|
+
for (const name of extension?.names.toJSON() ?? []) {
|
|
3075
|
+
if (name.type === "dns") {
|
|
3076
|
+
dns2.push(name.value);
|
|
3077
|
+
} else if (name.type === "ip") {
|
|
3078
|
+
ip.push(name.value);
|
|
3079
|
+
} else if (name.type === "url" && applicationUri === void 0) {
|
|
3080
|
+
applicationUri = name.value;
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
return { dns: dns2, ip, applicationUri };
|
|
3084
|
+
}
|
|
3085
|
+
function toOpenSslSubjectString(subjectName) {
|
|
3086
|
+
const parts = [];
|
|
3087
|
+
for (const rdn of subjectName.toJSON()) {
|
|
3088
|
+
for (const [type, values] of Object.entries(rdn)) {
|
|
3089
|
+
for (const value of values) {
|
|
3090
|
+
parts.push(`${type}=${value}`);
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
return `/${parts.join("/")}`;
|
|
3095
|
+
}
|
|
3096
|
+
var CA_CERT_VALIDITY_DAYS, RSA_SHA256, REASON_TO_X509_CRL_REASON, NativeCaBackend;
|
|
3097
|
+
var init_native_ca_backend = __esm({
|
|
3098
|
+
"packages/node-opcua-pki/lib/ca/backends/native_ca_backend.ts"() {
|
|
3099
|
+
"use strict";
|
|
3100
|
+
init_esm_shims();
|
|
3101
|
+
init_toolbox();
|
|
3102
|
+
init_ca_database();
|
|
3103
|
+
CA_CERT_VALIDITY_DAYS = 3650;
|
|
3104
|
+
RSA_SHA256 = { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-256" } };
|
|
3105
|
+
REASON_TO_X509_CRL_REASON = {
|
|
3106
|
+
unspecified: x509.X509CrlReason.unspecified,
|
|
3107
|
+
keyCompromise: x509.X509CrlReason.keyCompromise,
|
|
3108
|
+
CACompromise: x509.X509CrlReason.cACompromise,
|
|
3109
|
+
affiliationChanged: x509.X509CrlReason.affiliationChanged,
|
|
3110
|
+
superseded: x509.X509CrlReason.superseded,
|
|
3111
|
+
cessationOfOperation: x509.X509CrlReason.cessationOfOperation,
|
|
3112
|
+
certificateHold: x509.X509CrlReason.certificateHold,
|
|
3113
|
+
removeFromCRL: x509.X509CrlReason.removeFromCRL
|
|
3114
|
+
};
|
|
3115
|
+
NativeCaBackend = class {
|
|
3116
|
+
/** Nothing to check: this backend spawns no process and needs no tool on PATH. */
|
|
3117
|
+
/** Signing goes through `ca._getSigningKey()`, which may be an external signer. */
|
|
3118
|
+
supportsExternalSigner = true;
|
|
3119
|
+
async preflight() {
|
|
3120
|
+
}
|
|
3121
|
+
/**
|
|
3122
|
+
* Write a CSR for the CA's own key, the `[v3_ca_req]` equivalent. The
|
|
3123
|
+
* key comes from the CA rather than from `privateKeyFile`: on a
|
|
3124
|
+
* signer-backed CA that file does not exist.
|
|
3125
|
+
*/
|
|
3126
|
+
async generateCaCsr(ca, _caRootDir, _privateKeyFile, csrFile) {
|
|
3127
|
+
displayTitle("Generate a certificate request for the CA key");
|
|
3128
|
+
const signingKey = await ca._getSigningKey();
|
|
3129
|
+
const { csr } = await createCertificateSigningRequest2({
|
|
3130
|
+
privateKey: signingKey,
|
|
3131
|
+
subject: ca.subject.toString(),
|
|
3132
|
+
applicationUri: caSanUri(ca),
|
|
3133
|
+
purpose: CertificatePurpose2.ForCertificateAuthority
|
|
3134
|
+
});
|
|
3135
|
+
await fs10.promises.writeFile(csrFile, csr);
|
|
3136
|
+
}
|
|
3137
|
+
async bootstrap(ca) {
|
|
3138
|
+
const caRootDir = path7.resolve(ca.rootDir);
|
|
3139
|
+
const csrFile = path7.join(caRootDir, "private/cakey.csr");
|
|
3140
|
+
await this.generateCaCsr(ca, caRootDir, path7.join(caRootDir, "private/cakey.pem"), csrFile);
|
|
3141
|
+
const issuerCA = ca._issuerCA;
|
|
3142
|
+
if (issuerCA) {
|
|
3143
|
+
displayTitle("Generate CA Certificate (signed by issuer CA)");
|
|
3144
|
+
const signWithIssuer = () => this.signSubordinateCsr(issuerCA, csrFile, ca.caCertificate, CA_CERT_VALIDITY_DAYS);
|
|
3145
|
+
if (path7.resolve(issuerCA.rootDir) === caRootDir) {
|
|
3146
|
+
await signWithIssuer();
|
|
3147
|
+
} else {
|
|
3148
|
+
await issuerCA._withCaDirectoryLock(signWithIssuer);
|
|
3149
|
+
}
|
|
3150
|
+
} else {
|
|
3151
|
+
displayTitle("Generate CA Certificate (self-signed)");
|
|
3152
|
+
const csrPem = await fs10.promises.readFile(csrFile, "utf-8");
|
|
3153
|
+
const signingKey = await ca._getSigningKey();
|
|
3154
|
+
const request = new x509.Pkcs10CertificateRequest(csrPem);
|
|
3155
|
+
const notBefore = /* @__PURE__ */ new Date();
|
|
3156
|
+
const { cert } = await createCertificateFromCsr({
|
|
3157
|
+
csr: csrPem,
|
|
3158
|
+
// the CSR's own parsed subject, so issuer and subject are
|
|
3159
|
+
// byte-identical on a self-signed root: re-encoding a string
|
|
3160
|
+
// form could differ and break chain building
|
|
3161
|
+
issuerName: request.subjectName,
|
|
3162
|
+
issuerPublicKey: request.publicKey,
|
|
3163
|
+
signingKey,
|
|
3164
|
+
signingAlgorithm: signingAlgorithmOf(signingKey),
|
|
3165
|
+
notBefore,
|
|
3166
|
+
notAfter: daysFromNow(notBefore, CA_CERT_VALIDITY_DAYS),
|
|
3167
|
+
purpose: CertificatePurpose2.ForCertificateAuthority,
|
|
3168
|
+
applicationUri: caSanUri(ca),
|
|
3169
|
+
revocation: {
|
|
3170
|
+
crlDistributionUrl: ca.crlDistributionUrl,
|
|
3171
|
+
ocspResponderUrl: ca.ocspResponderUrl,
|
|
3172
|
+
caIssuersUrl: ca.caIssuersUrl
|
|
3173
|
+
}
|
|
3174
|
+
});
|
|
3175
|
+
await fs10.promises.writeFile(ca.caCertificate, cert);
|
|
3176
|
+
}
|
|
3177
|
+
displaySubtitle("generate initial CRL (Certificate Revocation List)");
|
|
3178
|
+
await this.#regenerateCrlLocked(ca, new CaDatabase(ca.rootDir));
|
|
3179
|
+
displayTitle("Create Certificate Authority (CA) ---> DONE");
|
|
3180
|
+
}
|
|
3181
|
+
/** Sign a subordinate CA's request with this CA's key, the `[v3_ca]` equivalent. */
|
|
3182
|
+
async signSubordinateCsr(ca, csrFile, certFile, validityDays) {
|
|
3183
|
+
const { signingKey, signingAlgorithm, issuerPublicKey, issuerName } = await this.#issuerContext(ca);
|
|
3184
|
+
const csrPem = await fs10.promises.readFile(csrFile, "utf-8");
|
|
3185
|
+
const db = new CaDatabase(ca.rootDir);
|
|
3186
|
+
const serialNumber = db.nextSerial();
|
|
3187
|
+
const notBefore = /* @__PURE__ */ new Date();
|
|
3188
|
+
const notAfter = daysFromNow(notBefore, validityDays);
|
|
3189
|
+
const { cert } = await createCertificateFromCsr({
|
|
3190
|
+
csr: csrPem,
|
|
3191
|
+
issuerName,
|
|
3192
|
+
issuerPublicKey,
|
|
3193
|
+
signingKey,
|
|
3194
|
+
signingAlgorithm,
|
|
3195
|
+
serialNumber,
|
|
3196
|
+
notBefore,
|
|
3197
|
+
notAfter,
|
|
3198
|
+
purpose: CertificatePurpose2.ForCertificateAuthority,
|
|
3199
|
+
// the subordinate's own SAN, not the issuer's
|
|
3200
|
+
...sanFromCsr(csrPem),
|
|
3201
|
+
revocation: {
|
|
3202
|
+
crlDistributionUrl: ca.crlDistributionUrl,
|
|
3203
|
+
ocspResponderUrl: ca.ocspResponderUrl,
|
|
3204
|
+
caIssuersUrl: ca.caIssuersUrl
|
|
3205
|
+
}
|
|
3206
|
+
});
|
|
3207
|
+
await fs10.promises.writeFile(certFile, cert);
|
|
3208
|
+
this.#record(db, serialNumber, cert, notAfter);
|
|
3209
|
+
}
|
|
3210
|
+
/**
|
|
3211
|
+
* Self-sign a certificate with a caller-supplied key file and record it
|
|
3212
|
+
* in this CA's database: the native equivalent of `openssl req -new`
|
|
3213
|
+
* followed by `openssl ca -selfsign`, which likewise applies the
|
|
3214
|
+
* end-entity profile and updates `index.txt`.
|
|
3215
|
+
*/
|
|
3216
|
+
async createSelfSignedCertificate(ca, certificateFile, privateKeyFile, params) {
|
|
3217
|
+
displaySubtitle("- the certificate signing request");
|
|
3218
|
+
const key = await privateKeyToCryptoKey(readPrivateKey2(privateKeyFile, await ca._privateKeyPassphrase()));
|
|
3219
|
+
const { csr } = await createCertificateSigningRequest2({
|
|
3220
|
+
privateKey: key,
|
|
3221
|
+
subject: params.subject ? new Subject4(params.subject).toString() : ca.subject.toString(),
|
|
3222
|
+
dns: params.dns,
|
|
3223
|
+
ip: params.ip,
|
|
3224
|
+
applicationUri: params.applicationUri,
|
|
3225
|
+
purpose: CertificatePurpose2.ForApplication
|
|
3226
|
+
});
|
|
3227
|
+
displaySubtitle("- creating the self-signed certificate");
|
|
3228
|
+
const request = new x509.Pkcs10CertificateRequest(csr);
|
|
3229
|
+
const db = new CaDatabase(ca.rootDir);
|
|
3230
|
+
const serialNumber = db.nextSerial();
|
|
3231
|
+
const notBefore = params.startDate ?? /* @__PURE__ */ new Date();
|
|
3232
|
+
const notAfter = params.endDate ?? daysFromNow(notBefore, params.validity ?? 365);
|
|
3233
|
+
const { cert } = await createCertificateFromCsr({
|
|
3234
|
+
csr,
|
|
3235
|
+
issuerName: request.subjectName,
|
|
3236
|
+
issuerPublicKey: request.publicKey,
|
|
3237
|
+
signingKey: key,
|
|
3238
|
+
signingAlgorithm: RSA_SHA256,
|
|
3239
|
+
serialNumber,
|
|
3240
|
+
notBefore,
|
|
3241
|
+
notAfter,
|
|
3242
|
+
purpose: CertificatePurpose2.ForApplication,
|
|
3243
|
+
...sanFromCsr(csr)
|
|
3244
|
+
});
|
|
3245
|
+
await fs10.promises.writeFile(certificateFile, cert);
|
|
3246
|
+
this.#record(db, serialNumber, cert, notAfter);
|
|
3247
|
+
}
|
|
3248
|
+
/** `certs/<SERIAL>.pem` plus the `V` row, exactly as `openssl ca` writes them. */
|
|
3249
|
+
#record(db, serialNumber, certPem, notAfter) {
|
|
3250
|
+
db.storeCertificate(serialNumber, certPem);
|
|
3251
|
+
db.appendIssued({
|
|
3252
|
+
serial: serialNumber,
|
|
3253
|
+
expiryDate: notAfter,
|
|
3254
|
+
subject: toOpenSslSubjectString(new x509.X509Certificate(certPem).subjectName)
|
|
3255
|
+
});
|
|
3256
|
+
}
|
|
3257
|
+
/**
|
|
3258
|
+
* The CA's signing key and issuer identity, resolved fresh on every
|
|
3259
|
+
* call and never cached across operations. The identity is read back
|
|
3260
|
+
* from the CA certificate on disk so that the issuer field of anything
|
|
3261
|
+
* this CA signs is byte-identical to that certificate's subject.
|
|
3262
|
+
*/
|
|
3263
|
+
async #issuerContext(ca) {
|
|
3264
|
+
const signingKey = await ca._getSigningKey();
|
|
3265
|
+
const caCertPem = await fs10.promises.readFile(ca.caCertificate, "utf-8");
|
|
3266
|
+
const caCert = new x509.X509Certificate(caCertPem);
|
|
3267
|
+
return {
|
|
3268
|
+
signingKey,
|
|
3269
|
+
signingAlgorithm: signingAlgorithmOf(signingKey),
|
|
3270
|
+
issuerPublicKey: caCert.publicKey,
|
|
3271
|
+
issuerName: caCert.subjectName
|
|
3272
|
+
};
|
|
3273
|
+
}
|
|
3274
|
+
async signEndEntityCsr(ca, certificate, csr, params, sanOverride) {
|
|
3275
|
+
const { signingKey, signingAlgorithm, issuerPublicKey, issuerName } = await this.#issuerContext(ca);
|
|
3276
|
+
const db = new CaDatabase(ca.rootDir);
|
|
3277
|
+
const serialNumber = db.nextSerial();
|
|
3278
|
+
const csrPem = await fs10.promises.readFile(csr, "utf-8");
|
|
3279
|
+
const notBefore = params.startDate ?? /* @__PURE__ */ new Date();
|
|
3280
|
+
const notAfter = params.endDate ?? new Date(notBefore.getTime() + (params.validity ?? 365) * 24 * 60 * 60 * 1e3);
|
|
3281
|
+
const { cert } = await createCertificateFromCsr({
|
|
3282
|
+
csr: csrPem,
|
|
3283
|
+
issuerName,
|
|
3284
|
+
issuerPublicKey,
|
|
3285
|
+
signingKey,
|
|
3286
|
+
signingAlgorithm,
|
|
3287
|
+
serialNumber,
|
|
3288
|
+
notBefore,
|
|
3289
|
+
notAfter,
|
|
3290
|
+
purpose: CertificatePurpose2.ForApplication,
|
|
3291
|
+
dns: sanOverride.dns,
|
|
3292
|
+
ip: sanOverride.ip,
|
|
3293
|
+
applicationUri: sanOverride.applicationUri,
|
|
3294
|
+
revocation: {
|
|
3295
|
+
crlDistributionUrl: ca.crlDistributionUrl,
|
|
3296
|
+
ocspResponderUrl: ca.ocspResponderUrl,
|
|
3297
|
+
caIssuersUrl: ca.caIssuersUrl
|
|
3298
|
+
}
|
|
3299
|
+
});
|
|
3300
|
+
await fs10.promises.writeFile(certificate, cert);
|
|
3301
|
+
this.#record(db, serialNumber, cert, notAfter);
|
|
3302
|
+
}
|
|
3303
|
+
async #regenerateCrlLocked(ca, db) {
|
|
3304
|
+
const { signingKey, signingAlgorithm, issuerPublicKey, issuerName } = await this.#issuerContext(ca);
|
|
3305
|
+
const crlNumber = db.nextCrlNumber();
|
|
3306
|
+
const entries = db.readIndex().filter((r) => r.status === "revoked").map((r) => ({
|
|
3307
|
+
serialNumber: r.serial,
|
|
3308
|
+
revocationDate: r.revocationDate ? new Date(r.revocationDate) : /* @__PURE__ */ new Date(),
|
|
3309
|
+
reason: r.reason ? REASON_TO_X509_CRL_REASON[r.reason] : void 0
|
|
3310
|
+
}));
|
|
3311
|
+
const { crl } = await createCrl({
|
|
3312
|
+
issuerName,
|
|
3313
|
+
issuerPublicKey,
|
|
3314
|
+
signingKey,
|
|
3315
|
+
signingAlgorithm,
|
|
3316
|
+
crlNumber: BigInt(`0x${crlNumber}`),
|
|
3317
|
+
entries
|
|
3318
|
+
});
|
|
3319
|
+
await fs10.promises.writeFile(ca.revocationList, crl);
|
|
3320
|
+
const der = new x509.X509Crl(crl).rawData;
|
|
3321
|
+
await fs10.promises.writeFile(ca.revocationListDER, Buffer.from(der));
|
|
3322
|
+
}
|
|
3323
|
+
async regenerateCrl(ca) {
|
|
3324
|
+
const db = new CaDatabase(ca.rootDir);
|
|
3325
|
+
await this.#regenerateCrlLocked(ca, db);
|
|
3326
|
+
}
|
|
3327
|
+
async revoke(ca, certificate, reason) {
|
|
3328
|
+
const certPem = await fs10.promises.readFile(certificate, "utf-8");
|
|
3329
|
+
const cert = new x509.X509Certificate(certPem);
|
|
3330
|
+
const db = new CaDatabase(ca.rootDir);
|
|
3331
|
+
db.markRevoked(cert.serialNumber, /* @__PURE__ */ new Date(), reason);
|
|
3332
|
+
await this.#regenerateCrlLocked(ca, db);
|
|
3333
|
+
}
|
|
3334
|
+
};
|
|
3335
|
+
}
|
|
3336
|
+
});
|
|
3337
|
+
|
|
3338
|
+
// packages/node-opcua-pki/lib/ca/backends/openssl_ca_backend.ts
|
|
3339
|
+
import fs11 from "fs";
|
|
3340
|
+
import path8 from "path";
|
|
3341
|
+
import { Subject as Subject5 } from "node-opcua-crypto";
|
|
3342
|
+
function caAltName(ca) {
|
|
3343
|
+
return `URI:urn:${ca.subject.commonName || "NodeOPCUA-CA"}`;
|
|
3344
|
+
}
|
|
3345
|
+
function caConfigEnvOverrides(ca, altName = caAltName(ca)) {
|
|
3346
|
+
const aiaLegs = [];
|
|
3347
|
+
if (ca.ocspResponderUrl) {
|
|
3348
|
+
aiaLegs.push(`OCSP;URI:${ca.ocspResponderUrl}`);
|
|
3349
|
+
}
|
|
3350
|
+
if (ca.caIssuersUrl) {
|
|
3351
|
+
aiaLegs.push(`caIssuers;URI:${ca.caIssuersUrl}`);
|
|
3352
|
+
}
|
|
3353
|
+
return {
|
|
3354
|
+
ALTNAME: altName,
|
|
3355
|
+
CDP_URL: ca.crlDistributionUrl ?? "",
|
|
3356
|
+
AIA_VALUE: aiaLegs.join(",")
|
|
3357
|
+
};
|
|
3358
|
+
}
|
|
3359
|
+
var n4, OpenSslCaBackend;
|
|
3360
|
+
var init_openssl_ca_backend = __esm({
|
|
3361
|
+
"packages/node-opcua-pki/lib/ca/backends/openssl_ca_backend.ts"() {
|
|
3362
|
+
"use strict";
|
|
3363
|
+
init_esm_shims();
|
|
3364
|
+
init_toolbox();
|
|
3365
|
+
init_with_openssl();
|
|
3366
|
+
n4 = makePath;
|
|
3367
|
+
OpenSslCaBackend = class {
|
|
3368
|
+
/** This backend is the `openssl` executable, so it has to be there. */
|
|
3369
|
+
/**
|
|
3370
|
+
* `-passin env:` argv and env for an openssl call that has to load this
|
|
3371
|
+
* CA's key. Built here rather than on the CA: the passphrase is the
|
|
3372
|
+
* CA's business, but expressing it as an openssl argument is this
|
|
3373
|
+
* backend's, and the core has no reason to know the flag exists.
|
|
3374
|
+
*/
|
|
3375
|
+
async #passin(ca) {
|
|
3376
|
+
return passinArg(await ca._privateKeyPassphrase());
|
|
3377
|
+
}
|
|
3378
|
+
/** The openssl CLI loads its key from a file, so it cannot call out to an HSM. */
|
|
3379
|
+
supportsExternalSigner = false;
|
|
3380
|
+
async preflight() {
|
|
3381
|
+
await ensure_openssl_installed();
|
|
3382
|
+
}
|
|
3383
|
+
/**
|
|
3384
|
+
* Render `conf/caconfig.cnf` once with explicit overrides, run `fn`
|
|
3385
|
+
* with the rendered path, and always remove the temp file. Structural
|
|
3386
|
+
* guarantee that (a) every render carries the three required env
|
|
3387
|
+
* values and (b) no rendered `<name>.<pid>-<n>.tmp` file is ever left
|
|
3388
|
+
* behind — each was previously a per-method discipline.
|
|
3389
|
+
*/
|
|
3390
|
+
async #withConfig(ca, altName, fn) {
|
|
3391
|
+
const caRootDir = path8.resolve(ca.rootDir);
|
|
3392
|
+
const options = { cwd: caRootDir };
|
|
3393
|
+
const configFile = generateStaticConfig("conf/caconfig.cnf", options, caConfigEnvOverrides(ca, altName));
|
|
3394
|
+
try {
|
|
3395
|
+
return await fn(configFile, options);
|
|
3396
|
+
} finally {
|
|
3397
|
+
await cleanupStaticConfig(configFile, options);
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
async #generateCaCsrWith(ca, configFile, options, privateKeyFile, csrFile) {
|
|
3401
|
+
const passin = await this.#passin(ca);
|
|
3402
|
+
displayTitle("Generate a certificate request for the CA key");
|
|
3403
|
+
await execute_openssl(
|
|
3404
|
+
[
|
|
3405
|
+
"req",
|
|
3406
|
+
"-new",
|
|
3407
|
+
"-sha256",
|
|
3408
|
+
"-extensions",
|
|
3409
|
+
"v3_ca_req",
|
|
3410
|
+
"-config",
|
|
3411
|
+
n4(configFile),
|
|
3412
|
+
"-key",
|
|
3413
|
+
n4(privateKeyFile),
|
|
3414
|
+
"-out",
|
|
3415
|
+
n4(csrFile),
|
|
3416
|
+
"-subj",
|
|
3417
|
+
ca.subject.toString(),
|
|
3418
|
+
...passin.args
|
|
3419
|
+
],
|
|
3420
|
+
{ ...options, env: passin.env }
|
|
3421
|
+
);
|
|
3422
|
+
}
|
|
3423
|
+
/**
|
|
3424
|
+
* `openssl ca -gencrl` signs the CRL with the CA key it finds through the
|
|
3425
|
+
* config file (`private_key = $dir/private/cakey.pem`), so it needs the
|
|
3426
|
+
* CA passphrase like every other `openssl ca` invocation.
|
|
3427
|
+
*/
|
|
3428
|
+
async #regenerateCrlWith(ca, configFile, options) {
|
|
3429
|
+
const passin = await this.#passin(ca);
|
|
3430
|
+
displaySubtitle("regenerate CRL (Certificate Revocation List)");
|
|
3431
|
+
await execute_openssl(["ca", "-gencrl", "-config", n4(configFile), "-out", "crl/revocation_list.crl", ...passin.args], {
|
|
3432
|
+
...options,
|
|
3433
|
+
env: passin.env
|
|
3434
|
+
});
|
|
3435
|
+
await execute_openssl(
|
|
3436
|
+
["crl", "-in", "crl/revocation_list.crl", "-out", "crl/revocation_list.der", "-outform", "der"],
|
|
3437
|
+
options
|
|
3438
|
+
);
|
|
3439
|
+
displaySubtitle("Display (Certificate Revocation List)");
|
|
3440
|
+
await execute_openssl(["crl", "-in", n4(ca.revocationList), "-text", "-noout"], options);
|
|
3441
|
+
}
|
|
3442
|
+
async generateCaCsr(ca, _caRootDir, privateKeyFile, csrFile) {
|
|
3443
|
+
await this.#withConfig(
|
|
3444
|
+
ca,
|
|
3445
|
+
caAltName(ca),
|
|
3446
|
+
(configFile, options) => this.#generateCaCsrWith(ca, configFile, options, privateKeyFile, csrFile)
|
|
3447
|
+
);
|
|
3448
|
+
}
|
|
3449
|
+
async bootstrap(ca) {
|
|
3450
|
+
const caRootDir = path8.resolve(ca.rootDir);
|
|
3451
|
+
const privateKeyFilename = path8.join(caRootDir, "private/cakey.pem");
|
|
3452
|
+
const csrFilename = path8.join(caRootDir, "private/cakey.csr");
|
|
3453
|
+
await this.#withConfig(ca, caAltName(ca), async (configFile, options) => {
|
|
3454
|
+
await this.#generateCaCsrWith(ca, configFile, options, privateKeyFilename, csrFilename);
|
|
3455
|
+
const issuerCA = ca._issuerCA;
|
|
3456
|
+
if (issuerCA) {
|
|
3457
|
+
displayTitle("Generate CA Certificate (signed by issuer CA)");
|
|
3458
|
+
const issuerCert = path8.resolve(issuerCA.caCertificate);
|
|
3459
|
+
const issuerKey = path8.resolve(issuerCA.rootDir, "private/cakey.pem");
|
|
3460
|
+
const issuerSerial = path8.resolve(issuerCA.rootDir, "serial");
|
|
3461
|
+
const issuerPassin = await this.#passin(issuerCA);
|
|
3462
|
+
const signWithIssuer = async () => {
|
|
3463
|
+
await execute_openssl(
|
|
3464
|
+
[
|
|
3465
|
+
"x509",
|
|
3466
|
+
"-sha256",
|
|
3467
|
+
"-req",
|
|
3468
|
+
"-days",
|
|
3469
|
+
"3650",
|
|
3470
|
+
"-extensions",
|
|
3471
|
+
"v3_ca",
|
|
3472
|
+
"-extfile",
|
|
3473
|
+
n4(configFile),
|
|
3474
|
+
"-in",
|
|
3475
|
+
"private/cakey.csr",
|
|
3476
|
+
"-CA",
|
|
3477
|
+
n4(issuerCert),
|
|
3478
|
+
"-CAkey",
|
|
3479
|
+
n4(issuerKey),
|
|
3480
|
+
"-CAserial",
|
|
3481
|
+
n4(issuerSerial),
|
|
3482
|
+
"-out",
|
|
3483
|
+
"public/cacert.pem",
|
|
3484
|
+
...issuerPassin.args
|
|
3485
|
+
],
|
|
3486
|
+
{ ...options, env: issuerPassin.env }
|
|
3487
|
+
);
|
|
3488
|
+
};
|
|
3489
|
+
if (path8.resolve(issuerCA.rootDir) === caRootDir) {
|
|
3490
|
+
await signWithIssuer();
|
|
3491
|
+
} else {
|
|
3492
|
+
await issuerCA._withCaDirectoryLock(signWithIssuer);
|
|
3493
|
+
}
|
|
3494
|
+
} else {
|
|
3495
|
+
displayTitle("Generate CA Certificate (self-signed)");
|
|
3496
|
+
const passin = await this.#passin(ca);
|
|
3497
|
+
await execute_openssl(
|
|
3498
|
+
[
|
|
3499
|
+
"x509",
|
|
3500
|
+
"-sha256",
|
|
3501
|
+
"-req",
|
|
3502
|
+
"-days",
|
|
3503
|
+
"3650",
|
|
3504
|
+
"-extensions",
|
|
3505
|
+
"v3_ca",
|
|
3506
|
+
"-extfile",
|
|
3507
|
+
n4(configFile),
|
|
3508
|
+
"-in",
|
|
3509
|
+
"private/cakey.csr",
|
|
3510
|
+
"-signkey",
|
|
3511
|
+
n4(privateKeyFilename),
|
|
3512
|
+
"-out",
|
|
3513
|
+
"public/cacert.pem",
|
|
3514
|
+
...passin.args
|
|
3515
|
+
],
|
|
3516
|
+
{ ...options, env: passin.env }
|
|
3517
|
+
);
|
|
3518
|
+
}
|
|
3519
|
+
displaySubtitle("generate initial CRL (Certificate Revocation List)");
|
|
3520
|
+
await this.#regenerateCrlWith(ca, configFile, options);
|
|
3521
|
+
});
|
|
3522
|
+
displayTitle("Create Certificate Authority (CA) ---> DONE");
|
|
3523
|
+
}
|
|
3524
|
+
async regenerateCrl(ca) {
|
|
3525
|
+
await this.#withConfig(ca, caAltName(ca), (configFile, options) => this.#regenerateCrlWith(ca, configFile, options));
|
|
3526
|
+
}
|
|
3527
|
+
async signSubordinateCsr(ca, csrFile, certFile, validityDays) {
|
|
3528
|
+
await this.#withConfig(ca, caAltName(ca), async (configFile, options) => {
|
|
3529
|
+
const caRootDir = options.cwd;
|
|
3530
|
+
const passin = await this.#passin(ca);
|
|
3531
|
+
await execute_openssl(
|
|
3532
|
+
[
|
|
3533
|
+
"x509",
|
|
3534
|
+
"-sha256",
|
|
3535
|
+
"-req",
|
|
3536
|
+
"-days",
|
|
3537
|
+
String(validityDays),
|
|
3538
|
+
"-extensions",
|
|
3539
|
+
"v3_ca",
|
|
3540
|
+
"-extfile",
|
|
3541
|
+
n4(configFile),
|
|
3542
|
+
"-in",
|
|
3543
|
+
n4(csrFile),
|
|
3544
|
+
"-CA",
|
|
3545
|
+
n4(ca.caCertificate),
|
|
3546
|
+
"-CAkey",
|
|
3547
|
+
n4(path8.join(caRootDir, "private/cakey.pem")),
|
|
3548
|
+
"-CAserial",
|
|
3549
|
+
n4(path8.join(caRootDir, "serial")),
|
|
3550
|
+
"-out",
|
|
3551
|
+
n4(certFile),
|
|
3552
|
+
...passin.args
|
|
3553
|
+
],
|
|
3554
|
+
{ ...options, env: passin.env }
|
|
3555
|
+
);
|
|
3556
|
+
});
|
|
3557
|
+
}
|
|
3558
|
+
async signEndEntityCsr(ca, certificate, csr, params1, sanOverride) {
|
|
3559
|
+
await this.#withConfig(ca, buildSubjectAltNameString(sanOverride), async (configFile, options) => {
|
|
3560
|
+
displaySubtitle("- then we ask the authority to sign the certificate signing request");
|
|
3561
|
+
const passin = await this.#passin(ca);
|
|
3562
|
+
await execute_openssl(
|
|
3563
|
+
[
|
|
3564
|
+
"ca",
|
|
3565
|
+
"-config",
|
|
3566
|
+
configFile,
|
|
3567
|
+
"-startdate",
|
|
3568
|
+
x509Date(params1.startDate),
|
|
3569
|
+
"-enddate",
|
|
3570
|
+
x509Date(params1.endDate),
|
|
3571
|
+
"-batch",
|
|
3572
|
+
"-out",
|
|
3573
|
+
n4(certificate),
|
|
3574
|
+
"-in",
|
|
3575
|
+
n4(csr),
|
|
3576
|
+
...passin.args
|
|
3577
|
+
],
|
|
3578
|
+
{ ...options, env: passin.env }
|
|
3579
|
+
);
|
|
3580
|
+
displaySubtitle("- dump the certificate for a check");
|
|
3581
|
+
await execute_openssl(["x509", "-in", n4(certificate), "-dates", "-fingerprint", "-purpose", "-noout"], options);
|
|
3582
|
+
});
|
|
3583
|
+
}
|
|
3584
|
+
async revoke(ca, certificate, reason) {
|
|
3585
|
+
setEnv("RANDFILE", path8.join(ca.rootDir, "random.rnd"));
|
|
3586
|
+
await this.#withConfig(ca, caAltName(ca), async (configFile, options) => {
|
|
3587
|
+
displaySubtitle("Revoke certificate");
|
|
3588
|
+
const passin = await this.#passin(ca);
|
|
3589
|
+
await execute_openssl_no_failure(
|
|
3590
|
+
["ca", "-verbose", "-config", n4(configFile), "-revoke", certificate, "-crl_reason", reason, ...passin.args],
|
|
3591
|
+
{ ...options, env: passin.env }
|
|
3592
|
+
);
|
|
3593
|
+
await this.#regenerateCrlWith(ca, configFile, options);
|
|
3594
|
+
displaySubtitle("Verify that certificate is revoked");
|
|
3595
|
+
await execute_openssl_no_failure(
|
|
3596
|
+
[
|
|
3597
|
+
"verify",
|
|
3598
|
+
"-verbose",
|
|
3599
|
+
"-CRLfile",
|
|
3600
|
+
n4(ca.revocationList),
|
|
3601
|
+
"-CAfile",
|
|
3602
|
+
n4(ca.caCertificate),
|
|
3603
|
+
"-crl_check",
|
|
3604
|
+
n4(certificate)
|
|
3605
|
+
],
|
|
3606
|
+
options
|
|
3607
|
+
);
|
|
3608
|
+
displaySubtitle("Produce CRL in DER form ");
|
|
3609
|
+
await execute_openssl(
|
|
3610
|
+
["crl", "-in", n4(ca.revocationList), "-out", "crl/revocation_list.der", "-outform", "der"],
|
|
3611
|
+
options
|
|
3612
|
+
);
|
|
3613
|
+
displaySubtitle("Produce CRL in PEM form ");
|
|
3614
|
+
await execute_openssl(
|
|
3615
|
+
["crl", "-in", n4(ca.revocationList), "-out", "crl/revocation_list.pem", "-outform", "pem", "-text"],
|
|
3616
|
+
options
|
|
3617
|
+
);
|
|
3618
|
+
});
|
|
3619
|
+
}
|
|
3620
|
+
async createSelfSignedCertificate(ca, certificateFile, privateKeyFile, params) {
|
|
3621
|
+
const envOverrides = caConfigEnvOverrides(ca, buildSubjectAltNameString(params));
|
|
3622
|
+
const configFile = generateStaticConfig(ca.configFile, { cwd: ca.rootDir }, envOverrides);
|
|
3623
|
+
const options = {
|
|
3624
|
+
cwd: ca.rootDir,
|
|
3625
|
+
openssl_conf: makePath(configFile)
|
|
3626
|
+
};
|
|
3627
|
+
try {
|
|
3628
|
+
const subject = params.subject ? new Subject5(params.subject).toString() : "";
|
|
3629
|
+
const subjectOptions = subject && subject.length > 1 ? ["-subj", subject] : [];
|
|
3630
|
+
const csrFile = `${certificateFile}_csr`;
|
|
3631
|
+
const passin = await this.#passin(ca);
|
|
3632
|
+
displaySubtitle("- the certificate signing request");
|
|
3633
|
+
await execute_openssl(
|
|
3634
|
+
[
|
|
3635
|
+
"req",
|
|
3636
|
+
"-new",
|
|
3637
|
+
"-sha256",
|
|
3638
|
+
...subjectOptions,
|
|
3639
|
+
"-batch",
|
|
3640
|
+
"-key",
|
|
3641
|
+
n4(privateKeyFile),
|
|
3642
|
+
"-out",
|
|
3643
|
+
n4(csrFile),
|
|
3644
|
+
...passin.args
|
|
3645
|
+
],
|
|
3646
|
+
{ ...options, env: passin.env }
|
|
3647
|
+
);
|
|
3648
|
+
displaySubtitle("- creating the self-signed certificate");
|
|
3649
|
+
await execute_openssl(
|
|
3650
|
+
[
|
|
3651
|
+
"ca",
|
|
3652
|
+
"-selfsign",
|
|
3653
|
+
"-keyfile",
|
|
3654
|
+
n4(privateKeyFile),
|
|
3655
|
+
"-startdate",
|
|
3656
|
+
x509Date(params.startDate),
|
|
3657
|
+
"-enddate",
|
|
3658
|
+
x509Date(params.endDate),
|
|
3659
|
+
"-batch",
|
|
3660
|
+
"-out",
|
|
3661
|
+
n4(certificateFile),
|
|
3662
|
+
"-in",
|
|
3663
|
+
n4(csrFile),
|
|
3664
|
+
...passin.args
|
|
3665
|
+
],
|
|
3666
|
+
{ ...options, env: passin.env }
|
|
3667
|
+
);
|
|
3668
|
+
displaySubtitle("- dump the certificate for a check");
|
|
3669
|
+
await execute_openssl(["x509", "-in", n4(certificateFile), "-dates", "-fingerprint", "-purpose", "-noout"], {});
|
|
3670
|
+
displaySubtitle("- verify self-signed certificate");
|
|
3671
|
+
await execute_openssl_no_failure(["verify", "-verbose", "-CAfile", n4(certificateFile), n4(certificateFile)], options);
|
|
3672
|
+
await fs11.promises.unlink(csrFile);
|
|
3673
|
+
} finally {
|
|
3674
|
+
await cleanupStaticConfig(configFile, { cwd: ca.rootDir });
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
};
|
|
3678
|
+
}
|
|
3679
|
+
});
|
|
3680
|
+
|
|
3681
|
+
// packages/node-opcua-pki/lib/ca/templates/ca_config_template.cnf.ts
|
|
3682
|
+
var config2, ca_config_template_cnf_default;
|
|
3683
|
+
var init_ca_config_template_cnf = __esm({
|
|
3684
|
+
"packages/node-opcua-pki/lib/ca/templates/ca_config_template.cnf.ts"() {
|
|
3685
|
+
"use strict";
|
|
3686
|
+
init_esm_shims();
|
|
3687
|
+
config2 = `#.........DO NOT MODIFY BY HAND .........................
|
|
3688
|
+
[ ca ]
|
|
3689
|
+
default_ca = CA_default
|
|
3690
|
+
[ CA_default ]
|
|
3691
|
+
dir = "%%ROOT_FOLDER%%" # the main CA folder (quoted: see renderCaConfig)
|
|
3692
|
+
certs = $dir/certs # where to store certificates
|
|
3693
|
+
new_certs_dir = $dir/certs #
|
|
3694
|
+
database = $dir/index.txt # the certificate database
|
|
3695
|
+
serial = $dir/serial # the serial number counter
|
|
3696
|
+
certificate = $dir/public/cacert.pem # The root CA certificate
|
|
3697
|
+
private_key = $dir/private/cakey.pem # the CA private key
|
|
3698
|
+
x509_extensions = usr_cert #
|
|
3699
|
+
default_days = 3650 # default validity : 10 years
|
|
3700
|
+
|
|
3701
|
+
# default_md = sha1
|
|
3702
|
+
|
|
3703
|
+
default_md = sha256 # The default digest algorithm
|
|
3704
|
+
|
|
3705
|
+
preserve = no
|
|
3706
|
+
policy = policy_match
|
|
3707
|
+
# randfile = $dir/random.rnd
|
|
3708
|
+
# default_startdate = YYMMDDHHMMSSZ
|
|
3709
|
+
# default_enddate = YYMMDDHHMMSSZ
|
|
3710
|
+
crl_dir = $dir/crl
|
|
3711
|
+
crl_extensions = crl_ext
|
|
3712
|
+
crl = $dir/crl/revocation_list.crl # the Revocation list
|
|
3713
|
+
crlnumber = $dir/crlnumber # CRL number file
|
|
3714
|
+
default_crl_days = 30
|
|
3715
|
+
default_crl_hours = 24
|
|
3716
|
+
#msie_hack
|
|
3717
|
+
|
|
3718
|
+
[ policy_match ]
|
|
3719
|
+
countryName = optional
|
|
3720
|
+
stateOrProvinceName = optional
|
|
3721
|
+
localityName = optional
|
|
3722
|
+
organizationName = optional
|
|
3723
|
+
organizationalUnitName = optional
|
|
3724
|
+
commonName = optional
|
|
3725
|
+
emailAddress = optional
|
|
3726
|
+
|
|
3727
|
+
[ req ]
|
|
3728
|
+
default_bits = 4096 # Size of keys
|
|
3729
|
+
default_keyfile = key.pem # name of generated keys
|
|
3730
|
+
distinguished_name = req_distinguished_name
|
|
3731
|
+
attributes = req_attributes
|
|
3732
|
+
x509_extensions = v3_ca
|
|
3733
|
+
#input_password
|
|
3734
|
+
#output_password
|
|
3735
|
+
string_mask = nombstr # permitted characters
|
|
2662
3736
|
req_extensions = v3_req
|
|
2663
3737
|
|
|
2664
3738
|
[ req_distinguished_name ]
|
|
@@ -2745,136 +3819,40 @@ authorityKeyIdentifier = keyid:always,issuer:always
|
|
|
2745
3819
|
}
|
|
2746
3820
|
});
|
|
2747
3821
|
|
|
2748
|
-
// packages/node-opcua-pki/lib/ca/
|
|
2749
|
-
import
|
|
2750
|
-
import
|
|
3822
|
+
// packages/node-opcua-pki/lib/ca/core/certificate_authority_core.ts
|
|
3823
|
+
import assert9 from "assert";
|
|
3824
|
+
import fs12 from "fs";
|
|
2751
3825
|
import os4 from "os";
|
|
2752
|
-
import
|
|
3826
|
+
import path9 from "path";
|
|
3827
|
+
import { withLock as withLock2 } from "@ster5/global-mutex";
|
|
2753
3828
|
import chalk6 from "chalk";
|
|
2754
3829
|
import {
|
|
2755
|
-
CertificatePurpose as
|
|
3830
|
+
CertificatePurpose as CertificatePurpose3,
|
|
2756
3831
|
certificateMatchesPrivateKey,
|
|
2757
|
-
convertPEMtoDER,
|
|
3832
|
+
convertPEMtoDER as convertPEMtoDER2,
|
|
3833
|
+
createCertificateSigningRequest as createCertificateSigningRequest3,
|
|
3834
|
+
createPfx,
|
|
2758
3835
|
exploreCertificate as exploreCertificate2,
|
|
2759
3836
|
exploreCertificateSigningRequest,
|
|
2760
3837
|
generatePrivateKeyFile as generatePrivateKeyFile2,
|
|
2761
|
-
|
|
3838
|
+
privateKeyToCryptoKey as privateKeyToCryptoKey2,
|
|
3839
|
+
readCertificatePEM as readCertificatePEM2,
|
|
2762
3840
|
readCertificateSigningRequest,
|
|
2763
|
-
readPrivateKey,
|
|
2764
|
-
Subject as
|
|
2765
|
-
toPem as toPem2
|
|
3841
|
+
readPrivateKey as readPrivateKey3,
|
|
3842
|
+
Subject as Subject6,
|
|
3843
|
+
toPem as toPem2,
|
|
3844
|
+
verifyCertificateSignature as verifyCertificateSignature2,
|
|
3845
|
+
writePrivateKeyFile as writePrivateKeyFile2,
|
|
3846
|
+
x509 as x5092
|
|
2766
3847
|
} from "node-opcua-crypto";
|
|
2767
|
-
function
|
|
2768
|
-
return
|
|
2769
|
-
}
|
|
2770
|
-
async function construct_CertificateAuthority(certificateAuthority) {
|
|
2771
|
-
const subject = certificateAuthority.subject;
|
|
2772
|
-
const caRootDir = path6.resolve(certificateAuthority.rootDir);
|
|
2773
|
-
async function make_folders() {
|
|
2774
|
-
mkdirRecursiveSync(caRootDir);
|
|
2775
|
-
mkdirRecursiveSync(path6.join(caRootDir, "private"));
|
|
2776
|
-
mkdirRecursiveSync(path6.join(caRootDir, "public"));
|
|
2777
|
-
mkdirRecursiveSync(path6.join(caRootDir, "certs"));
|
|
2778
|
-
mkdirRecursiveSync(path6.join(caRootDir, "crl"));
|
|
2779
|
-
mkdirRecursiveSync(path6.join(caRootDir, "conf"));
|
|
2780
|
-
}
|
|
2781
|
-
await make_folders();
|
|
2782
|
-
async function construct_default_files() {
|
|
2783
|
-
const serial = path6.join(caRootDir, "serial");
|
|
2784
|
-
if (!fs10.existsSync(serial)) {
|
|
2785
|
-
await fs10.promises.writeFile(serial, "1000");
|
|
2786
|
-
}
|
|
2787
|
-
const crlNumber = path6.join(caRootDir, "crlnumber");
|
|
2788
|
-
if (!fs10.existsSync(crlNumber)) {
|
|
2789
|
-
await fs10.promises.writeFile(crlNumber, "1000");
|
|
2790
|
-
}
|
|
2791
|
-
const indexFile = path6.join(caRootDir, "index.txt");
|
|
2792
|
-
if (!fs10.existsSync(indexFile)) {
|
|
2793
|
-
await fs10.promises.writeFile(indexFile, "");
|
|
2794
|
-
}
|
|
2795
|
-
}
|
|
2796
|
-
await construct_default_files();
|
|
2797
|
-
const caKeyExists = fs10.existsSync(path6.join(caRootDir, "private/cakey.pem"));
|
|
2798
|
-
const caCertExists = fs10.existsSync(path6.join(caRootDir, "public/cacert.pem"));
|
|
2799
|
-
if (caKeyExists && caCertExists && !config3.forceCA) {
|
|
2800
|
-
debugLog("CA private key and certificate already exist ... skipping");
|
|
2801
|
-
return;
|
|
2802
|
-
}
|
|
2803
|
-
if (caKeyExists && !caCertExists) {
|
|
2804
|
-
debugLog("CA private key exists but cacert.pem is missing \u2014 rebuilding CA");
|
|
2805
|
-
fs10.unlinkSync(path6.join(caRootDir, "private/cakey.pem"));
|
|
2806
|
-
const staleCsr = path6.join(caRootDir, "private/cakey.csr");
|
|
2807
|
-
if (fs10.existsSync(staleCsr)) {
|
|
2808
|
-
fs10.unlinkSync(staleCsr);
|
|
2809
|
-
}
|
|
2810
|
-
}
|
|
2811
|
-
displayTitle("Create Certificate Authority (CA)");
|
|
2812
|
-
const indexFileAttr = path6.join(caRootDir, "index.txt.attr");
|
|
2813
|
-
if (!fs10.existsSync(indexFileAttr)) {
|
|
2814
|
-
await fs10.promises.writeFile(indexFileAttr, "unique_subject = no");
|
|
2815
|
-
}
|
|
2816
|
-
const caConfigFile = certificateAuthority.configFile;
|
|
2817
|
-
if (1) {
|
|
2818
|
-
let data = configurationFileTemplate;
|
|
2819
|
-
data = makePath(data.replace(/%%ROOT_FOLDER%%/, caRootDir));
|
|
2820
|
-
await fs10.promises.writeFile(caConfigFile, data);
|
|
2821
|
-
}
|
|
2822
|
-
const subjectOpt = ` -subj "${subject.toString()}" `;
|
|
2823
|
-
const caCommonName = subject.commonName || "NodeOPCUA-CA";
|
|
2824
|
-
setEnv("ALTNAME", `URI:urn:${caCommonName}`);
|
|
2825
|
-
certificateAuthority._wireRevocationEnvVars();
|
|
2826
|
-
const options = { cwd: caRootDir };
|
|
2827
|
-
const configFile = generateStaticConfig("conf/caconfig.cnf", options);
|
|
2828
|
-
const configOption = ` -config ${q4(n5(configFile))}`;
|
|
2829
|
-
const keySize = certificateAuthority.keySize;
|
|
2830
|
-
const privateKeyFilename = path6.join(caRootDir, "private/cakey.pem");
|
|
2831
|
-
const csrFilename = path6.join(caRootDir, "private/cakey.csr");
|
|
2832
|
-
displayTitle(`Generate the CA private Key - ${keySize}`);
|
|
2833
|
-
await generatePrivateKeyFile2(privateKeyFilename, keySize);
|
|
2834
|
-
displayTitle("Generate a certificate request for the CA key");
|
|
2835
|
-
await execute_openssl(
|
|
2836
|
-
"req -new -sha256 -text -extensions v3_ca_req" + configOption + " -key " + q4(n5(privateKeyFilename)) + " -out " + q4(n5(csrFilename)) + " " + subjectOpt,
|
|
2837
|
-
options
|
|
2838
|
-
);
|
|
2839
|
-
const issuerCA = certificateAuthority._issuerCA;
|
|
2840
|
-
if (issuerCA) {
|
|
2841
|
-
displayTitle("Generate CA Certificate (signed by issuer CA)");
|
|
2842
|
-
const issuerCert = path6.resolve(issuerCA.caCertificate);
|
|
2843
|
-
const issuerKey = path6.resolve(issuerCA.rootDir, "private/cakey.pem");
|
|
2844
|
-
const issuerSerial = path6.resolve(issuerCA.rootDir, "serial");
|
|
2845
|
-
await execute_openssl(
|
|
2846
|
-
" x509 -sha256 -req -days 3650 -text -extensions v3_ca -extfile " + q4(n5(configFile)) + " -in private/cakey.csr -CA " + q4(n5(issuerCert)) + " -CAkey " + q4(n5(issuerKey)) + " -CAserial " + q4(n5(issuerSerial)) + " -out public/cacert.pem",
|
|
2847
|
-
options
|
|
2848
|
-
);
|
|
2849
|
-
} else {
|
|
2850
|
-
displayTitle("Generate CA Certificate (self-signed)");
|
|
2851
|
-
await execute_openssl(
|
|
2852
|
-
" x509 -sha256 -req -days 3650 -text -extensions v3_ca -extfile " + q4(n5(configFile)) + " -in private/cakey.csr -signkey " + q4(n5(privateKeyFilename)) + " -out public/cacert.pem",
|
|
2853
|
-
options
|
|
2854
|
-
);
|
|
2855
|
-
}
|
|
2856
|
-
displaySubtitle("generate initial CRL (Certificate Revocation List)");
|
|
2857
|
-
await regenerateCrl(certificateAuthority.revocationList, configOption, options);
|
|
2858
|
-
displayTitle("Create Certificate Authority (CA) ---> DONE");
|
|
3848
|
+
function escapeOpensslConfDoubleQuoted(value) {
|
|
3849
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
2859
3850
|
}
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
await execute_openssl(`ca -gencrl ${configOption} -out crl/revocation_list.crl`, options);
|
|
2863
|
-
await execute_openssl("crl -in crl/revocation_list.crl -out crl/revocation_list.der -outform der", options);
|
|
2864
|
-
displaySubtitle("Display (Certificate Revocation List)");
|
|
2865
|
-
await execute_openssl(`crl -in ${q4(n5(revocationList))} -text -noout`, options);
|
|
3851
|
+
function renderCaConfig(caRootDir) {
|
|
3852
|
+
return configurationFileTemplate.replace(/%%ROOT_FOLDER%%/, escapeOpensslConfDoubleQuoted(makePath(caRootDir)));
|
|
2866
3853
|
}
|
|
2867
|
-
function
|
|
2868
|
-
|
|
2869
|
-
if (raw.length < 12) return "";
|
|
2870
|
-
const yy = parseInt(raw.substring(0, 2), 10);
|
|
2871
|
-
const year = yy >= 70 ? 1900 + yy : 2e3 + yy;
|
|
2872
|
-
const month = raw.substring(2, 4);
|
|
2873
|
-
const day = raw.substring(4, 6);
|
|
2874
|
-
const hour = raw.substring(6, 8);
|
|
2875
|
-
const min = raw.substring(8, 10);
|
|
2876
|
-
const sec = raw.substring(10, 12);
|
|
2877
|
-
return `${year}-${month}-${day}T${hour}:${min}:${sec}Z`;
|
|
3854
|
+
function octetStringToIpAddress(a) {
|
|
3855
|
+
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();
|
|
2878
3856
|
}
|
|
2879
3857
|
function validateRevocationUrl(url, fieldName) {
|
|
2880
3858
|
if (url === void 0) {
|
|
@@ -2903,28 +3881,25 @@ function validateRevocationUrl(url, fieldName) {
|
|
|
2903
3881
|
}
|
|
2904
3882
|
return url;
|
|
2905
3883
|
}
|
|
2906
|
-
var defaultSubject, configurationFileTemplate,
|
|
2907
|
-
var
|
|
2908
|
-
"packages/node-opcua-pki/lib/ca/
|
|
3884
|
+
var defaultSubject, configurationFileTemplate, config3, CA_LOCK_MAX_WAIT_MS, CA_LOCK_RETRY, CertificateAuthorityCore;
|
|
3885
|
+
var init_certificate_authority_core = __esm({
|
|
3886
|
+
"packages/node-opcua-pki/lib/ca/core/certificate_authority_core.ts"() {
|
|
2909
3887
|
"use strict";
|
|
2910
3888
|
init_esm_shims();
|
|
2911
|
-
init_toolbox_pfx();
|
|
2912
3889
|
init_toolbox();
|
|
2913
|
-
|
|
2914
|
-
init_simple_config_template_cnf();
|
|
3890
|
+
init_ca_database();
|
|
2915
3891
|
init_ca_config_template_cnf();
|
|
2916
3892
|
defaultSubject = "/C=FR/ST=IDF/L=Paris/O=Local NODE-OPCUA Certificate Authority/CN=NodeOPCUA-CA";
|
|
2917
3893
|
configurationFileTemplate = ca_config_template_cnf_default;
|
|
2918
|
-
configurationFileSimpleTemplate2 = simple_config_template_cnf_default;
|
|
2919
3894
|
config3 = {
|
|
2920
3895
|
certificateDir: "INVALID",
|
|
2921
3896
|
forceCA: false,
|
|
2922
3897
|
pkiDir: "INVALID"
|
|
2923
3898
|
};
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
3899
|
+
CA_LOCK_MAX_WAIT_MS = 5 * 6e4;
|
|
3900
|
+
CA_LOCK_RETRY = { minTimeout: 20, maxTimeout: 250 };
|
|
3901
|
+
assert9(octetStringToIpAddress("c07b9179") === "192.123.145.121");
|
|
3902
|
+
CertificateAuthorityCore = class {
|
|
2928
3903
|
/** RSA key size used when generating the CA private key. */
|
|
2929
3904
|
keySize;
|
|
2930
3905
|
/** Root filesystem path of the CA directory structure. */
|
|
@@ -2937,13 +3912,45 @@ var init_certificate_authority = __esm({
|
|
|
2937
3912
|
_crlDistributionUrl;
|
|
2938
3913
|
_ocspResponderUrl;
|
|
2939
3914
|
_caIssuersUrl;
|
|
3915
|
+
#privateKeyPassphrase;
|
|
3916
|
+
/** resolved once (see `privateKeyPassphrase`); `#passphraseResolved` distinguishes "none" from "not yet" */
|
|
3917
|
+
#resolvedPassphrase;
|
|
3918
|
+
#passphraseResolved = false;
|
|
3919
|
+
/** Signing backend: `openssl` shells out to the CLI, `native` signs in-process. */
|
|
3920
|
+
#backend;
|
|
3921
|
+
/** External signing key (HSM/KMS), when one was supplied instead of a key file. */
|
|
3922
|
+
#signer;
|
|
3923
|
+
/** Read access to `index.txt` / `certs/<SERIAL>.pem`. */
|
|
3924
|
+
#db;
|
|
2940
3925
|
constructor(options) {
|
|
2941
|
-
|
|
2942
|
-
|
|
3926
|
+
assert9(Object.prototype.hasOwnProperty.call(options, "location"));
|
|
3927
|
+
assert9(Object.prototype.hasOwnProperty.call(options, "keySize"));
|
|
2943
3928
|
this.location = options.location;
|
|
2944
3929
|
this.keySize = options.keySize || 2048;
|
|
2945
|
-
this.subject = new
|
|
3930
|
+
this.subject = new Subject6(options.subject || defaultSubject);
|
|
2946
3931
|
this._issuerCA = options.issuerCA;
|
|
3932
|
+
this.#privateKeyPassphrase = options.privateKeyPassphrase;
|
|
3933
|
+
this.#signer = options.signer;
|
|
3934
|
+
if (options.signer) {
|
|
3935
|
+
const algorithm = options.signer.algorithm;
|
|
3936
|
+
if (algorithm.name !== "RSASSA-PKCS1-v1_5" && algorithm.name !== "ECDSA") {
|
|
3937
|
+
throw new Error(
|
|
3938
|
+
`CertificateAuthority: signer algorithm ${algorithm.name} is not supported - use RSASSA-PKCS1-v1_5 or ECDSA.`
|
|
3939
|
+
);
|
|
3940
|
+
}
|
|
3941
|
+
if (algorithm.name === "ECDSA" && !algorithm.namedCurve) {
|
|
3942
|
+
throw new Error(
|
|
3943
|
+
"CertificateAuthority: an ECDSA signer must declare its namedCurve (P-256, P-384 or P-521) - importing the signer's public key needs the curve, and an SPKI import cannot infer it."
|
|
3944
|
+
);
|
|
3945
|
+
}
|
|
3946
|
+
}
|
|
3947
|
+
if (options.signer && !options.backend.supportsExternalSigner) {
|
|
3948
|
+
throw new Error(
|
|
3949
|
+
"CertificateAuthority: this backend cannot sign with an external signer - it loads its key from a file. Use a backend that supports one, such as NativeCaBackend."
|
|
3950
|
+
);
|
|
3951
|
+
}
|
|
3952
|
+
this.#backend = options.backend;
|
|
3953
|
+
this.#db = new CaDatabase(this.location);
|
|
2947
3954
|
if (options.crlDistributionUrl !== void 0) {
|
|
2948
3955
|
this.setCrlDistributionUrl(options.crlDistributionUrl);
|
|
2949
3956
|
}
|
|
@@ -3007,38 +4014,159 @@ var init_certificate_authority = __esm({
|
|
|
3007
4014
|
setCaIssuersUrl(url) {
|
|
3008
4015
|
this._caIssuersUrl = validateRevocationUrl(url, "caIssuersUrl");
|
|
3009
4016
|
}
|
|
4017
|
+
/** Absolute path to the CA root directory (alias for {@link location}). */
|
|
4018
|
+
get rootDir() {
|
|
4019
|
+
return this.location;
|
|
4020
|
+
}
|
|
4021
|
+
/** Path to the OpenSSL configuration file (`conf/caconfig.cnf`). */
|
|
4022
|
+
get configFile() {
|
|
4023
|
+
return path9.normalize(path9.join(this.rootDir, "./conf/caconfig.cnf"));
|
|
4024
|
+
}
|
|
4025
|
+
/** Path to the CA private key (`private/cakey.pem`); may be passphrase-encrypted, see {@link getPrivateKey}. */
|
|
4026
|
+
get privateKey() {
|
|
4027
|
+
return path9.join(path9.resolve(this.rootDir), "private/cakey.pem");
|
|
4028
|
+
}
|
|
3010
4029
|
/**
|
|
3011
|
-
*
|
|
3012
|
-
*
|
|
3013
|
-
*
|
|
3014
|
-
* matching `{{#KEY}}...{{/KEY}}` blocks in the templates are
|
|
3015
|
-
* stripped. MUST be called before every `generateStaticConfig`
|
|
3016
|
-
* invocation that signs a certificate.
|
|
4030
|
+
* The CA private key, decrypted with the configured `privateKeyPassphrase`
|
|
4031
|
+
* if it is encrypted. Fails closed (`PrivateKeyPassphraseRequiredError`)
|
|
4032
|
+
* on an encrypted key with no or the wrong passphrase.
|
|
3017
4033
|
*/
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
4034
|
+
async getPrivateKey() {
|
|
4035
|
+
this.#assertKeyIsOnDisk("getPrivateKey");
|
|
4036
|
+
return readPrivateKey3(this.privateKey, await this._privateKeyPassphrase());
|
|
4037
|
+
}
|
|
4038
|
+
/**
|
|
4039
|
+
* True when this CA signs with an external {@link CaSigner} (HSM/KMS)
|
|
4040
|
+
* rather than with `private/cakey.pem`. There is then no private key
|
|
4041
|
+
* file on disk, and never was one.
|
|
4042
|
+
*/
|
|
4043
|
+
get hasExternalSigner() {
|
|
4044
|
+
return this.#signer !== void 0;
|
|
4045
|
+
}
|
|
4046
|
+
/**
|
|
4047
|
+
* Does `cert` certify the key this CA signs with? With the key on disk
|
|
4048
|
+
* that is a private-key match; with a signer there is no private key to
|
|
4049
|
+
* match, so compare the certified public key against the signer's own -
|
|
4050
|
+
* the same question, asked the only way an HSM allows.
|
|
4051
|
+
*/
|
|
4052
|
+
async #certificateMatchesOurKey(certPem, certDer) {
|
|
4053
|
+
if (this.#signer) {
|
|
4054
|
+
const ours = Buffer.from(await this.#signer.getPublicKey());
|
|
4055
|
+
const certified = Buffer.from(new x5092.X509Certificate(certPem).publicKey.rawData);
|
|
4056
|
+
return ours.equals(certified);
|
|
4057
|
+
}
|
|
4058
|
+
return certificateMatchesPrivateKey(certDer, await this.getPrivateKey());
|
|
4059
|
+
}
|
|
4060
|
+
/** Reject the key-file-only operations up front, rather than failing on a missing file later. */
|
|
4061
|
+
#assertKeyIsOnDisk(what) {
|
|
4062
|
+
if (this.#signer) {
|
|
4063
|
+
throw new Error(
|
|
4064
|
+
`CertificateAuthority.${what} is not available on a signer-backed CA: the private key lives in the signer (HSM/KMS) and is never written to disk.`
|
|
4065
|
+
);
|
|
3023
4066
|
}
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
4067
|
+
}
|
|
4068
|
+
/**
|
|
4069
|
+
* @internal The key every signing operation goes through. An injected
|
|
4070
|
+
* {@link CaSigner} is returned as-is; otherwise the on-disk key is read
|
|
4071
|
+
* (and decrypted) and handed over as a plain `CryptoKey`. Both are
|
|
4072
|
+
* accepted by the `node-opcua-crypto` signing primitives, so callers
|
|
4073
|
+
* never branch on which one they got.
|
|
4074
|
+
*/
|
|
4075
|
+
async _getSigningKey() {
|
|
4076
|
+
if (this.#signer) {
|
|
4077
|
+
return this.#signer;
|
|
3027
4078
|
}
|
|
3028
|
-
|
|
3029
|
-
|
|
4079
|
+
return privateKeyToCryptoKey2(await this.getPrivateKey());
|
|
4080
|
+
}
|
|
4081
|
+
/**
|
|
4082
|
+
* Enable, disable, or rotate the passphrase protecting `private/cakey.pem`
|
|
4083
|
+
* (temp file + atomic rename, temp file removed on failure). Only
|
|
4084
|
+
* rewrites the file: construct a new `CertificateAuthority` with the new
|
|
4085
|
+
* passphrase to continue using it.
|
|
4086
|
+
*/
|
|
4087
|
+
async reencryptPrivateKey(oldPassphrase, newPassphrase) {
|
|
4088
|
+
this.#assertKeyIsOnDisk("reencryptPrivateKey");
|
|
4089
|
+
const oldPass = await resolvePrivateKeyPassphrase(oldPassphrase);
|
|
4090
|
+
const newPass = await resolvePrivateKeyPassphrase(newPassphrase);
|
|
4091
|
+
const key = readPrivateKey3(this.privateKey, oldPass);
|
|
4092
|
+
await this.#rewritePrivateKeyFile(key, newPass);
|
|
4093
|
+
}
|
|
4094
|
+
async #rewritePrivateKeyFile(privateKey, passphrase) {
|
|
4095
|
+
const tmpFilename = `${this.privateKey}.${process.pid}-${Date.now()}.tmp`;
|
|
4096
|
+
try {
|
|
4097
|
+
await writePrivateKeyFile2(tmpFilename, privateKey, { passphrase });
|
|
4098
|
+
await fs12.promises.rename(tmpFilename, this.privateKey);
|
|
4099
|
+
} finally {
|
|
4100
|
+
await fs12.promises.rm(tmpFilename, { force: true });
|
|
3030
4101
|
}
|
|
3031
|
-
|
|
3032
|
-
|
|
4102
|
+
}
|
|
4103
|
+
/** @internal resolve the configured passphrase, at most once per instance */
|
|
4104
|
+
async _privateKeyPassphrase() {
|
|
4105
|
+
if (!this.#passphraseResolved) {
|
|
4106
|
+
this.#resolvedPassphrase = await resolvePrivateKeyPassphrase(this.#privateKeyPassphrase);
|
|
4107
|
+
this.#passphraseResolved = true;
|
|
3033
4108
|
}
|
|
4109
|
+
return this.#resolvedPassphrase;
|
|
3034
4110
|
}
|
|
3035
|
-
/**
|
|
3036
|
-
|
|
3037
|
-
|
|
4111
|
+
/**
|
|
4112
|
+
* @internal On an existing key: encrypt it in place if a passphrase is
|
|
4113
|
+
* configured and it is still plaintext (secure by default: the option
|
|
4114
|
+
* means "protect this key", not "ignore me"), then read it back so a
|
|
4115
|
+
* wrong or missing passphrase fails initialize() closed rather than the
|
|
4116
|
+
* first signing operation.
|
|
4117
|
+
*/
|
|
4118
|
+
async _ensurePrivateKeyProtection() {
|
|
4119
|
+
if (this.#signer || !fs12.existsSync(this.privateKey)) {
|
|
4120
|
+
return;
|
|
4121
|
+
}
|
|
4122
|
+
if (this.#privateKeyPassphrase !== void 0 && !isEncryptedPrivateKeyFile(this.privateKey)) {
|
|
4123
|
+
warningLog("CertificateAuthority: private key is plaintext but a passphrase is configured; encrypting it in place");
|
|
4124
|
+
const plaintextKey = readPrivateKey3(this.privateKey);
|
|
4125
|
+
await this.#rewritePrivateKeyFile(plaintextKey, await this._privateKeyPassphrase());
|
|
4126
|
+
}
|
|
4127
|
+
await this.getPrivateKey();
|
|
3038
4128
|
}
|
|
3039
|
-
/**
|
|
3040
|
-
|
|
3041
|
-
|
|
4129
|
+
/**
|
|
4130
|
+
* Acquire a file-based lock on this CA's directory for the duration of
|
|
4131
|
+
* `action` — serializes every operation that mutates the certificate
|
|
4132
|
+
* database (`index.txt`, `serial`, `crlnumber`, `certs/`,
|
|
4133
|
+
* `crl/revocation_list.*`) or the CA's own key/certificate, so
|
|
4134
|
+
* concurrent calls on one `CertificateAuthority` (or two instances
|
|
4135
|
+
* pointed at the same directory, in this or another process) cannot
|
|
4136
|
+
* interleave. Mirrors `CertificateManager.withLock2`.
|
|
4137
|
+
*
|
|
4138
|
+
* Only ever call this from a top-level public operation — nested calls
|
|
4139
|
+
* on the same instance would deadlock, since the underlying file lock
|
|
4140
|
+
* is not reentrant.
|
|
4141
|
+
*
|
|
4142
|
+
* The wait is bounded: a waiter gives up (throws) after
|
|
4143
|
+
* `CA_LOCK_MAX_WAIT_MS`. Without a bound, a holder whose openssl child
|
|
4144
|
+
* hangs would block every CA operation in every process forever and
|
|
4145
|
+
* silently — the lock library's keepalive keeps refreshing the lock
|
|
4146
|
+
* file's mtime as long as the holder process is alive, so stale-lock
|
|
4147
|
+
* recovery never fires for a live-but-stuck holder. A dead holder's
|
|
4148
|
+
* lock goes stale (default 2 minutes) and is taken over well within
|
|
4149
|
+
* this bound.
|
|
4150
|
+
*/
|
|
4151
|
+
async #withCaLock(action) {
|
|
4152
|
+
const lockFileName = path9.join(this.rootDir, ".ca.lock");
|
|
4153
|
+
return withLock2(
|
|
4154
|
+
{
|
|
4155
|
+
fileToLock: lockFileName,
|
|
4156
|
+
retries: { forever: true, maxRetryTime: CA_LOCK_MAX_WAIT_MS, ...CA_LOCK_RETRY }
|
|
4157
|
+
},
|
|
4158
|
+
action
|
|
4159
|
+
);
|
|
4160
|
+
}
|
|
4161
|
+
/**
|
|
4162
|
+
* @internal Run `action` under this CA's directory lock. For the one
|
|
4163
|
+
* legitimate cross-instance use: a subordinate CA's bootstrap signs its
|
|
4164
|
+
* certificate with `openssl x509 -CAserial`, which read-increment-writes
|
|
4165
|
+
* THIS issuer's `serial` file, and must therefore hold this issuer's
|
|
4166
|
+
* lock (ordering is always subordinate -> issuer, so no cycle).
|
|
4167
|
+
*/
|
|
4168
|
+
async _withCaDirectoryLock(action) {
|
|
4169
|
+
return this.#withCaLock(action);
|
|
3042
4170
|
}
|
|
3043
4171
|
/** Path to the CA certificate in PEM format (`public/cacert.pem`). */
|
|
3044
4172
|
get caCertificate() {
|
|
@@ -3087,8 +4215,8 @@ var init_certificate_authority = __esm({
|
|
|
3087
4215
|
* (call {@link initialize} first).
|
|
3088
4216
|
*/
|
|
3089
4217
|
getCACertificateDER() {
|
|
3090
|
-
const pem =
|
|
3091
|
-
return
|
|
4218
|
+
const pem = readCertificatePEM2(this.caCertificate);
|
|
4219
|
+
return convertPEMtoDER2(pem);
|
|
3092
4220
|
}
|
|
3093
4221
|
/**
|
|
3094
4222
|
* Return the CA certificate as a PEM-encoded string.
|
|
@@ -3097,7 +4225,7 @@ var init_certificate_authority = __esm({
|
|
|
3097
4225
|
* (call {@link initialize} first).
|
|
3098
4226
|
*/
|
|
3099
4227
|
getCACertificatePEM() {
|
|
3100
|
-
const raw =
|
|
4228
|
+
const raw = readCertificatePEM2(this.caCertificate);
|
|
3101
4229
|
const beginMarker = "-----BEGIN CERTIFICATE-----";
|
|
3102
4230
|
const idx = raw.indexOf(beginMarker);
|
|
3103
4231
|
if (idx > 0) {
|
|
@@ -3113,10 +4241,10 @@ var init_certificate_authority = __esm({
|
|
|
3113
4241
|
*/
|
|
3114
4242
|
getCRLDER() {
|
|
3115
4243
|
const crlPath = this.revocationListDER;
|
|
3116
|
-
if (!
|
|
4244
|
+
if (!fs12.existsSync(crlPath)) {
|
|
3117
4245
|
return Buffer.alloc(0);
|
|
3118
4246
|
}
|
|
3119
|
-
return
|
|
4247
|
+
return fs12.readFileSync(crlPath);
|
|
3120
4248
|
}
|
|
3121
4249
|
/**
|
|
3122
4250
|
* Return the current Certificate Revocation List as a
|
|
@@ -3126,10 +4254,10 @@ var init_certificate_authority = __esm({
|
|
|
3126
4254
|
*/
|
|
3127
4255
|
getCRLPEM() {
|
|
3128
4256
|
const crlPath = this.revocationList;
|
|
3129
|
-
if (!
|
|
4257
|
+
if (!fs12.existsSync(crlPath)) {
|
|
3130
4258
|
return "";
|
|
3131
4259
|
}
|
|
3132
|
-
const raw =
|
|
4260
|
+
const raw = fs12.readFileSync(crlPath, "utf-8");
|
|
3133
4261
|
const beginMarker = "-----BEGIN X509 CRL-----";
|
|
3134
4262
|
const idx = raw.indexOf(beginMarker);
|
|
3135
4263
|
if (idx > 0) {
|
|
@@ -3148,14 +4276,14 @@ var init_certificate_authority = __esm({
|
|
|
3148
4276
|
* expiry date, and (for revoked certs) the revocation date.
|
|
3149
4277
|
*/
|
|
3150
4278
|
getIssuedCertificates() {
|
|
3151
|
-
return this.
|
|
4279
|
+
return this.#db.readIndex();
|
|
3152
4280
|
}
|
|
3153
4281
|
/**
|
|
3154
4282
|
* Return the total number of certificates recorded in
|
|
3155
4283
|
* `index.txt`.
|
|
3156
4284
|
*/
|
|
3157
4285
|
getIssuedCertificateCount() {
|
|
3158
|
-
return this.
|
|
4286
|
+
return this.#db.readIndex().length;
|
|
3159
4287
|
}
|
|
3160
4288
|
/**
|
|
3161
4289
|
* Return the status of a certificate by its serial number.
|
|
@@ -3165,9 +4293,7 @@ var init_certificate_authority = __esm({
|
|
|
3165
4293
|
* `undefined` if not found
|
|
3166
4294
|
*/
|
|
3167
4295
|
getCertificateStatus(serial) {
|
|
3168
|
-
|
|
3169
|
-
const record = this._parseIndexTxt().find((r) => r.serial.toUpperCase() === upper);
|
|
3170
|
-
return record?.status;
|
|
4296
|
+
return this.#db.findBySerial(serial)?.status;
|
|
3171
4297
|
}
|
|
3172
4298
|
/**
|
|
3173
4299
|
* Read a specific issued certificate by serial number and
|
|
@@ -3180,82 +4306,13 @@ var init_certificate_authority = __esm({
|
|
|
3180
4306
|
* @returns the DER buffer, or `undefined` if not found
|
|
3181
4307
|
*/
|
|
3182
4308
|
getCertificateBySerial(serial) {
|
|
3183
|
-
|
|
3184
|
-
const certFile = path6.join(this.rootDir, "certs", `${upper}.pem`);
|
|
3185
|
-
if (!fs10.existsSync(certFile)) {
|
|
3186
|
-
return void 0;
|
|
3187
|
-
}
|
|
3188
|
-
const pem = readCertificatePEM(certFile);
|
|
3189
|
-
return convertPEMtoDER(pem);
|
|
4309
|
+
return this.#db.getCertificateBySerial(serial);
|
|
3190
4310
|
}
|
|
3191
4311
|
/**
|
|
3192
4312
|
* Path to the OpenSSL certificate database file.
|
|
3193
4313
|
*/
|
|
3194
4314
|
get indexFile() {
|
|
3195
|
-
return
|
|
3196
|
-
}
|
|
3197
|
-
/**
|
|
3198
|
-
* Parse the OpenSSL `index.txt` certificate database.
|
|
3199
|
-
*
|
|
3200
|
-
* Each line has tab-separated fields:
|
|
3201
|
-
* ```
|
|
3202
|
-
* status expiry [revocationDate] serial unknown subject
|
|
3203
|
-
* ```
|
|
3204
|
-
*
|
|
3205
|
-
* - status: `V` (valid), `R` (revoked), `E` (expired)
|
|
3206
|
-
* - expiry: `YYMMDDHHmmssZ`
|
|
3207
|
-
* - revocationDate: present only for revoked certs
|
|
3208
|
-
* - serial: hex string
|
|
3209
|
-
* - unknown: always `"unknown"`
|
|
3210
|
-
* - subject: X.500 slash-delimited string
|
|
3211
|
-
*/
|
|
3212
|
-
_parseIndexTxt() {
|
|
3213
|
-
const indexPath = this.indexFile;
|
|
3214
|
-
if (!fs10.existsSync(indexPath)) {
|
|
3215
|
-
return [];
|
|
3216
|
-
}
|
|
3217
|
-
const content = fs10.readFileSync(indexPath, "utf-8");
|
|
3218
|
-
const lines = content.split("\n").filter((l) => l.trim().length > 0);
|
|
3219
|
-
const records = [];
|
|
3220
|
-
for (const line of lines) {
|
|
3221
|
-
const fields = line.split(" ");
|
|
3222
|
-
if (fields.length < 4) continue;
|
|
3223
|
-
const statusChar = fields[0];
|
|
3224
|
-
const expiryStr = fields[1];
|
|
3225
|
-
let serial;
|
|
3226
|
-
let subject;
|
|
3227
|
-
let revocationDate;
|
|
3228
|
-
if (statusChar === "R") {
|
|
3229
|
-
revocationDate = fields[2];
|
|
3230
|
-
serial = fields[3];
|
|
3231
|
-
subject = fields.length >= 6 ? fields[5] : "";
|
|
3232
|
-
} else {
|
|
3233
|
-
serial = fields[3];
|
|
3234
|
-
subject = fields.length >= 6 ? fields[5] : "";
|
|
3235
|
-
}
|
|
3236
|
-
let status;
|
|
3237
|
-
switch (statusChar) {
|
|
3238
|
-
case "V":
|
|
3239
|
-
status = "valid";
|
|
3240
|
-
break;
|
|
3241
|
-
case "R":
|
|
3242
|
-
status = "revoked";
|
|
3243
|
-
break;
|
|
3244
|
-
case "E":
|
|
3245
|
-
status = "expired";
|
|
3246
|
-
break;
|
|
3247
|
-
default:
|
|
3248
|
-
continue;
|
|
3249
|
-
}
|
|
3250
|
-
records.push({
|
|
3251
|
-
serial,
|
|
3252
|
-
status,
|
|
3253
|
-
subject,
|
|
3254
|
-
expiryDate: parseOpenSSLDate(expiryStr),
|
|
3255
|
-
revocationDate: revocationDate ? parseOpenSSLDate(revocationDate) : void 0
|
|
3256
|
-
});
|
|
3257
|
-
}
|
|
3258
|
-
return records;
|
|
4315
|
+
return this.#db.indexFile;
|
|
3259
4316
|
}
|
|
3260
4317
|
// ---------------------------------------------------------------
|
|
3261
4318
|
// Buffer-based CA operations (US-058)
|
|
@@ -3277,12 +4334,12 @@ var init_certificate_authority = __esm({
|
|
|
3277
4334
|
* @returns the signed certificate as a DER-encoded buffer
|
|
3278
4335
|
*/
|
|
3279
4336
|
async signCertificateRequestFromDER(csrDer, options) {
|
|
3280
|
-
const tmpDir = await
|
|
4337
|
+
const tmpDir = await fs12.promises.mkdtemp(path9.join(os4.tmpdir(), "pki-sign-"));
|
|
3281
4338
|
try {
|
|
3282
|
-
const csrFile =
|
|
3283
|
-
const certFile =
|
|
4339
|
+
const csrFile = path9.join(tmpDir, "request.csr");
|
|
4340
|
+
const certFile = path9.join(tmpDir, "certificate.pem");
|
|
3284
4341
|
const csrPem = toPem2(csrDer, "CERTIFICATE REQUEST");
|
|
3285
|
-
await
|
|
4342
|
+
await fs12.promises.writeFile(csrFile, csrPem, "utf-8");
|
|
3286
4343
|
const signingParams = {};
|
|
3287
4344
|
if (options?.validityMs !== void 0) signingParams.validityMs = options.validityMs;
|
|
3288
4345
|
else signingParams.validity = options?.validity ?? 365;
|
|
@@ -3292,10 +4349,10 @@ var init_certificate_authority = __esm({
|
|
|
3292
4349
|
if (options?.applicationUri) signingParams.applicationUri = options.applicationUri;
|
|
3293
4350
|
if (options?.subject) signingParams.subject = options.subject;
|
|
3294
4351
|
await this.signCertificateRequest(certFile, csrFile, signingParams);
|
|
3295
|
-
const certPem =
|
|
3296
|
-
return
|
|
4352
|
+
const certPem = readCertificatePEM2(certFile);
|
|
4353
|
+
return convertPEMtoDER2(certPem);
|
|
3297
4354
|
} finally {
|
|
3298
|
-
await
|
|
4355
|
+
await fs12.promises.rm(tmpDir, {
|
|
3299
4356
|
recursive: true,
|
|
3300
4357
|
force: true
|
|
3301
4358
|
});
|
|
@@ -3345,27 +4402,35 @@ var init_certificate_authority = __esm({
|
|
|
3345
4402
|
* @returns `{ certificateDer, privateKey }` — certificate as DER,
|
|
3346
4403
|
* private key as a branded `PrivateKey` buffer
|
|
3347
4404
|
*/
|
|
4405
|
+
/**
|
|
4406
|
+
* An ephemeral key and a CSR for it, written into `tmpDir`. Both
|
|
4407
|
+
* `generateKeyPairAndSign*` methods need exactly this, and neither
|
|
4408
|
+
* needs a subprocess for it: the key comes from node's crypto and the
|
|
4409
|
+
* request is built and self-signed in process, so no `openssl.cnf` has
|
|
4410
|
+
* to be rendered either.
|
|
4411
|
+
*/
|
|
4412
|
+
async #createEphemeralKeyAndCsr(tmpDir, keySize, options) {
|
|
4413
|
+
const privateKeyFile = path9.join(tmpDir, "private_key.pem");
|
|
4414
|
+
await generatePrivateKeyFile2(privateKeyFile, keySize);
|
|
4415
|
+
const { csr } = await createCertificateSigningRequest3({
|
|
4416
|
+
privateKey: await privateKeyToCryptoKey2(readPrivateKey3(privateKeyFile)),
|
|
4417
|
+
subject: options.subject ? new Subject6(options.subject).toString() : void 0,
|
|
4418
|
+
applicationUri: options.applicationUri,
|
|
4419
|
+
dns: options.dns ?? [],
|
|
4420
|
+
ip: options.ip ?? [],
|
|
4421
|
+
purpose: CertificatePurpose3.ForApplication
|
|
4422
|
+
});
|
|
4423
|
+
const csrFile = path9.join(tmpDir, "request.csr");
|
|
4424
|
+
await fs12.promises.writeFile(csrFile, csr);
|
|
4425
|
+
return { privateKeyFile, csrFile };
|
|
4426
|
+
}
|
|
3348
4427
|
async generateKeyPairAndSignDER(options) {
|
|
3349
4428
|
const keySize = options.keySize ?? 2048;
|
|
3350
4429
|
const startDate = options.startDate ?? /* @__PURE__ */ new Date();
|
|
3351
|
-
const tmpDir = await
|
|
4430
|
+
const tmpDir = await fs12.promises.mkdtemp(path9.join(os4.tmpdir(), "pki-keygen-"));
|
|
3352
4431
|
try {
|
|
3353
|
-
const privateKeyFile =
|
|
3354
|
-
|
|
3355
|
-
const configFile = path6.join(tmpDir, "openssl.cnf");
|
|
3356
|
-
await fs10.promises.writeFile(configFile, configurationFileSimpleTemplate2, "utf-8");
|
|
3357
|
-
const csrFile = path6.join(tmpDir, "request.csr");
|
|
3358
|
-
await createCertificateSigningRequestWithOpenSSL(csrFile, {
|
|
3359
|
-
rootDir: tmpDir,
|
|
3360
|
-
configFile,
|
|
3361
|
-
privateKey: privateKeyFile,
|
|
3362
|
-
applicationUri: options.applicationUri,
|
|
3363
|
-
subject: options.subject,
|
|
3364
|
-
dns: options.dns ?? [],
|
|
3365
|
-
ip: options.ip ?? [],
|
|
3366
|
-
purpose: CertificatePurpose2.ForApplication
|
|
3367
|
-
});
|
|
3368
|
-
const certFile = path6.join(tmpDir, "certificate.pem");
|
|
4432
|
+
const { privateKeyFile, csrFile } = await this.#createEphemeralKeyAndCsr(tmpDir, keySize, options);
|
|
4433
|
+
const certFile = path9.join(tmpDir, "certificate.pem");
|
|
3369
4434
|
const signingParams = {
|
|
3370
4435
|
applicationUri: options.applicationUri,
|
|
3371
4436
|
dns: options.dns,
|
|
@@ -3375,12 +4440,12 @@ var init_certificate_authority = __esm({
|
|
|
3375
4440
|
if (options.validityMs !== void 0) signingParams.validityMs = options.validityMs;
|
|
3376
4441
|
else signingParams.validity = options.validity ?? 365;
|
|
3377
4442
|
await this.signCertificateRequest(certFile, csrFile, signingParams);
|
|
3378
|
-
const certPem =
|
|
3379
|
-
const certificateDer =
|
|
3380
|
-
const privateKey =
|
|
4443
|
+
const certPem = readCertificatePEM2(certFile);
|
|
4444
|
+
const certificateDer = convertPEMtoDER2(certPem);
|
|
4445
|
+
const privateKey = readPrivateKey3(privateKeyFile);
|
|
3381
4446
|
return { certificateDer, privateKey };
|
|
3382
4447
|
} finally {
|
|
3383
|
-
await
|
|
4448
|
+
await fs12.promises.rm(tmpDir, {
|
|
3384
4449
|
recursive: true,
|
|
3385
4450
|
force: true
|
|
3386
4451
|
});
|
|
@@ -3401,24 +4466,10 @@ var init_certificate_authority = __esm({
|
|
|
3401
4466
|
const keySize = options.keySize ?? 2048;
|
|
3402
4467
|
const startDate = options.startDate ?? /* @__PURE__ */ new Date();
|
|
3403
4468
|
const passphrase = options.passphrase ?? "";
|
|
3404
|
-
const tmpDir = await
|
|
4469
|
+
const tmpDir = await fs12.promises.mkdtemp(path9.join(os4.tmpdir(), "pki-keygen-pfx-"));
|
|
3405
4470
|
try {
|
|
3406
|
-
const privateKeyFile =
|
|
3407
|
-
|
|
3408
|
-
const configFile = path6.join(tmpDir, "openssl.cnf");
|
|
3409
|
-
await fs10.promises.writeFile(configFile, configurationFileSimpleTemplate2, "utf-8");
|
|
3410
|
-
const csrFile = path6.join(tmpDir, "request.csr");
|
|
3411
|
-
await createCertificateSigningRequestWithOpenSSL(csrFile, {
|
|
3412
|
-
rootDir: tmpDir,
|
|
3413
|
-
configFile,
|
|
3414
|
-
privateKey: privateKeyFile,
|
|
3415
|
-
applicationUri: options.applicationUri,
|
|
3416
|
-
subject: options.subject,
|
|
3417
|
-
dns: options.dns ?? [],
|
|
3418
|
-
ip: options.ip ?? [],
|
|
3419
|
-
purpose: CertificatePurpose2.ForApplication
|
|
3420
|
-
});
|
|
3421
|
-
const certFile = path6.join(tmpDir, "certificate.pem");
|
|
4471
|
+
const { privateKeyFile, csrFile } = await this.#createEphemeralKeyAndCsr(tmpDir, keySize, options);
|
|
4472
|
+
const certFile = path9.join(tmpDir, "certificate.pem");
|
|
3422
4473
|
const signingParams = {
|
|
3423
4474
|
applicationUri: options.applicationUri,
|
|
3424
4475
|
dns: options.dns,
|
|
@@ -3428,17 +4479,20 @@ var init_certificate_authority = __esm({
|
|
|
3428
4479
|
if (options.validityMs !== void 0) signingParams.validityMs = options.validityMs;
|
|
3429
4480
|
else signingParams.validity = options.validity ?? 365;
|
|
3430
4481
|
await this.signCertificateRequest(certFile, csrFile, signingParams);
|
|
3431
|
-
const
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
4482
|
+
const blocks = (await fs12.promises.readFile(certFile, "utf-8")).match(
|
|
4483
|
+
/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g
|
|
4484
|
+
);
|
|
4485
|
+
if (!blocks || blocks.length === 0) {
|
|
4486
|
+
throw new Error(`generateKeyPairAndSignPFX: no certificate was produced in ${certFile}`);
|
|
4487
|
+
}
|
|
4488
|
+
return await createPfx({
|
|
4489
|
+
certificate: convertPEMtoDER2(blocks[0]),
|
|
4490
|
+
certificateChain: blocks.slice(1).map(convertPEMtoDER2),
|
|
4491
|
+
privateKey: readPrivateKey3(privateKeyFile),
|
|
4492
|
+
password: passphrase
|
|
3438
4493
|
});
|
|
3439
|
-
return await fs10.promises.readFile(pfxFile);
|
|
3440
4494
|
} finally {
|
|
3441
|
-
await
|
|
4495
|
+
await fs12.promises.rm(tmpDir, {
|
|
3442
4496
|
recursive: true,
|
|
3443
4497
|
force: true
|
|
3444
4498
|
});
|
|
@@ -3459,8 +4513,8 @@ var init_certificate_authority = __esm({
|
|
|
3459
4513
|
async revokeCertificateDER(certDer, reason) {
|
|
3460
4514
|
const info = exploreCertificate2(certDer);
|
|
3461
4515
|
const serial = info.tbsCertificate.serialNumber.replace(/:/g, "").toUpperCase();
|
|
3462
|
-
const storedCertFile =
|
|
3463
|
-
if (!
|
|
4516
|
+
const storedCertFile = path9.join(this.rootDir, "certs", `${serial}.pem`);
|
|
4517
|
+
if (!fs12.existsSync(storedCertFile)) {
|
|
3464
4518
|
throw new Error(`Cannot revoke: no stored certificate found for serial ${serial} at ${storedCertFile}`);
|
|
3465
4519
|
}
|
|
3466
4520
|
await this.revokeCertificate(storedCertFile, {
|
|
@@ -3473,7 +4527,69 @@ var init_certificate_authority = __esm({
|
|
|
3473
4527
|
* already exist.
|
|
3474
4528
|
*/
|
|
3475
4529
|
async initialize() {
|
|
3476
|
-
|
|
4530
|
+
mkdirRecursiveSync(path9.resolve(this.rootDir));
|
|
4531
|
+
await this.#withCaLock(() => this.#bootstrap());
|
|
4532
|
+
}
|
|
4533
|
+
/**
|
|
4534
|
+
* @internal Shared (backend-agnostic) part of `initialize()`: directory
|
|
4535
|
+
* layout, default database files, the "already initialized" / "partial
|
|
4536
|
+
* init" checks, and the openssl config file — then delegates CSR
|
|
4537
|
+
* generation, CA-certificate signing, and the initial CRL to
|
|
4538
|
+
* {@link CaBackend.bootstrap}. Must be called under {@link #withCaLock}.
|
|
4539
|
+
*/
|
|
4540
|
+
async #bootstrap() {
|
|
4541
|
+
const caRootDir = path9.resolve(this.rootDir);
|
|
4542
|
+
mkdirRecursiveSync(caRootDir);
|
|
4543
|
+
ensurePrivateDirectory(path9.join(caRootDir, "private"));
|
|
4544
|
+
mkdirRecursiveSync(path9.join(caRootDir, "public"));
|
|
4545
|
+
mkdirRecursiveSync(path9.join(caRootDir, "certs"));
|
|
4546
|
+
mkdirRecursiveSync(path9.join(caRootDir, "crl"));
|
|
4547
|
+
mkdirRecursiveSync(path9.join(caRootDir, "conf"));
|
|
4548
|
+
const serial = path9.join(caRootDir, "serial");
|
|
4549
|
+
if (!fs12.existsSync(serial)) {
|
|
4550
|
+
await fs12.promises.writeFile(serial, "1000");
|
|
4551
|
+
}
|
|
4552
|
+
const crlNumber = path9.join(caRootDir, "crlnumber");
|
|
4553
|
+
if (!fs12.existsSync(crlNumber)) {
|
|
4554
|
+
await fs12.promises.writeFile(crlNumber, "1000");
|
|
4555
|
+
}
|
|
4556
|
+
const indexFile = path9.join(caRootDir, "index.txt");
|
|
4557
|
+
if (!fs12.existsSync(indexFile)) {
|
|
4558
|
+
await fs12.promises.writeFile(indexFile, "");
|
|
4559
|
+
}
|
|
4560
|
+
const signerBacked = this.hasExternalSigner;
|
|
4561
|
+
const caKeyExists = signerBacked || fs12.existsSync(path9.join(caRootDir, "private/cakey.pem"));
|
|
4562
|
+
const caCertExists = fs12.existsSync(path9.join(caRootDir, "public/cacert.pem"));
|
|
4563
|
+
if (caKeyExists && caCertExists && !config3.forceCA) {
|
|
4564
|
+
if (!signerBacked) {
|
|
4565
|
+
restrictPrivateFilePermissions(path9.join(caRootDir, "private/cakey.pem"), 384);
|
|
4566
|
+
await this._ensurePrivateKeyProtection();
|
|
4567
|
+
}
|
|
4568
|
+
debugLog("CA private key and certificate already exist ... skipping");
|
|
4569
|
+
return;
|
|
4570
|
+
}
|
|
4571
|
+
if (!signerBacked && caKeyExists && !caCertExists) {
|
|
4572
|
+
debugLog("CA private key exists but cacert.pem is missing \u2014 rebuilding CA");
|
|
4573
|
+
fs12.unlinkSync(path9.join(caRootDir, "private/cakey.pem"));
|
|
4574
|
+
const staleCsr = path9.join(caRootDir, "private/cakey.csr");
|
|
4575
|
+
if (fs12.existsSync(staleCsr)) {
|
|
4576
|
+
fs12.unlinkSync(staleCsr);
|
|
4577
|
+
}
|
|
4578
|
+
}
|
|
4579
|
+
displayTitle("Create Certificate Authority (CA)");
|
|
4580
|
+
const indexFileAttr = path9.join(caRootDir, "index.txt.attr");
|
|
4581
|
+
if (!fs12.existsSync(indexFileAttr)) {
|
|
4582
|
+
await fs12.promises.writeFile(indexFileAttr, "unique_subject = no");
|
|
4583
|
+
}
|
|
4584
|
+
const caConfigFile = this.configFile;
|
|
4585
|
+
await fs12.promises.writeFile(caConfigFile, renderCaConfig(caRootDir));
|
|
4586
|
+
if (!signerBacked) {
|
|
4587
|
+
const privateKeyFilename = path9.join(caRootDir, "private/cakey.pem");
|
|
4588
|
+
displayTitle(`Generate the CA private Key - ${this.keySize}`);
|
|
4589
|
+
await generatePrivateKeyFile2(privateKeyFilename, this.keySize, { passphrase: await this._privateKeyPassphrase() });
|
|
4590
|
+
restrictPrivateFilePermissions(privateKeyFilename, 384);
|
|
4591
|
+
}
|
|
4592
|
+
await this.#backend.bootstrap(this);
|
|
3477
4593
|
}
|
|
3478
4594
|
/**
|
|
3479
4595
|
* Initialize the CA directory structure and generate the
|
|
@@ -3495,16 +4611,25 @@ var init_certificate_authority = __esm({
|
|
|
3495
4611
|
* @returns an {@link InitializeCSRResult} describing the CA state
|
|
3496
4612
|
*/
|
|
3497
4613
|
async initializeCSR() {
|
|
3498
|
-
const caRootDir =
|
|
4614
|
+
const caRootDir = path9.resolve(this.rootDir);
|
|
3499
4615
|
mkdirRecursiveSync(caRootDir);
|
|
3500
|
-
|
|
3501
|
-
|
|
4616
|
+
return this.#withCaLock(() => this.#initializeCSRLocked(caRootDir));
|
|
4617
|
+
}
|
|
4618
|
+
async #initializeCSRLocked(caRootDir) {
|
|
4619
|
+
for (const dir of ["public", "certs", "crl", "conf"]) {
|
|
4620
|
+
mkdirRecursiveSync(path9.join(caRootDir, dir));
|
|
3502
4621
|
}
|
|
4622
|
+
ensurePrivateDirectory(path9.join(caRootDir, "private"));
|
|
3503
4623
|
const caCertFile = this.caCertificate;
|
|
3504
|
-
const privateKeyFile =
|
|
3505
|
-
const csrFile =
|
|
3506
|
-
|
|
3507
|
-
|
|
4624
|
+
const privateKeyFile = path9.join(caRootDir, "private/cakey.pem");
|
|
4625
|
+
const csrFile = path9.join(caRootDir, "private/cakey.csr");
|
|
4626
|
+
const keyAvailable = this.hasExternalSigner || fs12.existsSync(privateKeyFile);
|
|
4627
|
+
if (!this.hasExternalSigner && fs12.existsSync(privateKeyFile)) {
|
|
4628
|
+
restrictPrivateFilePermissions(privateKeyFile, 384);
|
|
4629
|
+
await this._ensurePrivateKeyProtection();
|
|
4630
|
+
}
|
|
4631
|
+
if (fs12.existsSync(caCertFile)) {
|
|
4632
|
+
const certDer = convertPEMtoDER2(readCertificatePEM2(caCertFile));
|
|
3508
4633
|
const certInfo = exploreCertificate2(certDer);
|
|
3509
4634
|
const notAfter = certInfo.tbsCertificate.validity.notAfter;
|
|
3510
4635
|
if (notAfter.getTime() < Date.now()) {
|
|
@@ -3515,32 +4640,31 @@ var init_certificate_authority = __esm({
|
|
|
3515
4640
|
debugLog("CA certificate already exists and is valid \u2014 ready");
|
|
3516
4641
|
return { status: "ready" };
|
|
3517
4642
|
}
|
|
3518
|
-
if (
|
|
4643
|
+
if (keyAvailable && fs12.existsSync(csrFile)) {
|
|
3519
4644
|
debugLog("CA key + CSR already exist \u2014 pending external signing");
|
|
3520
4645
|
return { status: "pending", csrPath: csrFile };
|
|
3521
4646
|
}
|
|
3522
|
-
const serial =
|
|
3523
|
-
if (!
|
|
3524
|
-
await
|
|
4647
|
+
const serial = path9.join(caRootDir, "serial");
|
|
4648
|
+
if (!fs12.existsSync(serial)) {
|
|
4649
|
+
await fs12.promises.writeFile(serial, "1000");
|
|
3525
4650
|
}
|
|
3526
|
-
const crlNumber =
|
|
3527
|
-
if (!
|
|
3528
|
-
await
|
|
4651
|
+
const crlNumber = path9.join(caRootDir, "crlnumber");
|
|
4652
|
+
if (!fs12.existsSync(crlNumber)) {
|
|
4653
|
+
await fs12.promises.writeFile(crlNumber, "1000");
|
|
3529
4654
|
}
|
|
3530
|
-
const indexFile =
|
|
3531
|
-
if (!
|
|
3532
|
-
await
|
|
4655
|
+
const indexFile = path9.join(caRootDir, "index.txt");
|
|
4656
|
+
if (!fs12.existsSync(indexFile)) {
|
|
4657
|
+
await fs12.promises.writeFile(indexFile, "");
|
|
3533
4658
|
}
|
|
3534
|
-
const indexFileAttr =
|
|
3535
|
-
if (!
|
|
3536
|
-
await
|
|
4659
|
+
const indexFileAttr = path9.join(caRootDir, "index.txt.attr");
|
|
4660
|
+
if (!fs12.existsSync(indexFileAttr)) {
|
|
4661
|
+
await fs12.promises.writeFile(indexFileAttr, "unique_subject = no");
|
|
3537
4662
|
}
|
|
3538
4663
|
const caConfigFile = this.configFile;
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
await generatePrivateKeyFile2(privateKeyFile, this.keySize);
|
|
4664
|
+
await fs12.promises.writeFile(caConfigFile, renderCaConfig(caRootDir));
|
|
4665
|
+
if (!keyAvailable) {
|
|
4666
|
+
await generatePrivateKeyFile2(privateKeyFile, this.keySize, { passphrase: await this._privateKeyPassphrase() });
|
|
4667
|
+
restrictPrivateFilePermissions(privateKeyFile, 384);
|
|
3544
4668
|
}
|
|
3545
4669
|
await this._generateCSR(caRootDir, privateKeyFile, csrFile);
|
|
3546
4670
|
return { status: "created", csrPath: csrFile };
|
|
@@ -3559,38 +4683,34 @@ var init_certificate_authority = __esm({
|
|
|
3559
4683
|
* renewal is needed, `"ready"` if the cert is still valid
|
|
3560
4684
|
*/
|
|
3561
4685
|
async renewCSR(thresholdDays = 30) {
|
|
3562
|
-
const caRootDir =
|
|
4686
|
+
const caRootDir = path9.resolve(this.rootDir);
|
|
3563
4687
|
const caCertFile = this.caCertificate;
|
|
3564
|
-
const privateKeyFile =
|
|
3565
|
-
const csrFile =
|
|
3566
|
-
if (!
|
|
4688
|
+
const privateKeyFile = path9.join(caRootDir, "private/cakey.pem");
|
|
4689
|
+
const csrFile = path9.join(caRootDir, "private/cakey.csr");
|
|
4690
|
+
if (!fs12.existsSync(caCertFile)) {
|
|
3567
4691
|
return this.initializeCSR();
|
|
3568
4692
|
}
|
|
3569
|
-
const certDer =
|
|
4693
|
+
const certDer = convertPEMtoDER2(readCertificatePEM2(caCertFile));
|
|
3570
4694
|
const certInfo = exploreCertificate2(certDer);
|
|
3571
4695
|
const notAfter = certInfo.tbsCertificate.validity.notAfter;
|
|
3572
4696
|
const thresholdMs = thresholdDays * 24 * 60 * 60 * 1e3;
|
|
3573
4697
|
if (notAfter.getTime() - Date.now() < thresholdMs) {
|
|
3574
4698
|
debugLog(`CA certificate expires within ${thresholdDays} days \u2014 generating renewal CSR`);
|
|
3575
|
-
await this._generateCSR(caRootDir, privateKeyFile, csrFile);
|
|
4699
|
+
await this.#withCaLock(() => this._generateCSR(caRootDir, privateKeyFile, csrFile));
|
|
3576
4700
|
return { status: "expired", csrPath: csrFile, expiryDate: notAfter };
|
|
3577
4701
|
}
|
|
3578
4702
|
return { status: "ready" };
|
|
3579
4703
|
}
|
|
3580
4704
|
/**
|
|
3581
4705
|
* Generate a CSR using the existing private key.
|
|
4706
|
+
* Must be called under {@link #withCaLock} — the lock is taken by the
|
|
4707
|
+
* public callers (`initializeCSR`, `renewCSR`), not here, so that
|
|
4708
|
+
* `initializeCSR` can hold one lock across key generation AND CSR
|
|
4709
|
+
* generation without the non-reentrant file lock deadlocking.
|
|
3582
4710
|
* @internal
|
|
3583
4711
|
*/
|
|
3584
4712
|
async _generateCSR(caRootDir, privateKeyFile, csrFile) {
|
|
3585
|
-
|
|
3586
|
-
processAltNames({});
|
|
3587
|
-
const options = { cwd: caRootDir };
|
|
3588
|
-
const configFile = generateStaticConfig("conf/caconfig.cnf", options);
|
|
3589
|
-
const configOption = ` -config ${q4(n5(configFile))}`;
|
|
3590
|
-
await execute_openssl(
|
|
3591
|
-
"req -new -sha256 -text -extensions v3_ca_req" + configOption + " -key " + q4(n5(privateKeyFile)) + " -out " + q4(n5(csrFile)) + " " + subjectOpt,
|
|
3592
|
-
options
|
|
3593
|
-
);
|
|
4713
|
+
await this.#backend.generateCaCsr(this, caRootDir, privateKeyFile, csrFile);
|
|
3594
4714
|
}
|
|
3595
4715
|
/**
|
|
3596
4716
|
* Install an externally-signed CA certificate and generate
|
|
@@ -3609,45 +4729,41 @@ var init_certificate_authority = __esm({
|
|
|
3609
4729
|
* `status: "success"` or `status: "error"` and a `reason`
|
|
3610
4730
|
*/
|
|
3611
4731
|
async installCACertificate(signedCertFile) {
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
}
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
}
|
|
3632
|
-
}
|
|
3633
|
-
await fs10.promises.writeFile(caCertFile, `${pemBlocks[0]}
|
|
4732
|
+
return this.#withCaLock(async () => {
|
|
4733
|
+
const caCertFile = this.caCertificate;
|
|
4734
|
+
const fullPem = await fs12.promises.readFile(signedCertFile, "utf8");
|
|
4735
|
+
const pemBlocks = fullPem.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g);
|
|
4736
|
+
if (!pemBlocks || pemBlocks.length === 0) {
|
|
4737
|
+
return {
|
|
4738
|
+
status: "error",
|
|
4739
|
+
reason: "no_certificate_found",
|
|
4740
|
+
message: "The provided file does not contain any PEM-encoded certificate."
|
|
4741
|
+
};
|
|
4742
|
+
}
|
|
4743
|
+
const certDer = convertPEMtoDER2(pemBlocks[0]);
|
|
4744
|
+
if (!await this.#certificateMatchesOurKey(pemBlocks[0], certDer)) {
|
|
4745
|
+
return {
|
|
4746
|
+
status: "error",
|
|
4747
|
+
reason: "certificate_key_mismatch",
|
|
4748
|
+
message: "The provided certificate does not match the CA private key. Ensure the certificate was signed from the CSR generated by initializeCSR()."
|
|
4749
|
+
};
|
|
4750
|
+
}
|
|
4751
|
+
await fs12.promises.writeFile(caCertFile, `${pemBlocks[0]}
|
|
3634
4752
|
`);
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
4753
|
+
const issuerChainFile = this.issuerCertificateChain;
|
|
4754
|
+
if (pemBlocks.length > 1) {
|
|
4755
|
+
const issuerPem = `${pemBlocks.slice(1).join("\n")}
|
|
3638
4756
|
`;
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
4757
|
+
await fs12.promises.writeFile(issuerChainFile, issuerPem);
|
|
4758
|
+
debugLog(`Stored ${pemBlocks.length - 1} issuer certificate(s) in issuer_chain.pem`);
|
|
4759
|
+
} else {
|
|
4760
|
+
if (fs12.existsSync(issuerChainFile)) {
|
|
4761
|
+
await fs12.promises.unlink(issuerChainFile);
|
|
4762
|
+
}
|
|
3644
4763
|
}
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
const configOption = ` -config ${q4(n5(configFile))}`;
|
|
3649
|
-
await regenerateCrl(this.revocationList, configOption, options);
|
|
3650
|
-
return { status: "success" };
|
|
4764
|
+
await this.#backend.regenerateCrl(this);
|
|
4765
|
+
return { status: "success" };
|
|
4766
|
+
});
|
|
3651
4767
|
}
|
|
3652
4768
|
/**
|
|
3653
4769
|
* Sign a CSR with CA extensions (`v3_ca`), producing a
|
|
@@ -3663,16 +4779,11 @@ var init_certificate_authority = __esm({
|
|
|
3663
4779
|
* @param params - signing parameters
|
|
3664
4780
|
*/
|
|
3665
4781
|
async signCACertificateRequest(certFile, csrFile, params) {
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
await execute_openssl(
|
|
3672
|
-
` x509 -sha256 -req -days ${validity} -text -extensions v3_ca -extfile ` + q4(n5(configFile)) + " -in " + q4(n5(csrFile)) + " -CA " + q4(n5(this.caCertificate)) + " -CAkey " + q4(n5(path6.join(caRootDir, "private/cakey.pem"))) + " -CAserial " + q4(n5(path6.join(caRootDir, "serial"))) + " -out " + q4(n5(certFile)),
|
|
3673
|
-
options
|
|
3674
|
-
);
|
|
3675
|
-
await this.constructCertificateChain(certFile);
|
|
4782
|
+
await this.#withCaLock(async () => {
|
|
4783
|
+
const validity = params.validity ?? 3650;
|
|
4784
|
+
await this.#backend.signSubordinateCsr(this, csrFile, certFile, validity);
|
|
4785
|
+
await this.constructCertificateChain(certFile);
|
|
4786
|
+
});
|
|
3676
4787
|
}
|
|
3677
4788
|
/**
|
|
3678
4789
|
* Rebuild the combined CA certificate + CRL file.
|
|
@@ -3683,13 +4794,13 @@ var init_certificate_authority = __esm({
|
|
|
3683
4794
|
*/
|
|
3684
4795
|
async constructCACertificateWithCRL() {
|
|
3685
4796
|
const cacertWithCRL = this.caCertificateWithCrl;
|
|
3686
|
-
if (
|
|
3687
|
-
await
|
|
4797
|
+
if (fs12.existsSync(this.revocationList)) {
|
|
4798
|
+
await fs12.promises.writeFile(
|
|
3688
4799
|
cacertWithCRL,
|
|
3689
|
-
|
|
4800
|
+
fs12.readFileSync(this.caCertificate, "utf8") + fs12.readFileSync(this.revocationList, "utf8")
|
|
3690
4801
|
);
|
|
3691
4802
|
} else {
|
|
3692
|
-
await
|
|
4803
|
+
await fs12.promises.writeFile(cacertWithCRL, fs12.readFileSync(this.caCertificate));
|
|
3693
4804
|
}
|
|
3694
4805
|
}
|
|
3695
4806
|
/**
|
|
@@ -3699,15 +4810,15 @@ var init_certificate_authority = __esm({
|
|
|
3699
4810
|
* @param certificate - path to the certificate file to extend
|
|
3700
4811
|
*/
|
|
3701
4812
|
async constructCertificateChain(certificate) {
|
|
3702
|
-
|
|
3703
|
-
|
|
4813
|
+
assert9(fs12.existsSync(certificate));
|
|
4814
|
+
assert9(fs12.existsSync(this.caCertificate));
|
|
3704
4815
|
debugLog(chalk6.yellow(" certificate file :"), chalk6.cyan(certificate));
|
|
3705
|
-
let chain = await
|
|
3706
|
-
chain += await
|
|
3707
|
-
if (
|
|
3708
|
-
chain += await
|
|
4816
|
+
let chain = await fs12.promises.readFile(certificate, "utf8");
|
|
4817
|
+
chain += await fs12.promises.readFile(this.caCertificate, "utf8");
|
|
4818
|
+
if (fs12.existsSync(this.issuerCertificateChain)) {
|
|
4819
|
+
chain += await fs12.promises.readFile(this.issuerCertificateChain, "utf8");
|
|
3709
4820
|
}
|
|
3710
|
-
await
|
|
4821
|
+
await fs12.promises.writeFile(certificate, chain);
|
|
3711
4822
|
}
|
|
3712
4823
|
/**
|
|
3713
4824
|
* Create a self-signed certificate using OpenSSL.
|
|
@@ -3717,39 +4828,30 @@ var init_certificate_authority = __esm({
|
|
|
3717
4828
|
* @param params - certificate parameters (subject, validity, SANs)
|
|
3718
4829
|
*/
|
|
3719
4830
|
async createSelfSignedCertificate(certificateFile, privateKey, params) {
|
|
3720
|
-
|
|
3721
|
-
|
|
4831
|
+
assert9(typeof privateKey === "string");
|
|
4832
|
+
assert9(fs12.existsSync(privateKey));
|
|
3722
4833
|
if (!certificateFileExist(certificateFile)) {
|
|
3723
4834
|
return;
|
|
3724
4835
|
}
|
|
3725
4836
|
adjustDate(params);
|
|
3726
4837
|
adjustApplicationUri(params);
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
await execute_openssl(
|
|
3745
|
-
"ca -selfsign -keyfile " + q4(n5(privateKey)) + " -startdate " + x509Date(params.startDate) + " -enddate " + x509Date(params.endDate) + " -batch -out " + q4(n5(certificateFile)) + " -in " + q4(n5(csrFile)),
|
|
3746
|
-
options
|
|
3747
|
-
);
|
|
3748
|
-
displaySubtitle("- dump the certificate for a check");
|
|
3749
|
-
await execute_openssl(`x509 -in ${q4(n5(certificateFile))} -dates -fingerprint -purpose -noout`, {});
|
|
3750
|
-
displaySubtitle("- verify self-signed certificate");
|
|
3751
|
-
await execute_openssl_no_failure(`verify -verbose -CAfile ${q4(n5(certificateFile))} ${q4(n5(certificateFile))}`, options);
|
|
3752
|
-
await fs10.promises.unlink(csrFile);
|
|
4838
|
+
params.dns = params.dns || [];
|
|
4839
|
+
params.ip = params.ip || [];
|
|
4840
|
+
await this.#withCaLock(async () => {
|
|
4841
|
+
await this.#backend.createSelfSignedCertificate(this, certificateFile, privateKey, params);
|
|
4842
|
+
});
|
|
4843
|
+
}
|
|
4844
|
+
/**
|
|
4845
|
+
* Regenerate `crl/revocation_list.{crl,der}` from the current database
|
|
4846
|
+
* state, without changing any certificate's status. Normally
|
|
4847
|
+
* unnecessary — `revokeCertificate` already regenerates the CRL as
|
|
4848
|
+
* part of revoking — but useful to force a refresh (e.g. after
|
|
4849
|
+
* switching a CA's `backend` between `"openssl"` and `"native"`).
|
|
4850
|
+
*/
|
|
4851
|
+
async regenerateCrl() {
|
|
4852
|
+
await this.#withCaLock(async () => {
|
|
4853
|
+
await this.#backend.regenerateCrl(this);
|
|
4854
|
+
});
|
|
3753
4855
|
}
|
|
3754
4856
|
/**
|
|
3755
4857
|
* Revoke a certificate and regenerate the CRL.
|
|
@@ -3770,30 +4872,12 @@ var init_certificate_authority = __esm({
|
|
|
3770
4872
|
"certificateHold",
|
|
3771
4873
|
"removeFromCRL"
|
|
3772
4874
|
];
|
|
3773
|
-
const configFile = generateStaticConfig("conf/caconfig.cnf", { cwd: this.rootDir });
|
|
3774
|
-
const options = {
|
|
3775
|
-
cwd: this.rootDir,
|
|
3776
|
-
openssl_conf: makePath(configFile)
|
|
3777
|
-
};
|
|
3778
|
-
setEnv("ALTNAME", "");
|
|
3779
|
-
const randomFile = path6.join(this.rootDir, "random.rnd");
|
|
3780
|
-
setEnv("RANDFILE", randomFile);
|
|
3781
|
-
const configOption = ` -config ${q4(n5(configFile))}`;
|
|
3782
4875
|
const reason = params.reason || "keyCompromise";
|
|
3783
|
-
|
|
4876
|
+
assert9(crlReasons.indexOf(reason) >= 0);
|
|
3784
4877
|
displayTitle(`Revoking certificate ${certificate}`);
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
displaySubtitle("Verify that certificate is revoked");
|
|
3789
|
-
await execute_openssl_no_failure(
|
|
3790
|
-
"verify -verbose -CRLfile " + q4(n5(this.revocationList)) + " -CAfile " + q4(n5(this.caCertificate)) + " -crl_check " + q4(n5(certificate)),
|
|
3791
|
-
options
|
|
3792
|
-
);
|
|
3793
|
-
displaySubtitle("Produce CRL in DER form ");
|
|
3794
|
-
await execute_openssl(`crl -in ${q4(n5(this.revocationList))} -out crl/revocation_list.der -outform der`, options);
|
|
3795
|
-
displaySubtitle("Produce CRL in PEM form ");
|
|
3796
|
-
await execute_openssl(`crl -in ${q4(n5(this.revocationList))} -out crl/revocation_list.pem -outform pem -text `, options);
|
|
4878
|
+
await this.#withCaLock(async () => {
|
|
4879
|
+
await this.#backend.revoke(this, certificate, reason);
|
|
4880
|
+
});
|
|
3797
4881
|
}
|
|
3798
4882
|
/**
|
|
3799
4883
|
* Sign a Certificate Signing Request (CSR) with this CA.
|
|
@@ -3808,15 +4892,13 @@ var init_certificate_authority = __esm({
|
|
|
3808
4892
|
* @returns the path to the signed certificate
|
|
3809
4893
|
*/
|
|
3810
4894
|
async signCertificateRequest(certificate, certificateSigningRequestFilename, params1) {
|
|
3811
|
-
await
|
|
3812
|
-
|
|
4895
|
+
await this.#backend.preflight();
|
|
4896
|
+
assert9(fs12.existsSync(certificateSigningRequestFilename));
|
|
3813
4897
|
if (!certificateFileExist(certificate)) {
|
|
3814
4898
|
return "";
|
|
3815
4899
|
}
|
|
3816
4900
|
adjustDate(params1);
|
|
3817
4901
|
adjustApplicationUri(params1);
|
|
3818
|
-
processAltNames(params1);
|
|
3819
|
-
const options = { cwd: this.rootDir };
|
|
3820
4902
|
const csr = await readCertificateSigningRequest(certificateSigningRequestFilename);
|
|
3821
4903
|
const csrInfo = exploreCertificateSigningRequest(csr);
|
|
3822
4904
|
const applicationUri = csrInfo.extensionRequest.subjectAltName.uniformResourceIdentifier ? csrInfo.extensionRequest.subjectAltName.uniformResourceIdentifier[0] : void 0;
|
|
@@ -3826,61 +4908,122 @@ var init_certificate_authority = __esm({
|
|
|
3826
4908
|
const dns2 = csrInfo.extensionRequest.subjectAltName.dNSName || [];
|
|
3827
4909
|
let ip = csrInfo.extensionRequest.subjectAltName.iPAddress || [];
|
|
3828
4910
|
ip = ip.map(octetStringToIpAddress);
|
|
3829
|
-
const
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
"ca " + configOption + " -startdate " + x509Date(params1.startDate) + " -enddate " + x509Date(params1.endDate) + " -batch -out " + q4(n5(certificate)) + " -in " + q4(n5(certificateSigningRequestFilename)),
|
|
3841
|
-
options
|
|
3842
|
-
);
|
|
3843
|
-
displaySubtitle("- dump the certificate for a check");
|
|
3844
|
-
await execute_openssl(`x509 -in ${q4(n5(certificate))} -dates -fingerprint -purpose -noout`, options);
|
|
3845
|
-
displaySubtitle("- construct CA certificate with CRL");
|
|
3846
|
-
await this.constructCACertificateWithCRL();
|
|
3847
|
-
displaySubtitle("- construct certificate chain");
|
|
3848
|
-
await this.constructCertificateChain(certificate);
|
|
3849
|
-
displaySubtitle("- verify certificate against the root CA");
|
|
3850
|
-
await this.verifyCertificate(certificate);
|
|
3851
|
-
return certificate;
|
|
4911
|
+
const sanOverride = { applicationUri, dns: dns2, ip };
|
|
4912
|
+
return this.#withCaLock(async () => {
|
|
4913
|
+
await this.#backend.signEndEntityCsr(this, certificate, certificateSigningRequestFilename, params1, sanOverride);
|
|
4914
|
+
displaySubtitle("- construct CA certificate with CRL");
|
|
4915
|
+
await this.constructCACertificateWithCRL();
|
|
4916
|
+
displaySubtitle("- construct certificate chain");
|
|
4917
|
+
await this.constructCertificateChain(certificate);
|
|
4918
|
+
displaySubtitle("- verify certificate against the root CA");
|
|
4919
|
+
await this.verifyCertificate(certificate);
|
|
4920
|
+
return certificate;
|
|
4921
|
+
});
|
|
3852
4922
|
}
|
|
3853
4923
|
/**
|
|
3854
|
-
*
|
|
4924
|
+
* Check that `certificate` really was signed by this CA, and throw if it
|
|
4925
|
+
* was not.
|
|
3855
4926
|
*
|
|
3856
|
-
*
|
|
4927
|
+
* This used to do nothing: `openssl verify` crashes on Windows, so the
|
|
4928
|
+
* check was left as a placeholder. It no longer needs a subprocess -
|
|
4929
|
+
* `verifyCertificateSignature` does it in pure JS, the same way
|
|
4930
|
+
* {@link CertificateManager} already validates a chain - so the check
|
|
4931
|
+
* that `signCertificateRequest` always claimed to perform now actually
|
|
4932
|
+
* happens on every issuance, whichever backend did the signing.
|
|
4933
|
+
*
|
|
4934
|
+
* Only the leading certificate is examined: the file may be a chain,
|
|
4935
|
+
* and the rest of it is this CA's own certificate and its issuers.
|
|
4936
|
+
*
|
|
4937
|
+
* @param certificate - path to the certificate (or chain) to verify
|
|
3857
4938
|
*/
|
|
3858
4939
|
async verifyCertificate(certificate) {
|
|
3859
|
-
const
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
`
|
|
3868
|
-
|
|
4940
|
+
const pem = await fs12.promises.readFile(certificate, "utf-8");
|
|
4941
|
+
const blocks = pem.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g);
|
|
4942
|
+
if (!blocks || blocks.length === 0) {
|
|
4943
|
+
throw new Error(`verifyCertificate: ${certificate} contains no PEM-encoded certificate`);
|
|
4944
|
+
}
|
|
4945
|
+
const caCertificateDer = convertPEMtoDER2(readCertificatePEM2(this.caCertificate));
|
|
4946
|
+
if (!verifyCertificateSignature2(convertPEMtoDER2(blocks[0]), caCertificateDer)) {
|
|
4947
|
+
throw new Error(
|
|
4948
|
+
`verifyCertificate: ${certificate} was not signed by this certificate authority (${this.caCertificate})`
|
|
4949
|
+
);
|
|
4950
|
+
}
|
|
4951
|
+
}
|
|
4952
|
+
};
|
|
4953
|
+
}
|
|
4954
|
+
});
|
|
4955
|
+
|
|
4956
|
+
// packages/node-opcua-pki/lib/ca/certificate_authority.ts
|
|
4957
|
+
var CertificateAuthority;
|
|
4958
|
+
var init_certificate_authority = __esm({
|
|
4959
|
+
"packages/node-opcua-pki/lib/ca/certificate_authority.ts"() {
|
|
4960
|
+
"use strict";
|
|
4961
|
+
init_esm_shims();
|
|
4962
|
+
init_with_openssl();
|
|
4963
|
+
init_native_ca_backend();
|
|
4964
|
+
init_openssl_ca_backend();
|
|
4965
|
+
init_certificate_authority_core();
|
|
4966
|
+
init_certificate_authority_core();
|
|
4967
|
+
CertificateAuthority = class extends CertificateAuthorityCore {
|
|
4968
|
+
constructor(options) {
|
|
4969
|
+
if (options.signer && options.backend === "openssl") {
|
|
4970
|
+
throw new Error(
|
|
4971
|
+
"CertificateAuthority: backend 'openssl' cannot be combined with 'signer' - the openssl CLI loads its key from a file and cannot call an external signer. Drop the backend option (a signer implies 'native') or drop the signer."
|
|
3869
4972
|
);
|
|
3870
4973
|
}
|
|
4974
|
+
const backend = options.signer || options.backend === "native" ? new NativeCaBackend() : new OpenSslCaBackend();
|
|
4975
|
+
super({ ...options, backend });
|
|
4976
|
+
}
|
|
4977
|
+
/**
|
|
4978
|
+
* @internal `-passin env:` argv + env for an openssl call that loads
|
|
4979
|
+
* this CA's key (always emitted, empty when none).
|
|
4980
|
+
*
|
|
4981
|
+
* The openssl backend builds its own now; this remains so that external
|
|
4982
|
+
* code calling it keeps working, and lives here rather than on the core
|
|
4983
|
+
* because the flag means nothing to a backend that spawns nothing.
|
|
4984
|
+
*/
|
|
4985
|
+
async _opensslPassin() {
|
|
4986
|
+
return passinArg(await this._privateKeyPassphrase());
|
|
4987
|
+
}
|
|
4988
|
+
/**
|
|
4989
|
+
* @internal
|
|
4990
|
+
* Legacy shim: publish the `CDP_URL` / `AIA_VALUE` config substitution
|
|
4991
|
+
* values to the shared env registry, or unset them so the matching
|
|
4992
|
+
* `{{#KEY}}...{{/KEY}}` blocks are stripped. Nothing in this package
|
|
4993
|
+
* reads the registry any more - every openssl config render receives
|
|
4994
|
+
* these values explicitly, from the same {@link caConfigEnvOverrides}
|
|
4995
|
+
* builder this delegates to, so the two cannot drift - kept only for
|
|
4996
|
+
* external code that renders openssl config templates against the
|
|
4997
|
+
* registry directly.
|
|
4998
|
+
*
|
|
4999
|
+
* It lives on this class rather than the core because it is meaningful
|
|
5000
|
+
* only to the openssl backend.
|
|
5001
|
+
*/
|
|
5002
|
+
_wireRevocationEnvVars() {
|
|
5003
|
+
const overrides = caConfigEnvOverrides(this);
|
|
5004
|
+
if (overrides.CDP_URL) {
|
|
5005
|
+
setEnv("CDP_URL", overrides.CDP_URL);
|
|
5006
|
+
} else {
|
|
5007
|
+
unsetEnv("CDP_URL");
|
|
5008
|
+
}
|
|
5009
|
+
if (overrides.AIA_VALUE) {
|
|
5010
|
+
setEnv("AIA_VALUE", overrides.AIA_VALUE);
|
|
5011
|
+
} else {
|
|
5012
|
+
unsetEnv("AIA_VALUE");
|
|
5013
|
+
}
|
|
3871
5014
|
}
|
|
3872
5015
|
};
|
|
3873
5016
|
}
|
|
3874
5017
|
});
|
|
3875
5018
|
|
|
3876
5019
|
// packages/node-opcua-pki/lib/ca/crypto_create_CA.ts
|
|
3877
|
-
import
|
|
3878
|
-
import
|
|
5020
|
+
import assert10 from "assert";
|
|
5021
|
+
import fs13 from "fs";
|
|
3879
5022
|
import { createRequire } from "module";
|
|
3880
5023
|
import os5 from "os";
|
|
3881
|
-
import
|
|
5024
|
+
import path10 from "path";
|
|
3882
5025
|
import chalk7 from "chalk";
|
|
3883
|
-
import { CertificatePurpose as
|
|
5026
|
+
import { CertificatePurpose as CertificatePurpose4, generatePrivateKeyFile as generatePrivateKeyFile3, Subject as Subject7 } from "node-opcua-crypto";
|
|
3884
5027
|
import commandLineArgs from "command-line-args";
|
|
3885
5028
|
import commandLineUsage from "command-line-usage";
|
|
3886
5029
|
function get_offset_date(date, nbDays) {
|
|
@@ -3888,9 +5031,9 @@ function get_offset_date(date, nbDays) {
|
|
|
3888
5031
|
d.setDate(d.getDate() + nbDays);
|
|
3889
5032
|
return d;
|
|
3890
5033
|
}
|
|
3891
|
-
async function
|
|
3892
|
-
|
|
3893
|
-
|
|
5034
|
+
async function construct_CertificateAuthority(subject) {
|
|
5035
|
+
assert10(typeof gLocalConfig.CAFolder === "string", "expecting a CAFolder in config");
|
|
5036
|
+
assert10(typeof gLocalConfig.keySize === "number", "expecting a keySize in config");
|
|
3894
5037
|
if (!g_certificateAuthority) {
|
|
3895
5038
|
g_certificateAuthority = new CertificateAuthority({
|
|
3896
5039
|
keySize: gLocalConfig.keySize,
|
|
@@ -3901,7 +5044,7 @@ async function construct_CertificateAuthority2(subject) {
|
|
|
3901
5044
|
}
|
|
3902
5045
|
}
|
|
3903
5046
|
async function construct_CertificateManager() {
|
|
3904
|
-
|
|
5047
|
+
assert10(typeof gLocalConfig.PKIFolder === "string", "expecting a PKIFolder in config");
|
|
3905
5048
|
if (!certificateManager) {
|
|
3906
5049
|
certificateManager = new CertificateManager({
|
|
3907
5050
|
keySize: gLocalConfig.keySize,
|
|
@@ -3912,35 +5055,35 @@ async function construct_CertificateManager() {
|
|
|
3912
5055
|
}
|
|
3913
5056
|
function default_template_content() {
|
|
3914
5057
|
if (process.pkg?.entrypoint) {
|
|
3915
|
-
const a =
|
|
5058
|
+
const a = fs13.readFileSync(path10.join(__dirname, "../../bin/pki_config.example.js"), "utf8");
|
|
3916
5059
|
return a;
|
|
3917
5060
|
}
|
|
3918
5061
|
function find_default_config_template() {
|
|
3919
5062
|
const rootFolder = find_module_root_folder();
|
|
3920
5063
|
const configName = "pki_config.example.js";
|
|
3921
|
-
let default_config_template2 =
|
|
3922
|
-
if (!
|
|
3923
|
-
default_config_template2 =
|
|
3924
|
-
if (!
|
|
3925
|
-
default_config_template2 =
|
|
5064
|
+
let default_config_template2 = path10.join(rootFolder, "bin", configName);
|
|
5065
|
+
if (!fs13.existsSync(default_config_template2)) {
|
|
5066
|
+
default_config_template2 = path10.join(__dirname, "..", configName);
|
|
5067
|
+
if (!fs13.existsSync(default_config_template2)) {
|
|
5068
|
+
default_config_template2 = path10.join(__dirname, `../bin/${configName}`);
|
|
3926
5069
|
}
|
|
3927
5070
|
}
|
|
3928
5071
|
return default_config_template2;
|
|
3929
5072
|
}
|
|
3930
5073
|
const default_config_template = find_default_config_template();
|
|
3931
|
-
|
|
3932
|
-
const default_config_template_content =
|
|
5074
|
+
assert10(fs13.existsSync(default_config_template));
|
|
5075
|
+
const default_config_template_content = fs13.readFileSync(default_config_template, "utf8");
|
|
3933
5076
|
return default_config_template_content;
|
|
3934
5077
|
}
|
|
3935
5078
|
function find_module_root_folder() {
|
|
3936
|
-
let rootFolder =
|
|
5079
|
+
let rootFolder = path10.join(__dirname);
|
|
3937
5080
|
for (let i = 0; i < 4; i++) {
|
|
3938
|
-
if (
|
|
5081
|
+
if (fs13.existsSync(path10.join(rootFolder, "package.json"))) {
|
|
3939
5082
|
return rootFolder;
|
|
3940
5083
|
}
|
|
3941
|
-
rootFolder =
|
|
5084
|
+
rootFolder = path10.join(rootFolder, "..");
|
|
3942
5085
|
}
|
|
3943
|
-
|
|
5086
|
+
assert10(fs13.existsSync(path10.join(rootFolder, "package.json")), "root folder must have a package.json file");
|
|
3944
5087
|
return rootFolder;
|
|
3945
5088
|
}
|
|
3946
5089
|
async function readConfiguration(argv) {
|
|
@@ -3965,41 +5108,41 @@ async function readConfiguration(argv) {
|
|
|
3965
5108
|
return str;
|
|
3966
5109
|
}
|
|
3967
5110
|
function prepare(file) {
|
|
3968
|
-
const tmp =
|
|
5111
|
+
const tmp = path10.resolve(performSubstitution(file));
|
|
3969
5112
|
return makePath(tmp);
|
|
3970
5113
|
}
|
|
3971
5114
|
certificateDir = argv.root;
|
|
3972
|
-
|
|
5115
|
+
assert10(typeof certificateDir === "string");
|
|
3973
5116
|
certificateDir = prepare(certificateDir);
|
|
3974
5117
|
mkdirRecursiveSync(certificateDir);
|
|
3975
|
-
|
|
3976
|
-
const default_config =
|
|
3977
|
-
if (!
|
|
5118
|
+
assert10(fs13.existsSync(certificateDir));
|
|
5119
|
+
const default_config = path10.join(certificateDir, "config.js");
|
|
5120
|
+
if (!fs13.existsSync(default_config)) {
|
|
3978
5121
|
debugLog(chalk7.yellow(" Creating default g_config file "), chalk7.cyan(default_config));
|
|
3979
5122
|
const default_config_template_content = default_template_content();
|
|
3980
|
-
|
|
5123
|
+
fs13.writeFileSync(default_config, default_config_template_content);
|
|
3981
5124
|
} else {
|
|
3982
5125
|
debugLog(chalk7.yellow(" using g_config file "), chalk7.cyan(default_config));
|
|
3983
5126
|
}
|
|
3984
|
-
if (!
|
|
5127
|
+
if (!fs13.existsSync(default_config)) {
|
|
3985
5128
|
debugLog(chalk7.redBright(" cannot find config file ", default_config));
|
|
3986
5129
|
}
|
|
3987
|
-
const defaultRandomFile =
|
|
5130
|
+
const defaultRandomFile = path10.join(path10.dirname(default_config), "random.rnd");
|
|
3988
5131
|
setEnv("RANDFILE", defaultRandomFile);
|
|
3989
5132
|
const _require = createRequire(__filename);
|
|
3990
5133
|
gLocalConfig = _require(default_config);
|
|
3991
|
-
gLocalConfig.subject = new
|
|
5134
|
+
gLocalConfig.subject = new Subject7(gLocalConfig.subject || "");
|
|
3992
5135
|
if (argv.subject) {
|
|
3993
|
-
gLocalConfig.subject = new
|
|
5136
|
+
gLocalConfig.subject = new Subject7(argv.subject);
|
|
3994
5137
|
}
|
|
3995
5138
|
if (!gLocalConfig.subject.commonName) {
|
|
3996
5139
|
throw new Error("subject must have a Common Name");
|
|
3997
5140
|
}
|
|
3998
5141
|
gLocalConfig.certificateDir = certificateDir;
|
|
3999
|
-
let CAFolder = argv.CAFolder ||
|
|
5142
|
+
let CAFolder = argv.CAFolder || path10.join(certificateDir, "CA");
|
|
4000
5143
|
CAFolder = prepare(CAFolder);
|
|
4001
5144
|
gLocalConfig.CAFolder = CAFolder;
|
|
4002
|
-
gLocalConfig.PKIFolder =
|
|
5145
|
+
gLocalConfig.PKIFolder = path10.join(gLocalConfig.certificateDir, "PKI");
|
|
4003
5146
|
if (argv.PKIFolder) {
|
|
4004
5147
|
gLocalConfig.PKIFolder = prepare(argv.PKIFolder);
|
|
4005
5148
|
}
|
|
@@ -4037,7 +5180,7 @@ async function readConfiguration(argv) {
|
|
|
4037
5180
|
}
|
|
4038
5181
|
}
|
|
4039
5182
|
async function createDefaultCertificate(base_name, prefix, key_length, applicationUri, dev) {
|
|
4040
|
-
|
|
5183
|
+
assert10(key_length === 1024 || key_length === 2048 || key_length === 3072 || key_length === 4096);
|
|
4041
5184
|
const private_key_file = makePath(base_name, `${prefix}key_${key_length}.pem`);
|
|
4042
5185
|
const public_key_file = makePath(base_name, `${prefix}public_key_${key_length}.pub`);
|
|
4043
5186
|
const certificate_file = makePath(base_name, `${prefix}cert_${key_length}.pem`);
|
|
@@ -4057,7 +5200,7 @@ async function createDefaultCertificate(base_name, prefix, key_length, applicati
|
|
|
4057
5200
|
}
|
|
4058
5201
|
const ip = [];
|
|
4059
5202
|
async function createCertificateIfNotExist(certificate, private_key, applicationUri2, startDate, validity) {
|
|
4060
|
-
if (
|
|
5203
|
+
if (fs13.existsSync(certificate)) {
|
|
4061
5204
|
warningLog(chalk7.yellow(" certificate"), chalk7.cyan(certificate), chalk7.yellow(" already exists => skipping"));
|
|
4062
5205
|
return "";
|
|
4063
5206
|
} else {
|
|
@@ -4076,7 +5219,7 @@ async function createDefaultCertificate(base_name, prefix, key_length, applicati
|
|
|
4076
5219
|
configFile,
|
|
4077
5220
|
dns: dns3,
|
|
4078
5221
|
ip: ip2,
|
|
4079
|
-
purpose:
|
|
5222
|
+
purpose: CertificatePurpose4.ForApplication
|
|
4080
5223
|
};
|
|
4081
5224
|
await createCertificateSigningRequestWithOpenSSL(certificateSigningRequestFile, params);
|
|
4082
5225
|
return await g_certificateAuthority.signCertificateRequest(certificate, certificateSigningRequestFile, {
|
|
@@ -4100,7 +5243,7 @@ async function createDefaultCertificate(base_name, prefix, key_length, applicati
|
|
|
4100
5243
|
await g_certificateAuthority.revokeCertificate(certificate, {});
|
|
4101
5244
|
}
|
|
4102
5245
|
async function createPrivateKeyIfNotExist(privateKey, keyLength) {
|
|
4103
|
-
if (
|
|
5246
|
+
if (fs13.existsSync(privateKey)) {
|
|
4104
5247
|
warningLog(chalk7.yellow(" privateKey"), chalk7.cyan(privateKey), chalk7.yellow(" already exists => skipping"));
|
|
4105
5248
|
return;
|
|
4106
5249
|
} else {
|
|
@@ -4114,14 +5257,14 @@ async function createDefaultCertificate(base_name, prefix, key_length, applicati
|
|
|
4114
5257
|
displaySubtitle(` create Certificate ${certificate_file}`);
|
|
4115
5258
|
await createCertificateIfNotExist(certificate_file, private_key_file, applicationUri, yesterday, 365);
|
|
4116
5259
|
displaySubtitle(` create self signed Certificate ${self_signed_certificate_file}`);
|
|
4117
|
-
if (
|
|
5260
|
+
if (fs13.existsSync(self_signed_certificate_file)) {
|
|
4118
5261
|
return;
|
|
4119
5262
|
}
|
|
4120
5263
|
await createSelfSignedCertificate2(self_signed_certificate_file, private_key_file, applicationUri, yesterday, 365);
|
|
4121
5264
|
if (dev) {
|
|
4122
5265
|
await createCertificateIfNotExist(certificate_file_outofdate, private_key_file, applicationUri, two_years_ago, 365);
|
|
4123
5266
|
await createCertificateIfNotExist(certificate_file_not_active_yet, private_key_file, applicationUri, next_year, 365);
|
|
4124
|
-
if (!
|
|
5267
|
+
if (!fs13.existsSync(certificate_revoked)) {
|
|
4125
5268
|
const certificate = await createCertificateIfNotExist(
|
|
4126
5269
|
certificate_revoked,
|
|
4127
5270
|
private_key_file,
|
|
@@ -4131,7 +5274,7 @@ async function createDefaultCertificate(base_name, prefix, key_length, applicati
|
|
|
4131
5274
|
365
|
|
4132
5275
|
);
|
|
4133
5276
|
warningLog(" certificate to revoke => ", certificate);
|
|
4134
|
-
revoke_certificate(certificate_revoked);
|
|
5277
|
+
await revoke_certificate(certificate_revoked);
|
|
4135
5278
|
}
|
|
4136
5279
|
}
|
|
4137
5280
|
}
|
|
@@ -4143,13 +5286,13 @@ async function wrap(func) {
|
|
|
4143
5286
|
}
|
|
4144
5287
|
}
|
|
4145
5288
|
async function create_default_certificates(dev) {
|
|
4146
|
-
|
|
5289
|
+
assert10(gLocalConfig);
|
|
4147
5290
|
const base_name = gLocalConfig.certificateDir || "";
|
|
4148
|
-
|
|
5291
|
+
assert10(fs13.existsSync(base_name));
|
|
4149
5292
|
let clientURN;
|
|
4150
5293
|
let serverURN;
|
|
4151
5294
|
let discoveryServerURN;
|
|
4152
|
-
wrap(async () => {
|
|
5295
|
+
await wrap(async () => {
|
|
4153
5296
|
await extractFullyQualifiedDomainName();
|
|
4154
5297
|
const hostname = os5.hostname();
|
|
4155
5298
|
const fqdn2 = getFullyQualifiedDomainName();
|
|
@@ -4176,7 +5319,7 @@ async function create_default_certificates(dev) {
|
|
|
4176
5319
|
});
|
|
4177
5320
|
}
|
|
4178
5321
|
async function createDefaultCertificates(dev) {
|
|
4179
|
-
await
|
|
5322
|
+
await construct_CertificateAuthority("");
|
|
4180
5323
|
await construct_CertificateManager();
|
|
4181
5324
|
await create_default_certificates(dev);
|
|
4182
5325
|
}
|
|
@@ -4242,7 +5385,7 @@ ${epilog}`
|
|
|
4242
5385
|
}
|
|
4243
5386
|
if (command === "version") {
|
|
4244
5387
|
const rootFolder = find_module_root_folder();
|
|
4245
|
-
const pkg = JSON.parse(
|
|
5388
|
+
const pkg = JSON.parse(fs13.readFileSync(path10.join(rootFolder, "package.json"), "utf-8"));
|
|
4246
5389
|
console.log(pkg.version);
|
|
4247
5390
|
return;
|
|
4248
5391
|
}
|
|
@@ -4267,12 +5410,12 @@ ${epilog}`
|
|
|
4267
5410
|
await readConfiguration(local_argv);
|
|
4268
5411
|
if (local_argv.clean) {
|
|
4269
5412
|
displayTitle("Cleaning old certificates");
|
|
4270
|
-
|
|
5413
|
+
assert10(gLocalConfig);
|
|
4271
5414
|
const certificateDir = gLocalConfig.certificateDir || "";
|
|
4272
|
-
const files = await
|
|
5415
|
+
const files = await fs13.promises.readdir(certificateDir);
|
|
4273
5416
|
for (const file of files) {
|
|
4274
5417
|
if (file.includes(".pem") || file.includes(".pub")) {
|
|
4275
|
-
await
|
|
5418
|
+
await fs13.promises.unlink(path10.join(certificateDir, file));
|
|
4276
5419
|
}
|
|
4277
5420
|
}
|
|
4278
5421
|
mkdirRecursiveSync(certificateDir);
|
|
@@ -4293,7 +5436,7 @@ ${epilog}`
|
|
|
4293
5436
|
await wrap(async () => {
|
|
4294
5437
|
await ensure_openssl_installed();
|
|
4295
5438
|
await readConfiguration(local_argv);
|
|
4296
|
-
await
|
|
5439
|
+
await construct_CertificateAuthority(local_argv.subject);
|
|
4297
5440
|
});
|
|
4298
5441
|
return;
|
|
4299
5442
|
}
|
|
@@ -4362,7 +5505,7 @@ ${epilog}`
|
|
|
4362
5505
|
await readConfiguration(local_argv2);
|
|
4363
5506
|
await construct_CertificateManager();
|
|
4364
5507
|
displaySubtitle(` create self signed Certificate ${gLocalConfig.outputFile}`);
|
|
4365
|
-
let subject = local_argv2.subject && local_argv2.subject.length > 1 ? new
|
|
5508
|
+
let subject = local_argv2.subject && local_argv2.subject.length > 1 ? new Subject7(local_argv2.subject) : gLocalConfig.subject || "";
|
|
4366
5509
|
subject = JSON.parse(JSON.stringify(subject));
|
|
4367
5510
|
const params = {
|
|
4368
5511
|
applicationUri: gLocalConfig.applicationUri || "",
|
|
@@ -4378,8 +5521,8 @@ ${epilog}`
|
|
|
4378
5521
|
async function command_full_certificate(local_argv2) {
|
|
4379
5522
|
await readConfiguration(local_argv2);
|
|
4380
5523
|
await construct_CertificateManager();
|
|
4381
|
-
await
|
|
4382
|
-
|
|
5524
|
+
await construct_CertificateAuthority("");
|
|
5525
|
+
assert10(fs13.existsSync(gLocalConfig.CAFolder || ""), " CA folder must exist");
|
|
4383
5526
|
gLocalConfig.privateKey = void 0;
|
|
4384
5527
|
gLocalConfig.subject = local_argv2.subject && local_argv2.subject.length > 1 ? local_argv2.subject : gLocalConfig.subject;
|
|
4385
5528
|
const csr_file = await certificateManager.createCertificateRequest(
|
|
@@ -4390,7 +5533,7 @@ ${epilog}`
|
|
|
4390
5533
|
}
|
|
4391
5534
|
warningLog(" csr_file = ", csr_file);
|
|
4392
5535
|
const certificate = csr_file.replace(".csr", ".pem");
|
|
4393
|
-
if (
|
|
5536
|
+
if (fs13.existsSync(certificate)) {
|
|
4394
5537
|
throw new Error(` File ${certificate} already exist`);
|
|
4395
5538
|
}
|
|
4396
5539
|
await g_certificateAuthority.signCertificateRequest(
|
|
@@ -4398,8 +5541,8 @@ ${epilog}`
|
|
|
4398
5541
|
csr_file,
|
|
4399
5542
|
gLocalConfig
|
|
4400
5543
|
);
|
|
4401
|
-
|
|
4402
|
-
|
|
5544
|
+
assert10(typeof gLocalConfig.outputFile === "string");
|
|
5545
|
+
fs13.writeFileSync(gLocalConfig.outputFile || "", fs13.readFileSync(certificate, "ascii"));
|
|
4403
5546
|
}
|
|
4404
5547
|
await wrap(async () => await command_certificate(local_argv));
|
|
4405
5548
|
return;
|
|
@@ -4418,13 +5561,13 @@ ${epilog}`
|
|
|
4418
5561
|
await g_certificateAuthority.revokeCertificate(certificate, {});
|
|
4419
5562
|
}
|
|
4420
5563
|
await wrap(async () => {
|
|
4421
|
-
const certificate =
|
|
5564
|
+
const certificate = path10.resolve(local_argv.certificateFile);
|
|
4422
5565
|
warningLog(chalk7.yellow(" Certificate to revoke : "), chalk7.cyan(certificate));
|
|
4423
|
-
if (!
|
|
5566
|
+
if (!fs13.existsSync(certificate)) {
|
|
4424
5567
|
throw new Error(`cannot find certificate to revoke ${certificate}`);
|
|
4425
5568
|
}
|
|
4426
5569
|
await readConfiguration(local_argv);
|
|
4427
|
-
await
|
|
5570
|
+
await construct_CertificateAuthority("");
|
|
4428
5571
|
await revoke_certificate(certificate);
|
|
4429
5572
|
warningLog("done ... ");
|
|
4430
5573
|
warningLog(" crl = ", g_certificateAuthority.revocationList);
|
|
@@ -4467,11 +5610,11 @@ ${epilog}`
|
|
|
4467
5610
|
if (local_argv.help) return showHelp("csr", "create a certificate signing request", optionsDef);
|
|
4468
5611
|
await wrap(async () => {
|
|
4469
5612
|
await readConfiguration(local_argv);
|
|
4470
|
-
if (!
|
|
5613
|
+
if (!fs13.existsSync(gLocalConfig.PKIFolder || "")) {
|
|
4471
5614
|
warningLog("PKI folder must exist");
|
|
4472
5615
|
}
|
|
4473
5616
|
await construct_CertificateManager();
|
|
4474
|
-
if (!gLocalConfig.outputFile ||
|
|
5617
|
+
if (!gLocalConfig.outputFile || fs13.existsSync(gLocalConfig.outputFile)) {
|
|
4475
5618
|
throw new Error(` File ${gLocalConfig.outputFile} already exist`);
|
|
4476
5619
|
}
|
|
4477
5620
|
gLocalConfig.privateKey = void 0;
|
|
@@ -4486,8 +5629,8 @@ ${epilog}`
|
|
|
4486
5629
|
warningLog("please specify a output file");
|
|
4487
5630
|
return;
|
|
4488
5631
|
}
|
|
4489
|
-
const csr = await
|
|
4490
|
-
|
|
5632
|
+
const csr = await fs13.promises.readFile(internal_csr_file, "utf-8");
|
|
5633
|
+
fs13.writeFileSync(gLocalConfig.outputFile || "", csr, "utf-8");
|
|
4491
5634
|
warningLog("Subject = ", gLocalConfig.subject);
|
|
4492
5635
|
warningLog("applicationUri = ", gLocalConfig.applicationUri);
|
|
4493
5636
|
warningLog("altNames = ", gLocalConfig.altNames);
|
|
@@ -4515,16 +5658,16 @@ ${epilog}`
|
|
|
4515
5658
|
return showHelp("sign", "validate a certificate signing request and generate a certificate", optionsDef);
|
|
4516
5659
|
await wrap(async () => {
|
|
4517
5660
|
await readConfiguration(local_argv);
|
|
4518
|
-
if (!
|
|
5661
|
+
if (!fs13.existsSync(gLocalConfig.CAFolder || "")) {
|
|
4519
5662
|
throw new Error(`CA folder must exist:${gLocalConfig.CAFolder}`);
|
|
4520
5663
|
}
|
|
4521
|
-
await
|
|
4522
|
-
const csr_file =
|
|
4523
|
-
if (!
|
|
5664
|
+
await construct_CertificateAuthority("");
|
|
5665
|
+
const csr_file = path10.resolve(local_argv.csr || "");
|
|
5666
|
+
if (!fs13.existsSync(csr_file)) {
|
|
4524
5667
|
throw new Error(`Certificate signing request doesn't exist: ${csr_file}`);
|
|
4525
5668
|
}
|
|
4526
|
-
const certificate =
|
|
4527
|
-
if (
|
|
5669
|
+
const certificate = path10.resolve(local_argv.output || csr_file.replace(".csr", ".pem"));
|
|
5670
|
+
if (fs13.existsSync(certificate)) {
|
|
4528
5671
|
throw new Error(` File ${certificate} already exist`);
|
|
4529
5672
|
}
|
|
4530
5673
|
await g_certificateAuthority.signCertificateRequest(
|
|
@@ -4532,8 +5675,8 @@ ${epilog}`
|
|
|
4532
5675
|
csr_file,
|
|
4533
5676
|
gLocalConfig
|
|
4534
5677
|
);
|
|
4535
|
-
|
|
4536
|
-
|
|
5678
|
+
assert10(typeof gLocalConfig.outputFile === "string");
|
|
5679
|
+
fs13.writeFileSync(gLocalConfig.outputFile || "", fs13.readFileSync(certificate, "ascii"));
|
|
4537
5680
|
});
|
|
4538
5681
|
return;
|
|
4539
5682
|
}
|