node-opcua-pki 6.12.2 → 6.14.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
@@ -455,7 +455,7 @@ function findIssuerCertificateInChain(certificate, chain) {
455
455
  }
456
456
  return null;
457
457
  }
458
- var configurationFileSimpleTemplate, fsWriteFile, forbiddenChars, CertificateManager;
458
+ var configurationFileSimpleTemplate, fsWriteFile, forbiddenChars, ChainCompletionStatus, CertificateManager;
459
459
  var init_certificate_manager = __esm({
460
460
  "packages/node-opcua-pki/lib/pki/certificate_manager.ts"() {
461
461
  "use strict";
@@ -467,6 +467,14 @@ var init_certificate_manager = __esm({
467
467
  configurationFileSimpleTemplate = simple_config_template_cnf_default;
468
468
  fsWriteFile = fs4.promises.writeFile;
469
469
  forbiddenChars = /[\x00-\x1F<>:"/\\|?*]/g;
470
+ ChainCompletionStatus = /* @__PURE__ */ ((ChainCompletionStatus2) => {
471
+ ChainCompletionStatus2["AlreadyComplete"] = "AlreadyComplete";
472
+ ChainCompletionStatus2["ChainCompleted"] = "ChainCompleted";
473
+ ChainCompletionStatus2["IssuerNotFound"] = "IssuerNotFound";
474
+ ChainCompletionStatus2["EmptyChain"] = "EmptyChain";
475
+ ChainCompletionStatus2["MaxDepthReached"] = "MaxDepthReached";
476
+ return ChainCompletionStatus2;
477
+ })(ChainCompletionStatus || {});
470
478
  CertificateManager = class _CertificateManager extends EventEmitter {
471
479
  // ── Global instance registry ─────────────────────────────────
472
480
  // Tracks all initialized CertificateManager instances so their
@@ -559,6 +567,7 @@ var init_certificate_manager = __esm({
559
567
  #filenameToHash = /* @__PURE__ */ new Map();
560
568
  #initializingPromise;
561
569
  #addCertValidation;
570
+ #disableFileWatchers;
562
571
  #thumbs = {
563
572
  rejected: /* @__PURE__ */ new Map(),
564
573
  trusted: /* @__PURE__ */ new Map(),
@@ -592,6 +601,7 @@ var init_certificate_manager = __esm({
592
601
  ignoreMissingRevocationList: v.ignoreMissingRevocationList ?? false,
593
602
  maxChainLength: v.maxChainLength ?? 5
594
603
  };
604
+ this.#disableFileWatchers = options.disableFileWatchers ?? process.env.OPCUA_PKI_DISABLE_FILE_WATCHERS === "true";
595
605
  mkdirRecursiveSync(options.location);
596
606
  if (!fs4.existsSync(this.#location)) {
597
607
  throw new Error(`CertificateManager cannot access location ${this.#location}`);
@@ -1456,6 +1466,75 @@ var init_certificate_manager = __esm({
1456
1466
  }
1457
1467
  return selectedTrustedCertificates.length > 0 ? selectedTrustedCertificates[0].certificate : null;
1458
1468
  }
1469
+ /**
1470
+ * Outcome status for {@link CertificateManager.completeCertificateChain}.
1471
+ */
1472
+ static ChainCompletionStatus = ChainCompletionStatus;
1473
+ /**
1474
+ * Complete a certificate chain by walking the issuer store.
1475
+ *
1476
+ * Starting from the last certificate in the provided chain, this method
1477
+ * repeatedly calls {@link findIssuerCertificate} to locate the parent
1478
+ * certificate until it reaches a self-signed root or can no longer find
1479
+ * an issuer.
1480
+ *
1481
+ * @param chain - the (potentially partial) certificate chain, leaf first
1482
+ * @param maxDepth - maximum number of issuers to append (default: 10)
1483
+ * @returns a {@link ChainCompletionResult} containing the (possibly completed)
1484
+ * chain, a status code, and an optional diagnostic message.
1485
+ */
1486
+ async completeCertificateChain(chain, maxDepth = 10) {
1487
+ if (chain.length === 0) {
1488
+ return {
1489
+ chain,
1490
+ status: "EmptyChain" /* EmptyChain */,
1491
+ message: "Input chain is empty \u2014 nothing to complete."
1492
+ };
1493
+ }
1494
+ await this.#scanCertFolder(this.issuersCertFolder, this.#thumbs.issuers.certs);
1495
+ const result = [...chain];
1496
+ let depth = 0;
1497
+ while (depth < maxDepth) {
1498
+ const lastCert = result[result.length - 1];
1499
+ const lastInfo = exploreCertificate(lastCert);
1500
+ if (isSelfSigned2(lastInfo)) {
1501
+ const wasExtended = result.length > chain.length;
1502
+ return {
1503
+ chain: result,
1504
+ status: wasExtended ? "ChainCompleted" /* ChainCompleted */ : "AlreadyComplete" /* AlreadyComplete */,
1505
+ message: wasExtended ? `Chain completed: ${result.length - chain.length} issuer(s) appended, ending at self-signed root "${lastInfo.tbsCertificate.subject.commonName}".` : `Chain is already complete (self-signed root "${lastInfo.tbsCertificate.subject.commonName}").`
1506
+ };
1507
+ }
1508
+ const issuerCert = await this.findIssuerCertificate(lastCert);
1509
+ if (!issuerCert) {
1510
+ const cn = lastInfo.tbsCertificate.subject.commonName ?? "?";
1511
+ const akid = lastInfo.tbsCertificate.extensions?.authorityKeyIdentifier?.keyIdentifier ?? "?";
1512
+ const msg = `Cannot find issuer for "${cn}" (authorityKeyIdentifier: ${akid}). Ensure the CA certificate is present in the issuers/certs folder.`;
1513
+ warningLog(`completeCertificateChain: ${msg}`);
1514
+ return {
1515
+ chain: result,
1516
+ status: "IssuerNotFound" /* IssuerNotFound */,
1517
+ message: msg
1518
+ };
1519
+ }
1520
+ const issuerFingerprint = makeFingerprint(issuerCert);
1521
+ const alreadyInChain = result.some((c) => makeFingerprint(c) === issuerFingerprint);
1522
+ if (alreadyInChain) {
1523
+ return {
1524
+ chain: result,
1525
+ status: "AlreadyComplete" /* AlreadyComplete */,
1526
+ message: `Chain ends at root "${exploreCertificate(issuerCert).tbsCertificate.subject.commonName}" (already present in chain).`
1527
+ };
1528
+ }
1529
+ result.push(issuerCert);
1530
+ depth++;
1531
+ }
1532
+ return {
1533
+ chain: result,
1534
+ status: "MaxDepthReached" /* MaxDepthReached */,
1535
+ message: `Chain completion stopped after ${maxDepth} iterations \u2014 possible circular chain or very deep hierarchy.`
1536
+ };
1537
+ }
1459
1538
  /**
1460
1539
  *
1461
1540
  * check if the certificate explicitly appear in the trust list, the reject list or none.
@@ -1651,11 +1730,15 @@ var init_certificate_manager = __esm({
1651
1730
  this.#scanCrlFolder(this.crlFolder, this.#thumbs.crl),
1652
1731
  this.#scanCrlFolder(this.issuersCrlFolder, this.#thumbs.issuersCrl)
1653
1732
  ]);
1654
- this.#startWatcher(this.trustedFolder, this.#thumbs.trusted, createUnreffedWatcher, "trusted");
1655
- this.#startWatcher(this.issuersCertFolder, this.#thumbs.issuers.certs, createUnreffedWatcher, "issuersCerts");
1656
- this.#startWatcher(this.rejectedFolder, this.#thumbs.rejected, createUnreffedWatcher, "rejected");
1657
- this.#startCrlWatcher(this.crlFolder, this.#thumbs.crl, createUnreffedWatcher, "crl");
1658
- this.#startCrlWatcher(this.issuersCrlFolder, this.#thumbs.issuersCrl, createUnreffedWatcher, "issuersCrl");
1733
+ if (this.#disableFileWatchers) {
1734
+ fs4.watch = origWatch;
1735
+ } else {
1736
+ this.#startWatcher(this.trustedFolder, this.#thumbs.trusted, createUnreffedWatcher, "trusted");
1737
+ this.#startWatcher(this.issuersCertFolder, this.#thumbs.issuers.certs, createUnreffedWatcher, "issuersCerts");
1738
+ this.#startWatcher(this.rejectedFolder, this.#thumbs.rejected, createUnreffedWatcher, "rejected");
1739
+ this.#startCrlWatcher(this.crlFolder, this.#thumbs.crl, createUnreffedWatcher, "crl");
1740
+ this.#startCrlWatcher(this.issuersCrlFolder, this.#thumbs.issuersCrl, createUnreffedWatcher, "issuersCrl");
1741
+ }
1659
1742
  }
1660
1743
  /**
1661
1744
  * Scan a certificate folder and populate the in-memory index.
@@ -2600,15 +2683,16 @@ nsComment = "CA Generated by Node-OPCUA Certificate utility usin
2600
2683
  subjectKeyIdentifier = hash
2601
2684
  basicConstraints = CA:TRUE
2602
2685
  keyUsage = critical, cRLSign, keyCertSign
2686
+ subjectAltName = $ENV::ALTNAME
2603
2687
  nsComment = "CA CSR generated by Node-OPCUA Certificate utility using openssl"
2604
2688
  [ v3_ca ]
2605
2689
  subjectKeyIdentifier = hash
2606
2690
  authorityKeyIdentifier = keyid:always,issuer:always
2607
2691
  basicConstraints = CA:TRUE
2608
2692
  keyUsage = critical, cRLSign, keyCertSign
2693
+ subjectAltName = $ENV::ALTNAME
2609
2694
  nsComment = "CA Certificate generated by Node-OPCUA Certificate utility using openssl"
2610
2695
  #nsCertType = sslCA, emailCA
2611
- #subjectAltName = email:copy
2612
2696
  #issuerAltName = issuer:copy
2613
2697
  #obj = DER:02:03
2614
2698
  crlDistributionPoints = @crl_info
@@ -2704,7 +2788,8 @@ async function construct_CertificateAuthority(certificateAuthority) {
2704
2788
  await fs10.promises.writeFile(caConfigFile, data);
2705
2789
  }
2706
2790
  const subjectOpt = ` -subj "${subject.toString()}" `;
2707
- processAltNames({});
2791
+ const caCommonName = subject.commonName || "NodeOPCUA-CA";
2792
+ setEnv("ALTNAME", `URI:urn:${caCommonName}`);
2708
2793
  const options = { cwd: caRootDir };
2709
2794
  const configFile = generateStaticConfig("conf/caconfig.cnf", options);
2710
2795
  const configOption = ` -config ${q4(n5(configFile))}`;