node-opcua-pki 6.12.1 → 6.13.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 +149 -36
- package/dist/bin/pki.mjs.map +1 -1
- package/dist/index.d.mts +65 -6
- package/dist/index.d.ts +65 -6
- package/dist/index.js +150 -35
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +149 -35
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
package/dist/bin/pki.mjs
CHANGED
|
@@ -384,9 +384,10 @@ function short(stringToShorten) {
|
|
|
384
384
|
return stringToShorten.substring(0, 10);
|
|
385
385
|
}
|
|
386
386
|
function buildIdealCertificateName(certificate) {
|
|
387
|
-
const
|
|
387
|
+
const chain = coerceCertificateChain(certificate);
|
|
388
|
+
const fingerprint2 = makeFingerprint(chain);
|
|
388
389
|
try {
|
|
389
|
-
const commonName = exploreCertificate(
|
|
390
|
+
const commonName = exploreCertificate(chain[0]).tbsCertificate.subject.commonName || "";
|
|
390
391
|
const sanitizedCommonName = commonName.replace(forbiddenChars, "_");
|
|
391
392
|
return `${sanitizedCommonName}[${fingerprint2}]`;
|
|
392
393
|
} catch (_err) {
|
|
@@ -454,7 +455,7 @@ function findIssuerCertificateInChain(certificate, chain) {
|
|
|
454
455
|
}
|
|
455
456
|
return null;
|
|
456
457
|
}
|
|
457
|
-
var configurationFileSimpleTemplate, fsWriteFile, forbiddenChars, CertificateManager;
|
|
458
|
+
var configurationFileSimpleTemplate, fsWriteFile, forbiddenChars, ChainCompletionStatus, CertificateManager;
|
|
458
459
|
var init_certificate_manager = __esm({
|
|
459
460
|
"packages/node-opcua-pki/lib/pki/certificate_manager.ts"() {
|
|
460
461
|
"use strict";
|
|
@@ -466,6 +467,14 @@ var init_certificate_manager = __esm({
|
|
|
466
467
|
configurationFileSimpleTemplate = simple_config_template_cnf_default;
|
|
467
468
|
fsWriteFile = fs4.promises.writeFile;
|
|
468
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 || {});
|
|
469
478
|
CertificateManager = class _CertificateManager extends EventEmitter {
|
|
470
479
|
// ── Global instance registry ─────────────────────────────────
|
|
471
480
|
// Tracks all initialized CertificateManager instances so their
|
|
@@ -558,6 +567,7 @@ var init_certificate_manager = __esm({
|
|
|
558
567
|
#filenameToHash = /* @__PURE__ */ new Map();
|
|
559
568
|
#initializingPromise;
|
|
560
569
|
#addCertValidation;
|
|
570
|
+
#disableFileWatchers;
|
|
561
571
|
#thumbs = {
|
|
562
572
|
rejected: /* @__PURE__ */ new Map(),
|
|
563
573
|
trusted: /* @__PURE__ */ new Map(),
|
|
@@ -591,6 +601,7 @@ var init_certificate_manager = __esm({
|
|
|
591
601
|
ignoreMissingRevocationList: v.ignoreMissingRevocationList ?? false,
|
|
592
602
|
maxChainLength: v.maxChainLength ?? 5
|
|
593
603
|
};
|
|
604
|
+
this.#disableFileWatchers = options.disableFileWatchers ?? process.env.OPCUA_PKI_DISABLE_FILE_WATCHERS === "true";
|
|
594
605
|
mkdirRecursiveSync(options.location);
|
|
595
606
|
if (!fs4.existsSync(this.#location)) {
|
|
596
607
|
throw new Error(`CertificateManager cannot access location ${this.#location}`);
|
|
@@ -615,18 +626,18 @@ var init_certificate_manager = __esm({
|
|
|
615
626
|
/**
|
|
616
627
|
* Move a certificate to the rejected store.
|
|
617
628
|
* If the certificate was previously trusted, it will be removed from the trusted folder.
|
|
618
|
-
* @param
|
|
629
|
+
* @param certificateOrChain - the DER-encoded certificate or certificate chain
|
|
619
630
|
*/
|
|
620
|
-
async rejectCertificate(
|
|
621
|
-
await this.#moveCertificate(
|
|
631
|
+
async rejectCertificate(certificateOrChain) {
|
|
632
|
+
await this.#moveCertificate(certificateOrChain, "rejected");
|
|
622
633
|
}
|
|
623
634
|
/**
|
|
624
635
|
* Move a certificate to the trusted store.
|
|
625
636
|
* If the certificate was previously rejected, it will be removed from the rejected folder.
|
|
626
|
-
* @param
|
|
637
|
+
* @param certificateOrChain - the DER-encoded certificate or certificate chain
|
|
627
638
|
*/
|
|
628
|
-
async trustCertificate(
|
|
629
|
-
await this.#moveCertificate(
|
|
639
|
+
async trustCertificate(certificateOrChain) {
|
|
640
|
+
await this.#moveCertificate(certificateOrChain, "trusted");
|
|
630
641
|
}
|
|
631
642
|
/**
|
|
632
643
|
* Check whether the trusted certificate store is empty.
|
|
@@ -680,31 +691,40 @@ var init_certificate_manager = __esm({
|
|
|
680
691
|
* @returns `"Good"` if trusted, `"BadCertificateUntrusted"` if rejected/unknown,
|
|
681
692
|
* or `"BadCertificateInvalid"` if the certificate cannot be parsed.
|
|
682
693
|
*/
|
|
683
|
-
async isCertificateTrusted(
|
|
684
|
-
let fingerprint2;
|
|
694
|
+
async isCertificateTrusted(certificateOrCertificateChain) {
|
|
685
695
|
try {
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
if (this.#thumbs.trusted.has(fingerprint2)) {
|
|
691
|
-
return "Good";
|
|
692
|
-
}
|
|
693
|
-
if (!this.#thumbs.rejected.has(fingerprint2)) {
|
|
694
|
-
if (!this.untrustUnknownCertificate) {
|
|
695
|
-
return "Good";
|
|
696
|
+
const chain = coerceCertificateChain(certificateOrCertificateChain);
|
|
697
|
+
const leafCertificate = chain[0];
|
|
698
|
+
if (chain.length < 1) {
|
|
699
|
+
return "BadCertificateInvalid";
|
|
696
700
|
}
|
|
701
|
+
let fingerprint2;
|
|
697
702
|
try {
|
|
698
|
-
|
|
703
|
+
fingerprint2 = makeFingerprint(chain[0]);
|
|
699
704
|
} catch (_err) {
|
|
700
705
|
return "BadCertificateInvalid";
|
|
701
706
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
this.#thumbs.rejected.
|
|
707
|
+
if (this.#thumbs.trusted.has(fingerprint2)) {
|
|
708
|
+
return "Good";
|
|
709
|
+
}
|
|
710
|
+
if (!this.#thumbs.rejected.has(fingerprint2)) {
|
|
711
|
+
if (!this.untrustUnknownCertificate) {
|
|
712
|
+
return "Good";
|
|
713
|
+
}
|
|
714
|
+
try {
|
|
715
|
+
exploreCertificateInfo(chain[0]);
|
|
716
|
+
} catch (_err) {
|
|
717
|
+
return "BadCertificateInvalid";
|
|
718
|
+
}
|
|
719
|
+
const filename = path2.join(this.rejectedFolder, `${buildIdealCertificateName(leafCertificate)}.pem`);
|
|
720
|
+
debugLog("certificate has never been seen before and is now rejected (untrusted) ", filename);
|
|
721
|
+
await fsWriteFile(filename, toPem(chain, "CERTIFICATE"));
|
|
722
|
+
this.#thumbs.rejected.set(fingerprint2, { certificate: leafCertificate, filename });
|
|
723
|
+
}
|
|
724
|
+
return "BadCertificateUntrusted";
|
|
725
|
+
} catch (_err) {
|
|
726
|
+
return "BadCertificateInvalid";
|
|
706
727
|
}
|
|
707
|
-
return "BadCertificateUntrusted";
|
|
708
728
|
}
|
|
709
729
|
async #innerVerifyCertificateAsync(certificateOrChain, _isIssuer, level, options) {
|
|
710
730
|
if (level >= 5) {
|
|
@@ -1374,7 +1394,7 @@ var init_certificate_manager = __esm({
|
|
|
1374
1394
|
return "BadCertificateInvalid" /* BadCertificateInvalid */;
|
|
1375
1395
|
}
|
|
1376
1396
|
}
|
|
1377
|
-
await this.trustCertificate(
|
|
1397
|
+
await this.trustCertificate(certificates);
|
|
1378
1398
|
return "Good" /* Good */;
|
|
1379
1399
|
}
|
|
1380
1400
|
/**
|
|
@@ -1446,6 +1466,75 @@ var init_certificate_manager = __esm({
|
|
|
1446
1466
|
}
|
|
1447
1467
|
return selectedTrustedCertificates.length > 0 ? selectedTrustedCertificates[0].certificate : null;
|
|
1448
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
|
+
}
|
|
1449
1538
|
/**
|
|
1450
1539
|
*
|
|
1451
1540
|
* check if the certificate explicitly appear in the trust list, the reject list or none.
|
|
@@ -1466,12 +1555,14 @@ var init_certificate_manager = __esm({
|
|
|
1466
1555
|
}
|
|
1467
1556
|
return "unknown";
|
|
1468
1557
|
}
|
|
1469
|
-
async #moveCertificate(
|
|
1558
|
+
async #moveCertificate(certificateOrChain, newStatus) {
|
|
1470
1559
|
await this.withLock2(async () => {
|
|
1560
|
+
const chain = coerceCertificateChain(certificateOrChain);
|
|
1561
|
+
const certificate = chain[0];
|
|
1471
1562
|
const fingerprint2 = makeFingerprint(certificate);
|
|
1472
1563
|
let status = await this.#checkRejectedOrTrusted(certificate);
|
|
1473
1564
|
if (status === "unknown") {
|
|
1474
|
-
const pem = toPem(
|
|
1565
|
+
const pem = toPem(chain, "CERTIFICATE");
|
|
1475
1566
|
const filename = path2.join(this.rejectedFolder, `${buildIdealCertificateName(certificate)}.pem`);
|
|
1476
1567
|
await fs4.promises.writeFile(filename, pem);
|
|
1477
1568
|
this.#thumbs.rejected.set(fingerprint2, { certificate, filename });
|
|
@@ -1639,11 +1730,15 @@ var init_certificate_manager = __esm({
|
|
|
1639
1730
|
this.#scanCrlFolder(this.crlFolder, this.#thumbs.crl),
|
|
1640
1731
|
this.#scanCrlFolder(this.issuersCrlFolder, this.#thumbs.issuersCrl)
|
|
1641
1732
|
]);
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
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
|
+
}
|
|
1647
1742
|
}
|
|
1648
1743
|
/**
|
|
1649
1744
|
* Scan a certificate folder and populate the in-memory index.
|
|
@@ -1658,7 +1753,25 @@ var init_certificate_manager = __esm({
|
|
|
1658
1753
|
try {
|
|
1659
1754
|
const stat = await fs4.promises.stat(filename);
|
|
1660
1755
|
if (!stat.isFile()) continue;
|
|
1661
|
-
const
|
|
1756
|
+
const certs = await readCertificateChainAsync(filename);
|
|
1757
|
+
if (certs.length === 0) continue;
|
|
1758
|
+
const certificate = certs[0];
|
|
1759
|
+
if (certs.length > 1) {
|
|
1760
|
+
try {
|
|
1761
|
+
await fs4.promises.writeFile(filename, toPem(certs, "CERTIFICATE"), "ascii");
|
|
1762
|
+
} catch (writeErr) {
|
|
1763
|
+
debugLog(`scanCertFolder: could not rewrite legacy PEM ${filename} (read-only fs?)`, writeErr);
|
|
1764
|
+
}
|
|
1765
|
+
for (let i = 1; i < certs.length; i++) {
|
|
1766
|
+
if (isIssuer(certs[i])) {
|
|
1767
|
+
try {
|
|
1768
|
+
await this.addIssuer(certs[i]);
|
|
1769
|
+
} catch (issuerErr) {
|
|
1770
|
+
debugLog(`scanCertFolder: could not auto-register issuer from ${filename}`, issuerErr);
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1662
1775
|
const info = exploreCertificate(certificate);
|
|
1663
1776
|
const fingerprint2 = makeFingerprint(certificate);
|
|
1664
1777
|
index.set(fingerprint2, { certificate, filename, info });
|