node-opcua-pki 6.20.0 → 6.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/pki.mjs +1627 -821
- package/dist/bin/pki.mjs.map +1 -1
- package/dist/chunk-EQ4DGI4M.mjs +1917 -0
- package/dist/chunk-EQ4DGI4M.mjs.map +1 -0
- package/dist/core-BwqSJPHc.d.mts +1041 -0
- package/dist/core-BwqSJPHc.d.ts +1041 -0
- package/dist/core.d.mts +2 -0
- package/dist/core.d.ts +2 -0
- package/dist/core.js +1902 -0
- package/dist/core.js.map +1 -0
- package/dist/core.mjs +21 -0
- package/dist/core.mjs.map +1 -0
- package/dist/index.d.mts +136 -833
- package/dist/index.d.ts +136 -833
- package/dist/index.js +1890 -1086
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +890 -1922
- package/dist/index.mjs.map +1 -1
- package/package.json +14 -3
package/dist/bin/pki.mjs
CHANGED
|
@@ -93,6 +93,9 @@ var init_hostname = __esm({
|
|
|
93
93
|
|
|
94
94
|
// packages/node-opcua-pki/lib/toolbox/common.ts
|
|
95
95
|
import assert2 from "assert";
|
|
96
|
+
function isOpaqueSigner(privateKey) {
|
|
97
|
+
return typeof privateKey !== "string" && typeof privateKey.sign === "function";
|
|
98
|
+
}
|
|
96
99
|
async function resolvePrivateKeyPassphrase(passphrase) {
|
|
97
100
|
if (passphrase === void 0) {
|
|
98
101
|
return void 0;
|
|
@@ -276,8 +279,9 @@ async function createCertificateSigningRequestAsync(certificateSigningRequestFil
|
|
|
276
279
|
assert4(typeof certificateSigningRequestFilename === "string");
|
|
277
280
|
const subject = params.subject ? new Subject(params.subject).toString() : void 0;
|
|
278
281
|
displaySubtitle("- Creating a Certificate Signing Request with subtile");
|
|
279
|
-
const
|
|
280
|
-
|
|
282
|
+
const privateKey = isOpaqueSigner(params.privateKey) ? params.privateKey : await pemToPrivateKey(
|
|
283
|
+
typeof params.privateKey === "string" ? await fs2.promises.readFile(params.privateKey, "utf-8") : coercePrivateKeyPem(params.privateKey)
|
|
284
|
+
);
|
|
281
285
|
const { csr } = await createCertificateSigningRequest({
|
|
282
286
|
privateKey,
|
|
283
287
|
dns: params.dns,
|
|
@@ -287,13 +291,16 @@ async function createCertificateSigningRequestAsync(certificateSigningRequestFil
|
|
|
287
291
|
purpose: params.purpose
|
|
288
292
|
});
|
|
289
293
|
await fs2.promises.writeFile(certificateSigningRequestFilename, csr, "utf-8");
|
|
290
|
-
display(
|
|
294
|
+
display(
|
|
295
|
+
`- privateKey ${typeof params.privateKey === "string" ? params.privateKey : isOpaqueSigner(params.privateKey) ? "<opaque-signer>" : "<in-memory>"}`
|
|
296
|
+
);
|
|
291
297
|
display(`- certificateSigningRequestFilename ${certificateSigningRequestFilename}`);
|
|
292
298
|
}
|
|
293
299
|
var init_create_certificate_signing_request = __esm({
|
|
294
300
|
"packages/node-opcua-pki/lib/toolbox/without_openssl/create_certificate_signing_request.ts"() {
|
|
295
301
|
"use strict";
|
|
296
302
|
init_esm_shims();
|
|
303
|
+
init_common();
|
|
297
304
|
init_display();
|
|
298
305
|
}
|
|
299
306
|
});
|
|
@@ -327,8 +334,9 @@ async function createSelfSignedCertificateAsync(certificate, params) {
|
|
|
327
334
|
subject = subject.toString();
|
|
328
335
|
const purpose = params.purpose;
|
|
329
336
|
displayTitle("Generate a certificate request");
|
|
330
|
-
const
|
|
331
|
-
|
|
337
|
+
const privateKey = isOpaqueSigner(params.privateKey) ? params.privateKey : await pemToPrivateKey2(
|
|
338
|
+
typeof params.privateKey === "string" ? await fs3.promises.readFile(params.privateKey, "utf-8") : coercePrivateKeyPem2(params.privateKey)
|
|
339
|
+
);
|
|
332
340
|
const { cert } = await createSelfSignedCertificate1({
|
|
333
341
|
privateKey,
|
|
334
342
|
notBefore: params.startDate,
|
|
@@ -383,11 +391,14 @@ import { drainPendingLocks, withLock } from "@ster5/global-mutex";
|
|
|
383
391
|
import chalk3 from "chalk";
|
|
384
392
|
import chokidar from "chokidar";
|
|
385
393
|
import {
|
|
394
|
+
caSignerFromKeyOperations,
|
|
386
395
|
exploreCertificate,
|
|
387
396
|
exploreCertificateInfo,
|
|
388
397
|
exploreCertificateRevocationList,
|
|
389
398
|
generatePrivateKeyFile,
|
|
399
|
+
keyOperationsFromPrivateKey,
|
|
390
400
|
makeSHA1Thumbprint,
|
|
401
|
+
PrivateKeyUnavailableError,
|
|
391
402
|
readCertificateChain,
|
|
392
403
|
readCertificateChainAsync,
|
|
393
404
|
readCertificateRevocationList,
|
|
@@ -519,7 +530,31 @@ var init_certificate_manager = __esm({
|
|
|
519
530
|
// even if the consumer forgets to call dispose().
|
|
520
531
|
static #activeInstances = /* @__PURE__ */ new Set();
|
|
521
532
|
static #cleanupInstalled = false;
|
|
522
|
-
static #
|
|
533
|
+
static #exitHandler;
|
|
534
|
+
/**
|
|
535
|
+
* Install a best-effort `exit` hook that closes any watcher
|
|
536
|
+
* still open when the process terminates.
|
|
537
|
+
*
|
|
538
|
+
* **This library never terminates the host process.** No
|
|
539
|
+
* SIGINT/SIGTERM handler is installed: deciding how (and
|
|
540
|
+
* whether) to shut down on a signal is the application's
|
|
541
|
+
* responsibility, and a listener registered here would both
|
|
542
|
+
* pre-empt the application's own graceful shutdown and
|
|
543
|
+
* silently suppress Node's default signal behaviour.
|
|
544
|
+
*
|
|
545
|
+
* Nothing here is load-bearing for process exit. The native
|
|
546
|
+
* `fs.watch` handles are `unref()`'d when the watchers are
|
|
547
|
+
* created (see `#readCertificates`), so an undisposed
|
|
548
|
+
* CertificateManager never keeps the event loop alive. This
|
|
549
|
+
* hook is only tidiness on the way out.
|
|
550
|
+
*
|
|
551
|
+
* `exit` rather than `beforeExit`: `beforeExit` fires when
|
|
552
|
+
* the loop merely drains and the loop can subsequently be
|
|
553
|
+
* resurrected, which would leave a still-in-use instance
|
|
554
|
+
* marked Disposed. `exit` is terminal, synchronous-only and
|
|
555
|
+
* cannot alter the exit code.
|
|
556
|
+
*/
|
|
557
|
+
static #installExitCleanup() {
|
|
523
558
|
if (_CertificateManager.#cleanupInstalled) return;
|
|
524
559
|
_CertificateManager.#cleanupInstalled = true;
|
|
525
560
|
const closeDanglingWatchers = () => {
|
|
@@ -535,13 +570,22 @@ var init_certificate_manager = __esm({
|
|
|
535
570
|
}
|
|
536
571
|
_CertificateManager.#activeInstances.clear();
|
|
537
572
|
};
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
573
|
+
_CertificateManager.#exitHandler = closeDanglingWatchers;
|
|
574
|
+
process.on("exit", closeDanglingWatchers);
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Remove the `exit` hook once the last instance is disposed,
|
|
578
|
+
* so a library that is initialized and disposed repeatedly
|
|
579
|
+
* does not accumulate process listeners. A later
|
|
580
|
+
* `initialize()` re-arms it.
|
|
581
|
+
*/
|
|
582
|
+
static #uninstallExitCleanupIfIdle() {
|
|
583
|
+
if (_CertificateManager.#activeInstances.size > 0) return;
|
|
584
|
+
if (_CertificateManager.#exitHandler) {
|
|
585
|
+
process.removeListener("exit", _CertificateManager.#exitHandler);
|
|
586
|
+
_CertificateManager.#exitHandler = void 0;
|
|
544
587
|
}
|
|
588
|
+
_CertificateManager.#cleanupInstalled = false;
|
|
545
589
|
}
|
|
546
590
|
/**
|
|
547
591
|
* Dispose **all** active CertificateManager instances,
|
|
@@ -607,6 +651,9 @@ var init_certificate_manager = __esm({
|
|
|
607
651
|
#disableFileWatchers;
|
|
608
652
|
#privateKeyPassphrase;
|
|
609
653
|
#privateKeyProvider;
|
|
654
|
+
#keyOperations;
|
|
655
|
+
/** The stable lazy wrap handed out by {@link getKeyOperations} for non-opaque configurations. */
|
|
656
|
+
#lazyLocalKeyOperations;
|
|
610
657
|
/**
|
|
611
658
|
* The on-disk key, decrypted once and kept for the instance's lifetime,
|
|
612
659
|
* so the passphrase (or its resolver function) is consulted at most
|
|
@@ -650,6 +697,17 @@ var init_certificate_manager = __esm({
|
|
|
650
697
|
this.#disableFileWatchers = options.disableFileWatchers ?? process.env.OPCUA_PKI_DISABLE_FILE_WATCHERS === "true";
|
|
651
698
|
this.#privateKeyPassphrase = options.privateKeyPassphrase;
|
|
652
699
|
this.#privateKeyProvider = options.privateKeyProvider;
|
|
700
|
+
this.#keyOperations = options.keyOperations;
|
|
701
|
+
if (this.#keyOperations && this.#privateKeyProvider) {
|
|
702
|
+
throw new Error(
|
|
703
|
+
"CertificateManager: 'keyOperations' and 'privateKeyProvider' are mutually exclusive: one hides the key, the other sources its material"
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
if (this.#keyOperations && this.#privateKeyPassphrase) {
|
|
707
|
+
throw new Error(
|
|
708
|
+
"CertificateManager: 'privateKeyPassphrase' is meaningless with 'keyOperations': there is no key material for a passphrase to protect"
|
|
709
|
+
);
|
|
710
|
+
}
|
|
653
711
|
mkdirRecursiveSync(options.location);
|
|
654
712
|
if (!fs4.existsSync(this.#location)) {
|
|
655
713
|
throw new Error(`CertificateManager cannot access location ${this.#location}`);
|
|
@@ -691,6 +749,11 @@ var init_certificate_manager = __esm({
|
|
|
691
749
|
* authority on what the current key is.
|
|
692
750
|
*/
|
|
693
751
|
async getPrivateKey() {
|
|
752
|
+
if (this.#keyOperations) {
|
|
753
|
+
throw new PrivateKeyUnavailableError(
|
|
754
|
+
"CertificateManager.getPrivateKey is not available when keyOperations is configured: the private key is held by the key-operations provider (HSM/KMS) and cannot be read \u2014 use getKeyOperations() instead"
|
|
755
|
+
);
|
|
756
|
+
}
|
|
694
757
|
if (this.#privateKeyProvider) {
|
|
695
758
|
return await this.#privateKeyProvider.getPrivateKey();
|
|
696
759
|
}
|
|
@@ -716,6 +779,75 @@ var init_certificate_manager = __esm({
|
|
|
716
779
|
}
|
|
717
780
|
/** In-flight first read of the on-disk key, so concurrent callers share one passphrase resolution. */
|
|
718
781
|
#privateKeyPromise;
|
|
782
|
+
/**
|
|
783
|
+
* True when this manager's key is opaque — configured through
|
|
784
|
+
* `keyOperations`, held by an HSM/KMS, never obtainable as material.
|
|
785
|
+
* When true, {@link getPrivateKey} throws `PrivateKeyUnavailableError`
|
|
786
|
+
* and {@link getKeyOperations} is the only way to use the key.
|
|
787
|
+
*/
|
|
788
|
+
isPrivateKeyOpaque() {
|
|
789
|
+
return !!this.#keyOperations;
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* The key as an opaque {@link IKeyOperations} — the recommended way to
|
|
793
|
+
* *use* the private key regardless of where it lives.
|
|
794
|
+
*
|
|
795
|
+
* Returns the configured `keyOperations` object when the key is opaque.
|
|
796
|
+
* Otherwise returns a stable lazy wrap over {@link getPrivateKey}: its
|
|
797
|
+
* methods resolve the key on first use (disk read, passphrase,
|
|
798
|
+
* `privateKeyProvider` — all async), so the wrap offers no synchronous
|
|
799
|
+
* fast path; callers that need one resolve the key themselves and build
|
|
800
|
+
* a `LocalKeyOperations` over it. The wrap follows key rotation: a
|
|
801
|
+
* `privateKeyProvider` that starts returning a different key gets a
|
|
802
|
+
* fresh underlying `LocalKeyOperations`.
|
|
803
|
+
*/
|
|
804
|
+
getKeyOperations() {
|
|
805
|
+
if (this.#keyOperations) {
|
|
806
|
+
return this.#keyOperations;
|
|
807
|
+
}
|
|
808
|
+
if (!this.#lazyLocalKeyOperations) {
|
|
809
|
+
let cached;
|
|
810
|
+
const resolve = async () => {
|
|
811
|
+
const key = await this.getPrivateKey();
|
|
812
|
+
if (!cached || cached.key !== key) {
|
|
813
|
+
cached = { key, ops: keyOperationsFromPrivateKey(key) };
|
|
814
|
+
}
|
|
815
|
+
return cached.ops;
|
|
816
|
+
};
|
|
817
|
+
this.#lazyLocalKeyOperations = {
|
|
818
|
+
sign: async (data, params) => (await resolve()).sign(data, params),
|
|
819
|
+
decryptBlock: async (block, params) => (await resolve()).decryptBlock(block, params),
|
|
820
|
+
getKeyMetadata: async () => (await resolve()).getKeyMetadata(),
|
|
821
|
+
getPublicKey: async () => (await resolve()).getPublicKey()
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
return this.#lazyLocalKeyOperations;
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Fail closed at `initialize()` time, not on the first certificate
|
|
828
|
+
* operation, if the key cannot actually be used: wrong/missing
|
|
829
|
+
* passphrase on an encrypted key, a broken `privateKeyProvider`, or an
|
|
830
|
+
* unreachable `keyOperations` provider (probed via `getKeyMetadata`).
|
|
831
|
+
*/
|
|
832
|
+
async #probePrivateKey() {
|
|
833
|
+
if (this.#keyOperations) {
|
|
834
|
+
await this.#keyOperations.getKeyMetadata();
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
await this.getPrivateKey();
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* The key to hand to a certificate-issuance primitive: the raw
|
|
841
|
+
* {@link PrivateKey} for a local configuration, or a {@link CaSigner}
|
|
842
|
+
* adapted from `keyOperations` when the key is opaque — which requires
|
|
843
|
+
* the provider to implement `getPublicKey` (the adapter says so if not).
|
|
844
|
+
*/
|
|
845
|
+
async #resolveSigningKey() {
|
|
846
|
+
if (this.#keyOperations) {
|
|
847
|
+
return caSignerFromKeyOperations(this.#keyOperations);
|
|
848
|
+
}
|
|
849
|
+
return await this.getPrivateKey();
|
|
850
|
+
}
|
|
719
851
|
/**
|
|
720
852
|
* Enable, disable, or rotate the passphrase protecting the on-disk
|
|
721
853
|
* private key: decrypt with `oldPassphrase` (omit if the key is
|
|
@@ -737,6 +869,11 @@ var init_certificate_manager = __esm({
|
|
|
737
869
|
* disk file for this method to rewrite).
|
|
738
870
|
*/
|
|
739
871
|
async reencryptPrivateKey(oldPassphrase, newPassphrase) {
|
|
872
|
+
if (this.#keyOperations) {
|
|
873
|
+
throw new PrivateKeyUnavailableError(
|
|
874
|
+
"reencryptPrivateKey: not supported when keyOperations is configured \u2014 there is no key material to rewrite"
|
|
875
|
+
);
|
|
876
|
+
}
|
|
740
877
|
if (this.#privateKeyProvider) {
|
|
741
878
|
throw new Error("reencryptPrivateKey: not supported when a privateKeyProvider is configured");
|
|
742
879
|
}
|
|
@@ -966,23 +1103,19 @@ var init_certificate_manager = __esm({
|
|
|
966
1103
|
return "BadCertificateUntrusted" /* BadCertificateUntrusted */;
|
|
967
1104
|
}
|
|
968
1105
|
}
|
|
969
|
-
const
|
|
970
|
-
debugLog("chain[1] info=", _c2);
|
|
971
|
-
const certificateInfo = exploreCertificateInfo(chain[0]);
|
|
1106
|
+
const { validity } = exploreCertificate(chain[0]).tbsCertificate;
|
|
972
1107
|
const now = /* @__PURE__ */ new Date();
|
|
973
1108
|
let isTimeInvalid = false;
|
|
974
|
-
if (
|
|
1109
|
+
if (validity.notBefore.getTime() > now.getTime()) {
|
|
975
1110
|
debugLog(
|
|
976
|
-
`${chalk3.red("certificate is invalid : certificate is not active yet !")} not before date =${
|
|
1111
|
+
`${chalk3.red("certificate is invalid : certificate is not active yet !")} not before date =${validity.notBefore}`
|
|
977
1112
|
);
|
|
978
1113
|
if (!options.acceptPendingCertificate) {
|
|
979
1114
|
isTimeInvalid = true;
|
|
980
1115
|
}
|
|
981
1116
|
}
|
|
982
|
-
if (
|
|
983
|
-
debugLog(
|
|
984
|
-
`${chalk3.red("certificate is invalid : certificate has expired !")} not after date =${certificateInfo.notAfter}`
|
|
985
|
-
);
|
|
1117
|
+
if (validity.notAfter.getTime() <= now.getTime()) {
|
|
1118
|
+
debugLog(`${chalk3.red("certificate is invalid : certificate has expired !")} not after date =${validity.notAfter}`);
|
|
986
1119
|
if (!options.acceptOutdatedCertificate) {
|
|
987
1120
|
isTimeInvalid = true;
|
|
988
1121
|
}
|
|
@@ -1021,7 +1154,7 @@ var init_certificate_manager = __esm({
|
|
|
1021
1154
|
const chain = coerceCertificateChain(certificate);
|
|
1022
1155
|
for (const element of chain) {
|
|
1023
1156
|
try {
|
|
1024
|
-
|
|
1157
|
+
exploreCertificate(element);
|
|
1025
1158
|
} catch (_err) {
|
|
1026
1159
|
return "BadCertificateInvalid" /* BadCertificateInvalid */;
|
|
1027
1160
|
}
|
|
@@ -1074,7 +1207,7 @@ var init_certificate_manager = __esm({
|
|
|
1074
1207
|
this.#initializingPromise = void 0;
|
|
1075
1208
|
this.state = 2 /* Initialized */;
|
|
1076
1209
|
_CertificateManager.#activeInstances.add(this);
|
|
1077
|
-
_CertificateManager.#
|
|
1210
|
+
_CertificateManager.#installExitCleanup();
|
|
1078
1211
|
}
|
|
1079
1212
|
async #initialize() {
|
|
1080
1213
|
const pkiDir = this.#location;
|
|
@@ -1089,7 +1222,7 @@ var init_certificate_manager = __esm({
|
|
|
1089
1222
|
mkdirRecursiveSync(path2.join(pkiDir, "issuers"));
|
|
1090
1223
|
mkdirRecursiveSync(path2.join(pkiDir, "issuers/certs"));
|
|
1091
1224
|
mkdirRecursiveSync(path2.join(pkiDir, "issuers/crl"));
|
|
1092
|
-
const ownsDiskKey = !this.#privateKeyProvider;
|
|
1225
|
+
const ownsDiskKey = !this.#privateKeyProvider && !this.#keyOperations;
|
|
1093
1226
|
const needsKeyGeneration = ownsDiskKey && !fs4.existsSync(this.privateKey);
|
|
1094
1227
|
const needsKeyEncryption = ownsDiskKey && !needsKeyGeneration && this.#privateKeyPassphrase !== void 0 && !isEncryptedPrivateKeyFile(this.privateKey);
|
|
1095
1228
|
if (!fs4.existsSync(this.configFile) || needsKeyGeneration || needsKeyEncryption) {
|
|
@@ -1115,14 +1248,14 @@ var init_certificate_manager = __esm({
|
|
|
1115
1248
|
if (ownsDiskKey) {
|
|
1116
1249
|
restrictPrivateFilePermissions(this.privateKey, 384);
|
|
1117
1250
|
}
|
|
1118
|
-
await this
|
|
1251
|
+
await this.#probePrivateKey();
|
|
1119
1252
|
await this.#readCertificates();
|
|
1120
1253
|
});
|
|
1121
1254
|
} else {
|
|
1122
1255
|
if (ownsDiskKey) {
|
|
1123
1256
|
restrictPrivateFilePermissions(this.privateKey, 384);
|
|
1124
1257
|
}
|
|
1125
|
-
await this
|
|
1258
|
+
await this.#probePrivateKey();
|
|
1126
1259
|
await this.#readCertificates();
|
|
1127
1260
|
}
|
|
1128
1261
|
}
|
|
@@ -1161,6 +1294,7 @@ var init_certificate_manager = __esm({
|
|
|
1161
1294
|
this.#cachedPrivateKey = void 0;
|
|
1162
1295
|
this.#privateKeyPromise = void 0;
|
|
1163
1296
|
_CertificateManager.#activeInstances.delete(this);
|
|
1297
|
+
_CertificateManager.#uninstallExitCleanupIfIdle();
|
|
1164
1298
|
}
|
|
1165
1299
|
}
|
|
1166
1300
|
/**
|
|
@@ -1206,7 +1340,7 @@ var init_certificate_manager = __esm({
|
|
|
1206
1340
|
if (typeof params.applicationUri !== "string") {
|
|
1207
1341
|
throw new Error("createSelfSignedCertificate: expecting applicationUri to be a string");
|
|
1208
1342
|
}
|
|
1209
|
-
if (!this.#privateKeyProvider && !fs4.existsSync(this.privateKey)) {
|
|
1343
|
+
if (!this.#privateKeyProvider && !this.#keyOperations && !fs4.existsSync(this.privateKey)) {
|
|
1210
1344
|
throw new Error(`Cannot find private key ${this.privateKey}`);
|
|
1211
1345
|
}
|
|
1212
1346
|
let certificateFilename = path2.join(this.rootDir, "own/certs/self_signed_certificate.pem");
|
|
@@ -1215,7 +1349,7 @@ var init_certificate_manager = __esm({
|
|
|
1215
1349
|
...params,
|
|
1216
1350
|
rootDir: this.rootDir,
|
|
1217
1351
|
configFile: this.configFile,
|
|
1218
|
-
privateKey: await this
|
|
1352
|
+
privateKey: await this.#resolveSigningKey(),
|
|
1219
1353
|
subject: params.subject || "CN=FIXME"
|
|
1220
1354
|
};
|
|
1221
1355
|
await this.withLock2(async () => {
|
|
@@ -1243,7 +1377,7 @@ var init_certificate_manager = __esm({
|
|
|
1243
1377
|
...params,
|
|
1244
1378
|
rootDir: path2.resolve(this.rootDir),
|
|
1245
1379
|
configFile: path2.resolve(this.configFile),
|
|
1246
|
-
privateKey: await this
|
|
1380
|
+
privateKey: await this.#resolveSigningKey()
|
|
1247
1381
|
};
|
|
1248
1382
|
return await this.withLock2(async () => {
|
|
1249
1383
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -1509,17 +1643,12 @@ var init_certificate_manager = __esm({
|
|
|
1509
1643
|
return "BadSecurityChecksFailed" /* BadSecurityChecksFailed */;
|
|
1510
1644
|
}
|
|
1511
1645
|
if (!opts.acceptExpiredCertificate) {
|
|
1512
|
-
|
|
1513
|
-
try {
|
|
1514
|
-
certDetails = exploreCertificateInfo(currentCert);
|
|
1515
|
-
} catch (_err) {
|
|
1516
|
-
return "BadCertificateInvalid" /* BadCertificateInvalid */;
|
|
1517
|
-
}
|
|
1646
|
+
const { validity } = currentInfo.tbsCertificate;
|
|
1518
1647
|
const now = /* @__PURE__ */ new Date();
|
|
1519
|
-
if (
|
|
1648
|
+
if (validity.notBefore.getTime() > now.getTime()) {
|
|
1520
1649
|
return "BadCertificateTimeInvalid" /* BadCertificateTimeInvalid */;
|
|
1521
1650
|
}
|
|
1522
|
-
if (
|
|
1651
|
+
if (validity.notAfter.getTime() <= now.getTime()) {
|
|
1523
1652
|
return depth === 1 ? "BadCertificateTimeInvalid" /* BadCertificateTimeInvalid */ : "BadCertificateIssuerTimeInvalid" /* BadCertificateIssuerTimeInvalid */;
|
|
1524
1653
|
}
|
|
1525
1654
|
}
|
|
@@ -1869,6 +1998,7 @@ var init_certificate_manager = __esm({
|
|
|
1869
1998
|
const chokidarOptions = {
|
|
1870
1999
|
usePolling,
|
|
1871
2000
|
...usePolling ? { interval: pollingInterval } : {},
|
|
2001
|
+
depth: 0,
|
|
1872
2002
|
persistent: false
|
|
1873
2003
|
};
|
|
1874
2004
|
const allCapturedHandles = [];
|
|
@@ -2145,21 +2275,17 @@ function getEnvironmentVarNames() {
|
|
|
2145
2275
|
return { key: varName, pattern: `\\$ENV\\:\\:${varName}` };
|
|
2146
2276
|
});
|
|
2147
2277
|
}
|
|
2278
|
+
function buildSubjectAltNameString(params) {
|
|
2279
|
+
return [
|
|
2280
|
+
`URI:${params.applicationUri}`,
|
|
2281
|
+
...(params.dns ?? []).map((d) => `DNS:${d}`),
|
|
2282
|
+
...(params.ip ?? []).map((d) => `IP:${d}`)
|
|
2283
|
+
].join(", ");
|
|
2284
|
+
}
|
|
2148
2285
|
function processAltNames(params) {
|
|
2149
2286
|
params.dns = params.dns || [];
|
|
2150
2287
|
params.ip = params.ip || [];
|
|
2151
|
-
|
|
2152
|
-
subjectAltName.push(`URI:${params.applicationUri}`);
|
|
2153
|
-
subjectAltName = [].concat(
|
|
2154
|
-
subjectAltName,
|
|
2155
|
-
params.dns.map((d) => `DNS:${d}`)
|
|
2156
|
-
);
|
|
2157
|
-
subjectAltName = [].concat(
|
|
2158
|
-
subjectAltName,
|
|
2159
|
-
params.ip.map((d) => `IP:${d}`)
|
|
2160
|
-
);
|
|
2161
|
-
const subjectAltNameString = subjectAltName.join(", ");
|
|
2162
|
-
setEnv("ALTNAME", subjectAltNameString);
|
|
2288
|
+
setEnv("ALTNAME", buildSubjectAltNameString(params));
|
|
2163
2289
|
}
|
|
2164
2290
|
var SAFE_ENV_PASSTHROUGH, exportedEnvVars;
|
|
2165
2291
|
var init_env = __esm({
|
|
@@ -2511,9 +2637,6 @@ import chalk5 from "chalk";
|
|
|
2511
2637
|
function passinArg(passphrase = "") {
|
|
2512
2638
|
return { args: ["-passin", `env:${PASSIN_ENV_VAR}`], env: { [PASSIN_ENV_VAR]: passphrase } };
|
|
2513
2639
|
}
|
|
2514
|
-
function passoutArg(passphrase = "") {
|
|
2515
|
-
return { args: ["-passout", `env:${PASSOUT_ENV_VAR}`], env: { [PASSOUT_ENV_VAR]: passphrase } };
|
|
2516
|
-
}
|
|
2517
2640
|
function renderForDisplay(file, args) {
|
|
2518
2641
|
return [file, ...args].map((a) => a === "" || /[\s"'`$\\]/.test(a) ? JSON.stringify(a) : a).join(" ");
|
|
2519
2642
|
}
|
|
@@ -2534,7 +2657,7 @@ async function execute2(file, args, options) {
|
|
|
2534
2657
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2535
2658
|
});
|
|
2536
2659
|
const fail = (message) => {
|
|
2537
|
-
if (!options.hideErrorMessage) {
|
|
2660
|
+
if (!options.hideErrorMessage && !g_config.silent) {
|
|
2538
2661
|
const fence = "###########################################";
|
|
2539
2662
|
console.error(chalk5.bgWhiteBright.redBright(`${fence} OPENSSL ERROR ${fence}`));
|
|
2540
2663
|
console.error(chalk5.bgWhiteBright.redBright(`CWD = ${options.cwd}`));
|
|
@@ -2628,7 +2751,7 @@ async function execute_openssl(args, options) {
|
|
|
2628
2751
|
await ensure_openssl_installed();
|
|
2629
2752
|
return await execute2(opensslPath, args, options);
|
|
2630
2753
|
}
|
|
2631
|
-
var opensslPath, n, PASSIN_ENV_VAR
|
|
2754
|
+
var opensslPath, n, PASSIN_ENV_VAR;
|
|
2632
2755
|
var init_execute_openssl = __esm({
|
|
2633
2756
|
"packages/node-opcua-pki/lib/toolbox/with_openssl/execute_openssl.ts"() {
|
|
2634
2757
|
"use strict";
|
|
@@ -2640,7 +2763,6 @@ var init_execute_openssl = __esm({
|
|
|
2640
2763
|
init_install_prerequisite();
|
|
2641
2764
|
n = makePath;
|
|
2642
2765
|
PASSIN_ENV_VAR = "NODE_OPCUA_PKI_OPENSSL_PASSIN";
|
|
2643
|
-
PASSOUT_ENV_VAR = "NODE_OPCUA_PKI_OPENSSL_PASSOUT";
|
|
2644
2766
|
}
|
|
2645
2767
|
});
|
|
2646
2768
|
|
|
@@ -2656,19 +2778,27 @@ function openssl_require2DigitYearInDate() {
|
|
|
2656
2778
|
}
|
|
2657
2779
|
return g_config.opensslVersion.match(/OpenSSL 0\.9/);
|
|
2658
2780
|
}
|
|
2659
|
-
function stripConditionalBlocks(template) {
|
|
2781
|
+
function stripConditionalBlocks(template, envOverrides) {
|
|
2660
2782
|
return template.replace(/\{\{#([A-Z_][A-Z0-9_]*)\}\}([\s\S]*?)\{\{\/\1\}\}\r?\n?/g, (_match, key, content) => {
|
|
2661
|
-
const keep = hasEnv(key) && getEnv(key) !== "";
|
|
2783
|
+
const keep = envOverrides && Object.prototype.hasOwnProperty.call(envOverrides, key) ? envOverrides[key] !== "" : hasEnv(key) && getEnv(key) !== "";
|
|
2662
2784
|
return keep ? content : "";
|
|
2663
2785
|
});
|
|
2664
2786
|
}
|
|
2665
|
-
function generateStaticConfig(configPath, options) {
|
|
2787
|
+
function generateStaticConfig(configPath, options, envOverrides) {
|
|
2666
2788
|
const prePath = options?.cwd || "";
|
|
2667
2789
|
const originalFilename = !path4.isAbsolute(configPath) ? path4.join(prePath, configPath) : configPath;
|
|
2668
2790
|
let staticConfig = fs7.readFileSync(originalFilename, { encoding: "utf8" });
|
|
2669
|
-
staticConfig = stripConditionalBlocks(staticConfig);
|
|
2791
|
+
staticConfig = stripConditionalBlocks(staticConfig, envOverrides);
|
|
2670
2792
|
for (const envVar of getEnvironmentVarNames()) {
|
|
2671
|
-
|
|
2793
|
+
if (envOverrides && Object.prototype.hasOwnProperty.call(envOverrides, envVar.key)) {
|
|
2794
|
+
continue;
|
|
2795
|
+
}
|
|
2796
|
+
staticConfig = staticConfig.replace(new RegExp(envVar.pattern, "gi"), () => getEnv(envVar.key));
|
|
2797
|
+
}
|
|
2798
|
+
if (envOverrides) {
|
|
2799
|
+
for (const [key, value] of Object.entries(envOverrides)) {
|
|
2800
|
+
staticConfig = staticConfig.replace(new RegExp(`\\$ENV\\:\\:${key}`, "gi"), () => value);
|
|
2801
|
+
}
|
|
2672
2802
|
}
|
|
2673
2803
|
const staticConfigPath = `${configPath}.${process.pid}-${_counter++}.tmp`;
|
|
2674
2804
|
const temporaryConfigPath = !path4.isAbsolute(configPath) ? path4.join(prePath, staticConfigPath) : staticConfigPath;
|
|
@@ -2679,6 +2809,9 @@ function generateStaticConfig(configPath, options) {
|
|
|
2679
2809
|
return temporaryConfigPath;
|
|
2680
2810
|
}
|
|
2681
2811
|
}
|
|
2812
|
+
async function cleanupStaticConfig(configFile, options) {
|
|
2813
|
+
await fs7.promises.rm(path4.resolve(options?.cwd ?? "", configFile), { force: true });
|
|
2814
|
+
}
|
|
2682
2815
|
async function getPublicKeyFromPrivateKey(privateKeyFilename, publicKeyFilename, passphrase) {
|
|
2683
2816
|
assert7(fs7.existsSync(privateKeyFilename));
|
|
2684
2817
|
const passin = passinArg(passphrase);
|
|
@@ -2747,26 +2880,30 @@ async function createCertificateSigningRequestWithOpenSSL(certificateSigningRequ
|
|
|
2747
2880
|
assert8(typeof certificateSigningRequestFilename === "string");
|
|
2748
2881
|
processAltNames(params);
|
|
2749
2882
|
const configFile = generateStaticConfig(params.configFile, { cwd: params.rootDir });
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2883
|
+
try {
|
|
2884
|
+
const options = { cwd: params.rootDir, openssl_conf: path5.relative(params.rootDir, configFile) };
|
|
2885
|
+
const subject = params.subject ? new Subject3(params.subject).toString() : void 0;
|
|
2886
|
+
displaySubtitle("- Creating a Certificate Signing Request with openssl");
|
|
2887
|
+
await execute_openssl(
|
|
2888
|
+
[
|
|
2889
|
+
"req",
|
|
2890
|
+
"-new",
|
|
2891
|
+
"-sha256",
|
|
2892
|
+
"-batch",
|
|
2893
|
+
"-text",
|
|
2894
|
+
"-config",
|
|
2895
|
+
n3(configFile),
|
|
2896
|
+
"-key",
|
|
2897
|
+
n3(params.privateKey),
|
|
2898
|
+
...subject ? ["-subj", subject] : [],
|
|
2899
|
+
"-out",
|
|
2900
|
+
n3(certificateSigningRequestFilename)
|
|
2901
|
+
],
|
|
2902
|
+
options
|
|
2903
|
+
);
|
|
2904
|
+
} finally {
|
|
2905
|
+
await cleanupStaticConfig(configFile, { cwd: params.rootDir });
|
|
2906
|
+
}
|
|
2770
2907
|
}
|
|
2771
2908
|
var n3;
|
|
2772
2909
|
var init_create_certificate_signing_request2 = __esm({
|
|
@@ -2797,57 +2934,875 @@ var init_with_openssl = __esm({
|
|
|
2797
2934
|
}
|
|
2798
2935
|
});
|
|
2799
2936
|
|
|
2800
|
-
// packages/node-opcua-pki/lib/
|
|
2801
|
-
import assert9 from "assert";
|
|
2937
|
+
// packages/node-opcua-pki/lib/ca/core/ca_database.ts
|
|
2802
2938
|
import fs9 from "fs";
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2939
|
+
import path6 from "path";
|
|
2940
|
+
import { convertPEMtoDER, readCertificatePEM } from "node-opcua-crypto";
|
|
2941
|
+
function parseOpenSSLDate(dateStr) {
|
|
2942
|
+
const raw = dateStr?.split(",")[0] ?? "";
|
|
2943
|
+
if (raw.length < 12) return "";
|
|
2944
|
+
const yy = parseInt(raw.substring(0, 2), 10);
|
|
2945
|
+
const year = yy >= 70 ? 1900 + yy : 2e3 + yy;
|
|
2946
|
+
const month = raw.substring(2, 4);
|
|
2947
|
+
const day = raw.substring(4, 6);
|
|
2948
|
+
const hour = raw.substring(6, 8);
|
|
2949
|
+
const min = raw.substring(8, 10);
|
|
2950
|
+
const sec = raw.substring(10, 12);
|
|
2951
|
+
return `${year}-${month}-${day}T${hour}:${min}:${sec}Z`;
|
|
2952
|
+
}
|
|
2953
|
+
function formatOpenSSLDate(date) {
|
|
2954
|
+
const yy = String(date.getUTCFullYear() % 100).padStart(2, "0");
|
|
2955
|
+
const mm = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
2956
|
+
const dd = String(date.getUTCDate()).padStart(2, "0");
|
|
2957
|
+
const hh = String(date.getUTCHours()).padStart(2, "0");
|
|
2958
|
+
const mi = String(date.getUTCMinutes()).padStart(2, "0");
|
|
2959
|
+
const ss = String(date.getUTCSeconds()).padStart(2, "0");
|
|
2960
|
+
return `${yy}${mm}${dd}${hh}${mi}${ss}Z`;
|
|
2961
|
+
}
|
|
2962
|
+
function parseRevocationReason(dateStr) {
|
|
2963
|
+
const parts = dateStr?.split(",");
|
|
2964
|
+
return parts && parts.length > 1 ? parts[1] : void 0;
|
|
2965
|
+
}
|
|
2966
|
+
function evenLengthHex(value) {
|
|
2967
|
+
const hex = value.toString(16).toUpperCase();
|
|
2968
|
+
return hex.length % 2 === 0 ? hex : `0${hex}`;
|
|
2969
|
+
}
|
|
2970
|
+
function escapeIndexField(value) {
|
|
2971
|
+
return value.replace(/[\x00-\x1f\x7f\\]/g, (c) => `\\x${c.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`);
|
|
2972
|
+
}
|
|
2973
|
+
function assertHexSerial(serial) {
|
|
2974
|
+
if (!/^[0-9A-Fa-f]+$/.test(serial)) {
|
|
2975
|
+
throw new Error(`Invalid certificate serial number: ${JSON.stringify(serial)}`);
|
|
2813
2976
|
}
|
|
2814
|
-
|
|
2815
|
-
const passout = passoutArg(passphrase);
|
|
2816
|
-
args.push("-out", n4(outputFile), ...passin.args, ...passout.args);
|
|
2817
|
-
await execute_openssl(args, { env: { ...passin.env, ...passout.env } });
|
|
2977
|
+
return serial.toUpperCase();
|
|
2818
2978
|
}
|
|
2819
|
-
var
|
|
2820
|
-
var
|
|
2821
|
-
"packages/node-opcua-pki/lib/
|
|
2979
|
+
var CaDatabase;
|
|
2980
|
+
var init_ca_database = __esm({
|
|
2981
|
+
"packages/node-opcua-pki/lib/ca/core/ca_database.ts"() {
|
|
2822
2982
|
"use strict";
|
|
2823
2983
|
init_esm_shims();
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2984
|
+
CaDatabase = class {
|
|
2985
|
+
#rootDir;
|
|
2986
|
+
constructor(rootDir) {
|
|
2987
|
+
this.#rootDir = rootDir;
|
|
2988
|
+
}
|
|
2989
|
+
/** Path to the OpenSSL certificate database file (`index.txt`). */
|
|
2990
|
+
get indexFile() {
|
|
2991
|
+
return path6.join(this.#rootDir, "index.txt");
|
|
2992
|
+
}
|
|
2993
|
+
/**
|
|
2994
|
+
* Parse the OpenSSL `index.txt` certificate database.
|
|
2995
|
+
*
|
|
2996
|
+
* Each line has tab-separated fields:
|
|
2997
|
+
* ```
|
|
2998
|
+
* status expiry [revocationDate] serial unknown subject
|
|
2999
|
+
* ```
|
|
3000
|
+
*
|
|
3001
|
+
* - status: `V` (valid), `R` (revoked), `E` (expired)
|
|
3002
|
+
* - expiry: `YYMMDDHHmmssZ`
|
|
3003
|
+
* - revocationDate: present only for revoked certs
|
|
3004
|
+
* - serial: hex string
|
|
3005
|
+
* - unknown: always `"unknown"`
|
|
3006
|
+
* - subject: X.500 slash-delimited string
|
|
3007
|
+
*/
|
|
3008
|
+
readIndex() {
|
|
3009
|
+
const indexPath = this.indexFile;
|
|
3010
|
+
if (!fs9.existsSync(indexPath)) {
|
|
3011
|
+
return [];
|
|
3012
|
+
}
|
|
3013
|
+
const content = fs9.readFileSync(indexPath, "utf-8");
|
|
3014
|
+
const lines = content.split("\n").filter((l) => l.trim().length > 0);
|
|
3015
|
+
const records = [];
|
|
3016
|
+
for (const line of lines) {
|
|
3017
|
+
const fields = line.split(" ");
|
|
3018
|
+
if (fields.length < 4) continue;
|
|
3019
|
+
const statusChar = fields[0];
|
|
3020
|
+
const expiryStr = fields[1];
|
|
3021
|
+
let serial;
|
|
3022
|
+
let subject;
|
|
3023
|
+
let revocationDate;
|
|
3024
|
+
if (statusChar === "R") {
|
|
3025
|
+
revocationDate = fields[2];
|
|
3026
|
+
serial = fields[3];
|
|
3027
|
+
subject = fields.length >= 6 ? fields[5] : "";
|
|
3028
|
+
} else {
|
|
3029
|
+
serial = fields[3];
|
|
3030
|
+
subject = fields.length >= 6 ? fields[5] : "";
|
|
3031
|
+
}
|
|
3032
|
+
let status;
|
|
3033
|
+
switch (statusChar) {
|
|
3034
|
+
case "V":
|
|
3035
|
+
status = "valid";
|
|
3036
|
+
break;
|
|
3037
|
+
case "R":
|
|
3038
|
+
status = "revoked";
|
|
3039
|
+
break;
|
|
3040
|
+
case "E":
|
|
3041
|
+
status = "expired";
|
|
3042
|
+
break;
|
|
3043
|
+
default:
|
|
3044
|
+
continue;
|
|
3045
|
+
}
|
|
3046
|
+
records.push({
|
|
3047
|
+
serial,
|
|
3048
|
+
status,
|
|
3049
|
+
subject,
|
|
3050
|
+
expiryDate: parseOpenSSLDate(expiryStr),
|
|
3051
|
+
revocationDate: revocationDate ? parseOpenSSLDate(revocationDate) : void 0,
|
|
3052
|
+
reason: revocationDate ? parseRevocationReason(revocationDate) : void 0
|
|
3053
|
+
});
|
|
3054
|
+
}
|
|
3055
|
+
return records;
|
|
3056
|
+
}
|
|
3057
|
+
/** Look up one record by serial number (case-insensitive). */
|
|
3058
|
+
findBySerial(serial) {
|
|
3059
|
+
const upper = serial.toUpperCase();
|
|
3060
|
+
return this.readIndex().find((r) => r.serial.toUpperCase() === upper);
|
|
3061
|
+
}
|
|
3062
|
+
/**
|
|
3063
|
+
* Read a specific issued certificate by serial number.
|
|
3064
|
+
*
|
|
3065
|
+
* OpenSSL stores signed certificates in the `certs/` directory using
|
|
3066
|
+
* the naming convention `<SERIAL>.pem`.
|
|
3067
|
+
*
|
|
3068
|
+
* @param serial - hex-encoded serial number (e.g. `"1000"`)
|
|
3069
|
+
* @returns the DER buffer, or `undefined` if not found
|
|
3070
|
+
*/
|
|
3071
|
+
getCertificateBySerial(serial) {
|
|
3072
|
+
const upper = serial.toUpperCase();
|
|
3073
|
+
const certFile = path6.join(this.#rootDir, "certs", `${upper}.pem`);
|
|
3074
|
+
if (!fs9.existsSync(certFile)) {
|
|
3075
|
+
return void 0;
|
|
3076
|
+
}
|
|
3077
|
+
const pem = readCertificatePEM(certFile);
|
|
3078
|
+
return convertPEMtoDER(pem);
|
|
3079
|
+
}
|
|
3080
|
+
/**
|
|
3081
|
+
* Read-increment-write a hex counter file (`serial`/`crlnumber`),
|
|
3082
|
+
* returning the value to hand out — matching `openssl ca`'s own
|
|
3083
|
+
* behavior: the file holds the *next* value to assign, and is bumped
|
|
3084
|
+
* to the following one immediately after being read. A `.old` backup
|
|
3085
|
+
* of the pre-bump content is kept, as `openssl` itself does.
|
|
3086
|
+
*/
|
|
3087
|
+
#nextHexCounter(fileName) {
|
|
3088
|
+
const counterFile = path6.join(this.#rootDir, fileName);
|
|
3089
|
+
const current = BigInt(`0x${fs9.readFileSync(counterFile, "utf-8").trim()}`);
|
|
3090
|
+
fs9.copyFileSync(counterFile, `${counterFile}.old`);
|
|
3091
|
+
fs9.writeFileSync(counterFile, evenLengthHex(current + 1n));
|
|
3092
|
+
return evenLengthHex(current);
|
|
3093
|
+
}
|
|
3094
|
+
/** Hand out the next certificate serial number, bumping the `serial` file. */
|
|
3095
|
+
nextSerial() {
|
|
3096
|
+
return this.#nextHexCounter("serial");
|
|
3097
|
+
}
|
|
3098
|
+
/** Hand out the next CRL number, bumping the `crlnumber` file. */
|
|
3099
|
+
nextCrlNumber() {
|
|
3100
|
+
return this.#nextHexCounter("crlnumber");
|
|
3101
|
+
}
|
|
3102
|
+
/** Append a newly-issued certificate's `V` (valid) row to `index.txt`. */
|
|
3103
|
+
appendIssued(record) {
|
|
3104
|
+
const serial = assertHexSerial(record.serial);
|
|
3105
|
+
const subject = escapeIndexField(record.subject);
|
|
3106
|
+
const line = `V ${formatOpenSSLDate(record.expiryDate)} ${serial} unknown ${subject}
|
|
3107
|
+
`;
|
|
3108
|
+
fs9.appendFileSync(this.indexFile, line);
|
|
3109
|
+
}
|
|
3110
|
+
/**
|
|
3111
|
+
* Rewrite a certificate's `index.txt` row from `V` to `R` (revoked).
|
|
3112
|
+
* Throws if the serial is not found, or is already revoked — the same
|
|
3113
|
+
* "already revoked" case `openssl ca -revoke` itself rejects.
|
|
3114
|
+
*/
|
|
3115
|
+
markRevoked(serial, revocationDate, reason) {
|
|
3116
|
+
const upper = assertHexSerial(serial);
|
|
3117
|
+
if (!/^[A-Za-z]+$/.test(reason)) {
|
|
3118
|
+
throw new Error(`Invalid CRL reason: ${JSON.stringify(reason)}`);
|
|
3119
|
+
}
|
|
3120
|
+
const lines = fs9.readFileSync(this.indexFile, "utf-8").split("\n").filter((l) => l.trim().length > 0);
|
|
3121
|
+
let found = false;
|
|
3122
|
+
const rewritten = lines.map((line) => {
|
|
3123
|
+
const fields = line.split(" ");
|
|
3124
|
+
if (fields.length < 4 || fields[3]?.toUpperCase() !== upper) {
|
|
3125
|
+
return line;
|
|
3126
|
+
}
|
|
3127
|
+
if (fields[0] === "R") {
|
|
3128
|
+
throw new Error(`Certificate ${upper} is already revoked`);
|
|
3129
|
+
}
|
|
3130
|
+
found = true;
|
|
3131
|
+
const subject = fields.length >= 6 ? fields[5] : "";
|
|
3132
|
+
return `R ${fields[1]} ${formatOpenSSLDate(revocationDate)},${reason} ${upper} unknown ${subject}`;
|
|
3133
|
+
});
|
|
3134
|
+
if (!found) {
|
|
3135
|
+
throw new Error(`Certificate ${upper} not found in the certificate database`);
|
|
3136
|
+
}
|
|
3137
|
+
fs9.writeFileSync(this.indexFile, `${rewritten.join("\n")}
|
|
3138
|
+
`);
|
|
3139
|
+
}
|
|
3140
|
+
/** Store a signed certificate's PEM under `certs/<SERIAL>.pem`, as `openssl ca` does. */
|
|
3141
|
+
storeCertificate(serial, pem) {
|
|
3142
|
+
const certFile = path6.join(this.#rootDir, "certs", `${assertHexSerial(serial)}.pem`);
|
|
3143
|
+
fs9.writeFileSync(certFile, pem);
|
|
3144
|
+
}
|
|
3145
|
+
};
|
|
2827
3146
|
}
|
|
2828
3147
|
});
|
|
2829
3148
|
|
|
2830
|
-
// packages/node-opcua-pki/lib/ca/
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
3149
|
+
// packages/node-opcua-pki/lib/ca/backends/native_ca_backend.ts
|
|
3150
|
+
import fs10 from "fs";
|
|
3151
|
+
import path7 from "path";
|
|
3152
|
+
import {
|
|
3153
|
+
CertificatePurpose as CertificatePurpose2,
|
|
3154
|
+
createCertificateFromCsr,
|
|
3155
|
+
createCertificateSigningRequest as createCertificateSigningRequest2,
|
|
3156
|
+
createCrl,
|
|
3157
|
+
isCaSigner,
|
|
3158
|
+
privateKeyToCryptoKey,
|
|
3159
|
+
readPrivateKey as readPrivateKey2,
|
|
3160
|
+
Subject as Subject4,
|
|
3161
|
+
x509
|
|
3162
|
+
} from "node-opcua-crypto";
|
|
3163
|
+
function signingAlgorithmOf(key) {
|
|
3164
|
+
return isCaSigner(key) ? key.algorithm : RSA_SHA256;
|
|
3165
|
+
}
|
|
3166
|
+
function daysFromNow(notBefore, days) {
|
|
3167
|
+
return new Date(notBefore.getTime() + days * 24 * 60 * 60 * 1e3);
|
|
3168
|
+
}
|
|
3169
|
+
function caSanUri(ca) {
|
|
3170
|
+
return `urn:${ca.subject.commonName || "NodeOPCUA-CA"}`;
|
|
3171
|
+
}
|
|
3172
|
+
function sanFromCsr(csrPem) {
|
|
3173
|
+
const request = new x509.Pkcs10CertificateRequest(csrPem);
|
|
3174
|
+
const extension = request.getExtension("2.5.29.17");
|
|
3175
|
+
const dns2 = [];
|
|
3176
|
+
const ip = [];
|
|
3177
|
+
let applicationUri;
|
|
3178
|
+
for (const name of extension?.names.toJSON() ?? []) {
|
|
3179
|
+
if (name.type === "dns") {
|
|
3180
|
+
dns2.push(name.value);
|
|
3181
|
+
} else if (name.type === "ip") {
|
|
3182
|
+
ip.push(name.value);
|
|
3183
|
+
} else if (name.type === "url" && applicationUri === void 0) {
|
|
3184
|
+
applicationUri = name.value;
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
return { dns: dns2, ip, applicationUri };
|
|
3188
|
+
}
|
|
3189
|
+
function toOpenSslSubjectString(subjectName) {
|
|
3190
|
+
const parts = [];
|
|
3191
|
+
for (const rdn of subjectName.toJSON()) {
|
|
3192
|
+
for (const [type, values] of Object.entries(rdn)) {
|
|
3193
|
+
for (const value of values) {
|
|
3194
|
+
parts.push(`${type}=${value}`);
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
return `/${parts.join("/")}`;
|
|
3199
|
+
}
|
|
3200
|
+
var CA_CERT_VALIDITY_DAYS, RSA_SHA256, REASON_TO_X509_CRL_REASON, NativeCaBackend;
|
|
3201
|
+
var init_native_ca_backend = __esm({
|
|
3202
|
+
"packages/node-opcua-pki/lib/ca/backends/native_ca_backend.ts"() {
|
|
2834
3203
|
"use strict";
|
|
2835
3204
|
init_esm_shims();
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
3205
|
+
init_toolbox();
|
|
3206
|
+
init_ca_database();
|
|
3207
|
+
CA_CERT_VALIDITY_DAYS = 3650;
|
|
3208
|
+
RSA_SHA256 = { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-256" } };
|
|
3209
|
+
REASON_TO_X509_CRL_REASON = {
|
|
3210
|
+
unspecified: x509.X509CrlReason.unspecified,
|
|
3211
|
+
keyCompromise: x509.X509CrlReason.keyCompromise,
|
|
3212
|
+
CACompromise: x509.X509CrlReason.cACompromise,
|
|
3213
|
+
affiliationChanged: x509.X509CrlReason.affiliationChanged,
|
|
3214
|
+
superseded: x509.X509CrlReason.superseded,
|
|
3215
|
+
cessationOfOperation: x509.X509CrlReason.cessationOfOperation,
|
|
3216
|
+
certificateHold: x509.X509CrlReason.certificateHold,
|
|
3217
|
+
removeFromCRL: x509.X509CrlReason.removeFromCRL
|
|
3218
|
+
};
|
|
3219
|
+
NativeCaBackend = class {
|
|
3220
|
+
/** Nothing to check: this backend spawns no process and needs no tool on PATH. */
|
|
3221
|
+
/** Signing goes through `ca._getSigningKey()`, which may be an external signer. */
|
|
3222
|
+
supportsExternalSigner = true;
|
|
3223
|
+
async preflight() {
|
|
3224
|
+
}
|
|
3225
|
+
/**
|
|
3226
|
+
* Write a CSR for the CA's own key, the `[v3_ca_req]` equivalent. The
|
|
3227
|
+
* key comes from the CA rather than from `privateKeyFile`: on a
|
|
3228
|
+
* signer-backed CA that file does not exist.
|
|
3229
|
+
*/
|
|
3230
|
+
async generateCaCsr(ca, _caRootDir, _privateKeyFile, csrFile) {
|
|
3231
|
+
displayTitle("Generate a certificate request for the CA key");
|
|
3232
|
+
const signingKey = await ca._getSigningKey();
|
|
3233
|
+
const { csr } = await createCertificateSigningRequest2({
|
|
3234
|
+
privateKey: signingKey,
|
|
3235
|
+
subject: ca.subject.toString(),
|
|
3236
|
+
applicationUri: caSanUri(ca),
|
|
3237
|
+
purpose: CertificatePurpose2.ForCertificateAuthority
|
|
3238
|
+
});
|
|
3239
|
+
await fs10.promises.writeFile(csrFile, csr);
|
|
3240
|
+
}
|
|
3241
|
+
async bootstrap(ca) {
|
|
3242
|
+
const caRootDir = path7.resolve(ca.rootDir);
|
|
3243
|
+
const csrFile = path7.join(caRootDir, "private/cakey.csr");
|
|
3244
|
+
await this.generateCaCsr(ca, caRootDir, path7.join(caRootDir, "private/cakey.pem"), csrFile);
|
|
3245
|
+
const issuerCA = ca._issuerCA;
|
|
3246
|
+
if (issuerCA) {
|
|
3247
|
+
displayTitle("Generate CA Certificate (signed by issuer CA)");
|
|
3248
|
+
const signWithIssuer = () => this.signSubordinateCsr(issuerCA, csrFile, ca.caCertificate, CA_CERT_VALIDITY_DAYS);
|
|
3249
|
+
if (path7.resolve(issuerCA.rootDir) === caRootDir) {
|
|
3250
|
+
await signWithIssuer();
|
|
3251
|
+
} else {
|
|
3252
|
+
await issuerCA._withCaDirectoryLock(signWithIssuer);
|
|
3253
|
+
}
|
|
3254
|
+
} else {
|
|
3255
|
+
displayTitle("Generate CA Certificate (self-signed)");
|
|
3256
|
+
const csrPem = await fs10.promises.readFile(csrFile, "utf-8");
|
|
3257
|
+
const signingKey = await ca._getSigningKey();
|
|
3258
|
+
const request = new x509.Pkcs10CertificateRequest(csrPem);
|
|
3259
|
+
const notBefore = /* @__PURE__ */ new Date();
|
|
3260
|
+
const { cert } = await createCertificateFromCsr({
|
|
3261
|
+
csr: csrPem,
|
|
3262
|
+
// the CSR's own parsed subject, so issuer and subject are
|
|
3263
|
+
// byte-identical on a self-signed root: re-encoding a string
|
|
3264
|
+
// form could differ and break chain building
|
|
3265
|
+
issuerName: request.subjectName,
|
|
3266
|
+
issuerPublicKey: request.publicKey,
|
|
3267
|
+
signingKey,
|
|
3268
|
+
signingAlgorithm: signingAlgorithmOf(signingKey),
|
|
3269
|
+
notBefore,
|
|
3270
|
+
notAfter: daysFromNow(notBefore, CA_CERT_VALIDITY_DAYS),
|
|
3271
|
+
purpose: CertificatePurpose2.ForCertificateAuthority,
|
|
3272
|
+
applicationUri: caSanUri(ca),
|
|
3273
|
+
revocation: {
|
|
3274
|
+
crlDistributionUrl: ca.crlDistributionUrl,
|
|
3275
|
+
ocspResponderUrl: ca.ocspResponderUrl,
|
|
3276
|
+
caIssuersUrl: ca.caIssuersUrl
|
|
3277
|
+
}
|
|
3278
|
+
});
|
|
3279
|
+
await fs10.promises.writeFile(ca.caCertificate, cert);
|
|
3280
|
+
}
|
|
3281
|
+
displaySubtitle("generate initial CRL (Certificate Revocation List)");
|
|
3282
|
+
await this.#regenerateCrlLocked(ca, new CaDatabase(ca.rootDir));
|
|
3283
|
+
displayTitle("Create Certificate Authority (CA) ---> DONE");
|
|
3284
|
+
}
|
|
3285
|
+
/** Sign a subordinate CA's request with this CA's key, the `[v3_ca]` equivalent. */
|
|
3286
|
+
async signSubordinateCsr(ca, csrFile, certFile, validityDays) {
|
|
3287
|
+
const { signingKey, signingAlgorithm, issuerPublicKey, issuerName } = await this.#issuerContext(ca);
|
|
3288
|
+
const csrPem = await fs10.promises.readFile(csrFile, "utf-8");
|
|
3289
|
+
const db = new CaDatabase(ca.rootDir);
|
|
3290
|
+
const serialNumber = db.nextSerial();
|
|
3291
|
+
const notBefore = /* @__PURE__ */ new Date();
|
|
3292
|
+
const notAfter = daysFromNow(notBefore, validityDays);
|
|
3293
|
+
const { cert } = await createCertificateFromCsr({
|
|
3294
|
+
csr: csrPem,
|
|
3295
|
+
issuerName,
|
|
3296
|
+
issuerPublicKey,
|
|
3297
|
+
signingKey,
|
|
3298
|
+
signingAlgorithm,
|
|
3299
|
+
serialNumber,
|
|
3300
|
+
notBefore,
|
|
3301
|
+
notAfter,
|
|
3302
|
+
purpose: CertificatePurpose2.ForCertificateAuthority,
|
|
3303
|
+
// the subordinate's own SAN, not the issuer's
|
|
3304
|
+
...sanFromCsr(csrPem),
|
|
3305
|
+
revocation: {
|
|
3306
|
+
crlDistributionUrl: ca.crlDistributionUrl,
|
|
3307
|
+
ocspResponderUrl: ca.ocspResponderUrl,
|
|
3308
|
+
caIssuersUrl: ca.caIssuersUrl
|
|
3309
|
+
}
|
|
3310
|
+
});
|
|
3311
|
+
await fs10.promises.writeFile(certFile, cert);
|
|
3312
|
+
this.#record(db, serialNumber, cert, notAfter);
|
|
3313
|
+
}
|
|
3314
|
+
/**
|
|
3315
|
+
* Self-sign a certificate with a caller-supplied key file and record it
|
|
3316
|
+
* in this CA's database: the native equivalent of `openssl req -new`
|
|
3317
|
+
* followed by `openssl ca -selfsign`, which likewise applies the
|
|
3318
|
+
* end-entity profile and updates `index.txt`.
|
|
3319
|
+
*/
|
|
3320
|
+
async createSelfSignedCertificate(ca, certificateFile, privateKeyFile, params) {
|
|
3321
|
+
displaySubtitle("- the certificate signing request");
|
|
3322
|
+
const key = await privateKeyToCryptoKey(readPrivateKey2(privateKeyFile, await ca._privateKeyPassphrase()));
|
|
3323
|
+
const { csr } = await createCertificateSigningRequest2({
|
|
3324
|
+
privateKey: key,
|
|
3325
|
+
subject: params.subject ? new Subject4(params.subject).toString() : ca.subject.toString(),
|
|
3326
|
+
dns: params.dns,
|
|
3327
|
+
ip: params.ip,
|
|
3328
|
+
applicationUri: params.applicationUri,
|
|
3329
|
+
purpose: CertificatePurpose2.ForApplication
|
|
3330
|
+
});
|
|
3331
|
+
displaySubtitle("- creating the self-signed certificate");
|
|
3332
|
+
const request = new x509.Pkcs10CertificateRequest(csr);
|
|
3333
|
+
const db = new CaDatabase(ca.rootDir);
|
|
3334
|
+
const serialNumber = db.nextSerial();
|
|
3335
|
+
const notBefore = params.startDate ?? /* @__PURE__ */ new Date();
|
|
3336
|
+
const notAfter = params.endDate ?? daysFromNow(notBefore, params.validity ?? 365);
|
|
3337
|
+
const { cert } = await createCertificateFromCsr({
|
|
3338
|
+
csr,
|
|
3339
|
+
issuerName: request.subjectName,
|
|
3340
|
+
issuerPublicKey: request.publicKey,
|
|
3341
|
+
signingKey: key,
|
|
3342
|
+
signingAlgorithm: RSA_SHA256,
|
|
3343
|
+
serialNumber,
|
|
3344
|
+
notBefore,
|
|
3345
|
+
notAfter,
|
|
3346
|
+
purpose: CertificatePurpose2.ForApplication,
|
|
3347
|
+
...sanFromCsr(csr)
|
|
3348
|
+
});
|
|
3349
|
+
await fs10.promises.writeFile(certificateFile, cert);
|
|
3350
|
+
this.#record(db, serialNumber, cert, notAfter);
|
|
3351
|
+
}
|
|
3352
|
+
/** `certs/<SERIAL>.pem` plus the `V` row, exactly as `openssl ca` writes them. */
|
|
3353
|
+
#record(db, serialNumber, certPem, notAfter) {
|
|
3354
|
+
db.storeCertificate(serialNumber, certPem);
|
|
3355
|
+
db.appendIssued({
|
|
3356
|
+
serial: serialNumber,
|
|
3357
|
+
expiryDate: notAfter,
|
|
3358
|
+
subject: toOpenSslSubjectString(new x509.X509Certificate(certPem).subjectName)
|
|
3359
|
+
});
|
|
3360
|
+
}
|
|
3361
|
+
/**
|
|
3362
|
+
* The CA's signing key and issuer identity, resolved fresh on every
|
|
3363
|
+
* call and never cached across operations. The identity is read back
|
|
3364
|
+
* from the CA certificate on disk so that the issuer field of anything
|
|
3365
|
+
* this CA signs is byte-identical to that certificate's subject.
|
|
3366
|
+
*/
|
|
3367
|
+
async #issuerContext(ca) {
|
|
3368
|
+
const signingKey = await ca._getSigningKey();
|
|
3369
|
+
const caCertPem = await fs10.promises.readFile(ca.caCertificate, "utf-8");
|
|
3370
|
+
const caCert = new x509.X509Certificate(caCertPem);
|
|
3371
|
+
return {
|
|
3372
|
+
signingKey,
|
|
3373
|
+
signingAlgorithm: signingAlgorithmOf(signingKey),
|
|
3374
|
+
issuerPublicKey: caCert.publicKey,
|
|
3375
|
+
issuerName: caCert.subjectName
|
|
3376
|
+
};
|
|
3377
|
+
}
|
|
3378
|
+
async signEndEntityCsr(ca, certificate, csr, params, sanOverride) {
|
|
3379
|
+
const { signingKey, signingAlgorithm, issuerPublicKey, issuerName } = await this.#issuerContext(ca);
|
|
3380
|
+
const db = new CaDatabase(ca.rootDir);
|
|
3381
|
+
const serialNumber = db.nextSerial();
|
|
3382
|
+
const csrPem = await fs10.promises.readFile(csr, "utf-8");
|
|
3383
|
+
const notBefore = params.startDate ?? /* @__PURE__ */ new Date();
|
|
3384
|
+
const notAfter = params.endDate ?? new Date(notBefore.getTime() + (params.validity ?? 365) * 24 * 60 * 60 * 1e3);
|
|
3385
|
+
const { cert } = await createCertificateFromCsr({
|
|
3386
|
+
csr: csrPem,
|
|
3387
|
+
issuerName,
|
|
3388
|
+
issuerPublicKey,
|
|
3389
|
+
signingKey,
|
|
3390
|
+
signingAlgorithm,
|
|
3391
|
+
serialNumber,
|
|
3392
|
+
notBefore,
|
|
3393
|
+
notAfter,
|
|
3394
|
+
purpose: CertificatePurpose2.ForApplication,
|
|
3395
|
+
dns: sanOverride.dns,
|
|
3396
|
+
ip: sanOverride.ip,
|
|
3397
|
+
applicationUri: sanOverride.applicationUri,
|
|
3398
|
+
revocation: {
|
|
3399
|
+
crlDistributionUrl: ca.crlDistributionUrl,
|
|
3400
|
+
ocspResponderUrl: ca.ocspResponderUrl,
|
|
3401
|
+
caIssuersUrl: ca.caIssuersUrl
|
|
3402
|
+
}
|
|
3403
|
+
});
|
|
3404
|
+
await fs10.promises.writeFile(certificate, cert);
|
|
3405
|
+
this.#record(db, serialNumber, cert, notAfter);
|
|
3406
|
+
}
|
|
3407
|
+
async #regenerateCrlLocked(ca, db) {
|
|
3408
|
+
const { signingKey, signingAlgorithm, issuerPublicKey, issuerName } = await this.#issuerContext(ca);
|
|
3409
|
+
const crlNumber = db.nextCrlNumber();
|
|
3410
|
+
const entries = db.readIndex().filter((r) => r.status === "revoked").map((r) => ({
|
|
3411
|
+
serialNumber: r.serial,
|
|
3412
|
+
revocationDate: r.revocationDate ? new Date(r.revocationDate) : /* @__PURE__ */ new Date(),
|
|
3413
|
+
reason: r.reason ? REASON_TO_X509_CRL_REASON[r.reason] : void 0
|
|
3414
|
+
}));
|
|
3415
|
+
const { crl } = await createCrl({
|
|
3416
|
+
issuerName,
|
|
3417
|
+
issuerPublicKey,
|
|
3418
|
+
signingKey,
|
|
3419
|
+
signingAlgorithm,
|
|
3420
|
+
crlNumber: BigInt(`0x${crlNumber}`),
|
|
3421
|
+
entries
|
|
3422
|
+
});
|
|
3423
|
+
await fs10.promises.writeFile(ca.revocationList, crl);
|
|
3424
|
+
const der = new x509.X509Crl(crl).rawData;
|
|
3425
|
+
await fs10.promises.writeFile(ca.revocationListDER, Buffer.from(der));
|
|
3426
|
+
}
|
|
3427
|
+
async regenerateCrl(ca) {
|
|
3428
|
+
const db = new CaDatabase(ca.rootDir);
|
|
3429
|
+
await this.#regenerateCrlLocked(ca, db);
|
|
3430
|
+
}
|
|
3431
|
+
async revoke(ca, certificate, reason) {
|
|
3432
|
+
const certPem = await fs10.promises.readFile(certificate, "utf-8");
|
|
3433
|
+
const cert = new x509.X509Certificate(certPem);
|
|
3434
|
+
const db = new CaDatabase(ca.rootDir);
|
|
3435
|
+
db.markRevoked(cert.serialNumber, /* @__PURE__ */ new Date(), reason);
|
|
3436
|
+
await this.#regenerateCrlLocked(ca, db);
|
|
3437
|
+
}
|
|
3438
|
+
};
|
|
3439
|
+
}
|
|
3440
|
+
});
|
|
2849
3441
|
|
|
2850
|
-
|
|
3442
|
+
// packages/node-opcua-pki/lib/ca/backends/openssl_ca_backend.ts
|
|
3443
|
+
import fs11 from "fs";
|
|
3444
|
+
import path8 from "path";
|
|
3445
|
+
import { Subject as Subject5 } from "node-opcua-crypto";
|
|
3446
|
+
function caAltName(ca) {
|
|
3447
|
+
return `URI:urn:${ca.subject.commonName || "NodeOPCUA-CA"}`;
|
|
3448
|
+
}
|
|
3449
|
+
function caConfigEnvOverrides(ca, altName = caAltName(ca)) {
|
|
3450
|
+
const aiaLegs = [];
|
|
3451
|
+
if (ca.ocspResponderUrl) {
|
|
3452
|
+
aiaLegs.push(`OCSP;URI:${ca.ocspResponderUrl}`);
|
|
3453
|
+
}
|
|
3454
|
+
if (ca.caIssuersUrl) {
|
|
3455
|
+
aiaLegs.push(`caIssuers;URI:${ca.caIssuersUrl}`);
|
|
3456
|
+
}
|
|
3457
|
+
return {
|
|
3458
|
+
ALTNAME: altName,
|
|
3459
|
+
CDP_URL: ca.crlDistributionUrl ?? "",
|
|
3460
|
+
AIA_VALUE: aiaLegs.join(",")
|
|
3461
|
+
};
|
|
3462
|
+
}
|
|
3463
|
+
var n4, OpenSslCaBackend;
|
|
3464
|
+
var init_openssl_ca_backend = __esm({
|
|
3465
|
+
"packages/node-opcua-pki/lib/ca/backends/openssl_ca_backend.ts"() {
|
|
3466
|
+
"use strict";
|
|
3467
|
+
init_esm_shims();
|
|
3468
|
+
init_toolbox();
|
|
3469
|
+
init_with_openssl();
|
|
3470
|
+
n4 = makePath;
|
|
3471
|
+
OpenSslCaBackend = class {
|
|
3472
|
+
/** This backend is the `openssl` executable, so it has to be there. */
|
|
3473
|
+
/**
|
|
3474
|
+
* `-passin env:` argv and env for an openssl call that has to load this
|
|
3475
|
+
* CA's key. Built here rather than on the CA: the passphrase is the
|
|
3476
|
+
* CA's business, but expressing it as an openssl argument is this
|
|
3477
|
+
* backend's, and the core has no reason to know the flag exists.
|
|
3478
|
+
*/
|
|
3479
|
+
async #passin(ca) {
|
|
3480
|
+
return passinArg(await ca._privateKeyPassphrase());
|
|
3481
|
+
}
|
|
3482
|
+
/** The openssl CLI loads its key from a file, so it cannot call out to an HSM. */
|
|
3483
|
+
supportsExternalSigner = false;
|
|
3484
|
+
async preflight() {
|
|
3485
|
+
await ensure_openssl_installed();
|
|
3486
|
+
}
|
|
3487
|
+
/**
|
|
3488
|
+
* Render `conf/caconfig.cnf` once with explicit overrides, run `fn`
|
|
3489
|
+
* with the rendered path, and always remove the temp file. Structural
|
|
3490
|
+
* guarantee that (a) every render carries the three required env
|
|
3491
|
+
* values and (b) no rendered `<name>.<pid>-<n>.tmp` file is ever left
|
|
3492
|
+
* behind — each was previously a per-method discipline.
|
|
3493
|
+
*/
|
|
3494
|
+
async #withConfig(ca, altName, fn) {
|
|
3495
|
+
const caRootDir = path8.resolve(ca.rootDir);
|
|
3496
|
+
const options = { cwd: caRootDir };
|
|
3497
|
+
const configFile = generateStaticConfig("conf/caconfig.cnf", options, caConfigEnvOverrides(ca, altName));
|
|
3498
|
+
try {
|
|
3499
|
+
return await fn(configFile, options);
|
|
3500
|
+
} finally {
|
|
3501
|
+
await cleanupStaticConfig(configFile, options);
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
async #generateCaCsrWith(ca, configFile, options, privateKeyFile, csrFile) {
|
|
3505
|
+
const passin = await this.#passin(ca);
|
|
3506
|
+
displayTitle("Generate a certificate request for the CA key");
|
|
3507
|
+
await execute_openssl(
|
|
3508
|
+
[
|
|
3509
|
+
"req",
|
|
3510
|
+
"-new",
|
|
3511
|
+
"-sha256",
|
|
3512
|
+
"-extensions",
|
|
3513
|
+
"v3_ca_req",
|
|
3514
|
+
"-config",
|
|
3515
|
+
n4(configFile),
|
|
3516
|
+
"-key",
|
|
3517
|
+
n4(privateKeyFile),
|
|
3518
|
+
"-out",
|
|
3519
|
+
n4(csrFile),
|
|
3520
|
+
"-subj",
|
|
3521
|
+
ca.subject.toString(),
|
|
3522
|
+
...passin.args
|
|
3523
|
+
],
|
|
3524
|
+
{ ...options, env: passin.env }
|
|
3525
|
+
);
|
|
3526
|
+
}
|
|
3527
|
+
/**
|
|
3528
|
+
* `openssl ca -gencrl` signs the CRL with the CA key it finds through the
|
|
3529
|
+
* config file (`private_key = $dir/private/cakey.pem`), so it needs the
|
|
3530
|
+
* CA passphrase like every other `openssl ca` invocation.
|
|
3531
|
+
*/
|
|
3532
|
+
async #regenerateCrlWith(ca, configFile, options) {
|
|
3533
|
+
const passin = await this.#passin(ca);
|
|
3534
|
+
displaySubtitle("regenerate CRL (Certificate Revocation List)");
|
|
3535
|
+
await execute_openssl(["ca", "-gencrl", "-config", n4(configFile), "-out", "crl/revocation_list.crl", ...passin.args], {
|
|
3536
|
+
...options,
|
|
3537
|
+
env: passin.env
|
|
3538
|
+
});
|
|
3539
|
+
await execute_openssl(
|
|
3540
|
+
["crl", "-in", "crl/revocation_list.crl", "-out", "crl/revocation_list.der", "-outform", "der"],
|
|
3541
|
+
options
|
|
3542
|
+
);
|
|
3543
|
+
displaySubtitle("Display (Certificate Revocation List)");
|
|
3544
|
+
await execute_openssl(["crl", "-in", n4(ca.revocationList), "-text", "-noout"], options);
|
|
3545
|
+
}
|
|
3546
|
+
async generateCaCsr(ca, _caRootDir, privateKeyFile, csrFile) {
|
|
3547
|
+
await this.#withConfig(
|
|
3548
|
+
ca,
|
|
3549
|
+
caAltName(ca),
|
|
3550
|
+
(configFile, options) => this.#generateCaCsrWith(ca, configFile, options, privateKeyFile, csrFile)
|
|
3551
|
+
);
|
|
3552
|
+
}
|
|
3553
|
+
async bootstrap(ca) {
|
|
3554
|
+
const caRootDir = path8.resolve(ca.rootDir);
|
|
3555
|
+
const privateKeyFilename = path8.join(caRootDir, "private/cakey.pem");
|
|
3556
|
+
const csrFilename = path8.join(caRootDir, "private/cakey.csr");
|
|
3557
|
+
await this.#withConfig(ca, caAltName(ca), async (configFile, options) => {
|
|
3558
|
+
await this.#generateCaCsrWith(ca, configFile, options, privateKeyFilename, csrFilename);
|
|
3559
|
+
const issuerCA = ca._issuerCA;
|
|
3560
|
+
if (issuerCA) {
|
|
3561
|
+
displayTitle("Generate CA Certificate (signed by issuer CA)");
|
|
3562
|
+
const issuerCert = path8.resolve(issuerCA.caCertificate);
|
|
3563
|
+
const issuerKey = path8.resolve(issuerCA.rootDir, "private/cakey.pem");
|
|
3564
|
+
const issuerSerial = path8.resolve(issuerCA.rootDir, "serial");
|
|
3565
|
+
const issuerPassin = await this.#passin(issuerCA);
|
|
3566
|
+
const signWithIssuer = async () => {
|
|
3567
|
+
await execute_openssl(
|
|
3568
|
+
[
|
|
3569
|
+
"x509",
|
|
3570
|
+
"-sha256",
|
|
3571
|
+
"-req",
|
|
3572
|
+
"-days",
|
|
3573
|
+
"3650",
|
|
3574
|
+
"-extensions",
|
|
3575
|
+
"v3_ca",
|
|
3576
|
+
"-extfile",
|
|
3577
|
+
n4(configFile),
|
|
3578
|
+
"-in",
|
|
3579
|
+
"private/cakey.csr",
|
|
3580
|
+
"-CA",
|
|
3581
|
+
n4(issuerCert),
|
|
3582
|
+
"-CAkey",
|
|
3583
|
+
n4(issuerKey),
|
|
3584
|
+
"-CAserial",
|
|
3585
|
+
n4(issuerSerial),
|
|
3586
|
+
"-out",
|
|
3587
|
+
"public/cacert.pem",
|
|
3588
|
+
...issuerPassin.args
|
|
3589
|
+
],
|
|
3590
|
+
{ ...options, env: issuerPassin.env }
|
|
3591
|
+
);
|
|
3592
|
+
};
|
|
3593
|
+
if (path8.resolve(issuerCA.rootDir) === caRootDir) {
|
|
3594
|
+
await signWithIssuer();
|
|
3595
|
+
} else {
|
|
3596
|
+
await issuerCA._withCaDirectoryLock(signWithIssuer);
|
|
3597
|
+
}
|
|
3598
|
+
} else {
|
|
3599
|
+
displayTitle("Generate CA Certificate (self-signed)");
|
|
3600
|
+
const passin = await this.#passin(ca);
|
|
3601
|
+
await execute_openssl(
|
|
3602
|
+
[
|
|
3603
|
+
"x509",
|
|
3604
|
+
"-sha256",
|
|
3605
|
+
"-req",
|
|
3606
|
+
"-days",
|
|
3607
|
+
"3650",
|
|
3608
|
+
"-extensions",
|
|
3609
|
+
"v3_ca",
|
|
3610
|
+
"-extfile",
|
|
3611
|
+
n4(configFile),
|
|
3612
|
+
"-in",
|
|
3613
|
+
"private/cakey.csr",
|
|
3614
|
+
"-signkey",
|
|
3615
|
+
n4(privateKeyFilename),
|
|
3616
|
+
"-out",
|
|
3617
|
+
"public/cacert.pem",
|
|
3618
|
+
...passin.args
|
|
3619
|
+
],
|
|
3620
|
+
{ ...options, env: passin.env }
|
|
3621
|
+
);
|
|
3622
|
+
}
|
|
3623
|
+
displaySubtitle("generate initial CRL (Certificate Revocation List)");
|
|
3624
|
+
await this.#regenerateCrlWith(ca, configFile, options);
|
|
3625
|
+
});
|
|
3626
|
+
displayTitle("Create Certificate Authority (CA) ---> DONE");
|
|
3627
|
+
}
|
|
3628
|
+
async regenerateCrl(ca) {
|
|
3629
|
+
await this.#withConfig(ca, caAltName(ca), (configFile, options) => this.#regenerateCrlWith(ca, configFile, options));
|
|
3630
|
+
}
|
|
3631
|
+
async signSubordinateCsr(ca, csrFile, certFile, validityDays) {
|
|
3632
|
+
await this.#withConfig(ca, caAltName(ca), async (configFile, options) => {
|
|
3633
|
+
const caRootDir = options.cwd;
|
|
3634
|
+
const passin = await this.#passin(ca);
|
|
3635
|
+
await execute_openssl(
|
|
3636
|
+
[
|
|
3637
|
+
"x509",
|
|
3638
|
+
"-sha256",
|
|
3639
|
+
"-req",
|
|
3640
|
+
"-days",
|
|
3641
|
+
String(validityDays),
|
|
3642
|
+
"-extensions",
|
|
3643
|
+
"v3_ca",
|
|
3644
|
+
"-extfile",
|
|
3645
|
+
n4(configFile),
|
|
3646
|
+
"-in",
|
|
3647
|
+
n4(csrFile),
|
|
3648
|
+
"-CA",
|
|
3649
|
+
n4(ca.caCertificate),
|
|
3650
|
+
"-CAkey",
|
|
3651
|
+
n4(path8.join(caRootDir, "private/cakey.pem")),
|
|
3652
|
+
"-CAserial",
|
|
3653
|
+
n4(path8.join(caRootDir, "serial")),
|
|
3654
|
+
"-out",
|
|
3655
|
+
n4(certFile),
|
|
3656
|
+
...passin.args
|
|
3657
|
+
],
|
|
3658
|
+
{ ...options, env: passin.env }
|
|
3659
|
+
);
|
|
3660
|
+
});
|
|
3661
|
+
}
|
|
3662
|
+
async signEndEntityCsr(ca, certificate, csr, params1, sanOverride) {
|
|
3663
|
+
await this.#withConfig(ca, buildSubjectAltNameString(sanOverride), async (configFile, options) => {
|
|
3664
|
+
displaySubtitle("- then we ask the authority to sign the certificate signing request");
|
|
3665
|
+
const passin = await this.#passin(ca);
|
|
3666
|
+
await execute_openssl(
|
|
3667
|
+
[
|
|
3668
|
+
"ca",
|
|
3669
|
+
"-config",
|
|
3670
|
+
configFile,
|
|
3671
|
+
"-startdate",
|
|
3672
|
+
x509Date(params1.startDate),
|
|
3673
|
+
"-enddate",
|
|
3674
|
+
x509Date(params1.endDate),
|
|
3675
|
+
"-batch",
|
|
3676
|
+
"-out",
|
|
3677
|
+
n4(certificate),
|
|
3678
|
+
"-in",
|
|
3679
|
+
n4(csr),
|
|
3680
|
+
...passin.args
|
|
3681
|
+
],
|
|
3682
|
+
{ ...options, env: passin.env }
|
|
3683
|
+
);
|
|
3684
|
+
displaySubtitle("- dump the certificate for a check");
|
|
3685
|
+
await execute_openssl(["x509", "-in", n4(certificate), "-dates", "-fingerprint", "-purpose", "-noout"], options);
|
|
3686
|
+
});
|
|
3687
|
+
}
|
|
3688
|
+
async revoke(ca, certificate, reason) {
|
|
3689
|
+
setEnv("RANDFILE", path8.join(ca.rootDir, "random.rnd"));
|
|
3690
|
+
await this.#withConfig(ca, caAltName(ca), async (configFile, options) => {
|
|
3691
|
+
displaySubtitle("Revoke certificate");
|
|
3692
|
+
const passin = await this.#passin(ca);
|
|
3693
|
+
await execute_openssl_no_failure(
|
|
3694
|
+
["ca", "-verbose", "-config", n4(configFile), "-revoke", certificate, "-crl_reason", reason, ...passin.args],
|
|
3695
|
+
{ ...options, env: passin.env }
|
|
3696
|
+
);
|
|
3697
|
+
await this.#regenerateCrlWith(ca, configFile, options);
|
|
3698
|
+
displaySubtitle("Verify that certificate is revoked");
|
|
3699
|
+
await execute_openssl_no_failure(
|
|
3700
|
+
[
|
|
3701
|
+
"verify",
|
|
3702
|
+
"-verbose",
|
|
3703
|
+
"-CRLfile",
|
|
3704
|
+
n4(ca.revocationList),
|
|
3705
|
+
"-CAfile",
|
|
3706
|
+
n4(ca.caCertificate),
|
|
3707
|
+
"-crl_check",
|
|
3708
|
+
n4(certificate)
|
|
3709
|
+
],
|
|
3710
|
+
options
|
|
3711
|
+
);
|
|
3712
|
+
displaySubtitle("Produce CRL in DER form ");
|
|
3713
|
+
await execute_openssl(
|
|
3714
|
+
["crl", "-in", n4(ca.revocationList), "-out", "crl/revocation_list.der", "-outform", "der"],
|
|
3715
|
+
options
|
|
3716
|
+
);
|
|
3717
|
+
displaySubtitle("Produce CRL in PEM form ");
|
|
3718
|
+
await execute_openssl(
|
|
3719
|
+
["crl", "-in", n4(ca.revocationList), "-out", "crl/revocation_list.pem", "-outform", "pem", "-text"],
|
|
3720
|
+
options
|
|
3721
|
+
);
|
|
3722
|
+
});
|
|
3723
|
+
}
|
|
3724
|
+
async createSelfSignedCertificate(ca, certificateFile, privateKeyFile, params) {
|
|
3725
|
+
const envOverrides = caConfigEnvOverrides(ca, buildSubjectAltNameString(params));
|
|
3726
|
+
const configFile = generateStaticConfig(ca.configFile, { cwd: ca.rootDir }, envOverrides);
|
|
3727
|
+
const options = {
|
|
3728
|
+
cwd: ca.rootDir,
|
|
3729
|
+
openssl_conf: makePath(configFile)
|
|
3730
|
+
};
|
|
3731
|
+
try {
|
|
3732
|
+
const subject = params.subject ? new Subject5(params.subject).toString() : "";
|
|
3733
|
+
const subjectOptions = subject && subject.length > 1 ? ["-subj", subject] : [];
|
|
3734
|
+
const csrFile = `${certificateFile}_csr`;
|
|
3735
|
+
const passin = await this.#passin(ca);
|
|
3736
|
+
displaySubtitle("- the certificate signing request");
|
|
3737
|
+
await execute_openssl(
|
|
3738
|
+
[
|
|
3739
|
+
"req",
|
|
3740
|
+
"-new",
|
|
3741
|
+
"-sha256",
|
|
3742
|
+
...subjectOptions,
|
|
3743
|
+
"-batch",
|
|
3744
|
+
"-key",
|
|
3745
|
+
n4(privateKeyFile),
|
|
3746
|
+
"-out",
|
|
3747
|
+
n4(csrFile),
|
|
3748
|
+
...passin.args
|
|
3749
|
+
],
|
|
3750
|
+
{ ...options, env: passin.env }
|
|
3751
|
+
);
|
|
3752
|
+
displaySubtitle("- creating the self-signed certificate");
|
|
3753
|
+
await execute_openssl(
|
|
3754
|
+
[
|
|
3755
|
+
"ca",
|
|
3756
|
+
"-selfsign",
|
|
3757
|
+
"-keyfile",
|
|
3758
|
+
n4(privateKeyFile),
|
|
3759
|
+
"-startdate",
|
|
3760
|
+
x509Date(params.startDate),
|
|
3761
|
+
"-enddate",
|
|
3762
|
+
x509Date(params.endDate),
|
|
3763
|
+
"-batch",
|
|
3764
|
+
"-out",
|
|
3765
|
+
n4(certificateFile),
|
|
3766
|
+
"-in",
|
|
3767
|
+
n4(csrFile),
|
|
3768
|
+
...passin.args
|
|
3769
|
+
],
|
|
3770
|
+
{ ...options, env: passin.env }
|
|
3771
|
+
);
|
|
3772
|
+
displaySubtitle("- dump the certificate for a check");
|
|
3773
|
+
await execute_openssl(["x509", "-in", n4(certificateFile), "-dates", "-fingerprint", "-purpose", "-noout"], {});
|
|
3774
|
+
displaySubtitle("- verify self-signed certificate");
|
|
3775
|
+
await execute_openssl_no_failure(["verify", "-verbose", "-CAfile", n4(certificateFile), n4(certificateFile)], options);
|
|
3776
|
+
await fs11.promises.unlink(csrFile);
|
|
3777
|
+
} finally {
|
|
3778
|
+
await cleanupStaticConfig(configFile, { cwd: ca.rootDir });
|
|
3779
|
+
}
|
|
3780
|
+
}
|
|
3781
|
+
};
|
|
3782
|
+
}
|
|
3783
|
+
});
|
|
3784
|
+
|
|
3785
|
+
// packages/node-opcua-pki/lib/ca/templates/ca_config_template.cnf.ts
|
|
3786
|
+
var config2, ca_config_template_cnf_default;
|
|
3787
|
+
var init_ca_config_template_cnf = __esm({
|
|
3788
|
+
"packages/node-opcua-pki/lib/ca/templates/ca_config_template.cnf.ts"() {
|
|
3789
|
+
"use strict";
|
|
3790
|
+
init_esm_shims();
|
|
3791
|
+
config2 = `#.........DO NOT MODIFY BY HAND .........................
|
|
3792
|
+
[ ca ]
|
|
3793
|
+
default_ca = CA_default
|
|
3794
|
+
[ CA_default ]
|
|
3795
|
+
dir = "%%ROOT_FOLDER%%" # the main CA folder (quoted: see renderCaConfig)
|
|
3796
|
+
certs = $dir/certs # where to store certificates
|
|
3797
|
+
new_certs_dir = $dir/certs #
|
|
3798
|
+
database = $dir/index.txt # the certificate database
|
|
3799
|
+
serial = $dir/serial # the serial number counter
|
|
3800
|
+
certificate = $dir/public/cacert.pem # The root CA certificate
|
|
3801
|
+
private_key = $dir/private/cakey.pem # the CA private key
|
|
3802
|
+
x509_extensions = usr_cert #
|
|
3803
|
+
default_days = 3650 # default validity : 10 years
|
|
3804
|
+
|
|
3805
|
+
# default_md = sha1
|
|
2851
3806
|
|
|
2852
3807
|
default_md = sha256 # The default digest algorithm
|
|
2853
3808
|
|
|
@@ -2858,7 +3813,7 @@ policy = policy_match
|
|
|
2858
3813
|
# default_enddate = YYMMDDHHMMSSZ
|
|
2859
3814
|
crl_dir = $dir/crl
|
|
2860
3815
|
crl_extensions = crl_ext
|
|
2861
|
-
crl = $dir/revocation_list.crl # the Revocation list
|
|
3816
|
+
crl = $dir/crl/revocation_list.crl # the Revocation list
|
|
2862
3817
|
crlnumber = $dir/crlnumber # CRL number file
|
|
2863
3818
|
default_crl_days = 30
|
|
2864
3819
|
default_crl_hours = 24
|
|
@@ -2968,25 +3923,31 @@ authorityKeyIdentifier = keyid:always,issuer:always
|
|
|
2968
3923
|
}
|
|
2969
3924
|
});
|
|
2970
3925
|
|
|
2971
|
-
// packages/node-opcua-pki/lib/ca/
|
|
2972
|
-
import
|
|
2973
|
-
import
|
|
3926
|
+
// packages/node-opcua-pki/lib/ca/core/certificate_authority_core.ts
|
|
3927
|
+
import assert9 from "assert";
|
|
3928
|
+
import fs12 from "fs";
|
|
2974
3929
|
import os4 from "os";
|
|
2975
|
-
import
|
|
3930
|
+
import path9 from "path";
|
|
3931
|
+
import { withLock as withLock2 } from "@ster5/global-mutex";
|
|
2976
3932
|
import chalk6 from "chalk";
|
|
2977
3933
|
import {
|
|
2978
|
-
CertificatePurpose as
|
|
3934
|
+
CertificatePurpose as CertificatePurpose3,
|
|
2979
3935
|
certificateMatchesPrivateKey,
|
|
2980
|
-
convertPEMtoDER,
|
|
3936
|
+
convertPEMtoDER as convertPEMtoDER2,
|
|
3937
|
+
createCertificateSigningRequest as createCertificateSigningRequest3,
|
|
3938
|
+
createPfx,
|
|
2981
3939
|
exploreCertificate as exploreCertificate2,
|
|
2982
3940
|
exploreCertificateSigningRequest,
|
|
2983
3941
|
generatePrivateKeyFile as generatePrivateKeyFile2,
|
|
2984
|
-
|
|
3942
|
+
privateKeyToCryptoKey as privateKeyToCryptoKey2,
|
|
3943
|
+
readCertificatePEM as readCertificatePEM2,
|
|
2985
3944
|
readCertificateSigningRequest,
|
|
2986
|
-
readPrivateKey as
|
|
2987
|
-
Subject as
|
|
3945
|
+
readPrivateKey as readPrivateKey3,
|
|
3946
|
+
Subject as Subject6,
|
|
2988
3947
|
toPem as toPem2,
|
|
2989
|
-
|
|
3948
|
+
verifyCertificateSignature as verifyCertificateSignature2,
|
|
3949
|
+
writePrivateKeyFile as writePrivateKeyFile2,
|
|
3950
|
+
x509 as x5092
|
|
2990
3951
|
} from "node-opcua-crypto";
|
|
2991
3952
|
function escapeOpensslConfDoubleQuoted(value) {
|
|
2992
3953
|
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
@@ -2997,175 +3958,6 @@ function renderCaConfig(caRootDir) {
|
|
|
2997
3958
|
function octetStringToIpAddress(a) {
|
|
2998
3959
|
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();
|
|
2999
3960
|
}
|
|
3000
|
-
async function construct_CertificateAuthority(certificateAuthority) {
|
|
3001
|
-
const subject = certificateAuthority.subject;
|
|
3002
|
-
const caRootDir = path6.resolve(certificateAuthority.rootDir);
|
|
3003
|
-
async function make_folders() {
|
|
3004
|
-
mkdirRecursiveSync(caRootDir);
|
|
3005
|
-
ensurePrivateDirectory(path6.join(caRootDir, "private"));
|
|
3006
|
-
mkdirRecursiveSync(path6.join(caRootDir, "public"));
|
|
3007
|
-
mkdirRecursiveSync(path6.join(caRootDir, "certs"));
|
|
3008
|
-
mkdirRecursiveSync(path6.join(caRootDir, "crl"));
|
|
3009
|
-
mkdirRecursiveSync(path6.join(caRootDir, "conf"));
|
|
3010
|
-
}
|
|
3011
|
-
await make_folders();
|
|
3012
|
-
async function construct_default_files() {
|
|
3013
|
-
const serial = path6.join(caRootDir, "serial");
|
|
3014
|
-
if (!fs10.existsSync(serial)) {
|
|
3015
|
-
await fs10.promises.writeFile(serial, "1000");
|
|
3016
|
-
}
|
|
3017
|
-
const crlNumber = path6.join(caRootDir, "crlnumber");
|
|
3018
|
-
if (!fs10.existsSync(crlNumber)) {
|
|
3019
|
-
await fs10.promises.writeFile(crlNumber, "1000");
|
|
3020
|
-
}
|
|
3021
|
-
const indexFile = path6.join(caRootDir, "index.txt");
|
|
3022
|
-
if (!fs10.existsSync(indexFile)) {
|
|
3023
|
-
await fs10.promises.writeFile(indexFile, "");
|
|
3024
|
-
}
|
|
3025
|
-
}
|
|
3026
|
-
await construct_default_files();
|
|
3027
|
-
const caKeyExists = fs10.existsSync(path6.join(caRootDir, "private/cakey.pem"));
|
|
3028
|
-
const caCertExists = fs10.existsSync(path6.join(caRootDir, "public/cacert.pem"));
|
|
3029
|
-
if (caKeyExists && caCertExists && !config3.forceCA) {
|
|
3030
|
-
restrictPrivateFilePermissions(path6.join(caRootDir, "private/cakey.pem"), 384);
|
|
3031
|
-
await certificateAuthority._ensurePrivateKeyProtection();
|
|
3032
|
-
debugLog("CA private key and certificate already exist ... skipping");
|
|
3033
|
-
return;
|
|
3034
|
-
}
|
|
3035
|
-
if (caKeyExists && !caCertExists) {
|
|
3036
|
-
debugLog("CA private key exists but cacert.pem is missing \u2014 rebuilding CA");
|
|
3037
|
-
fs10.unlinkSync(path6.join(caRootDir, "private/cakey.pem"));
|
|
3038
|
-
const staleCsr = path6.join(caRootDir, "private/cakey.csr");
|
|
3039
|
-
if (fs10.existsSync(staleCsr)) {
|
|
3040
|
-
fs10.unlinkSync(staleCsr);
|
|
3041
|
-
}
|
|
3042
|
-
}
|
|
3043
|
-
displayTitle("Create Certificate Authority (CA)");
|
|
3044
|
-
const indexFileAttr = path6.join(caRootDir, "index.txt.attr");
|
|
3045
|
-
if (!fs10.existsSync(indexFileAttr)) {
|
|
3046
|
-
await fs10.promises.writeFile(indexFileAttr, "unique_subject = no");
|
|
3047
|
-
}
|
|
3048
|
-
const caConfigFile = certificateAuthority.configFile;
|
|
3049
|
-
if (1) {
|
|
3050
|
-
await fs10.promises.writeFile(caConfigFile, renderCaConfig(caRootDir));
|
|
3051
|
-
}
|
|
3052
|
-
const subjectOpt = ["-subj", subject.toString()];
|
|
3053
|
-
const caCommonName = subject.commonName || "NodeOPCUA-CA";
|
|
3054
|
-
setEnv("ALTNAME", `URI:urn:${caCommonName}`);
|
|
3055
|
-
certificateAuthority._wireRevocationEnvVars();
|
|
3056
|
-
const options = { cwd: caRootDir };
|
|
3057
|
-
const configFile = generateStaticConfig("conf/caconfig.cnf", options);
|
|
3058
|
-
const configOption = ["-config", n5(configFile)];
|
|
3059
|
-
const keySize = certificateAuthority.keySize;
|
|
3060
|
-
const privateKeyFilename = path6.join(caRootDir, "private/cakey.pem");
|
|
3061
|
-
const csrFilename = path6.join(caRootDir, "private/cakey.csr");
|
|
3062
|
-
displayTitle(`Generate the CA private Key - ${keySize}`);
|
|
3063
|
-
const passin = await certificateAuthority._opensslPassin();
|
|
3064
|
-
await generatePrivateKeyFile2(privateKeyFilename, keySize, { passphrase: await certificateAuthority._privateKeyPassphrase() });
|
|
3065
|
-
restrictPrivateFilePermissions(privateKeyFilename, 384);
|
|
3066
|
-
displayTitle("Generate a certificate request for the CA key");
|
|
3067
|
-
await execute_openssl(
|
|
3068
|
-
[
|
|
3069
|
-
"req",
|
|
3070
|
-
"-new",
|
|
3071
|
-
"-sha256",
|
|
3072
|
-
"-text",
|
|
3073
|
-
"-extensions",
|
|
3074
|
-
"v3_ca_req",
|
|
3075
|
-
...configOption,
|
|
3076
|
-
"-key",
|
|
3077
|
-
n5(privateKeyFilename),
|
|
3078
|
-
"-out",
|
|
3079
|
-
n5(csrFilename),
|
|
3080
|
-
...subjectOpt,
|
|
3081
|
-
...passin.args
|
|
3082
|
-
],
|
|
3083
|
-
{ ...options, env: passin.env }
|
|
3084
|
-
);
|
|
3085
|
-
const issuerCA = certificateAuthority._issuerCA;
|
|
3086
|
-
if (issuerCA) {
|
|
3087
|
-
displayTitle("Generate CA Certificate (signed by issuer CA)");
|
|
3088
|
-
const issuerCert = path6.resolve(issuerCA.caCertificate);
|
|
3089
|
-
const issuerKey = path6.resolve(issuerCA.rootDir, "private/cakey.pem");
|
|
3090
|
-
const issuerSerial = path6.resolve(issuerCA.rootDir, "serial");
|
|
3091
|
-
const issuerPassin = await issuerCA._opensslPassin();
|
|
3092
|
-
await execute_openssl(
|
|
3093
|
-
[
|
|
3094
|
-
"x509",
|
|
3095
|
-
"-sha256",
|
|
3096
|
-
"-req",
|
|
3097
|
-
"-days",
|
|
3098
|
-
"3650",
|
|
3099
|
-
"-text",
|
|
3100
|
-
"-extensions",
|
|
3101
|
-
"v3_ca",
|
|
3102
|
-
"-extfile",
|
|
3103
|
-
n5(configFile),
|
|
3104
|
-
"-in",
|
|
3105
|
-
"private/cakey.csr",
|
|
3106
|
-
"-CA",
|
|
3107
|
-
n5(issuerCert),
|
|
3108
|
-
"-CAkey",
|
|
3109
|
-
n5(issuerKey),
|
|
3110
|
-
"-CAserial",
|
|
3111
|
-
n5(issuerSerial),
|
|
3112
|
-
"-out",
|
|
3113
|
-
"public/cacert.pem",
|
|
3114
|
-
...issuerPassin.args
|
|
3115
|
-
],
|
|
3116
|
-
{ ...options, env: issuerPassin.env }
|
|
3117
|
-
);
|
|
3118
|
-
} else {
|
|
3119
|
-
displayTitle("Generate CA Certificate (self-signed)");
|
|
3120
|
-
await execute_openssl(
|
|
3121
|
-
[
|
|
3122
|
-
"x509",
|
|
3123
|
-
"-sha256",
|
|
3124
|
-
"-req",
|
|
3125
|
-
"-days",
|
|
3126
|
-
"3650",
|
|
3127
|
-
"-text",
|
|
3128
|
-
"-extensions",
|
|
3129
|
-
"v3_ca",
|
|
3130
|
-
"-extfile",
|
|
3131
|
-
n5(configFile),
|
|
3132
|
-
"-in",
|
|
3133
|
-
"private/cakey.csr",
|
|
3134
|
-
"-signkey",
|
|
3135
|
-
n5(privateKeyFilename),
|
|
3136
|
-
"-out",
|
|
3137
|
-
"public/cacert.pem",
|
|
3138
|
-
...passin.args
|
|
3139
|
-
],
|
|
3140
|
-
{ ...options, env: passin.env }
|
|
3141
|
-
);
|
|
3142
|
-
}
|
|
3143
|
-
displaySubtitle("generate initial CRL (Certificate Revocation List)");
|
|
3144
|
-
await regenerateCrl(certificateAuthority.revocationList, configOption, options, passin);
|
|
3145
|
-
displayTitle("Create Certificate Authority (CA) ---> DONE");
|
|
3146
|
-
}
|
|
3147
|
-
async function regenerateCrl(revocationList, configOption, options, passin) {
|
|
3148
|
-
displaySubtitle("regenerate CRL (Certificate Revocation List)");
|
|
3149
|
-
await execute_openssl(["ca", "-gencrl", ...configOption, "-out", "crl/revocation_list.crl", ...passin.args], {
|
|
3150
|
-
...options,
|
|
3151
|
-
env: passin.env
|
|
3152
|
-
});
|
|
3153
|
-
await execute_openssl(["crl", "-in", "crl/revocation_list.crl", "-out", "crl/revocation_list.der", "-outform", "der"], options);
|
|
3154
|
-
displaySubtitle("Display (Certificate Revocation List)");
|
|
3155
|
-
await execute_openssl(["crl", "-in", n5(revocationList), "-text", "-noout"], options);
|
|
3156
|
-
}
|
|
3157
|
-
function parseOpenSSLDate(dateStr) {
|
|
3158
|
-
const raw = dateStr?.split(",")[0] ?? "";
|
|
3159
|
-
if (raw.length < 12) return "";
|
|
3160
|
-
const yy = parseInt(raw.substring(0, 2), 10);
|
|
3161
|
-
const year = yy >= 70 ? 1900 + yy : 2e3 + yy;
|
|
3162
|
-
const month = raw.substring(2, 4);
|
|
3163
|
-
const day = raw.substring(4, 6);
|
|
3164
|
-
const hour = raw.substring(6, 8);
|
|
3165
|
-
const min = raw.substring(8, 10);
|
|
3166
|
-
const sec = raw.substring(10, 12);
|
|
3167
|
-
return `${year}-${month}-${day}T${hour}:${min}:${sec}Z`;
|
|
3168
|
-
}
|
|
3169
3961
|
function validateRevocationUrl(url, fieldName) {
|
|
3170
3962
|
if (url === void 0) {
|
|
3171
3963
|
return void 0;
|
|
@@ -3193,27 +3985,25 @@ function validateRevocationUrl(url, fieldName) {
|
|
|
3193
3985
|
}
|
|
3194
3986
|
return url;
|
|
3195
3987
|
}
|
|
3196
|
-
var defaultSubject, configurationFileTemplate,
|
|
3197
|
-
var
|
|
3198
|
-
"packages/node-opcua-pki/lib/ca/
|
|
3988
|
+
var defaultSubject, configurationFileTemplate, config3, CA_LOCK_MAX_WAIT_MS, CA_LOCK_RETRY, CertificateAuthorityCore;
|
|
3989
|
+
var init_certificate_authority_core = __esm({
|
|
3990
|
+
"packages/node-opcua-pki/lib/ca/core/certificate_authority_core.ts"() {
|
|
3199
3991
|
"use strict";
|
|
3200
3992
|
init_esm_shims();
|
|
3201
|
-
init_toolbox_pfx();
|
|
3202
3993
|
init_toolbox();
|
|
3203
|
-
|
|
3204
|
-
init_simple_config_template_cnf();
|
|
3994
|
+
init_ca_database();
|
|
3205
3995
|
init_ca_config_template_cnf();
|
|
3206
3996
|
defaultSubject = "/C=FR/ST=IDF/L=Paris/O=Local NODE-OPCUA Certificate Authority/CN=NodeOPCUA-CA";
|
|
3207
3997
|
configurationFileTemplate = ca_config_template_cnf_default;
|
|
3208
|
-
configurationFileSimpleTemplate2 = simple_config_template_cnf_default;
|
|
3209
3998
|
config3 = {
|
|
3210
3999
|
certificateDir: "INVALID",
|
|
3211
4000
|
forceCA: false,
|
|
3212
4001
|
pkiDir: "INVALID"
|
|
3213
4002
|
};
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
4003
|
+
CA_LOCK_MAX_WAIT_MS = 5 * 6e4;
|
|
4004
|
+
CA_LOCK_RETRY = { minTimeout: 20, maxTimeout: 250 };
|
|
4005
|
+
assert9(octetStringToIpAddress("c07b9179") === "192.123.145.121");
|
|
4006
|
+
CertificateAuthorityCore = class {
|
|
3217
4007
|
/** RSA key size used when generating the CA private key. */
|
|
3218
4008
|
keySize;
|
|
3219
4009
|
/** Root filesystem path of the CA directory structure. */
|
|
@@ -3230,14 +4020,41 @@ var init_certificate_authority = __esm({
|
|
|
3230
4020
|
/** resolved once (see `privateKeyPassphrase`); `#passphraseResolved` distinguishes "none" from "not yet" */
|
|
3231
4021
|
#resolvedPassphrase;
|
|
3232
4022
|
#passphraseResolved = false;
|
|
4023
|
+
/** Signing backend: `openssl` shells out to the CLI, `native` signs in-process. */
|
|
4024
|
+
#backend;
|
|
4025
|
+
/** External signing key (HSM/KMS), when one was supplied instead of a key file. */
|
|
4026
|
+
#signer;
|
|
4027
|
+
/** Read access to `index.txt` / `certs/<SERIAL>.pem`. */
|
|
4028
|
+
#db;
|
|
3233
4029
|
constructor(options) {
|
|
3234
|
-
|
|
3235
|
-
|
|
4030
|
+
assert9(Object.prototype.hasOwnProperty.call(options, "location"));
|
|
4031
|
+
assert9(Object.prototype.hasOwnProperty.call(options, "keySize"));
|
|
3236
4032
|
this.location = options.location;
|
|
3237
4033
|
this.keySize = options.keySize || 2048;
|
|
3238
|
-
this.subject = new
|
|
4034
|
+
this.subject = new Subject6(options.subject || defaultSubject);
|
|
3239
4035
|
this._issuerCA = options.issuerCA;
|
|
3240
4036
|
this.#privateKeyPassphrase = options.privateKeyPassphrase;
|
|
4037
|
+
this.#signer = options.signer;
|
|
4038
|
+
if (options.signer) {
|
|
4039
|
+
const algorithm = options.signer.algorithm;
|
|
4040
|
+
if (algorithm.name !== "RSASSA-PKCS1-v1_5" && algorithm.name !== "ECDSA") {
|
|
4041
|
+
throw new Error(
|
|
4042
|
+
`CertificateAuthority: signer algorithm ${algorithm.name} is not supported - use RSASSA-PKCS1-v1_5 or ECDSA.`
|
|
4043
|
+
);
|
|
4044
|
+
}
|
|
4045
|
+
if (algorithm.name === "ECDSA" && !algorithm.namedCurve) {
|
|
4046
|
+
throw new Error(
|
|
4047
|
+
"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."
|
|
4048
|
+
);
|
|
4049
|
+
}
|
|
4050
|
+
}
|
|
4051
|
+
if (options.signer && !options.backend.supportsExternalSigner) {
|
|
4052
|
+
throw new Error(
|
|
4053
|
+
"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."
|
|
4054
|
+
);
|
|
4055
|
+
}
|
|
4056
|
+
this.#backend = options.backend;
|
|
4057
|
+
this.#db = new CaDatabase(this.location);
|
|
3241
4058
|
if (options.crlDistributionUrl !== void 0) {
|
|
3242
4059
|
this.setCrlDistributionUrl(options.crlDistributionUrl);
|
|
3243
4060
|
}
|
|
@@ -3301,42 +4118,17 @@ var init_certificate_authority = __esm({
|
|
|
3301
4118
|
setCaIssuersUrl(url) {
|
|
3302
4119
|
this._caIssuersUrl = validateRevocationUrl(url, "caIssuersUrl");
|
|
3303
4120
|
}
|
|
3304
|
-
/**
|
|
3305
|
-
* @internal
|
|
3306
|
-
* Populate the OpenSSL config substitution env vars (`CDP_URL` and
|
|
3307
|
-
* `AIA_VALUE`) from the configured URLs, or unset them so the
|
|
3308
|
-
* matching `{{#KEY}}...{{/KEY}}` blocks in the templates are
|
|
3309
|
-
* stripped. MUST be called before every `generateStaticConfig`
|
|
3310
|
-
* invocation that signs a certificate.
|
|
3311
|
-
*/
|
|
3312
|
-
_wireRevocationEnvVars() {
|
|
3313
|
-
unsetEnv("CDP_URL");
|
|
3314
|
-
unsetEnv("AIA_VALUE");
|
|
3315
|
-
if (this._crlDistributionUrl) {
|
|
3316
|
-
setEnv("CDP_URL", this._crlDistributionUrl);
|
|
3317
|
-
}
|
|
3318
|
-
const aiaLegs = [];
|
|
3319
|
-
if (this._ocspResponderUrl) {
|
|
3320
|
-
aiaLegs.push(`OCSP;URI:${this._ocspResponderUrl}`);
|
|
3321
|
-
}
|
|
3322
|
-
if (this._caIssuersUrl) {
|
|
3323
|
-
aiaLegs.push(`caIssuers;URI:${this._caIssuersUrl}`);
|
|
3324
|
-
}
|
|
3325
|
-
if (aiaLegs.length > 0) {
|
|
3326
|
-
setEnv("AIA_VALUE", aiaLegs.join(","));
|
|
3327
|
-
}
|
|
3328
|
-
}
|
|
3329
4121
|
/** Absolute path to the CA root directory (alias for {@link location}). */
|
|
3330
4122
|
get rootDir() {
|
|
3331
4123
|
return this.location;
|
|
3332
4124
|
}
|
|
3333
4125
|
/** Path to the OpenSSL configuration file (`conf/caconfig.cnf`). */
|
|
3334
4126
|
get configFile() {
|
|
3335
|
-
return
|
|
4127
|
+
return path9.normalize(path9.join(this.rootDir, "./conf/caconfig.cnf"));
|
|
3336
4128
|
}
|
|
3337
4129
|
/** Path to the CA private key (`private/cakey.pem`); may be passphrase-encrypted, see {@link getPrivateKey}. */
|
|
3338
4130
|
get privateKey() {
|
|
3339
|
-
return
|
|
4131
|
+
return path9.join(path9.resolve(this.rootDir), "private/cakey.pem");
|
|
3340
4132
|
}
|
|
3341
4133
|
/**
|
|
3342
4134
|
* The CA private key, decrypted with the configured `privateKeyPassphrase`
|
|
@@ -3344,7 +4136,51 @@ var init_certificate_authority = __esm({
|
|
|
3344
4136
|
* on an encrypted key with no or the wrong passphrase.
|
|
3345
4137
|
*/
|
|
3346
4138
|
async getPrivateKey() {
|
|
3347
|
-
|
|
4139
|
+
this.#assertKeyIsOnDisk("getPrivateKey");
|
|
4140
|
+
return readPrivateKey3(this.privateKey, await this._privateKeyPassphrase());
|
|
4141
|
+
}
|
|
4142
|
+
/**
|
|
4143
|
+
* True when this CA signs with an external {@link CaSigner} (HSM/KMS)
|
|
4144
|
+
* rather than with `private/cakey.pem`. There is then no private key
|
|
4145
|
+
* file on disk, and never was one.
|
|
4146
|
+
*/
|
|
4147
|
+
get hasExternalSigner() {
|
|
4148
|
+
return this.#signer !== void 0;
|
|
4149
|
+
}
|
|
4150
|
+
/**
|
|
4151
|
+
* Does `cert` certify the key this CA signs with? With the key on disk
|
|
4152
|
+
* that is a private-key match; with a signer there is no private key to
|
|
4153
|
+
* match, so compare the certified public key against the signer's own -
|
|
4154
|
+
* the same question, asked the only way an HSM allows.
|
|
4155
|
+
*/
|
|
4156
|
+
async #certificateMatchesOurKey(certPem, certDer) {
|
|
4157
|
+
if (this.#signer) {
|
|
4158
|
+
const ours = Buffer.from(await this.#signer.getPublicKey());
|
|
4159
|
+
const certified = Buffer.from(new x5092.X509Certificate(certPem).publicKey.rawData);
|
|
4160
|
+
return ours.equals(certified);
|
|
4161
|
+
}
|
|
4162
|
+
return certificateMatchesPrivateKey(certDer, await this.getPrivateKey());
|
|
4163
|
+
}
|
|
4164
|
+
/** Reject the key-file-only operations up front, rather than failing on a missing file later. */
|
|
4165
|
+
#assertKeyIsOnDisk(what) {
|
|
4166
|
+
if (this.#signer) {
|
|
4167
|
+
throw new Error(
|
|
4168
|
+
`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.`
|
|
4169
|
+
);
|
|
4170
|
+
}
|
|
4171
|
+
}
|
|
4172
|
+
/**
|
|
4173
|
+
* @internal The key every signing operation goes through. An injected
|
|
4174
|
+
* {@link CaSigner} is returned as-is; otherwise the on-disk key is read
|
|
4175
|
+
* (and decrypted) and handed over as a plain `CryptoKey`. Both are
|
|
4176
|
+
* accepted by the `node-opcua-crypto` signing primitives, so callers
|
|
4177
|
+
* never branch on which one they got.
|
|
4178
|
+
*/
|
|
4179
|
+
async _getSigningKey() {
|
|
4180
|
+
if (this.#signer) {
|
|
4181
|
+
return this.#signer;
|
|
4182
|
+
}
|
|
4183
|
+
return privateKeyToCryptoKey2(await this.getPrivateKey());
|
|
3348
4184
|
}
|
|
3349
4185
|
/**
|
|
3350
4186
|
* Enable, disable, or rotate the passphrase protecting `private/cakey.pem`
|
|
@@ -3353,18 +4189,19 @@ var init_certificate_authority = __esm({
|
|
|
3353
4189
|
* passphrase to continue using it.
|
|
3354
4190
|
*/
|
|
3355
4191
|
async reencryptPrivateKey(oldPassphrase, newPassphrase) {
|
|
4192
|
+
this.#assertKeyIsOnDisk("reencryptPrivateKey");
|
|
3356
4193
|
const oldPass = await resolvePrivateKeyPassphrase(oldPassphrase);
|
|
3357
4194
|
const newPass = await resolvePrivateKeyPassphrase(newPassphrase);
|
|
3358
|
-
const key =
|
|
4195
|
+
const key = readPrivateKey3(this.privateKey, oldPass);
|
|
3359
4196
|
await this.#rewritePrivateKeyFile(key, newPass);
|
|
3360
4197
|
}
|
|
3361
4198
|
async #rewritePrivateKeyFile(privateKey, passphrase) {
|
|
3362
4199
|
const tmpFilename = `${this.privateKey}.${process.pid}-${Date.now()}.tmp`;
|
|
3363
4200
|
try {
|
|
3364
4201
|
await writePrivateKeyFile2(tmpFilename, privateKey, { passphrase });
|
|
3365
|
-
await
|
|
4202
|
+
await fs12.promises.rename(tmpFilename, this.privateKey);
|
|
3366
4203
|
} finally {
|
|
3367
|
-
await
|
|
4204
|
+
await fs12.promises.rm(tmpFilename, { force: true });
|
|
3368
4205
|
}
|
|
3369
4206
|
}
|
|
3370
4207
|
/** @internal resolve the configured passphrase, at most once per instance */
|
|
@@ -3375,10 +4212,6 @@ var init_certificate_authority = __esm({
|
|
|
3375
4212
|
}
|
|
3376
4213
|
return this.#resolvedPassphrase;
|
|
3377
4214
|
}
|
|
3378
|
-
/** @internal `-passin env:` argv + env for every openssl call that loads this CA's key (always emitted, empty when none) */
|
|
3379
|
-
async _opensslPassin() {
|
|
3380
|
-
return passinArg(await this._privateKeyPassphrase());
|
|
3381
|
-
}
|
|
3382
4215
|
/**
|
|
3383
4216
|
* @internal On an existing key: encrypt it in place if a passphrase is
|
|
3384
4217
|
* configured and it is still plaintext (secure by default: the option
|
|
@@ -3387,16 +4220,58 @@ var init_certificate_authority = __esm({
|
|
|
3387
4220
|
* first signing operation.
|
|
3388
4221
|
*/
|
|
3389
4222
|
async _ensurePrivateKeyProtection() {
|
|
3390
|
-
if (!
|
|
4223
|
+
if (this.#signer || !fs12.existsSync(this.privateKey)) {
|
|
3391
4224
|
return;
|
|
3392
4225
|
}
|
|
3393
4226
|
if (this.#privateKeyPassphrase !== void 0 && !isEncryptedPrivateKeyFile(this.privateKey)) {
|
|
3394
4227
|
warningLog("CertificateAuthority: private key is plaintext but a passphrase is configured; encrypting it in place");
|
|
3395
|
-
const plaintextKey =
|
|
4228
|
+
const plaintextKey = readPrivateKey3(this.privateKey);
|
|
3396
4229
|
await this.#rewritePrivateKeyFile(plaintextKey, await this._privateKeyPassphrase());
|
|
3397
4230
|
}
|
|
3398
4231
|
await this.getPrivateKey();
|
|
3399
4232
|
}
|
|
4233
|
+
/**
|
|
4234
|
+
* Acquire a file-based lock on this CA's directory for the duration of
|
|
4235
|
+
* `action` — serializes every operation that mutates the certificate
|
|
4236
|
+
* database (`index.txt`, `serial`, `crlnumber`, `certs/`,
|
|
4237
|
+
* `crl/revocation_list.*`) or the CA's own key/certificate, so
|
|
4238
|
+
* concurrent calls on one `CertificateAuthority` (or two instances
|
|
4239
|
+
* pointed at the same directory, in this or another process) cannot
|
|
4240
|
+
* interleave. Mirrors `CertificateManager.withLock2`.
|
|
4241
|
+
*
|
|
4242
|
+
* Only ever call this from a top-level public operation — nested calls
|
|
4243
|
+
* on the same instance would deadlock, since the underlying file lock
|
|
4244
|
+
* is not reentrant.
|
|
4245
|
+
*
|
|
4246
|
+
* The wait is bounded: a waiter gives up (throws) after
|
|
4247
|
+
* `CA_LOCK_MAX_WAIT_MS`. Without a bound, a holder whose openssl child
|
|
4248
|
+
* hangs would block every CA operation in every process forever and
|
|
4249
|
+
* silently — the lock library's keepalive keeps refreshing the lock
|
|
4250
|
+
* file's mtime as long as the holder process is alive, so stale-lock
|
|
4251
|
+
* recovery never fires for a live-but-stuck holder. A dead holder's
|
|
4252
|
+
* lock goes stale (default 2 minutes) and is taken over well within
|
|
4253
|
+
* this bound.
|
|
4254
|
+
*/
|
|
4255
|
+
async #withCaLock(action) {
|
|
4256
|
+
const lockFileName = path9.join(this.rootDir, ".ca.lock");
|
|
4257
|
+
return withLock2(
|
|
4258
|
+
{
|
|
4259
|
+
fileToLock: lockFileName,
|
|
4260
|
+
retries: { forever: true, maxRetryTime: CA_LOCK_MAX_WAIT_MS, ...CA_LOCK_RETRY }
|
|
4261
|
+
},
|
|
4262
|
+
action
|
|
4263
|
+
);
|
|
4264
|
+
}
|
|
4265
|
+
/**
|
|
4266
|
+
* @internal Run `action` under this CA's directory lock. For the one
|
|
4267
|
+
* legitimate cross-instance use: a subordinate CA's bootstrap signs its
|
|
4268
|
+
* certificate with `openssl x509 -CAserial`, which read-increment-writes
|
|
4269
|
+
* THIS issuer's `serial` file, and must therefore hold this issuer's
|
|
4270
|
+
* lock (ordering is always subordinate -> issuer, so no cycle).
|
|
4271
|
+
*/
|
|
4272
|
+
async _withCaDirectoryLock(action) {
|
|
4273
|
+
return this.#withCaLock(action);
|
|
4274
|
+
}
|
|
3400
4275
|
/** Path to the CA certificate in PEM format (`public/cacert.pem`). */
|
|
3401
4276
|
get caCertificate() {
|
|
3402
4277
|
return makePath(this.rootDir, "./public/cacert.pem");
|
|
@@ -3444,8 +4319,8 @@ var init_certificate_authority = __esm({
|
|
|
3444
4319
|
* (call {@link initialize} first).
|
|
3445
4320
|
*/
|
|
3446
4321
|
getCACertificateDER() {
|
|
3447
|
-
const pem =
|
|
3448
|
-
return
|
|
4322
|
+
const pem = readCertificatePEM2(this.caCertificate);
|
|
4323
|
+
return convertPEMtoDER2(pem);
|
|
3449
4324
|
}
|
|
3450
4325
|
/**
|
|
3451
4326
|
* Return the CA certificate as a PEM-encoded string.
|
|
@@ -3454,7 +4329,7 @@ var init_certificate_authority = __esm({
|
|
|
3454
4329
|
* (call {@link initialize} first).
|
|
3455
4330
|
*/
|
|
3456
4331
|
getCACertificatePEM() {
|
|
3457
|
-
const raw =
|
|
4332
|
+
const raw = readCertificatePEM2(this.caCertificate);
|
|
3458
4333
|
const beginMarker = "-----BEGIN CERTIFICATE-----";
|
|
3459
4334
|
const idx = raw.indexOf(beginMarker);
|
|
3460
4335
|
if (idx > 0) {
|
|
@@ -3470,10 +4345,10 @@ var init_certificate_authority = __esm({
|
|
|
3470
4345
|
*/
|
|
3471
4346
|
getCRLDER() {
|
|
3472
4347
|
const crlPath = this.revocationListDER;
|
|
3473
|
-
if (!
|
|
4348
|
+
if (!fs12.existsSync(crlPath)) {
|
|
3474
4349
|
return Buffer.alloc(0);
|
|
3475
4350
|
}
|
|
3476
|
-
return
|
|
4351
|
+
return fs12.readFileSync(crlPath);
|
|
3477
4352
|
}
|
|
3478
4353
|
/**
|
|
3479
4354
|
* Return the current Certificate Revocation List as a
|
|
@@ -3483,10 +4358,10 @@ var init_certificate_authority = __esm({
|
|
|
3483
4358
|
*/
|
|
3484
4359
|
getCRLPEM() {
|
|
3485
4360
|
const crlPath = this.revocationList;
|
|
3486
|
-
if (!
|
|
4361
|
+
if (!fs12.existsSync(crlPath)) {
|
|
3487
4362
|
return "";
|
|
3488
4363
|
}
|
|
3489
|
-
const raw =
|
|
4364
|
+
const raw = fs12.readFileSync(crlPath, "utf-8");
|
|
3490
4365
|
const beginMarker = "-----BEGIN X509 CRL-----";
|
|
3491
4366
|
const idx = raw.indexOf(beginMarker);
|
|
3492
4367
|
if (idx > 0) {
|
|
@@ -3505,14 +4380,14 @@ var init_certificate_authority = __esm({
|
|
|
3505
4380
|
* expiry date, and (for revoked certs) the revocation date.
|
|
3506
4381
|
*/
|
|
3507
4382
|
getIssuedCertificates() {
|
|
3508
|
-
return this.
|
|
4383
|
+
return this.#db.readIndex();
|
|
3509
4384
|
}
|
|
3510
4385
|
/**
|
|
3511
4386
|
* Return the total number of certificates recorded in
|
|
3512
4387
|
* `index.txt`.
|
|
3513
4388
|
*/
|
|
3514
4389
|
getIssuedCertificateCount() {
|
|
3515
|
-
return this.
|
|
4390
|
+
return this.#db.readIndex().length;
|
|
3516
4391
|
}
|
|
3517
4392
|
/**
|
|
3518
4393
|
* Return the status of a certificate by its serial number.
|
|
@@ -3522,9 +4397,7 @@ var init_certificate_authority = __esm({
|
|
|
3522
4397
|
* `undefined` if not found
|
|
3523
4398
|
*/
|
|
3524
4399
|
getCertificateStatus(serial) {
|
|
3525
|
-
|
|
3526
|
-
const record = this._parseIndexTxt().find((r) => r.serial.toUpperCase() === upper);
|
|
3527
|
-
return record?.status;
|
|
4400
|
+
return this.#db.findBySerial(serial)?.status;
|
|
3528
4401
|
}
|
|
3529
4402
|
/**
|
|
3530
4403
|
* Read a specific issued certificate by serial number and
|
|
@@ -3537,82 +4410,13 @@ var init_certificate_authority = __esm({
|
|
|
3537
4410
|
* @returns the DER buffer, or `undefined` if not found
|
|
3538
4411
|
*/
|
|
3539
4412
|
getCertificateBySerial(serial) {
|
|
3540
|
-
|
|
3541
|
-
const certFile = path6.join(this.rootDir, "certs", `${upper}.pem`);
|
|
3542
|
-
if (!fs10.existsSync(certFile)) {
|
|
3543
|
-
return void 0;
|
|
3544
|
-
}
|
|
3545
|
-
const pem = readCertificatePEM(certFile);
|
|
3546
|
-
return convertPEMtoDER(pem);
|
|
4413
|
+
return this.#db.getCertificateBySerial(serial);
|
|
3547
4414
|
}
|
|
3548
4415
|
/**
|
|
3549
4416
|
* Path to the OpenSSL certificate database file.
|
|
3550
4417
|
*/
|
|
3551
4418
|
get indexFile() {
|
|
3552
|
-
return
|
|
3553
|
-
}
|
|
3554
|
-
/**
|
|
3555
|
-
* Parse the OpenSSL `index.txt` certificate database.
|
|
3556
|
-
*
|
|
3557
|
-
* Each line has tab-separated fields:
|
|
3558
|
-
* ```
|
|
3559
|
-
* status expiry [revocationDate] serial unknown subject
|
|
3560
|
-
* ```
|
|
3561
|
-
*
|
|
3562
|
-
* - status: `V` (valid), `R` (revoked), `E` (expired)
|
|
3563
|
-
* - expiry: `YYMMDDHHmmssZ`
|
|
3564
|
-
* - revocationDate: present only for revoked certs
|
|
3565
|
-
* - serial: hex string
|
|
3566
|
-
* - unknown: always `"unknown"`
|
|
3567
|
-
* - subject: X.500 slash-delimited string
|
|
3568
|
-
*/
|
|
3569
|
-
_parseIndexTxt() {
|
|
3570
|
-
const indexPath = this.indexFile;
|
|
3571
|
-
if (!fs10.existsSync(indexPath)) {
|
|
3572
|
-
return [];
|
|
3573
|
-
}
|
|
3574
|
-
const content = fs10.readFileSync(indexPath, "utf-8");
|
|
3575
|
-
const lines = content.split("\n").filter((l) => l.trim().length > 0);
|
|
3576
|
-
const records = [];
|
|
3577
|
-
for (const line of lines) {
|
|
3578
|
-
const fields = line.split(" ");
|
|
3579
|
-
if (fields.length < 4) continue;
|
|
3580
|
-
const statusChar = fields[0];
|
|
3581
|
-
const expiryStr = fields[1];
|
|
3582
|
-
let serial;
|
|
3583
|
-
let subject;
|
|
3584
|
-
let revocationDate;
|
|
3585
|
-
if (statusChar === "R") {
|
|
3586
|
-
revocationDate = fields[2];
|
|
3587
|
-
serial = fields[3];
|
|
3588
|
-
subject = fields.length >= 6 ? fields[5] : "";
|
|
3589
|
-
} else {
|
|
3590
|
-
serial = fields[3];
|
|
3591
|
-
subject = fields.length >= 6 ? fields[5] : "";
|
|
3592
|
-
}
|
|
3593
|
-
let status;
|
|
3594
|
-
switch (statusChar) {
|
|
3595
|
-
case "V":
|
|
3596
|
-
status = "valid";
|
|
3597
|
-
break;
|
|
3598
|
-
case "R":
|
|
3599
|
-
status = "revoked";
|
|
3600
|
-
break;
|
|
3601
|
-
case "E":
|
|
3602
|
-
status = "expired";
|
|
3603
|
-
break;
|
|
3604
|
-
default:
|
|
3605
|
-
continue;
|
|
3606
|
-
}
|
|
3607
|
-
records.push({
|
|
3608
|
-
serial,
|
|
3609
|
-
status,
|
|
3610
|
-
subject,
|
|
3611
|
-
expiryDate: parseOpenSSLDate(expiryStr),
|
|
3612
|
-
revocationDate: revocationDate ? parseOpenSSLDate(revocationDate) : void 0
|
|
3613
|
-
});
|
|
3614
|
-
}
|
|
3615
|
-
return records;
|
|
4419
|
+
return this.#db.indexFile;
|
|
3616
4420
|
}
|
|
3617
4421
|
// ---------------------------------------------------------------
|
|
3618
4422
|
// Buffer-based CA operations (US-058)
|
|
@@ -3634,12 +4438,12 @@ var init_certificate_authority = __esm({
|
|
|
3634
4438
|
* @returns the signed certificate as a DER-encoded buffer
|
|
3635
4439
|
*/
|
|
3636
4440
|
async signCertificateRequestFromDER(csrDer, options) {
|
|
3637
|
-
const tmpDir = await
|
|
4441
|
+
const tmpDir = await fs12.promises.mkdtemp(path9.join(os4.tmpdir(), "pki-sign-"));
|
|
3638
4442
|
try {
|
|
3639
|
-
const csrFile =
|
|
3640
|
-
const certFile =
|
|
4443
|
+
const csrFile = path9.join(tmpDir, "request.csr");
|
|
4444
|
+
const certFile = path9.join(tmpDir, "certificate.pem");
|
|
3641
4445
|
const csrPem = toPem2(csrDer, "CERTIFICATE REQUEST");
|
|
3642
|
-
await
|
|
4446
|
+
await fs12.promises.writeFile(csrFile, csrPem, "utf-8");
|
|
3643
4447
|
const signingParams = {};
|
|
3644
4448
|
if (options?.validityMs !== void 0) signingParams.validityMs = options.validityMs;
|
|
3645
4449
|
else signingParams.validity = options?.validity ?? 365;
|
|
@@ -3649,10 +4453,10 @@ var init_certificate_authority = __esm({
|
|
|
3649
4453
|
if (options?.applicationUri) signingParams.applicationUri = options.applicationUri;
|
|
3650
4454
|
if (options?.subject) signingParams.subject = options.subject;
|
|
3651
4455
|
await this.signCertificateRequest(certFile, csrFile, signingParams);
|
|
3652
|
-
const certPem =
|
|
3653
|
-
return
|
|
4456
|
+
const certPem = readCertificatePEM2(certFile);
|
|
4457
|
+
return convertPEMtoDER2(certPem);
|
|
3654
4458
|
} finally {
|
|
3655
|
-
await
|
|
4459
|
+
await fs12.promises.rm(tmpDir, {
|
|
3656
4460
|
recursive: true,
|
|
3657
4461
|
force: true
|
|
3658
4462
|
});
|
|
@@ -3702,27 +4506,35 @@ var init_certificate_authority = __esm({
|
|
|
3702
4506
|
* @returns `{ certificateDer, privateKey }` — certificate as DER,
|
|
3703
4507
|
* private key as a branded `PrivateKey` buffer
|
|
3704
4508
|
*/
|
|
4509
|
+
/**
|
|
4510
|
+
* An ephemeral key and a CSR for it, written into `tmpDir`. Both
|
|
4511
|
+
* `generateKeyPairAndSign*` methods need exactly this, and neither
|
|
4512
|
+
* needs a subprocess for it: the key comes from node's crypto and the
|
|
4513
|
+
* request is built and self-signed in process, so no `openssl.cnf` has
|
|
4514
|
+
* to be rendered either.
|
|
4515
|
+
*/
|
|
4516
|
+
async #createEphemeralKeyAndCsr(tmpDir, keySize, options) {
|
|
4517
|
+
const privateKeyFile = path9.join(tmpDir, "private_key.pem");
|
|
4518
|
+
await generatePrivateKeyFile2(privateKeyFile, keySize);
|
|
4519
|
+
const { csr } = await createCertificateSigningRequest3({
|
|
4520
|
+
privateKey: await privateKeyToCryptoKey2(readPrivateKey3(privateKeyFile)),
|
|
4521
|
+
subject: options.subject ? new Subject6(options.subject).toString() : void 0,
|
|
4522
|
+
applicationUri: options.applicationUri,
|
|
4523
|
+
dns: options.dns ?? [],
|
|
4524
|
+
ip: options.ip ?? [],
|
|
4525
|
+
purpose: CertificatePurpose3.ForApplication
|
|
4526
|
+
});
|
|
4527
|
+
const csrFile = path9.join(tmpDir, "request.csr");
|
|
4528
|
+
await fs12.promises.writeFile(csrFile, csr);
|
|
4529
|
+
return { privateKeyFile, csrFile };
|
|
4530
|
+
}
|
|
3705
4531
|
async generateKeyPairAndSignDER(options) {
|
|
3706
4532
|
const keySize = options.keySize ?? 2048;
|
|
3707
4533
|
const startDate = options.startDate ?? /* @__PURE__ */ new Date();
|
|
3708
|
-
const tmpDir = await
|
|
4534
|
+
const tmpDir = await fs12.promises.mkdtemp(path9.join(os4.tmpdir(), "pki-keygen-"));
|
|
3709
4535
|
try {
|
|
3710
|
-
const privateKeyFile =
|
|
3711
|
-
|
|
3712
|
-
const configFile = path6.join(tmpDir, "openssl.cnf");
|
|
3713
|
-
await fs10.promises.writeFile(configFile, configurationFileSimpleTemplate2, "utf-8");
|
|
3714
|
-
const csrFile = path6.join(tmpDir, "request.csr");
|
|
3715
|
-
await createCertificateSigningRequestWithOpenSSL(csrFile, {
|
|
3716
|
-
rootDir: tmpDir,
|
|
3717
|
-
configFile,
|
|
3718
|
-
privateKey: privateKeyFile,
|
|
3719
|
-
applicationUri: options.applicationUri,
|
|
3720
|
-
subject: options.subject,
|
|
3721
|
-
dns: options.dns ?? [],
|
|
3722
|
-
ip: options.ip ?? [],
|
|
3723
|
-
purpose: CertificatePurpose2.ForApplication
|
|
3724
|
-
});
|
|
3725
|
-
const certFile = path6.join(tmpDir, "certificate.pem");
|
|
4536
|
+
const { privateKeyFile, csrFile } = await this.#createEphemeralKeyAndCsr(tmpDir, keySize, options);
|
|
4537
|
+
const certFile = path9.join(tmpDir, "certificate.pem");
|
|
3726
4538
|
const signingParams = {
|
|
3727
4539
|
applicationUri: options.applicationUri,
|
|
3728
4540
|
dns: options.dns,
|
|
@@ -3732,12 +4544,12 @@ var init_certificate_authority = __esm({
|
|
|
3732
4544
|
if (options.validityMs !== void 0) signingParams.validityMs = options.validityMs;
|
|
3733
4545
|
else signingParams.validity = options.validity ?? 365;
|
|
3734
4546
|
await this.signCertificateRequest(certFile, csrFile, signingParams);
|
|
3735
|
-
const certPem =
|
|
3736
|
-
const certificateDer =
|
|
3737
|
-
const privateKey =
|
|
4547
|
+
const certPem = readCertificatePEM2(certFile);
|
|
4548
|
+
const certificateDer = convertPEMtoDER2(certPem);
|
|
4549
|
+
const privateKey = readPrivateKey3(privateKeyFile);
|
|
3738
4550
|
return { certificateDer, privateKey };
|
|
3739
4551
|
} finally {
|
|
3740
|
-
await
|
|
4552
|
+
await fs12.promises.rm(tmpDir, {
|
|
3741
4553
|
recursive: true,
|
|
3742
4554
|
force: true
|
|
3743
4555
|
});
|
|
@@ -3758,24 +4570,10 @@ var init_certificate_authority = __esm({
|
|
|
3758
4570
|
const keySize = options.keySize ?? 2048;
|
|
3759
4571
|
const startDate = options.startDate ?? /* @__PURE__ */ new Date();
|
|
3760
4572
|
const passphrase = options.passphrase ?? "";
|
|
3761
|
-
const tmpDir = await
|
|
4573
|
+
const tmpDir = await fs12.promises.mkdtemp(path9.join(os4.tmpdir(), "pki-keygen-pfx-"));
|
|
3762
4574
|
try {
|
|
3763
|
-
const privateKeyFile =
|
|
3764
|
-
|
|
3765
|
-
const configFile = path6.join(tmpDir, "openssl.cnf");
|
|
3766
|
-
await fs10.promises.writeFile(configFile, configurationFileSimpleTemplate2, "utf-8");
|
|
3767
|
-
const csrFile = path6.join(tmpDir, "request.csr");
|
|
3768
|
-
await createCertificateSigningRequestWithOpenSSL(csrFile, {
|
|
3769
|
-
rootDir: tmpDir,
|
|
3770
|
-
configFile,
|
|
3771
|
-
privateKey: privateKeyFile,
|
|
3772
|
-
applicationUri: options.applicationUri,
|
|
3773
|
-
subject: options.subject,
|
|
3774
|
-
dns: options.dns ?? [],
|
|
3775
|
-
ip: options.ip ?? [],
|
|
3776
|
-
purpose: CertificatePurpose2.ForApplication
|
|
3777
|
-
});
|
|
3778
|
-
const certFile = path6.join(tmpDir, "certificate.pem");
|
|
4575
|
+
const { privateKeyFile, csrFile } = await this.#createEphemeralKeyAndCsr(tmpDir, keySize, options);
|
|
4576
|
+
const certFile = path9.join(tmpDir, "certificate.pem");
|
|
3779
4577
|
const signingParams = {
|
|
3780
4578
|
applicationUri: options.applicationUri,
|
|
3781
4579
|
dns: options.dns,
|
|
@@ -3785,17 +4583,20 @@ var init_certificate_authority = __esm({
|
|
|
3785
4583
|
if (options.validityMs !== void 0) signingParams.validityMs = options.validityMs;
|
|
3786
4584
|
else signingParams.validity = options.validity ?? 365;
|
|
3787
4585
|
await this.signCertificateRequest(certFile, csrFile, signingParams);
|
|
3788
|
-
const
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
4586
|
+
const blocks = (await fs12.promises.readFile(certFile, "utf-8")).match(
|
|
4587
|
+
/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g
|
|
4588
|
+
);
|
|
4589
|
+
if (!blocks || blocks.length === 0) {
|
|
4590
|
+
throw new Error(`generateKeyPairAndSignPFX: no certificate was produced in ${certFile}`);
|
|
4591
|
+
}
|
|
4592
|
+
return await createPfx({
|
|
4593
|
+
certificate: convertPEMtoDER2(blocks[0]),
|
|
4594
|
+
certificateChain: blocks.slice(1).map(convertPEMtoDER2),
|
|
4595
|
+
privateKey: readPrivateKey3(privateKeyFile),
|
|
4596
|
+
password: passphrase
|
|
3795
4597
|
});
|
|
3796
|
-
return await fs10.promises.readFile(pfxFile);
|
|
3797
4598
|
} finally {
|
|
3798
|
-
await
|
|
4599
|
+
await fs12.promises.rm(tmpDir, {
|
|
3799
4600
|
recursive: true,
|
|
3800
4601
|
force: true
|
|
3801
4602
|
});
|
|
@@ -3816,8 +4617,8 @@ var init_certificate_authority = __esm({
|
|
|
3816
4617
|
async revokeCertificateDER(certDer, reason) {
|
|
3817
4618
|
const info = exploreCertificate2(certDer);
|
|
3818
4619
|
const serial = info.tbsCertificate.serialNumber.replace(/:/g, "").toUpperCase();
|
|
3819
|
-
const storedCertFile =
|
|
3820
|
-
if (!
|
|
4620
|
+
const storedCertFile = path9.join(this.rootDir, "certs", `${serial}.pem`);
|
|
4621
|
+
if (!fs12.existsSync(storedCertFile)) {
|
|
3821
4622
|
throw new Error(`Cannot revoke: no stored certificate found for serial ${serial} at ${storedCertFile}`);
|
|
3822
4623
|
}
|
|
3823
4624
|
await this.revokeCertificate(storedCertFile, {
|
|
@@ -3830,7 +4631,69 @@ var init_certificate_authority = __esm({
|
|
|
3830
4631
|
* already exist.
|
|
3831
4632
|
*/
|
|
3832
4633
|
async initialize() {
|
|
3833
|
-
|
|
4634
|
+
mkdirRecursiveSync(path9.resolve(this.rootDir));
|
|
4635
|
+
await this.#withCaLock(() => this.#bootstrap());
|
|
4636
|
+
}
|
|
4637
|
+
/**
|
|
4638
|
+
* @internal Shared (backend-agnostic) part of `initialize()`: directory
|
|
4639
|
+
* layout, default database files, the "already initialized" / "partial
|
|
4640
|
+
* init" checks, and the openssl config file — then delegates CSR
|
|
4641
|
+
* generation, CA-certificate signing, and the initial CRL to
|
|
4642
|
+
* {@link CaBackend.bootstrap}. Must be called under {@link #withCaLock}.
|
|
4643
|
+
*/
|
|
4644
|
+
async #bootstrap() {
|
|
4645
|
+
const caRootDir = path9.resolve(this.rootDir);
|
|
4646
|
+
mkdirRecursiveSync(caRootDir);
|
|
4647
|
+
ensurePrivateDirectory(path9.join(caRootDir, "private"));
|
|
4648
|
+
mkdirRecursiveSync(path9.join(caRootDir, "public"));
|
|
4649
|
+
mkdirRecursiveSync(path9.join(caRootDir, "certs"));
|
|
4650
|
+
mkdirRecursiveSync(path9.join(caRootDir, "crl"));
|
|
4651
|
+
mkdirRecursiveSync(path9.join(caRootDir, "conf"));
|
|
4652
|
+
const serial = path9.join(caRootDir, "serial");
|
|
4653
|
+
if (!fs12.existsSync(serial)) {
|
|
4654
|
+
await fs12.promises.writeFile(serial, "1000");
|
|
4655
|
+
}
|
|
4656
|
+
const crlNumber = path9.join(caRootDir, "crlnumber");
|
|
4657
|
+
if (!fs12.existsSync(crlNumber)) {
|
|
4658
|
+
await fs12.promises.writeFile(crlNumber, "1000");
|
|
4659
|
+
}
|
|
4660
|
+
const indexFile = path9.join(caRootDir, "index.txt");
|
|
4661
|
+
if (!fs12.existsSync(indexFile)) {
|
|
4662
|
+
await fs12.promises.writeFile(indexFile, "");
|
|
4663
|
+
}
|
|
4664
|
+
const signerBacked = this.hasExternalSigner;
|
|
4665
|
+
const caKeyExists = signerBacked || fs12.existsSync(path9.join(caRootDir, "private/cakey.pem"));
|
|
4666
|
+
const caCertExists = fs12.existsSync(path9.join(caRootDir, "public/cacert.pem"));
|
|
4667
|
+
if (caKeyExists && caCertExists && !config3.forceCA) {
|
|
4668
|
+
if (!signerBacked) {
|
|
4669
|
+
restrictPrivateFilePermissions(path9.join(caRootDir, "private/cakey.pem"), 384);
|
|
4670
|
+
await this._ensurePrivateKeyProtection();
|
|
4671
|
+
}
|
|
4672
|
+
debugLog("CA private key and certificate already exist ... skipping");
|
|
4673
|
+
return;
|
|
4674
|
+
}
|
|
4675
|
+
if (!signerBacked && caKeyExists && !caCertExists) {
|
|
4676
|
+
debugLog("CA private key exists but cacert.pem is missing \u2014 rebuilding CA");
|
|
4677
|
+
fs12.unlinkSync(path9.join(caRootDir, "private/cakey.pem"));
|
|
4678
|
+
const staleCsr = path9.join(caRootDir, "private/cakey.csr");
|
|
4679
|
+
if (fs12.existsSync(staleCsr)) {
|
|
4680
|
+
fs12.unlinkSync(staleCsr);
|
|
4681
|
+
}
|
|
4682
|
+
}
|
|
4683
|
+
displayTitle("Create Certificate Authority (CA)");
|
|
4684
|
+
const indexFileAttr = path9.join(caRootDir, "index.txt.attr");
|
|
4685
|
+
if (!fs12.existsSync(indexFileAttr)) {
|
|
4686
|
+
await fs12.promises.writeFile(indexFileAttr, "unique_subject = no");
|
|
4687
|
+
}
|
|
4688
|
+
const caConfigFile = this.configFile;
|
|
4689
|
+
await fs12.promises.writeFile(caConfigFile, renderCaConfig(caRootDir));
|
|
4690
|
+
if (!signerBacked) {
|
|
4691
|
+
const privateKeyFilename = path9.join(caRootDir, "private/cakey.pem");
|
|
4692
|
+
displayTitle(`Generate the CA private Key - ${this.keySize}`);
|
|
4693
|
+
await generatePrivateKeyFile2(privateKeyFilename, this.keySize, { passphrase: await this._privateKeyPassphrase() });
|
|
4694
|
+
restrictPrivateFilePermissions(privateKeyFilename, 384);
|
|
4695
|
+
}
|
|
4696
|
+
await this.#backend.bootstrap(this);
|
|
3834
4697
|
}
|
|
3835
4698
|
/**
|
|
3836
4699
|
* Initialize the CA directory structure and generate the
|
|
@@ -3852,21 +4715,25 @@ var init_certificate_authority = __esm({
|
|
|
3852
4715
|
* @returns an {@link InitializeCSRResult} describing the CA state
|
|
3853
4716
|
*/
|
|
3854
4717
|
async initializeCSR() {
|
|
3855
|
-
const caRootDir =
|
|
4718
|
+
const caRootDir = path9.resolve(this.rootDir);
|
|
3856
4719
|
mkdirRecursiveSync(caRootDir);
|
|
4720
|
+
return this.#withCaLock(() => this.#initializeCSRLocked(caRootDir));
|
|
4721
|
+
}
|
|
4722
|
+
async #initializeCSRLocked(caRootDir) {
|
|
3857
4723
|
for (const dir of ["public", "certs", "crl", "conf"]) {
|
|
3858
|
-
mkdirRecursiveSync(
|
|
4724
|
+
mkdirRecursiveSync(path9.join(caRootDir, dir));
|
|
3859
4725
|
}
|
|
3860
|
-
ensurePrivateDirectory(
|
|
4726
|
+
ensurePrivateDirectory(path9.join(caRootDir, "private"));
|
|
3861
4727
|
const caCertFile = this.caCertificate;
|
|
3862
|
-
const privateKeyFile =
|
|
3863
|
-
const csrFile =
|
|
3864
|
-
|
|
4728
|
+
const privateKeyFile = path9.join(caRootDir, "private/cakey.pem");
|
|
4729
|
+
const csrFile = path9.join(caRootDir, "private/cakey.csr");
|
|
4730
|
+
const keyAvailable = this.hasExternalSigner || fs12.existsSync(privateKeyFile);
|
|
4731
|
+
if (!this.hasExternalSigner && fs12.existsSync(privateKeyFile)) {
|
|
3865
4732
|
restrictPrivateFilePermissions(privateKeyFile, 384);
|
|
3866
4733
|
await this._ensurePrivateKeyProtection();
|
|
3867
4734
|
}
|
|
3868
|
-
if (
|
|
3869
|
-
const certDer =
|
|
4735
|
+
if (fs12.existsSync(caCertFile)) {
|
|
4736
|
+
const certDer = convertPEMtoDER2(readCertificatePEM2(caCertFile));
|
|
3870
4737
|
const certInfo = exploreCertificate2(certDer);
|
|
3871
4738
|
const notAfter = certInfo.tbsCertificate.validity.notAfter;
|
|
3872
4739
|
if (notAfter.getTime() < Date.now()) {
|
|
@@ -3877,29 +4744,29 @@ var init_certificate_authority = __esm({
|
|
|
3877
4744
|
debugLog("CA certificate already exists and is valid \u2014 ready");
|
|
3878
4745
|
return { status: "ready" };
|
|
3879
4746
|
}
|
|
3880
|
-
if (
|
|
4747
|
+
if (keyAvailable && fs12.existsSync(csrFile)) {
|
|
3881
4748
|
debugLog("CA key + CSR already exist \u2014 pending external signing");
|
|
3882
4749
|
return { status: "pending", csrPath: csrFile };
|
|
3883
4750
|
}
|
|
3884
|
-
const serial =
|
|
3885
|
-
if (!
|
|
3886
|
-
await
|
|
4751
|
+
const serial = path9.join(caRootDir, "serial");
|
|
4752
|
+
if (!fs12.existsSync(serial)) {
|
|
4753
|
+
await fs12.promises.writeFile(serial, "1000");
|
|
3887
4754
|
}
|
|
3888
|
-
const crlNumber =
|
|
3889
|
-
if (!
|
|
3890
|
-
await
|
|
4755
|
+
const crlNumber = path9.join(caRootDir, "crlnumber");
|
|
4756
|
+
if (!fs12.existsSync(crlNumber)) {
|
|
4757
|
+
await fs12.promises.writeFile(crlNumber, "1000");
|
|
3891
4758
|
}
|
|
3892
|
-
const indexFile =
|
|
3893
|
-
if (!
|
|
3894
|
-
await
|
|
4759
|
+
const indexFile = path9.join(caRootDir, "index.txt");
|
|
4760
|
+
if (!fs12.existsSync(indexFile)) {
|
|
4761
|
+
await fs12.promises.writeFile(indexFile, "");
|
|
3895
4762
|
}
|
|
3896
|
-
const indexFileAttr =
|
|
3897
|
-
if (!
|
|
3898
|
-
await
|
|
4763
|
+
const indexFileAttr = path9.join(caRootDir, "index.txt.attr");
|
|
4764
|
+
if (!fs12.existsSync(indexFileAttr)) {
|
|
4765
|
+
await fs12.promises.writeFile(indexFileAttr, "unique_subject = no");
|
|
3899
4766
|
}
|
|
3900
4767
|
const caConfigFile = this.configFile;
|
|
3901
|
-
await
|
|
3902
|
-
if (!
|
|
4768
|
+
await fs12.promises.writeFile(caConfigFile, renderCaConfig(caRootDir));
|
|
4769
|
+
if (!keyAvailable) {
|
|
3903
4770
|
await generatePrivateKeyFile2(privateKeyFile, this.keySize, { passphrase: await this._privateKeyPassphrase() });
|
|
3904
4771
|
restrictPrivateFilePermissions(privateKeyFile, 384);
|
|
3905
4772
|
}
|
|
@@ -3920,53 +4787,34 @@ var init_certificate_authority = __esm({
|
|
|
3920
4787
|
* renewal is needed, `"ready"` if the cert is still valid
|
|
3921
4788
|
*/
|
|
3922
4789
|
async renewCSR(thresholdDays = 30) {
|
|
3923
|
-
const caRootDir =
|
|
4790
|
+
const caRootDir = path9.resolve(this.rootDir);
|
|
3924
4791
|
const caCertFile = this.caCertificate;
|
|
3925
|
-
const privateKeyFile =
|
|
3926
|
-
const csrFile =
|
|
3927
|
-
if (!
|
|
4792
|
+
const privateKeyFile = path9.join(caRootDir, "private/cakey.pem");
|
|
4793
|
+
const csrFile = path9.join(caRootDir, "private/cakey.csr");
|
|
4794
|
+
if (!fs12.existsSync(caCertFile)) {
|
|
3928
4795
|
return this.initializeCSR();
|
|
3929
4796
|
}
|
|
3930
|
-
const certDer =
|
|
4797
|
+
const certDer = convertPEMtoDER2(readCertificatePEM2(caCertFile));
|
|
3931
4798
|
const certInfo = exploreCertificate2(certDer);
|
|
3932
4799
|
const notAfter = certInfo.tbsCertificate.validity.notAfter;
|
|
3933
4800
|
const thresholdMs = thresholdDays * 24 * 60 * 60 * 1e3;
|
|
3934
4801
|
if (notAfter.getTime() - Date.now() < thresholdMs) {
|
|
3935
4802
|
debugLog(`CA certificate expires within ${thresholdDays} days \u2014 generating renewal CSR`);
|
|
3936
|
-
await this._generateCSR(caRootDir, privateKeyFile, csrFile);
|
|
4803
|
+
await this.#withCaLock(() => this._generateCSR(caRootDir, privateKeyFile, csrFile));
|
|
3937
4804
|
return { status: "expired", csrPath: csrFile, expiryDate: notAfter };
|
|
3938
4805
|
}
|
|
3939
4806
|
return { status: "ready" };
|
|
3940
4807
|
}
|
|
3941
4808
|
/**
|
|
3942
4809
|
* Generate a CSR using the existing private key.
|
|
4810
|
+
* Must be called under {@link #withCaLock} — the lock is taken by the
|
|
4811
|
+
* public callers (`initializeCSR`, `renewCSR`), not here, so that
|
|
4812
|
+
* `initializeCSR` can hold one lock across key generation AND CSR
|
|
4813
|
+
* generation without the non-reentrant file lock deadlocking.
|
|
3943
4814
|
* @internal
|
|
3944
4815
|
*/
|
|
3945
4816
|
async _generateCSR(caRootDir, privateKeyFile, csrFile) {
|
|
3946
|
-
|
|
3947
|
-
const options = { cwd: caRootDir };
|
|
3948
|
-
const configFile = generateStaticConfig("conf/caconfig.cnf", options);
|
|
3949
|
-
const passin = await this._opensslPassin();
|
|
3950
|
-
await execute_openssl(
|
|
3951
|
-
[
|
|
3952
|
-
"req",
|
|
3953
|
-
"-new",
|
|
3954
|
-
"-sha256",
|
|
3955
|
-
"-text",
|
|
3956
|
-
"-extensions",
|
|
3957
|
-
"v3_ca_req",
|
|
3958
|
-
"-config",
|
|
3959
|
-
n5(configFile),
|
|
3960
|
-
"-key",
|
|
3961
|
-
n5(privateKeyFile),
|
|
3962
|
-
"-out",
|
|
3963
|
-
n5(csrFile),
|
|
3964
|
-
"-subj",
|
|
3965
|
-
this.subject.toString(),
|
|
3966
|
-
...passin.args
|
|
3967
|
-
],
|
|
3968
|
-
{ ...options, env: passin.env }
|
|
3969
|
-
);
|
|
4817
|
+
await this.#backend.generateCaCsr(this, caRootDir, privateKeyFile, csrFile);
|
|
3970
4818
|
}
|
|
3971
4819
|
/**
|
|
3972
4820
|
* Install an externally-signed CA certificate and generate
|
|
@@ -3985,43 +4833,41 @@ var init_certificate_authority = __esm({
|
|
|
3985
4833
|
* `status: "success"` or `status: "error"` and a `reason`
|
|
3986
4834
|
*/
|
|
3987
4835
|
async installCACertificate(signedCertFile) {
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
}
|
|
4007
|
-
|
|
4008
|
-
await fs10.promises.writeFile(caCertFile, `${pemBlocks[0]}
|
|
4836
|
+
return this.#withCaLock(async () => {
|
|
4837
|
+
const caCertFile = this.caCertificate;
|
|
4838
|
+
const fullPem = await fs12.promises.readFile(signedCertFile, "utf8");
|
|
4839
|
+
const pemBlocks = fullPem.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g);
|
|
4840
|
+
if (!pemBlocks || pemBlocks.length === 0) {
|
|
4841
|
+
return {
|
|
4842
|
+
status: "error",
|
|
4843
|
+
reason: "no_certificate_found",
|
|
4844
|
+
message: "The provided file does not contain any PEM-encoded certificate."
|
|
4845
|
+
};
|
|
4846
|
+
}
|
|
4847
|
+
const certDer = convertPEMtoDER2(pemBlocks[0]);
|
|
4848
|
+
if (!await this.#certificateMatchesOurKey(pemBlocks[0], certDer)) {
|
|
4849
|
+
return {
|
|
4850
|
+
status: "error",
|
|
4851
|
+
reason: "certificate_key_mismatch",
|
|
4852
|
+
message: "The provided certificate does not match the CA private key. Ensure the certificate was signed from the CSR generated by initializeCSR()."
|
|
4853
|
+
};
|
|
4854
|
+
}
|
|
4855
|
+
await fs12.promises.writeFile(caCertFile, `${pemBlocks[0]}
|
|
4009
4856
|
`);
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4857
|
+
const issuerChainFile = this.issuerCertificateChain;
|
|
4858
|
+
if (pemBlocks.length > 1) {
|
|
4859
|
+
const issuerPem = `${pemBlocks.slice(1).join("\n")}
|
|
4013
4860
|
`;
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4861
|
+
await fs12.promises.writeFile(issuerChainFile, issuerPem);
|
|
4862
|
+
debugLog(`Stored ${pemBlocks.length - 1} issuer certificate(s) in issuer_chain.pem`);
|
|
4863
|
+
} else {
|
|
4864
|
+
if (fs12.existsSync(issuerChainFile)) {
|
|
4865
|
+
await fs12.promises.unlink(issuerChainFile);
|
|
4866
|
+
}
|
|
4019
4867
|
}
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
await regenerateCrl(this.revocationList, ["-config", n5(configFile)], options, await this._opensslPassin());
|
|
4024
|
-
return { status: "success" };
|
|
4868
|
+
await this.#backend.regenerateCrl(this);
|
|
4869
|
+
return { status: "success" };
|
|
4870
|
+
});
|
|
4025
4871
|
}
|
|
4026
4872
|
/**
|
|
4027
4873
|
* Sign a CSR with CA extensions (`v3_ca`), producing a
|
|
@@ -4037,39 +4883,11 @@ var init_certificate_authority = __esm({
|
|
|
4037
4883
|
* @param params - signing parameters
|
|
4038
4884
|
*/
|
|
4039
4885
|
async signCACertificateRequest(certFile, csrFile, params) {
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
const passin = await this._opensslPassin();
|
|
4046
|
-
await execute_openssl(
|
|
4047
|
-
[
|
|
4048
|
-
"x509",
|
|
4049
|
-
"-sha256",
|
|
4050
|
-
"-req",
|
|
4051
|
-
"-days",
|
|
4052
|
-
String(validity),
|
|
4053
|
-
"-text",
|
|
4054
|
-
"-extensions",
|
|
4055
|
-
"v3_ca",
|
|
4056
|
-
"-extfile",
|
|
4057
|
-
n5(configFile),
|
|
4058
|
-
"-in",
|
|
4059
|
-
n5(csrFile),
|
|
4060
|
-
"-CA",
|
|
4061
|
-
n5(this.caCertificate),
|
|
4062
|
-
"-CAkey",
|
|
4063
|
-
n5(path6.join(caRootDir, "private/cakey.pem")),
|
|
4064
|
-
"-CAserial",
|
|
4065
|
-
n5(path6.join(caRootDir, "serial")),
|
|
4066
|
-
"-out",
|
|
4067
|
-
n5(certFile),
|
|
4068
|
-
...passin.args
|
|
4069
|
-
],
|
|
4070
|
-
{ ...options, env: passin.env }
|
|
4071
|
-
);
|
|
4072
|
-
await this.constructCertificateChain(certFile);
|
|
4886
|
+
await this.#withCaLock(async () => {
|
|
4887
|
+
const validity = params.validity ?? 3650;
|
|
4888
|
+
await this.#backend.signSubordinateCsr(this, csrFile, certFile, validity);
|
|
4889
|
+
await this.constructCertificateChain(certFile);
|
|
4890
|
+
});
|
|
4073
4891
|
}
|
|
4074
4892
|
/**
|
|
4075
4893
|
* Rebuild the combined CA certificate + CRL file.
|
|
@@ -4080,13 +4898,13 @@ var init_certificate_authority = __esm({
|
|
|
4080
4898
|
*/
|
|
4081
4899
|
async constructCACertificateWithCRL() {
|
|
4082
4900
|
const cacertWithCRL = this.caCertificateWithCrl;
|
|
4083
|
-
if (
|
|
4084
|
-
await
|
|
4901
|
+
if (fs12.existsSync(this.revocationList)) {
|
|
4902
|
+
await fs12.promises.writeFile(
|
|
4085
4903
|
cacertWithCRL,
|
|
4086
|
-
|
|
4904
|
+
fs12.readFileSync(this.caCertificate, "utf8") + fs12.readFileSync(this.revocationList, "utf8")
|
|
4087
4905
|
);
|
|
4088
4906
|
} else {
|
|
4089
|
-
await
|
|
4907
|
+
await fs12.promises.writeFile(cacertWithCRL, fs12.readFileSync(this.caCertificate));
|
|
4090
4908
|
}
|
|
4091
4909
|
}
|
|
4092
4910
|
/**
|
|
@@ -4096,15 +4914,15 @@ var init_certificate_authority = __esm({
|
|
|
4096
4914
|
* @param certificate - path to the certificate file to extend
|
|
4097
4915
|
*/
|
|
4098
4916
|
async constructCertificateChain(certificate) {
|
|
4099
|
-
|
|
4100
|
-
|
|
4917
|
+
assert9(fs12.existsSync(certificate));
|
|
4918
|
+
assert9(fs12.existsSync(this.caCertificate));
|
|
4101
4919
|
debugLog(chalk6.yellow(" certificate file :"), chalk6.cyan(certificate));
|
|
4102
|
-
let chain = await
|
|
4103
|
-
chain += await
|
|
4104
|
-
if (
|
|
4105
|
-
chain += await
|
|
4920
|
+
let chain = await fs12.promises.readFile(certificate, "utf8");
|
|
4921
|
+
chain += await fs12.promises.readFile(this.caCertificate, "utf8");
|
|
4922
|
+
if (fs12.existsSync(this.issuerCertificateChain)) {
|
|
4923
|
+
chain += await fs12.promises.readFile(this.issuerCertificateChain, "utf8");
|
|
4106
4924
|
}
|
|
4107
|
-
await
|
|
4925
|
+
await fs12.promises.writeFile(certificate, chain);
|
|
4108
4926
|
}
|
|
4109
4927
|
/**
|
|
4110
4928
|
* Create a self-signed certificate using OpenSSL.
|
|
@@ -4114,52 +4932,30 @@ var init_certificate_authority = __esm({
|
|
|
4114
4932
|
* @param params - certificate parameters (subject, validity, SANs)
|
|
4115
4933
|
*/
|
|
4116
4934
|
async createSelfSignedCertificate(certificateFile, privateKey, params) {
|
|
4117
|
-
|
|
4118
|
-
|
|
4935
|
+
assert9(typeof privateKey === "string");
|
|
4936
|
+
assert9(fs12.existsSync(privateKey));
|
|
4119
4937
|
if (!certificateFileExist(certificateFile)) {
|
|
4120
4938
|
return;
|
|
4121
4939
|
}
|
|
4122
4940
|
adjustDate(params);
|
|
4123
4941
|
adjustApplicationUri(params);
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
)
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
[
|
|
4142
|
-
"ca",
|
|
4143
|
-
"-selfsign",
|
|
4144
|
-
"-keyfile",
|
|
4145
|
-
n5(privateKey),
|
|
4146
|
-
"-startdate",
|
|
4147
|
-
x509Date(params.startDate),
|
|
4148
|
-
"-enddate",
|
|
4149
|
-
x509Date(params.endDate),
|
|
4150
|
-
"-batch",
|
|
4151
|
-
"-out",
|
|
4152
|
-
n5(certificateFile),
|
|
4153
|
-
"-in",
|
|
4154
|
-
n5(csrFile)
|
|
4155
|
-
],
|
|
4156
|
-
options
|
|
4157
|
-
);
|
|
4158
|
-
displaySubtitle("- dump the certificate for a check");
|
|
4159
|
-
await execute_openssl(["x509", "-in", n5(certificateFile), "-dates", "-fingerprint", "-purpose", "-noout"], {});
|
|
4160
|
-
displaySubtitle("- verify self-signed certificate");
|
|
4161
|
-
await execute_openssl_no_failure(["verify", "-verbose", "-CAfile", n5(certificateFile), n5(certificateFile)], options);
|
|
4162
|
-
await fs10.promises.unlink(csrFile);
|
|
4942
|
+
params.dns = params.dns || [];
|
|
4943
|
+
params.ip = params.ip || [];
|
|
4944
|
+
await this.#withCaLock(async () => {
|
|
4945
|
+
await this.#backend.createSelfSignedCertificate(this, certificateFile, privateKey, params);
|
|
4946
|
+
});
|
|
4947
|
+
}
|
|
4948
|
+
/**
|
|
4949
|
+
* Regenerate `crl/revocation_list.{crl,der}` from the current database
|
|
4950
|
+
* state, without changing any certificate's status. Normally
|
|
4951
|
+
* unnecessary — `revokeCertificate` already regenerates the CRL as
|
|
4952
|
+
* part of revoking — but useful to force a refresh (e.g. after
|
|
4953
|
+
* switching a CA's `backend` between `"openssl"` and `"native"`).
|
|
4954
|
+
*/
|
|
4955
|
+
async regenerateCrl() {
|
|
4956
|
+
await this.#withCaLock(async () => {
|
|
4957
|
+
await this.#backend.regenerateCrl(this);
|
|
4958
|
+
});
|
|
4163
4959
|
}
|
|
4164
4960
|
/**
|
|
4165
4961
|
* Revoke a certificate and regenerate the CRL.
|
|
@@ -4180,49 +4976,12 @@ var init_certificate_authority = __esm({
|
|
|
4180
4976
|
"certificateHold",
|
|
4181
4977
|
"removeFromCRL"
|
|
4182
4978
|
];
|
|
4183
|
-
const configFile = generateStaticConfig("conf/caconfig.cnf", { cwd: this.rootDir });
|
|
4184
|
-
const options = {
|
|
4185
|
-
cwd: this.rootDir,
|
|
4186
|
-
openssl_conf: makePath(configFile)
|
|
4187
|
-
};
|
|
4188
|
-
setEnv("ALTNAME", "");
|
|
4189
|
-
const randomFile = path6.join(this.rootDir, "random.rnd");
|
|
4190
|
-
setEnv("RANDFILE", randomFile);
|
|
4191
|
-
const configOption = ["-config", n5(configFile)];
|
|
4192
4979
|
const reason = params.reason || "keyCompromise";
|
|
4193
|
-
|
|
4980
|
+
assert9(crlReasons.indexOf(reason) >= 0);
|
|
4194
4981
|
displayTitle(`Revoking certificate ${certificate}`);
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
["ca", "-verbose", ...configOption, "-revoke", certificate, "-crl_reason", reason, ...passin.args],
|
|
4199
|
-
{ ...options, env: passin.env }
|
|
4200
|
-
);
|
|
4201
|
-
await regenerateCrl(this.revocationList, configOption, options, passin);
|
|
4202
|
-
displaySubtitle("Verify that certificate is revoked");
|
|
4203
|
-
await execute_openssl_no_failure(
|
|
4204
|
-
[
|
|
4205
|
-
"verify",
|
|
4206
|
-
"-verbose",
|
|
4207
|
-
"-CRLfile",
|
|
4208
|
-
n5(this.revocationList),
|
|
4209
|
-
"-CAfile",
|
|
4210
|
-
n5(this.caCertificate),
|
|
4211
|
-
"-crl_check",
|
|
4212
|
-
n5(certificate)
|
|
4213
|
-
],
|
|
4214
|
-
options
|
|
4215
|
-
);
|
|
4216
|
-
displaySubtitle("Produce CRL in DER form ");
|
|
4217
|
-
await execute_openssl(
|
|
4218
|
-
["crl", "-in", n5(this.revocationList), "-out", "crl/revocation_list.der", "-outform", "der"],
|
|
4219
|
-
options
|
|
4220
|
-
);
|
|
4221
|
-
displaySubtitle("Produce CRL in PEM form ");
|
|
4222
|
-
await execute_openssl(
|
|
4223
|
-
["crl", "-in", n5(this.revocationList), "-out", "crl/revocation_list.pem", "-outform", "pem", "-text"],
|
|
4224
|
-
options
|
|
4225
|
-
);
|
|
4982
|
+
await this.#withCaLock(async () => {
|
|
4983
|
+
await this.#backend.revoke(this, certificate, reason);
|
|
4984
|
+
});
|
|
4226
4985
|
}
|
|
4227
4986
|
/**
|
|
4228
4987
|
* Sign a Certificate Signing Request (CSR) with this CA.
|
|
@@ -4237,15 +4996,13 @@ var init_certificate_authority = __esm({
|
|
|
4237
4996
|
* @returns the path to the signed certificate
|
|
4238
4997
|
*/
|
|
4239
4998
|
async signCertificateRequest(certificate, certificateSigningRequestFilename, params1) {
|
|
4240
|
-
await
|
|
4241
|
-
|
|
4999
|
+
await this.#backend.preflight();
|
|
5000
|
+
assert9(fs12.existsSync(certificateSigningRequestFilename));
|
|
4242
5001
|
if (!certificateFileExist(certificate)) {
|
|
4243
5002
|
return "";
|
|
4244
5003
|
}
|
|
4245
5004
|
adjustDate(params1);
|
|
4246
5005
|
adjustApplicationUri(params1);
|
|
4247
|
-
processAltNames(params1);
|
|
4248
|
-
const options = { cwd: this.rootDir };
|
|
4249
5006
|
const csr = await readCertificateSigningRequest(certificateSigningRequestFilename);
|
|
4250
5007
|
const csrInfo = exploreCertificateSigningRequest(csr);
|
|
4251
5008
|
const applicationUri = csrInfo.extensionRequest.subjectAltName.uniformResourceIdentifier ? csrInfo.extensionRequest.subjectAltName.uniformResourceIdentifier[0] : void 0;
|
|
@@ -4255,73 +5012,122 @@ var init_certificate_authority = __esm({
|
|
|
4255
5012
|
const dns2 = csrInfo.extensionRequest.subjectAltName.dNSName || [];
|
|
4256
5013
|
let ip = csrInfo.extensionRequest.subjectAltName.iPAddress || [];
|
|
4257
5014
|
ip = ip.map(octetStringToIpAddress);
|
|
4258
|
-
const
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
[
|
|
4270
|
-
"ca",
|
|
4271
|
-
"-config",
|
|
4272
|
-
configFile,
|
|
4273
|
-
"-startdate",
|
|
4274
|
-
x509Date(params1.startDate),
|
|
4275
|
-
"-enddate",
|
|
4276
|
-
x509Date(params1.endDate),
|
|
4277
|
-
"-batch",
|
|
4278
|
-
"-out",
|
|
4279
|
-
n5(certificate),
|
|
4280
|
-
"-in",
|
|
4281
|
-
n5(certificateSigningRequestFilename),
|
|
4282
|
-
...passin.args
|
|
4283
|
-
],
|
|
4284
|
-
{ ...options, env: passin.env }
|
|
4285
|
-
);
|
|
4286
|
-
displaySubtitle("- dump the certificate for a check");
|
|
4287
|
-
await execute_openssl(["x509", "-in", n5(certificate), "-dates", "-fingerprint", "-purpose", "-noout"], options);
|
|
4288
|
-
displaySubtitle("- construct CA certificate with CRL");
|
|
4289
|
-
await this.constructCACertificateWithCRL();
|
|
4290
|
-
displaySubtitle("- construct certificate chain");
|
|
4291
|
-
await this.constructCertificateChain(certificate);
|
|
4292
|
-
displaySubtitle("- verify certificate against the root CA");
|
|
4293
|
-
await this.verifyCertificate(certificate);
|
|
4294
|
-
return certificate;
|
|
5015
|
+
const sanOverride = { applicationUri, dns: dns2, ip };
|
|
5016
|
+
return this.#withCaLock(async () => {
|
|
5017
|
+
await this.#backend.signEndEntityCsr(this, certificate, certificateSigningRequestFilename, params1, sanOverride);
|
|
5018
|
+
displaySubtitle("- construct CA certificate with CRL");
|
|
5019
|
+
await this.constructCACertificateWithCRL();
|
|
5020
|
+
displaySubtitle("- construct certificate chain");
|
|
5021
|
+
await this.constructCertificateChain(certificate);
|
|
5022
|
+
displaySubtitle("- verify certificate against the root CA");
|
|
5023
|
+
await this.verifyCertificate(certificate);
|
|
5024
|
+
return certificate;
|
|
5025
|
+
});
|
|
4295
5026
|
}
|
|
4296
5027
|
/**
|
|
4297
|
-
*
|
|
5028
|
+
* Check that `certificate` really was signed by this CA, and throw if it
|
|
5029
|
+
* was not.
|
|
5030
|
+
*
|
|
5031
|
+
* This used to do nothing: `openssl verify` crashes on Windows, so the
|
|
5032
|
+
* check was left as a placeholder. It no longer needs a subprocess -
|
|
5033
|
+
* `verifyCertificateSignature` does it in pure JS, the same way
|
|
5034
|
+
* {@link CertificateManager} already validates a chain - so the check
|
|
5035
|
+
* that `signCertificateRequest` always claimed to perform now actually
|
|
5036
|
+
* happens on every issuance, whichever backend did the signing.
|
|
4298
5037
|
*
|
|
4299
|
-
*
|
|
5038
|
+
* Only the leading certificate is examined: the file may be a chain,
|
|
5039
|
+
* and the rest of it is this CA's own certificate and its issuers.
|
|
5040
|
+
*
|
|
5041
|
+
* @param certificate - path to the certificate (or chain) to verify
|
|
4300
5042
|
*/
|
|
4301
5043
|
async verifyCertificate(certificate) {
|
|
4302
|
-
const
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
5044
|
+
const pem = await fs12.promises.readFile(certificate, "utf-8");
|
|
5045
|
+
const blocks = pem.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g);
|
|
5046
|
+
if (!blocks || blocks.length === 0) {
|
|
5047
|
+
throw new Error(`verifyCertificate: ${certificate} contains no PEM-encoded certificate`);
|
|
5048
|
+
}
|
|
5049
|
+
const caCertificateDer = convertPEMtoDER2(readCertificatePEM2(this.caCertificate));
|
|
5050
|
+
if (!verifyCertificateSignature2(convertPEMtoDER2(blocks[0]), caCertificateDer)) {
|
|
5051
|
+
throw new Error(
|
|
5052
|
+
`verifyCertificate: ${certificate} was not signed by this certificate authority (${this.caCertificate})`
|
|
5053
|
+
);
|
|
5054
|
+
}
|
|
5055
|
+
}
|
|
5056
|
+
};
|
|
5057
|
+
}
|
|
5058
|
+
});
|
|
5059
|
+
|
|
5060
|
+
// packages/node-opcua-pki/lib/ca/certificate_authority.ts
|
|
5061
|
+
var CertificateAuthority;
|
|
5062
|
+
var init_certificate_authority = __esm({
|
|
5063
|
+
"packages/node-opcua-pki/lib/ca/certificate_authority.ts"() {
|
|
5064
|
+
"use strict";
|
|
5065
|
+
init_esm_shims();
|
|
5066
|
+
init_with_openssl();
|
|
5067
|
+
init_native_ca_backend();
|
|
5068
|
+
init_openssl_ca_backend();
|
|
5069
|
+
init_certificate_authority_core();
|
|
5070
|
+
init_certificate_authority_core();
|
|
5071
|
+
CertificateAuthority = class extends CertificateAuthorityCore {
|
|
5072
|
+
constructor(options) {
|
|
5073
|
+
if (options.signer && options.backend === "openssl") {
|
|
5074
|
+
throw new Error(
|
|
5075
|
+
"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."
|
|
4310
5076
|
);
|
|
4311
5077
|
}
|
|
5078
|
+
const backend = options.signer || options.backend === "native" ? new NativeCaBackend() : new OpenSslCaBackend();
|
|
5079
|
+
super({ ...options, backend });
|
|
5080
|
+
}
|
|
5081
|
+
/**
|
|
5082
|
+
* @internal `-passin env:` argv + env for an openssl call that loads
|
|
5083
|
+
* this CA's key (always emitted, empty when none).
|
|
5084
|
+
*
|
|
5085
|
+
* The openssl backend builds its own now; this remains so that external
|
|
5086
|
+
* code calling it keeps working, and lives here rather than on the core
|
|
5087
|
+
* because the flag means nothing to a backend that spawns nothing.
|
|
5088
|
+
*/
|
|
5089
|
+
async _opensslPassin() {
|
|
5090
|
+
return passinArg(await this._privateKeyPassphrase());
|
|
5091
|
+
}
|
|
5092
|
+
/**
|
|
5093
|
+
* @internal
|
|
5094
|
+
* Legacy shim: publish the `CDP_URL` / `AIA_VALUE` config substitution
|
|
5095
|
+
* values to the shared env registry, or unset them so the matching
|
|
5096
|
+
* `{{#KEY}}...{{/KEY}}` blocks are stripped. Nothing in this package
|
|
5097
|
+
* reads the registry any more - every openssl config render receives
|
|
5098
|
+
* these values explicitly, from the same {@link caConfigEnvOverrides}
|
|
5099
|
+
* builder this delegates to, so the two cannot drift - kept only for
|
|
5100
|
+
* external code that renders openssl config templates against the
|
|
5101
|
+
* registry directly.
|
|
5102
|
+
*
|
|
5103
|
+
* It lives on this class rather than the core because it is meaningful
|
|
5104
|
+
* only to the openssl backend.
|
|
5105
|
+
*/
|
|
5106
|
+
_wireRevocationEnvVars() {
|
|
5107
|
+
const overrides = caConfigEnvOverrides(this);
|
|
5108
|
+
if (overrides.CDP_URL) {
|
|
5109
|
+
setEnv("CDP_URL", overrides.CDP_URL);
|
|
5110
|
+
} else {
|
|
5111
|
+
unsetEnv("CDP_URL");
|
|
5112
|
+
}
|
|
5113
|
+
if (overrides.AIA_VALUE) {
|
|
5114
|
+
setEnv("AIA_VALUE", overrides.AIA_VALUE);
|
|
5115
|
+
} else {
|
|
5116
|
+
unsetEnv("AIA_VALUE");
|
|
5117
|
+
}
|
|
4312
5118
|
}
|
|
4313
5119
|
};
|
|
4314
5120
|
}
|
|
4315
5121
|
});
|
|
4316
5122
|
|
|
4317
5123
|
// packages/node-opcua-pki/lib/ca/crypto_create_CA.ts
|
|
4318
|
-
import
|
|
4319
|
-
import
|
|
5124
|
+
import assert10 from "assert";
|
|
5125
|
+
import fs13 from "fs";
|
|
4320
5126
|
import { createRequire } from "module";
|
|
4321
5127
|
import os5 from "os";
|
|
4322
|
-
import
|
|
5128
|
+
import path10 from "path";
|
|
4323
5129
|
import chalk7 from "chalk";
|
|
4324
|
-
import { CertificatePurpose as
|
|
5130
|
+
import { CertificatePurpose as CertificatePurpose4, generatePrivateKeyFile as generatePrivateKeyFile3, Subject as Subject7 } from "node-opcua-crypto";
|
|
4325
5131
|
import commandLineArgs from "command-line-args";
|
|
4326
5132
|
import commandLineUsage from "command-line-usage";
|
|
4327
5133
|
function get_offset_date(date, nbDays) {
|
|
@@ -4329,9 +5135,9 @@ function get_offset_date(date, nbDays) {
|
|
|
4329
5135
|
d.setDate(d.getDate() + nbDays);
|
|
4330
5136
|
return d;
|
|
4331
5137
|
}
|
|
4332
|
-
async function
|
|
4333
|
-
|
|
4334
|
-
|
|
5138
|
+
async function construct_CertificateAuthority(subject) {
|
|
5139
|
+
assert10(typeof gLocalConfig.CAFolder === "string", "expecting a CAFolder in config");
|
|
5140
|
+
assert10(typeof gLocalConfig.keySize === "number", "expecting a keySize in config");
|
|
4335
5141
|
if (!g_certificateAuthority) {
|
|
4336
5142
|
g_certificateAuthority = new CertificateAuthority({
|
|
4337
5143
|
keySize: gLocalConfig.keySize,
|
|
@@ -4342,7 +5148,7 @@ async function construct_CertificateAuthority2(subject) {
|
|
|
4342
5148
|
}
|
|
4343
5149
|
}
|
|
4344
5150
|
async function construct_CertificateManager() {
|
|
4345
|
-
|
|
5151
|
+
assert10(typeof gLocalConfig.PKIFolder === "string", "expecting a PKIFolder in config");
|
|
4346
5152
|
if (!certificateManager) {
|
|
4347
5153
|
certificateManager = new CertificateManager({
|
|
4348
5154
|
keySize: gLocalConfig.keySize,
|
|
@@ -4353,35 +5159,35 @@ async function construct_CertificateManager() {
|
|
|
4353
5159
|
}
|
|
4354
5160
|
function default_template_content() {
|
|
4355
5161
|
if (process.pkg?.entrypoint) {
|
|
4356
|
-
const a =
|
|
5162
|
+
const a = fs13.readFileSync(path10.join(__dirname, "../../bin/pki_config.example.js"), "utf8");
|
|
4357
5163
|
return a;
|
|
4358
5164
|
}
|
|
4359
5165
|
function find_default_config_template() {
|
|
4360
5166
|
const rootFolder = find_module_root_folder();
|
|
4361
5167
|
const configName = "pki_config.example.js";
|
|
4362
|
-
let default_config_template2 =
|
|
4363
|
-
if (!
|
|
4364
|
-
default_config_template2 =
|
|
4365
|
-
if (!
|
|
4366
|
-
default_config_template2 =
|
|
5168
|
+
let default_config_template2 = path10.join(rootFolder, "bin", configName);
|
|
5169
|
+
if (!fs13.existsSync(default_config_template2)) {
|
|
5170
|
+
default_config_template2 = path10.join(__dirname, "..", configName);
|
|
5171
|
+
if (!fs13.existsSync(default_config_template2)) {
|
|
5172
|
+
default_config_template2 = path10.join(__dirname, `../bin/${configName}`);
|
|
4367
5173
|
}
|
|
4368
5174
|
}
|
|
4369
5175
|
return default_config_template2;
|
|
4370
5176
|
}
|
|
4371
5177
|
const default_config_template = find_default_config_template();
|
|
4372
|
-
|
|
4373
|
-
const default_config_template_content =
|
|
5178
|
+
assert10(fs13.existsSync(default_config_template));
|
|
5179
|
+
const default_config_template_content = fs13.readFileSync(default_config_template, "utf8");
|
|
4374
5180
|
return default_config_template_content;
|
|
4375
5181
|
}
|
|
4376
5182
|
function find_module_root_folder() {
|
|
4377
|
-
let rootFolder =
|
|
5183
|
+
let rootFolder = path10.join(__dirname);
|
|
4378
5184
|
for (let i = 0; i < 4; i++) {
|
|
4379
|
-
if (
|
|
5185
|
+
if (fs13.existsSync(path10.join(rootFolder, "package.json"))) {
|
|
4380
5186
|
return rootFolder;
|
|
4381
5187
|
}
|
|
4382
|
-
rootFolder =
|
|
5188
|
+
rootFolder = path10.join(rootFolder, "..");
|
|
4383
5189
|
}
|
|
4384
|
-
|
|
5190
|
+
assert10(fs13.existsSync(path10.join(rootFolder, "package.json")), "root folder must have a package.json file");
|
|
4385
5191
|
return rootFolder;
|
|
4386
5192
|
}
|
|
4387
5193
|
async function readConfiguration(argv) {
|
|
@@ -4406,41 +5212,41 @@ async function readConfiguration(argv) {
|
|
|
4406
5212
|
return str;
|
|
4407
5213
|
}
|
|
4408
5214
|
function prepare(file) {
|
|
4409
|
-
const tmp =
|
|
5215
|
+
const tmp = path10.resolve(performSubstitution(file));
|
|
4410
5216
|
return makePath(tmp);
|
|
4411
5217
|
}
|
|
4412
5218
|
certificateDir = argv.root;
|
|
4413
|
-
|
|
5219
|
+
assert10(typeof certificateDir === "string");
|
|
4414
5220
|
certificateDir = prepare(certificateDir);
|
|
4415
5221
|
mkdirRecursiveSync(certificateDir);
|
|
4416
|
-
|
|
4417
|
-
const default_config =
|
|
4418
|
-
if (!
|
|
5222
|
+
assert10(fs13.existsSync(certificateDir));
|
|
5223
|
+
const default_config = path10.join(certificateDir, "config.js");
|
|
5224
|
+
if (!fs13.existsSync(default_config)) {
|
|
4419
5225
|
debugLog(chalk7.yellow(" Creating default g_config file "), chalk7.cyan(default_config));
|
|
4420
5226
|
const default_config_template_content = default_template_content();
|
|
4421
|
-
|
|
5227
|
+
fs13.writeFileSync(default_config, default_config_template_content);
|
|
4422
5228
|
} else {
|
|
4423
5229
|
debugLog(chalk7.yellow(" using g_config file "), chalk7.cyan(default_config));
|
|
4424
5230
|
}
|
|
4425
|
-
if (!
|
|
5231
|
+
if (!fs13.existsSync(default_config)) {
|
|
4426
5232
|
debugLog(chalk7.redBright(" cannot find config file ", default_config));
|
|
4427
5233
|
}
|
|
4428
|
-
const defaultRandomFile =
|
|
5234
|
+
const defaultRandomFile = path10.join(path10.dirname(default_config), "random.rnd");
|
|
4429
5235
|
setEnv("RANDFILE", defaultRandomFile);
|
|
4430
5236
|
const _require = createRequire(__filename);
|
|
4431
5237
|
gLocalConfig = _require(default_config);
|
|
4432
|
-
gLocalConfig.subject = new
|
|
5238
|
+
gLocalConfig.subject = new Subject7(gLocalConfig.subject || "");
|
|
4433
5239
|
if (argv.subject) {
|
|
4434
|
-
gLocalConfig.subject = new
|
|
5240
|
+
gLocalConfig.subject = new Subject7(argv.subject);
|
|
4435
5241
|
}
|
|
4436
5242
|
if (!gLocalConfig.subject.commonName) {
|
|
4437
5243
|
throw new Error("subject must have a Common Name");
|
|
4438
5244
|
}
|
|
4439
5245
|
gLocalConfig.certificateDir = certificateDir;
|
|
4440
|
-
let CAFolder = argv.CAFolder ||
|
|
5246
|
+
let CAFolder = argv.CAFolder || path10.join(certificateDir, "CA");
|
|
4441
5247
|
CAFolder = prepare(CAFolder);
|
|
4442
5248
|
gLocalConfig.CAFolder = CAFolder;
|
|
4443
|
-
gLocalConfig.PKIFolder =
|
|
5249
|
+
gLocalConfig.PKIFolder = path10.join(gLocalConfig.certificateDir, "PKI");
|
|
4444
5250
|
if (argv.PKIFolder) {
|
|
4445
5251
|
gLocalConfig.PKIFolder = prepare(argv.PKIFolder);
|
|
4446
5252
|
}
|
|
@@ -4478,7 +5284,7 @@ async function readConfiguration(argv) {
|
|
|
4478
5284
|
}
|
|
4479
5285
|
}
|
|
4480
5286
|
async function createDefaultCertificate(base_name, prefix, key_length, applicationUri, dev) {
|
|
4481
|
-
|
|
5287
|
+
assert10(key_length === 1024 || key_length === 2048 || key_length === 3072 || key_length === 4096);
|
|
4482
5288
|
const private_key_file = makePath(base_name, `${prefix}key_${key_length}.pem`);
|
|
4483
5289
|
const public_key_file = makePath(base_name, `${prefix}public_key_${key_length}.pub`);
|
|
4484
5290
|
const certificate_file = makePath(base_name, `${prefix}cert_${key_length}.pem`);
|
|
@@ -4498,7 +5304,7 @@ async function createDefaultCertificate(base_name, prefix, key_length, applicati
|
|
|
4498
5304
|
}
|
|
4499
5305
|
const ip = [];
|
|
4500
5306
|
async function createCertificateIfNotExist(certificate, private_key, applicationUri2, startDate, validity) {
|
|
4501
|
-
if (
|
|
5307
|
+
if (fs13.existsSync(certificate)) {
|
|
4502
5308
|
warningLog(chalk7.yellow(" certificate"), chalk7.cyan(certificate), chalk7.yellow(" already exists => skipping"));
|
|
4503
5309
|
return "";
|
|
4504
5310
|
} else {
|
|
@@ -4517,7 +5323,7 @@ async function createDefaultCertificate(base_name, prefix, key_length, applicati
|
|
|
4517
5323
|
configFile,
|
|
4518
5324
|
dns: dns3,
|
|
4519
5325
|
ip: ip2,
|
|
4520
|
-
purpose:
|
|
5326
|
+
purpose: CertificatePurpose4.ForApplication
|
|
4521
5327
|
};
|
|
4522
5328
|
await createCertificateSigningRequestWithOpenSSL(certificateSigningRequestFile, params);
|
|
4523
5329
|
return await g_certificateAuthority.signCertificateRequest(certificate, certificateSigningRequestFile, {
|
|
@@ -4541,7 +5347,7 @@ async function createDefaultCertificate(base_name, prefix, key_length, applicati
|
|
|
4541
5347
|
await g_certificateAuthority.revokeCertificate(certificate, {});
|
|
4542
5348
|
}
|
|
4543
5349
|
async function createPrivateKeyIfNotExist(privateKey, keyLength) {
|
|
4544
|
-
if (
|
|
5350
|
+
if (fs13.existsSync(privateKey)) {
|
|
4545
5351
|
warningLog(chalk7.yellow(" privateKey"), chalk7.cyan(privateKey), chalk7.yellow(" already exists => skipping"));
|
|
4546
5352
|
return;
|
|
4547
5353
|
} else {
|
|
@@ -4555,14 +5361,14 @@ async function createDefaultCertificate(base_name, prefix, key_length, applicati
|
|
|
4555
5361
|
displaySubtitle(` create Certificate ${certificate_file}`);
|
|
4556
5362
|
await createCertificateIfNotExist(certificate_file, private_key_file, applicationUri, yesterday, 365);
|
|
4557
5363
|
displaySubtitle(` create self signed Certificate ${self_signed_certificate_file}`);
|
|
4558
|
-
if (
|
|
5364
|
+
if (fs13.existsSync(self_signed_certificate_file)) {
|
|
4559
5365
|
return;
|
|
4560
5366
|
}
|
|
4561
5367
|
await createSelfSignedCertificate2(self_signed_certificate_file, private_key_file, applicationUri, yesterday, 365);
|
|
4562
5368
|
if (dev) {
|
|
4563
5369
|
await createCertificateIfNotExist(certificate_file_outofdate, private_key_file, applicationUri, two_years_ago, 365);
|
|
4564
5370
|
await createCertificateIfNotExist(certificate_file_not_active_yet, private_key_file, applicationUri, next_year, 365);
|
|
4565
|
-
if (!
|
|
5371
|
+
if (!fs13.existsSync(certificate_revoked)) {
|
|
4566
5372
|
const certificate = await createCertificateIfNotExist(
|
|
4567
5373
|
certificate_revoked,
|
|
4568
5374
|
private_key_file,
|
|
@@ -4584,9 +5390,9 @@ async function wrap(func) {
|
|
|
4584
5390
|
}
|
|
4585
5391
|
}
|
|
4586
5392
|
async function create_default_certificates(dev) {
|
|
4587
|
-
|
|
5393
|
+
assert10(gLocalConfig);
|
|
4588
5394
|
const base_name = gLocalConfig.certificateDir || "";
|
|
4589
|
-
|
|
5395
|
+
assert10(fs13.existsSync(base_name));
|
|
4590
5396
|
let clientURN;
|
|
4591
5397
|
let serverURN;
|
|
4592
5398
|
let discoveryServerURN;
|
|
@@ -4617,7 +5423,7 @@ async function create_default_certificates(dev) {
|
|
|
4617
5423
|
});
|
|
4618
5424
|
}
|
|
4619
5425
|
async function createDefaultCertificates(dev) {
|
|
4620
|
-
await
|
|
5426
|
+
await construct_CertificateAuthority("");
|
|
4621
5427
|
await construct_CertificateManager();
|
|
4622
5428
|
await create_default_certificates(dev);
|
|
4623
5429
|
}
|
|
@@ -4683,7 +5489,7 @@ ${epilog}`
|
|
|
4683
5489
|
}
|
|
4684
5490
|
if (command === "version") {
|
|
4685
5491
|
const rootFolder = find_module_root_folder();
|
|
4686
|
-
const pkg = JSON.parse(
|
|
5492
|
+
const pkg = JSON.parse(fs13.readFileSync(path10.join(rootFolder, "package.json"), "utf-8"));
|
|
4687
5493
|
console.log(pkg.version);
|
|
4688
5494
|
return;
|
|
4689
5495
|
}
|
|
@@ -4708,12 +5514,12 @@ ${epilog}`
|
|
|
4708
5514
|
await readConfiguration(local_argv);
|
|
4709
5515
|
if (local_argv.clean) {
|
|
4710
5516
|
displayTitle("Cleaning old certificates");
|
|
4711
|
-
|
|
5517
|
+
assert10(gLocalConfig);
|
|
4712
5518
|
const certificateDir = gLocalConfig.certificateDir || "";
|
|
4713
|
-
const files = await
|
|
5519
|
+
const files = await fs13.promises.readdir(certificateDir);
|
|
4714
5520
|
for (const file of files) {
|
|
4715
5521
|
if (file.includes(".pem") || file.includes(".pub")) {
|
|
4716
|
-
await
|
|
5522
|
+
await fs13.promises.unlink(path10.join(certificateDir, file));
|
|
4717
5523
|
}
|
|
4718
5524
|
}
|
|
4719
5525
|
mkdirRecursiveSync(certificateDir);
|
|
@@ -4734,7 +5540,7 @@ ${epilog}`
|
|
|
4734
5540
|
await wrap(async () => {
|
|
4735
5541
|
await ensure_openssl_installed();
|
|
4736
5542
|
await readConfiguration(local_argv);
|
|
4737
|
-
await
|
|
5543
|
+
await construct_CertificateAuthority(local_argv.subject);
|
|
4738
5544
|
});
|
|
4739
5545
|
return;
|
|
4740
5546
|
}
|
|
@@ -4803,7 +5609,7 @@ ${epilog}`
|
|
|
4803
5609
|
await readConfiguration(local_argv2);
|
|
4804
5610
|
await construct_CertificateManager();
|
|
4805
5611
|
displaySubtitle(` create self signed Certificate ${gLocalConfig.outputFile}`);
|
|
4806
|
-
let subject = local_argv2.subject && local_argv2.subject.length > 1 ? new
|
|
5612
|
+
let subject = local_argv2.subject && local_argv2.subject.length > 1 ? new Subject7(local_argv2.subject) : gLocalConfig.subject || "";
|
|
4807
5613
|
subject = JSON.parse(JSON.stringify(subject));
|
|
4808
5614
|
const params = {
|
|
4809
5615
|
applicationUri: gLocalConfig.applicationUri || "",
|
|
@@ -4819,8 +5625,8 @@ ${epilog}`
|
|
|
4819
5625
|
async function command_full_certificate(local_argv2) {
|
|
4820
5626
|
await readConfiguration(local_argv2);
|
|
4821
5627
|
await construct_CertificateManager();
|
|
4822
|
-
await
|
|
4823
|
-
|
|
5628
|
+
await construct_CertificateAuthority("");
|
|
5629
|
+
assert10(fs13.existsSync(gLocalConfig.CAFolder || ""), " CA folder must exist");
|
|
4824
5630
|
gLocalConfig.privateKey = void 0;
|
|
4825
5631
|
gLocalConfig.subject = local_argv2.subject && local_argv2.subject.length > 1 ? local_argv2.subject : gLocalConfig.subject;
|
|
4826
5632
|
const csr_file = await certificateManager.createCertificateRequest(
|
|
@@ -4831,7 +5637,7 @@ ${epilog}`
|
|
|
4831
5637
|
}
|
|
4832
5638
|
warningLog(" csr_file = ", csr_file);
|
|
4833
5639
|
const certificate = csr_file.replace(".csr", ".pem");
|
|
4834
|
-
if (
|
|
5640
|
+
if (fs13.existsSync(certificate)) {
|
|
4835
5641
|
throw new Error(` File ${certificate} already exist`);
|
|
4836
5642
|
}
|
|
4837
5643
|
await g_certificateAuthority.signCertificateRequest(
|
|
@@ -4839,8 +5645,8 @@ ${epilog}`
|
|
|
4839
5645
|
csr_file,
|
|
4840
5646
|
gLocalConfig
|
|
4841
5647
|
);
|
|
4842
|
-
|
|
4843
|
-
|
|
5648
|
+
assert10(typeof gLocalConfig.outputFile === "string");
|
|
5649
|
+
fs13.writeFileSync(gLocalConfig.outputFile || "", fs13.readFileSync(certificate, "ascii"));
|
|
4844
5650
|
}
|
|
4845
5651
|
await wrap(async () => await command_certificate(local_argv));
|
|
4846
5652
|
return;
|
|
@@ -4859,13 +5665,13 @@ ${epilog}`
|
|
|
4859
5665
|
await g_certificateAuthority.revokeCertificate(certificate, {});
|
|
4860
5666
|
}
|
|
4861
5667
|
await wrap(async () => {
|
|
4862
|
-
const certificate =
|
|
5668
|
+
const certificate = path10.resolve(local_argv.certificateFile);
|
|
4863
5669
|
warningLog(chalk7.yellow(" Certificate to revoke : "), chalk7.cyan(certificate));
|
|
4864
|
-
if (!
|
|
5670
|
+
if (!fs13.existsSync(certificate)) {
|
|
4865
5671
|
throw new Error(`cannot find certificate to revoke ${certificate}`);
|
|
4866
5672
|
}
|
|
4867
5673
|
await readConfiguration(local_argv);
|
|
4868
|
-
await
|
|
5674
|
+
await construct_CertificateAuthority("");
|
|
4869
5675
|
await revoke_certificate(certificate);
|
|
4870
5676
|
warningLog("done ... ");
|
|
4871
5677
|
warningLog(" crl = ", g_certificateAuthority.revocationList);
|
|
@@ -4908,11 +5714,11 @@ ${epilog}`
|
|
|
4908
5714
|
if (local_argv.help) return showHelp("csr", "create a certificate signing request", optionsDef);
|
|
4909
5715
|
await wrap(async () => {
|
|
4910
5716
|
await readConfiguration(local_argv);
|
|
4911
|
-
if (!
|
|
5717
|
+
if (!fs13.existsSync(gLocalConfig.PKIFolder || "")) {
|
|
4912
5718
|
warningLog("PKI folder must exist");
|
|
4913
5719
|
}
|
|
4914
5720
|
await construct_CertificateManager();
|
|
4915
|
-
if (!gLocalConfig.outputFile ||
|
|
5721
|
+
if (!gLocalConfig.outputFile || fs13.existsSync(gLocalConfig.outputFile)) {
|
|
4916
5722
|
throw new Error(` File ${gLocalConfig.outputFile} already exist`);
|
|
4917
5723
|
}
|
|
4918
5724
|
gLocalConfig.privateKey = void 0;
|
|
@@ -4927,8 +5733,8 @@ ${epilog}`
|
|
|
4927
5733
|
warningLog("please specify a output file");
|
|
4928
5734
|
return;
|
|
4929
5735
|
}
|
|
4930
|
-
const csr = await
|
|
4931
|
-
|
|
5736
|
+
const csr = await fs13.promises.readFile(internal_csr_file, "utf-8");
|
|
5737
|
+
fs13.writeFileSync(gLocalConfig.outputFile || "", csr, "utf-8");
|
|
4932
5738
|
warningLog("Subject = ", gLocalConfig.subject);
|
|
4933
5739
|
warningLog("applicationUri = ", gLocalConfig.applicationUri);
|
|
4934
5740
|
warningLog("altNames = ", gLocalConfig.altNames);
|
|
@@ -4956,16 +5762,16 @@ ${epilog}`
|
|
|
4956
5762
|
return showHelp("sign", "validate a certificate signing request and generate a certificate", optionsDef);
|
|
4957
5763
|
await wrap(async () => {
|
|
4958
5764
|
await readConfiguration(local_argv);
|
|
4959
|
-
if (!
|
|
5765
|
+
if (!fs13.existsSync(gLocalConfig.CAFolder || "")) {
|
|
4960
5766
|
throw new Error(`CA folder must exist:${gLocalConfig.CAFolder}`);
|
|
4961
5767
|
}
|
|
4962
|
-
await
|
|
4963
|
-
const csr_file =
|
|
4964
|
-
if (!
|
|
5768
|
+
await construct_CertificateAuthority("");
|
|
5769
|
+
const csr_file = path10.resolve(local_argv.csr || "");
|
|
5770
|
+
if (!fs13.existsSync(csr_file)) {
|
|
4965
5771
|
throw new Error(`Certificate signing request doesn't exist: ${csr_file}`);
|
|
4966
5772
|
}
|
|
4967
|
-
const certificate =
|
|
4968
|
-
if (
|
|
5773
|
+
const certificate = path10.resolve(local_argv.output || csr_file.replace(".csr", ".pem"));
|
|
5774
|
+
if (fs13.existsSync(certificate)) {
|
|
4969
5775
|
throw new Error(` File ${certificate} already exist`);
|
|
4970
5776
|
}
|
|
4971
5777
|
await g_certificateAuthority.signCertificateRequest(
|
|
@@ -4973,8 +5779,8 @@ ${epilog}`
|
|
|
4973
5779
|
csr_file,
|
|
4974
5780
|
gLocalConfig
|
|
4975
5781
|
);
|
|
4976
|
-
|
|
4977
|
-
|
|
5782
|
+
assert10(typeof gLocalConfig.outputFile === "string");
|
|
5783
|
+
fs13.writeFileSync(gLocalConfig.outputFile || "", fs13.readFileSync(certificate, "ascii"));
|
|
4978
5784
|
});
|
|
4979
5785
|
return;
|
|
4980
5786
|
}
|