node-opcua-pki 6.5.0 → 6.6.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 +129 -64
- package/dist/bin/pki.mjs.map +1 -1
- package/dist/index.d.mts +62 -2
- package/dist/index.d.ts +62 -2
- package/dist/index.js +128 -64
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +129 -64
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -4
- package/readme.md +28 -4
package/dist/bin/pki.mjs
CHANGED
|
@@ -342,6 +342,7 @@ var init_simple_config_template_cnf = __esm({
|
|
|
342
342
|
});
|
|
343
343
|
|
|
344
344
|
// packages/node-opcua-pki/lib/pki/certificate_manager.ts
|
|
345
|
+
import { EventEmitter } from "events";
|
|
345
346
|
import fs4 from "fs";
|
|
346
347
|
import path2 from "path";
|
|
347
348
|
import { withLock } from "@ster5/global-mutex";
|
|
@@ -354,6 +355,7 @@ import {
|
|
|
354
355
|
generatePrivateKeyFile,
|
|
355
356
|
makeSHA1Thumbprint,
|
|
356
357
|
readCertificate,
|
|
358
|
+
readCertificateAsync,
|
|
357
359
|
readCertificateRevocationList,
|
|
358
360
|
split_der,
|
|
359
361
|
toPem,
|
|
@@ -434,7 +436,7 @@ var init_certificate_manager = __esm({
|
|
|
434
436
|
configurationFileSimpleTemplate = simple_config_template_cnf_default;
|
|
435
437
|
fsWriteFile = fs4.promises.writeFile;
|
|
436
438
|
forbiddenChars = /[\x00-\x1F<>:"/\\|?*]/g;
|
|
437
|
-
CertificateManager = class _CertificateManager {
|
|
439
|
+
CertificateManager = class _CertificateManager extends EventEmitter {
|
|
438
440
|
// ── Global instance registry ─────────────────────────────────
|
|
439
441
|
// Tracks all initialized CertificateManager instances so their
|
|
440
442
|
// file watchers can be closed automatically on process exit,
|
|
@@ -474,7 +476,7 @@ var init_certificate_manager = __esm({
|
|
|
474
476
|
*/
|
|
475
477
|
static async disposeAll() {
|
|
476
478
|
const instances = [..._CertificateManager.#activeInstances];
|
|
477
|
-
await Promise.all(instances.map((cm) =>
|
|
479
|
+
await Promise.all(instances.map((cm) => _CertificateManager.prototype.dispose.call(cm)));
|
|
478
480
|
}
|
|
479
481
|
/**
|
|
480
482
|
* Assert that all CertificateManager instances have been
|
|
@@ -521,6 +523,7 @@ var init_certificate_manager = __esm({
|
|
|
521
523
|
keySize;
|
|
522
524
|
#location;
|
|
523
525
|
#watchers = [];
|
|
526
|
+
#pendingUnrefs = /* @__PURE__ */ new Set();
|
|
524
527
|
#readCertificatesCalled = false;
|
|
525
528
|
#filenameToHash = /* @__PURE__ */ new Map();
|
|
526
529
|
#initializingPromise;
|
|
@@ -543,6 +546,7 @@ var init_certificate_manager = __esm({
|
|
|
543
546
|
* @param options - configuration options
|
|
544
547
|
*/
|
|
545
548
|
constructor(options) {
|
|
549
|
+
super();
|
|
546
550
|
options.keySize = options.keySize || 2048;
|
|
547
551
|
if (!options.location) {
|
|
548
552
|
throw new Error("CertificateManager: missing 'location' option");
|
|
@@ -743,7 +747,7 @@ var init_certificate_manager = __esm({
|
|
|
743
747
|
let isTimeInvalid = false;
|
|
744
748
|
if (certificateInfo.notBefore.getTime() > now.getTime()) {
|
|
745
749
|
debugLog(
|
|
746
|
-
chalk3.red("certificate is invalid : certificate is not active yet !")
|
|
750
|
+
`${chalk3.red("certificate is invalid : certificate is not active yet !")} not before date =${certificateInfo.notBefore}`
|
|
747
751
|
);
|
|
748
752
|
if (!options.acceptPendingCertificate) {
|
|
749
753
|
isTimeInvalid = true;
|
|
@@ -886,6 +890,10 @@ var init_certificate_manager = __esm({
|
|
|
886
890
|
}
|
|
887
891
|
try {
|
|
888
892
|
this.state = 3 /* Disposing */;
|
|
893
|
+
for (const unreff of this.#pendingUnrefs) {
|
|
894
|
+
unreff();
|
|
895
|
+
}
|
|
896
|
+
this.#pendingUnrefs.clear();
|
|
889
897
|
await Promise.all(this.#watchers.map((w) => w.close()));
|
|
890
898
|
this.#watchers.forEach((w) => {
|
|
891
899
|
w.removeAllListeners();
|
|
@@ -1167,6 +1175,9 @@ var init_certificate_manager = __esm({
|
|
|
1167
1175
|
* verifies that each issuer is already registered via
|
|
1168
1176
|
* {@link addIssuer} before trusting the leaf.
|
|
1169
1177
|
*
|
|
1178
|
+
* If one of the certificates in the chain is not registered in the issuers store,
|
|
1179
|
+
* the leaf certificate will be rejected.
|
|
1180
|
+
*
|
|
1170
1181
|
* @param certificateChain - DER-encoded certificate or chain
|
|
1171
1182
|
* @returns `VerificationStatus.Good` on success, or an error
|
|
1172
1183
|
* status indicating why the certificate was rejected.
|
|
@@ -1184,23 +1195,9 @@ var init_certificate_manager = __esm({
|
|
|
1184
1195
|
return "BadCertificateInvalid" /* BadCertificateInvalid */;
|
|
1185
1196
|
}
|
|
1186
1197
|
if (certificates.length > 1) {
|
|
1187
|
-
const issuerFolder = this.issuersCertFolder;
|
|
1188
|
-
const issuerThumbprints = /* @__PURE__ */ new Set();
|
|
1189
|
-
const files = await fs4.promises.readdir(issuerFolder);
|
|
1190
|
-
for (const file of files) {
|
|
1191
|
-
const ext = path2.extname(file).toLowerCase();
|
|
1192
|
-
if (ext === ".pem" || ext === ".der") {
|
|
1193
|
-
try {
|
|
1194
|
-
const issuerCert = readCertificate(path2.join(issuerFolder, file));
|
|
1195
|
-
const fp = makeFingerprint(issuerCert);
|
|
1196
|
-
issuerThumbprints.add(fp);
|
|
1197
|
-
} catch (_err) {
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
1198
|
for (const issuerCert of certificates.slice(1)) {
|
|
1202
1199
|
const thumbprint = makeFingerprint(issuerCert);
|
|
1203
|
-
if (!
|
|
1200
|
+
if (!await this.hasIssuer(thumbprint)) {
|
|
1204
1201
|
return "BadCertificateChainIncomplete" /* BadCertificateChainIncomplete */;
|
|
1205
1202
|
}
|
|
1206
1203
|
}
|
|
@@ -1373,7 +1370,7 @@ var init_certificate_manager = __esm({
|
|
|
1373
1370
|
return "Good" /* Good */;
|
|
1374
1371
|
}
|
|
1375
1372
|
#pendingCrlToProcess = 0;
|
|
1376
|
-
#
|
|
1373
|
+
#onCrlProcessWaiters = [];
|
|
1377
1374
|
#queue = [];
|
|
1378
1375
|
#onCrlFileAdded(index, filename) {
|
|
1379
1376
|
this.#queue.push({ index, filename });
|
|
@@ -1409,10 +1406,10 @@ var init_certificate_manager = __esm({
|
|
|
1409
1406
|
}
|
|
1410
1407
|
this.#pendingCrlToProcess -= 1;
|
|
1411
1408
|
if (this.#pendingCrlToProcess === 0) {
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
this.#onCrlProcess = void 0;
|
|
1409
|
+
for (const waiter of this.#onCrlProcessWaiters) {
|
|
1410
|
+
waiter();
|
|
1415
1411
|
}
|
|
1412
|
+
this.#onCrlProcessWaiters.length = 0;
|
|
1416
1413
|
} else {
|
|
1417
1414
|
this.#processNextCrl();
|
|
1418
1415
|
}
|
|
@@ -1423,9 +1420,11 @@ var init_certificate_manager = __esm({
|
|
|
1423
1420
|
}
|
|
1424
1421
|
this.#readCertificatesCalled = true;
|
|
1425
1422
|
const usePolling = process.env.OPCUA_PKI_USE_POLLING === "true";
|
|
1423
|
+
const envInterval = process.env.OPCUA_PKI_POLLING_INTERVAL ? parseInt(process.env.OPCUA_PKI_POLLING_INTERVAL, 10) : void 0;
|
|
1424
|
+
const pollingInterval = Math.min(10 * 60 * 1e3, Math.max(100, envInterval ?? this.folderPollingInterval));
|
|
1426
1425
|
const chokidarOptions = {
|
|
1427
1426
|
usePolling,
|
|
1428
|
-
...usePolling ? { interval:
|
|
1427
|
+
...usePolling ? { interval: pollingInterval } : {},
|
|
1429
1428
|
persistent: false
|
|
1430
1429
|
};
|
|
1431
1430
|
const createUnreffedWatcher = (folder) => {
|
|
@@ -1445,47 +1444,110 @@ var init_certificate_manager = __esm({
|
|
|
1445
1444
|
};
|
|
1446
1445
|
return { w, capturedHandles, unreffAll };
|
|
1447
1446
|
};
|
|
1448
|
-
|
|
1449
|
-
this.#
|
|
1450
|
-
this.#
|
|
1451
|
-
this.#
|
|
1452
|
-
this.#
|
|
1453
|
-
this.#
|
|
1454
|
-
];
|
|
1455
|
-
|
|
1447
|
+
await Promise.all([
|
|
1448
|
+
this.#scanCertFolder(this.trustedFolder, this.#thumbs.trusted),
|
|
1449
|
+
this.#scanCertFolder(this.issuersCertFolder, this.#thumbs.issuers.certs),
|
|
1450
|
+
this.#scanCertFolder(this.rejectedFolder, this.#thumbs.rejected),
|
|
1451
|
+
this.#scanCrlFolder(this.crlFolder, this.#thumbs.crl),
|
|
1452
|
+
this.#scanCrlFolder(this.issuersCrlFolder, this.#thumbs.issuersCrl)
|
|
1453
|
+
]);
|
|
1454
|
+
this.#startWatcher(this.trustedFolder, this.#thumbs.trusted, createUnreffedWatcher, "trusted");
|
|
1455
|
+
this.#startWatcher(this.issuersCertFolder, this.#thumbs.issuers.certs, createUnreffedWatcher, "issuersCerts");
|
|
1456
|
+
this.#startWatcher(this.rejectedFolder, this.#thumbs.rejected, createUnreffedWatcher, "rejected");
|
|
1457
|
+
this.#startCrlWatcher(this.crlFolder, this.#thumbs.crl, createUnreffedWatcher, "crl");
|
|
1458
|
+
this.#startCrlWatcher(this.issuersCrlFolder, this.#thumbs.issuersCrl, createUnreffedWatcher, "issuersCrl");
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* Scan a certificate folder and populate the in-memory index.
|
|
1462
|
+
* Uses async readdir/stat to yield the event loop between
|
|
1463
|
+
* file reads, preventing main-loop stalls with large folders.
|
|
1464
|
+
*/
|
|
1465
|
+
async #scanCertFolder(folder, index) {
|
|
1466
|
+
if (!fs4.existsSync(folder)) return;
|
|
1467
|
+
const files = await fs4.promises.readdir(folder);
|
|
1468
|
+
for (const file of files) {
|
|
1469
|
+
const filename = path2.join(folder, file);
|
|
1470
|
+
try {
|
|
1471
|
+
const stat = await fs4.promises.stat(filename);
|
|
1472
|
+
if (!stat.isFile()) continue;
|
|
1473
|
+
const certificate = await readCertificateAsync(filename);
|
|
1474
|
+
const info = exploreCertificate(certificate);
|
|
1475
|
+
const fingerprint2 = makeFingerprint(certificate);
|
|
1476
|
+
index.set(fingerprint2, { certificate, filename, info });
|
|
1477
|
+
this.#filenameToHash.set(filename, fingerprint2);
|
|
1478
|
+
} catch (err) {
|
|
1479
|
+
debugLog(`scanCertFolder: skipping ${filename}`, err);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
/**
|
|
1484
|
+
* Scan a CRL folder and populate the in-memory CRL index.
|
|
1485
|
+
*/
|
|
1486
|
+
async #scanCrlFolder(folder, index) {
|
|
1487
|
+
if (!fs4.existsSync(folder)) return;
|
|
1488
|
+
const files = await fs4.promises.readdir(folder);
|
|
1489
|
+
for (const file of files) {
|
|
1490
|
+
const filename = path2.join(folder, file);
|
|
1491
|
+
try {
|
|
1492
|
+
const stat = await fs4.promises.stat(filename);
|
|
1493
|
+
if (!stat.isFile()) continue;
|
|
1494
|
+
this.#onCrlFileAdded(index, filename);
|
|
1495
|
+
} catch (err) {
|
|
1496
|
+
debugLog(`scanCrlFolder: skipping ${filename}`, err);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1456
1499
|
await this.#waitAndCheckCRLProcessingStatus();
|
|
1457
1500
|
}
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1501
|
+
/**
|
|
1502
|
+
* Start a chokidar watcher for a CRL folder.
|
|
1503
|
+
* Non-blocking — does NOT await "ready".
|
|
1504
|
+
*/
|
|
1505
|
+
#startCrlWatcher(folder, index, createUnreffedWatcher, store) {
|
|
1506
|
+
const { w, unreffAll } = createUnreffedWatcher(folder);
|
|
1507
|
+
let ready = false;
|
|
1508
|
+
w.on("unlink", (filename) => {
|
|
1509
|
+
for (const [key, data] of index.entries()) {
|
|
1510
|
+
data.crls = data.crls.filter((c) => c.filename !== filename);
|
|
1511
|
+
if (data.crls.length === 0) {
|
|
1512
|
+
index.delete(key);
|
|
1467
1513
|
}
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1514
|
+
}
|
|
1515
|
+
if (ready) {
|
|
1516
|
+
this.emit("crlRemoved", { store, filename });
|
|
1517
|
+
}
|
|
1518
|
+
});
|
|
1519
|
+
w.on("add", (filename) => {
|
|
1520
|
+
if (ready) {
|
|
1470
1521
|
this.#onCrlFileAdded(index, filename);
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1522
|
+
this.emit("crlAdded", { store, filename });
|
|
1523
|
+
}
|
|
1524
|
+
});
|
|
1525
|
+
w.on("change", (changedPath) => {
|
|
1526
|
+
debugLog("change in folder ", folder, changedPath);
|
|
1527
|
+
});
|
|
1528
|
+
this.#watchers.push(w);
|
|
1529
|
+
this.#pendingUnrefs.add(unreffAll);
|
|
1530
|
+
w.on("ready", () => {
|
|
1531
|
+
ready = true;
|
|
1532
|
+
this.#pendingUnrefs.delete(unreffAll);
|
|
1533
|
+
unreffAll();
|
|
1480
1534
|
});
|
|
1481
1535
|
}
|
|
1482
|
-
|
|
1536
|
+
/**
|
|
1537
|
+
* Start a chokidar watcher for a certificate folder.
|
|
1538
|
+
* Non-blocking — does NOT await "ready".
|
|
1539
|
+
*/
|
|
1540
|
+
#startWatcher(folder, index, createUnreffedWatcher, store) {
|
|
1483
1541
|
const { w, unreffAll } = createUnreffedWatcher(folder);
|
|
1542
|
+
let ready = false;
|
|
1484
1543
|
w.on("unlink", (filename) => {
|
|
1485
1544
|
debugLog(chalk3.cyan(`unlink in folder ${folder}`), filename);
|
|
1486
1545
|
const h = this.#filenameToHash.get(filename);
|
|
1487
1546
|
if (h && index.has(h)) {
|
|
1488
1547
|
index.delete(h);
|
|
1548
|
+
if (ready) {
|
|
1549
|
+
this.emit("certificateRemoved", { store, fingerprint: h, filename });
|
|
1550
|
+
}
|
|
1489
1551
|
}
|
|
1490
1552
|
});
|
|
1491
1553
|
w.on("add", (filename) => {
|
|
@@ -1502,6 +1564,9 @@ var init_certificate_manager = __esm({
|
|
|
1502
1564
|
info.tbsCertificate.serialNumber,
|
|
1503
1565
|
info.tbsCertificate.extensions?.authorityKeyIdentifier?.authorityCertIssuerFingerPrint
|
|
1504
1566
|
);
|
|
1567
|
+
if (ready) {
|
|
1568
|
+
this.emit("certificateAdded", { store, certificate, fingerprint: fingerprint2, filename });
|
|
1569
|
+
}
|
|
1505
1570
|
} catch (err) {
|
|
1506
1571
|
debugLog(`Walk files in folder ${folder} with file ${filename}`);
|
|
1507
1572
|
debugLog(err);
|
|
@@ -1518,31 +1583,31 @@ var init_certificate_manager = __esm({
|
|
|
1518
1583
|
}
|
|
1519
1584
|
index.set(newFingerprint, { certificate, filename: changedPath, info: exploreCertificate(certificate) });
|
|
1520
1585
|
this.#filenameToHash.set(changedPath, newFingerprint);
|
|
1586
|
+
if (ready) {
|
|
1587
|
+
this.emit("certificateChange", { store, certificate, fingerprint: newFingerprint, filename: changedPath });
|
|
1588
|
+
}
|
|
1521
1589
|
} catch (err) {
|
|
1522
1590
|
debugLog(`change event: failed to re-read ${changedPath}`, err);
|
|
1523
1591
|
}
|
|
1524
1592
|
});
|
|
1525
1593
|
this.#watchers.push(w);
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1594
|
+
this.#pendingUnrefs.add(unreffAll);
|
|
1595
|
+
w.on("ready", () => {
|
|
1596
|
+
ready = true;
|
|
1597
|
+
this.#pendingUnrefs.delete(unreffAll);
|
|
1598
|
+
unreffAll();
|
|
1599
|
+
debugLog("ready");
|
|
1600
|
+
debugLog([...index.keys()].map((k) => k.substring(0, 10)));
|
|
1533
1601
|
});
|
|
1534
1602
|
}
|
|
1535
1603
|
// make sure that all crls have been processed.
|
|
1536
1604
|
async #waitAndCheckCRLProcessingStatus() {
|
|
1537
|
-
return new Promise((resolve,
|
|
1605
|
+
return new Promise((resolve, _reject) => {
|
|
1538
1606
|
if (this.#pendingCrlToProcess === 0) {
|
|
1539
1607
|
setImmediate(resolve);
|
|
1540
1608
|
return;
|
|
1541
1609
|
}
|
|
1542
|
-
|
|
1543
|
-
return reject(new Error("Internal Error"));
|
|
1544
|
-
}
|
|
1545
|
-
this.#onCrlProcess = resolve;
|
|
1610
|
+
this.#onCrlProcessWaiters.push(resolve);
|
|
1546
1611
|
});
|
|
1547
1612
|
}
|
|
1548
1613
|
};
|