node-opcua-pki 6.5.1 → 6.7.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 +126 -63
- 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 +125 -63
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +126 -63
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -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,
|
|
@@ -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,10 @@ var init_certificate_manager = __esm({
|
|
|
1184
1195
|
return "BadCertificateInvalid" /* BadCertificateInvalid */;
|
|
1185
1196
|
}
|
|
1186
1197
|
if (certificates.length > 1) {
|
|
1187
|
-
|
|
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
|
-
}
|
|
1198
|
+
await this.#scanCertFolder(this.issuersCertFolder, this.#thumbs.issuers.certs);
|
|
1201
1199
|
for (const issuerCert of certificates.slice(1)) {
|
|
1202
1200
|
const thumbprint = makeFingerprint(issuerCert);
|
|
1203
|
-
if (!
|
|
1201
|
+
if (!await this.hasIssuer(thumbprint)) {
|
|
1204
1202
|
return "BadCertificateChainIncomplete" /* BadCertificateChainIncomplete */;
|
|
1205
1203
|
}
|
|
1206
1204
|
}
|
|
@@ -1373,7 +1371,7 @@ var init_certificate_manager = __esm({
|
|
|
1373
1371
|
return "Good" /* Good */;
|
|
1374
1372
|
}
|
|
1375
1373
|
#pendingCrlToProcess = 0;
|
|
1376
|
-
#
|
|
1374
|
+
#onCrlProcessWaiters = [];
|
|
1377
1375
|
#queue = [];
|
|
1378
1376
|
#onCrlFileAdded(index, filename) {
|
|
1379
1377
|
this.#queue.push({ index, filename });
|
|
@@ -1409,10 +1407,10 @@ var init_certificate_manager = __esm({
|
|
|
1409
1407
|
}
|
|
1410
1408
|
this.#pendingCrlToProcess -= 1;
|
|
1411
1409
|
if (this.#pendingCrlToProcess === 0) {
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
this.#onCrlProcess = void 0;
|
|
1410
|
+
for (const waiter of this.#onCrlProcessWaiters) {
|
|
1411
|
+
waiter();
|
|
1415
1412
|
}
|
|
1413
|
+
this.#onCrlProcessWaiters.length = 0;
|
|
1416
1414
|
} else {
|
|
1417
1415
|
this.#processNextCrl();
|
|
1418
1416
|
}
|
|
@@ -1423,9 +1421,11 @@ var init_certificate_manager = __esm({
|
|
|
1423
1421
|
}
|
|
1424
1422
|
this.#readCertificatesCalled = true;
|
|
1425
1423
|
const usePolling = process.env.OPCUA_PKI_USE_POLLING === "true";
|
|
1424
|
+
const envInterval = process.env.OPCUA_PKI_POLLING_INTERVAL ? parseInt(process.env.OPCUA_PKI_POLLING_INTERVAL, 10) : void 0;
|
|
1425
|
+
const pollingInterval = Math.min(10 * 60 * 1e3, Math.max(100, envInterval ?? this.folderPollingInterval));
|
|
1426
1426
|
const chokidarOptions = {
|
|
1427
1427
|
usePolling,
|
|
1428
|
-
...usePolling ? { interval:
|
|
1428
|
+
...usePolling ? { interval: pollingInterval } : {},
|
|
1429
1429
|
persistent: false
|
|
1430
1430
|
};
|
|
1431
1431
|
const createUnreffedWatcher = (folder) => {
|
|
@@ -1445,47 +1445,108 @@ var init_certificate_manager = __esm({
|
|
|
1445
1445
|
};
|
|
1446
1446
|
return { w, capturedHandles, unreffAll };
|
|
1447
1447
|
};
|
|
1448
|
-
|
|
1449
|
-
this.#
|
|
1450
|
-
this.#
|
|
1451
|
-
this.#
|
|
1452
|
-
this.#
|
|
1453
|
-
this.#
|
|
1454
|
-
];
|
|
1455
|
-
|
|
1448
|
+
await Promise.all([
|
|
1449
|
+
this.#scanCertFolder(this.trustedFolder, this.#thumbs.trusted),
|
|
1450
|
+
this.#scanCertFolder(this.issuersCertFolder, this.#thumbs.issuers.certs),
|
|
1451
|
+
this.#scanCertFolder(this.rejectedFolder, this.#thumbs.rejected),
|
|
1452
|
+
this.#scanCrlFolder(this.crlFolder, this.#thumbs.crl),
|
|
1453
|
+
this.#scanCrlFolder(this.issuersCrlFolder, this.#thumbs.issuersCrl)
|
|
1454
|
+
]);
|
|
1455
|
+
this.#startWatcher(this.trustedFolder, this.#thumbs.trusted, createUnreffedWatcher, "trusted");
|
|
1456
|
+
this.#startWatcher(this.issuersCertFolder, this.#thumbs.issuers.certs, createUnreffedWatcher, "issuersCerts");
|
|
1457
|
+
this.#startWatcher(this.rejectedFolder, this.#thumbs.rejected, createUnreffedWatcher, "rejected");
|
|
1458
|
+
this.#startCrlWatcher(this.crlFolder, this.#thumbs.crl, createUnreffedWatcher, "crl");
|
|
1459
|
+
this.#startCrlWatcher(this.issuersCrlFolder, this.#thumbs.issuersCrl, createUnreffedWatcher, "issuersCrl");
|
|
1460
|
+
}
|
|
1461
|
+
/**
|
|
1462
|
+
* Scan a certificate folder and populate the in-memory index.
|
|
1463
|
+
* Uses async readdir/stat to yield the event loop between
|
|
1464
|
+
* file reads, preventing main-loop stalls with large folders.
|
|
1465
|
+
*/
|
|
1466
|
+
async #scanCertFolder(folder, index) {
|
|
1467
|
+
if (!fs4.existsSync(folder)) return;
|
|
1468
|
+
const files = await fs4.promises.readdir(folder);
|
|
1469
|
+
for (const file of files) {
|
|
1470
|
+
const filename = path2.join(folder, file);
|
|
1471
|
+
try {
|
|
1472
|
+
const stat = await fs4.promises.stat(filename);
|
|
1473
|
+
if (!stat.isFile()) continue;
|
|
1474
|
+
const certificate = await readCertificateAsync(filename);
|
|
1475
|
+
const info = exploreCertificate(certificate);
|
|
1476
|
+
const fingerprint2 = makeFingerprint(certificate);
|
|
1477
|
+
index.set(fingerprint2, { certificate, filename, info });
|
|
1478
|
+
this.#filenameToHash.set(filename, fingerprint2);
|
|
1479
|
+
} catch (err) {
|
|
1480
|
+
debugLog(`scanCertFolder: skipping ${filename}`, err);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
/**
|
|
1485
|
+
* Scan a CRL folder and populate the in-memory CRL index.
|
|
1486
|
+
*/
|
|
1487
|
+
async #scanCrlFolder(folder, index) {
|
|
1488
|
+
if (!fs4.existsSync(folder)) return;
|
|
1489
|
+
const files = await fs4.promises.readdir(folder);
|
|
1490
|
+
for (const file of files) {
|
|
1491
|
+
const filename = path2.join(folder, file);
|
|
1492
|
+
try {
|
|
1493
|
+
const stat = await fs4.promises.stat(filename);
|
|
1494
|
+
if (!stat.isFile()) continue;
|
|
1495
|
+
this.#onCrlFileAdded(index, filename);
|
|
1496
|
+
} catch (err) {
|
|
1497
|
+
debugLog(`scanCrlFolder: skipping ${filename}`, err);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1456
1500
|
await this.#waitAndCheckCRLProcessingStatus();
|
|
1457
1501
|
}
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1502
|
+
/**
|
|
1503
|
+
* Start a chokidar watcher for a CRL folder.
|
|
1504
|
+
* Non-blocking — does NOT await "ready".
|
|
1505
|
+
*/
|
|
1506
|
+
#startCrlWatcher(folder, index, createUnreffedWatcher, store) {
|
|
1507
|
+
const { w, unreffAll } = createUnreffedWatcher(folder);
|
|
1508
|
+
let ready = false;
|
|
1509
|
+
w.on("unlink", (filename) => {
|
|
1510
|
+
for (const [key, data] of index.entries()) {
|
|
1511
|
+
data.crls = data.crls.filter((c) => c.filename !== filename);
|
|
1512
|
+
if (data.crls.length === 0) {
|
|
1513
|
+
index.delete(key);
|
|
1467
1514
|
}
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1515
|
+
}
|
|
1516
|
+
if (ready) {
|
|
1517
|
+
this.emit("crlRemoved", { store, filename });
|
|
1518
|
+
}
|
|
1519
|
+
});
|
|
1520
|
+
w.on("add", (filename) => {
|
|
1521
|
+
if (ready) {
|
|
1470
1522
|
this.#onCrlFileAdded(index, filename);
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1523
|
+
this.emit("crlAdded", { store, filename });
|
|
1524
|
+
}
|
|
1525
|
+
});
|
|
1526
|
+
w.on("change", (changedPath) => {
|
|
1527
|
+
debugLog("change in folder ", folder, changedPath);
|
|
1528
|
+
});
|
|
1529
|
+
this.#watchers.push(w);
|
|
1530
|
+
this.#pendingUnrefs.add(unreffAll);
|
|
1531
|
+
w.on("ready", () => {
|
|
1532
|
+
ready = true;
|
|
1533
|
+
this.#pendingUnrefs.delete(unreffAll);
|
|
1534
|
+
unreffAll();
|
|
1480
1535
|
});
|
|
1481
1536
|
}
|
|
1482
|
-
|
|
1537
|
+
/**
|
|
1538
|
+
* Start a chokidar watcher for a certificate folder.
|
|
1539
|
+
* Non-blocking — does NOT await "ready".
|
|
1540
|
+
*/
|
|
1541
|
+
#startWatcher(folder, index, createUnreffedWatcher, store) {
|
|
1483
1542
|
const { w, unreffAll } = createUnreffedWatcher(folder);
|
|
1543
|
+
let ready = false;
|
|
1484
1544
|
w.on("unlink", (filename) => {
|
|
1485
1545
|
debugLog(chalk3.cyan(`unlink in folder ${folder}`), filename);
|
|
1486
1546
|
const h = this.#filenameToHash.get(filename);
|
|
1487
1547
|
if (h && index.has(h)) {
|
|
1488
1548
|
index.delete(h);
|
|
1549
|
+
this.emit("certificateRemoved", { store, fingerprint: h, filename });
|
|
1489
1550
|
}
|
|
1490
1551
|
});
|
|
1491
1552
|
w.on("add", (filename) => {
|
|
@@ -1494,6 +1555,7 @@ var init_certificate_manager = __esm({
|
|
|
1494
1555
|
const certificate = readCertificate(filename);
|
|
1495
1556
|
const info = exploreCertificate(certificate);
|
|
1496
1557
|
const fingerprint2 = makeFingerprint(certificate);
|
|
1558
|
+
const isNew = !index.has(fingerprint2);
|
|
1497
1559
|
index.set(fingerprint2, { certificate, filename, info });
|
|
1498
1560
|
this.#filenameToHash.set(filename, fingerprint2);
|
|
1499
1561
|
debugLog(
|
|
@@ -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 || isNew) {
|
|
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,29 @@ 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
|
+
this.emit("certificateChange", { store, certificate, fingerprint: newFingerprint, filename: changedPath });
|
|
1521
1587
|
} catch (err) {
|
|
1522
1588
|
debugLog(`change event: failed to re-read ${changedPath}`, err);
|
|
1523
1589
|
}
|
|
1524
1590
|
});
|
|
1525
1591
|
this.#watchers.push(w);
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1592
|
+
this.#pendingUnrefs.add(unreffAll);
|
|
1593
|
+
w.on("ready", () => {
|
|
1594
|
+
ready = true;
|
|
1595
|
+
this.#pendingUnrefs.delete(unreffAll);
|
|
1596
|
+
unreffAll();
|
|
1597
|
+
debugLog("ready");
|
|
1598
|
+
debugLog([...index.keys()].map((k) => k.substring(0, 10)));
|
|
1533
1599
|
});
|
|
1534
1600
|
}
|
|
1535
1601
|
// make sure that all crls have been processed.
|
|
1536
1602
|
async #waitAndCheckCRLProcessingStatus() {
|
|
1537
|
-
return new Promise((resolve,
|
|
1603
|
+
return new Promise((resolve, _reject) => {
|
|
1538
1604
|
if (this.#pendingCrlToProcess === 0) {
|
|
1539
1605
|
setImmediate(resolve);
|
|
1540
1606
|
return;
|
|
1541
1607
|
}
|
|
1542
|
-
|
|
1543
|
-
return reject(new Error("Internal Error"));
|
|
1544
|
-
}
|
|
1545
|
-
this.#onCrlProcess = resolve;
|
|
1608
|
+
this.#onCrlProcessWaiters.push(resolve);
|
|
1546
1609
|
});
|
|
1547
1610
|
}
|
|
1548
1611
|
};
|