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 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 !") + " 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,10 @@ 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
- }
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 (!issuerThumbprints.has(thumbprint)) {
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
- #onCrlProcess;
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
- if (this.#onCrlProcess) {
1413
- this.#onCrlProcess();
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: Math.min(10 * 60 * 1e3, Math.max(100, this.folderPoolingInterval)) } : {},
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
- 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);
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
- 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
- }
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
- w.on("add", (filename) => {
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
- 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
- });
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
- async #walkAllFiles(folder, index, createUnreffedWatcher) {
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
- 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
- });
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, reject) => {
1603
+ return new Promise((resolve, _reject) => {
1538
1604
  if (this.#pendingCrlToProcess === 0) {
1539
1605
  setImmediate(resolve);
1540
1606
  return;
1541
1607
  }
1542
- if (this.#onCrlProcess) {
1543
- return reject(new Error("Internal Error"));
1544
- }
1545
- this.#onCrlProcess = resolve;
1608
+ this.#onCrlProcessWaiters.push(resolve);
1546
1609
  });
1547
1610
  }
1548
1611
  };