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 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) => cm.dispose()));
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 !") + " not before date =" + certificateInfo.notBefore
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 (!issuerThumbprints.has(thumbprint)) {
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
- #onCrlProcess;
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
- if (this.#onCrlProcess) {
1413
- this.#onCrlProcess();
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: Math.min(10 * 60 * 1e3, Math.max(100, this.folderPoolingInterval)) } : {},
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
- const promises = [
1449
- this.#walkAllFiles(this.trustedFolder, this.#thumbs.trusted, createUnreffedWatcher),
1450
- this.#walkAllFiles(this.issuersCertFolder, this.#thumbs.issuers.certs, createUnreffedWatcher),
1451
- this.#walkAllFiles(this.rejectedFolder, this.#thumbs.rejected, createUnreffedWatcher),
1452
- this.#walkCRLFiles(this.crlFolder, this.#thumbs.crl, createUnreffedWatcher),
1453
- this.#walkCRLFiles(this.issuersCrlFolder, this.#thumbs.issuersCrl, createUnreffedWatcher)
1454
- ];
1455
- await Promise.all(promises);
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
- async #walkCRLFiles(folder, index, createUnreffedWatcher) {
1459
- await new Promise((resolve, _reject) => {
1460
- const { w, unreffAll } = createUnreffedWatcher(folder);
1461
- w.on("unlink", (filename) => {
1462
- for (const [key, data] of index.entries()) {
1463
- data.crls = data.crls.filter((c) => c.filename !== filename);
1464
- if (data.crls.length === 0) {
1465
- index.delete(key);
1466
- }
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
- w.on("add", (filename) => {
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
- w.on("change", (changedPath) => {
1473
- debugLog("change in folder ", folder, changedPath);
1474
- });
1475
- this.#watchers.push(w);
1476
- w.on("ready", () => {
1477
- unreffAll();
1478
- resolve();
1479
- });
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
- async #walkAllFiles(folder, index, createUnreffedWatcher) {
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
- await new Promise((resolve, _reject) => {
1527
- w.on("ready", () => {
1528
- unreffAll();
1529
- debugLog("ready");
1530
- debugLog([...index.keys()].map((k) => k.substring(0, 10)));
1531
- resolve();
1532
- });
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, reject) => {
1605
+ return new Promise((resolve, _reject) => {
1538
1606
  if (this.#pendingCrlToProcess === 0) {
1539
1607
  setImmediate(resolve);
1540
1608
  return;
1541
1609
  }
1542
- if (this.#onCrlProcess) {
1543
- return reject(new Error("Internal Error"));
1544
- }
1545
- this.#onCrlProcess = resolve;
1610
+ this.#onCrlProcessWaiters.push(resolve);
1546
1611
  });
1547
1612
  }
1548
1613
  };