node-opcua-pki 6.12.0 → 6.12.2

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
@@ -336,7 +336,7 @@ var init_simple_config_template_cnf = __esm({
336
336
  "packages/node-opcua-pki/lib/pki/templates/simple_config_template.cnf.ts"() {
337
337
  "use strict";
338
338
  init_esm_shims();
339
- config = '##################################################################################################\n## SIMPLE OPENSSL CONFIG FILE FOR SELF-SIGNED CERTIFICATE GENERATION\n################################################################################################################\n\ndistinguished_name = req_distinguished_name\ndefault_md = sha1\n\ndefault_md = sha256 # The default digest algorithm\n\n[ v3_ca ]\nsubjectKeyIdentifier = hash\nauthorityKeyIdentifier = keyid:always,issuer:always\n\n# authorityKeyIdentifier = keyid\nbasicConstraints = CA:TRUE\nkeyUsage = critical, cRLSign, keyCertSign\nnsComment = "Self-signed Certificate for CA generated by Node-OPCUA Certificate utility"\n#nsCertType = sslCA, emailCA\n#subjectAltName = email:copy\n#issuerAltName = issuer:copy\n#obj = DER:02:03\n# crlDistributionPoints = @crl_info\n# [ crl_info ]\n# URI.0 = http://localhost:8900/crl.pem\nsubjectAltName = $ENV::ALTNAME\n\n[ req ]\ndays = 390\nreq_extensions = v3_req\nx509_extensions = v3_ca\n\n[v3_req]\nbasicConstraints = CA:false\nkeyUsage = critical, cRLSign, keyCertSign\nsubjectAltName = $ENV::ALTNAME\n\n[ v3_ca_signed]\nsubjectKeyIdentifier = hash\nauthorityKeyIdentifier = keyid,issuer\nbasicConstraints = critical, CA:FALSE\nkeyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment, keyCertSign\nextendedKeyUsage = clientAuth,serverAuth \nnsComment = "certificate generated by Node-OPCUA Certificate utility and signed by a CA"\nsubjectAltName = $ENV::ALTNAME\n[ v3_selfsigned]\nsubjectKeyIdentifier = hash\nauthorityKeyIdentifier = keyid,issuer\nbasicConstraints = critical, CA:FALSE\nkeyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment, keyCertSign\nextendedKeyUsage = clientAuth,serverAuth \nnsComment = "Self-signed certificate generated by Node-OPCUA Certificate utility"\nsubjectAltName = $ENV::ALTNAME\n[ req_distinguished_name ]\ncountryName = Country Name (2 letter code)\ncountryName_default = FR\ncountryName_min = 2\ncountryName_max = 2\n# stateOrProvinceName = State or Province Name (full name)\n# stateOrProvinceName_default = Ile de France\n# localityName = Locality Name (city, district)\n# localityName_default = Paris\norganizationName = Organization Name (company)\norganizationName_default = NodeOPCUA\n# organizationalUnitName = Organizational Unit Name (department, division)\n# organizationalUnitName_default = R&D\ncommonName = Common Name (hostname, FQDN, IP, or your name)\ncommonName_max = 256\ncommonName_default = NodeOPCUA\n# emailAddress = Email Address\n# emailAddress_max = 40\n# emailAddress_default = node-opcua (at) node-opcua (dot) com\nsubjectAltName = $ENV::ALTNAME';
339
+ config = '##################################################################################################\n## SIMPLE OPENSSL CONFIG FILE FOR SELF-SIGNED CERTIFICATE GENERATION\n################################################################################################################\n\ndistinguished_name = req_distinguished_name\ndefault_md = sha1\n\ndefault_md = sha256 # The default digest algorithm\n\n[ v3_ca ]\nsubjectKeyIdentifier = hash\nauthorityKeyIdentifier = keyid:always,issuer:always\n\n# authorityKeyIdentifier = keyid\nbasicConstraints = CA:TRUE\nkeyUsage = critical, cRLSign, keyCertSign\nnsComment = "Self-signed Certificate for CA generated by Node-OPCUA Certificate utility"\n#nsCertType = sslCA, emailCA\n#subjectAltName = email:copy\n#issuerAltName = issuer:copy\n#obj = DER:02:03\n# crlDistributionPoints = @crl_info\n# [ crl_info ]\n# URI.0 = http://localhost:8900/crl.pem\nsubjectAltName = $ENV::ALTNAME\n\n[ req ]\ndays = 390\nreq_extensions = v3_req\nx509_extensions = v3_ca\n\n[v3_req]\nbasicConstraints = CA:false\nkeyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment\nsubjectAltName = $ENV::ALTNAME\n\n[ v3_ca_signed]\nsubjectKeyIdentifier = hash\nauthorityKeyIdentifier = keyid,issuer\nbasicConstraints = critical, CA:FALSE\nkeyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment\nextendedKeyUsage = clientAuth,serverAuth \nnsComment = "certificate generated by Node-OPCUA Certificate utility and signed by a CA"\nsubjectAltName = $ENV::ALTNAME\n[ v3_selfsigned]\nsubjectKeyIdentifier = hash\nauthorityKeyIdentifier = keyid,issuer\nbasicConstraints = critical, CA:FALSE\nkeyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment\nextendedKeyUsage = clientAuth,serverAuth \nnsComment = "Self-signed certificate generated by Node-OPCUA Certificate utility"\nsubjectAltName = $ENV::ALTNAME\n[ req_distinguished_name ]\ncountryName = Country Name (2 letter code)\ncountryName_default = FR\ncountryName_min = 2\ncountryName_max = 2\n# stateOrProvinceName = State or Province Name (full name)\n# stateOrProvinceName_default = Ile de France\n# localityName = Locality Name (city, district)\n# localityName_default = Paris\norganizationName = Organization Name (company)\norganizationName_default = NodeOPCUA\n# organizationalUnitName = Organizational Unit Name (department, division)\n# organizationalUnitName_default = R&D\ncommonName = Common Name (hostname, FQDN, IP, or your name)\ncommonName_max = 256\ncommonName_default = NodeOPCUA\n# emailAddress = Email Address\n# emailAddress_max = 40\n# emailAddress_default = node-opcua (at) node-opcua (dot) com\nsubjectAltName = $ENV::ALTNAME';
340
340
  simple_config_template_cnf_default = config;
341
341
  }
342
342
  });
@@ -367,17 +367,27 @@ function getOrComputeInfo(entry) {
367
367
  }
368
368
  return entry.info;
369
369
  }
370
+ function coerceCertificateChain(certificate) {
371
+ if (Array.isArray(certificate)) {
372
+ if (certificate.length === 0) return [];
373
+ return certificate.reduce((acc, cert) => {
374
+ return acc.concat(split_der(cert));
375
+ }, []);
376
+ }
377
+ return split_der(certificate);
378
+ }
370
379
  function makeFingerprint(certificate) {
371
- const chain = Array.isArray(certificate) ? certificate : split_der(certificate);
380
+ const chain = coerceCertificateChain(certificate);
372
381
  return makeSHA1Thumbprint(chain[0]).toString("hex");
373
382
  }
374
383
  function short(stringToShorten) {
375
384
  return stringToShorten.substring(0, 10);
376
385
  }
377
386
  function buildIdealCertificateName(certificate) {
378
- const fingerprint2 = makeFingerprint(certificate);
387
+ const chain = coerceCertificateChain(certificate);
388
+ const fingerprint2 = makeFingerprint(chain);
379
389
  try {
380
- const commonName = exploreCertificate(certificate).tbsCertificate.subject.commonName || "";
390
+ const commonName = exploreCertificate(chain[0]).tbsCertificate.subject.commonName || "";
381
391
  const sanitizedCommonName = commonName.replace(forbiddenChars, "_");
382
392
  return `${sanitizedCommonName}[${fingerprint2}]`;
383
393
  } catch (_err) {
@@ -417,7 +427,8 @@ function isIssuer(certificate) {
417
427
  }
418
428
  }
419
429
  function findIssuerCertificateInChain(certificate, chain) {
420
- const firstCertificate = Array.isArray(certificate) ? certificate[0] : certificate;
430
+ const coercedCertificate = coerceCertificateChain(certificate);
431
+ const firstCertificate = coercedCertificate[0];
421
432
  if (!firstCertificate) {
422
433
  return null;
423
434
  }
@@ -430,7 +441,8 @@ function findIssuerCertificateInChain(certificate, chain) {
430
441
  debugLog("Certificate has no extension 3");
431
442
  return null;
432
443
  }
433
- const potentialIssuers = chain.filter((c) => {
444
+ const coercedChain = coerceCertificateChain(chain);
445
+ const potentialIssuers = coercedChain.filter((c) => {
434
446
  const info = exploreCertificate(c);
435
447
  return info.tbsCertificate.extensions && info.tbsCertificate.extensions.subjectKeyIdentifier === wantedIssuerKey;
436
448
  });
@@ -604,18 +616,18 @@ var init_certificate_manager = __esm({
604
616
  /**
605
617
  * Move a certificate to the rejected store.
606
618
  * If the certificate was previously trusted, it will be removed from the trusted folder.
607
- * @param certificate - the DER-encoded certificate
619
+ * @param certificateOrChain - the DER-encoded certificate or certificate chain
608
620
  */
609
- async rejectCertificate(certificate) {
610
- await this.#moveCertificate(certificate, "rejected");
621
+ async rejectCertificate(certificateOrChain) {
622
+ await this.#moveCertificate(certificateOrChain, "rejected");
611
623
  }
612
624
  /**
613
625
  * Move a certificate to the trusted store.
614
626
  * If the certificate was previously rejected, it will be removed from the rejected folder.
615
- * @param certificate - the DER-encoded certificate
627
+ * @param certificateOrChain - the DER-encoded certificate or certificate chain
616
628
  */
617
- async trustCertificate(certificate) {
618
- await this.#moveCertificate(certificate, "trusted");
629
+ async trustCertificate(certificateOrChain) {
630
+ await this.#moveCertificate(certificateOrChain, "trusted");
619
631
  }
620
632
  /**
621
633
  * Check whether the trusted certificate store is empty.
@@ -669,37 +681,46 @@ var init_certificate_manager = __esm({
669
681
  * @returns `"Good"` if trusted, `"BadCertificateUntrusted"` if rejected/unknown,
670
682
  * or `"BadCertificateInvalid"` if the certificate cannot be parsed.
671
683
  */
672
- async isCertificateTrusted(certificate) {
673
- let fingerprint2;
684
+ async isCertificateTrusted(certificateOrCertificateChain) {
674
685
  try {
675
- fingerprint2 = makeFingerprint(certificate);
676
- } catch (_err) {
677
- return "BadCertificateInvalid";
678
- }
679
- if (this.#thumbs.trusted.has(fingerprint2)) {
680
- return "Good";
681
- }
682
- if (!this.#thumbs.rejected.has(fingerprint2)) {
683
- if (!this.untrustUnknownCertificate) {
684
- return "Good";
686
+ const chain = coerceCertificateChain(certificateOrCertificateChain);
687
+ const leafCertificate = chain[0];
688
+ if (chain.length < 1) {
689
+ return "BadCertificateInvalid";
685
690
  }
691
+ let fingerprint2;
686
692
  try {
687
- exploreCertificateInfo(certificate);
693
+ fingerprint2 = makeFingerprint(chain[0]);
688
694
  } catch (_err) {
689
695
  return "BadCertificateInvalid";
690
696
  }
691
- const filename = path2.join(this.rejectedFolder, `${buildIdealCertificateName(certificate)}.pem`);
692
- debugLog("certificate has never been seen before and is now rejected (untrusted) ", filename);
693
- await fsWriteFile(filename, toPem(certificate, "CERTIFICATE"));
694
- this.#thumbs.rejected.set(fingerprint2, { certificate, filename });
697
+ if (this.#thumbs.trusted.has(fingerprint2)) {
698
+ return "Good";
699
+ }
700
+ if (!this.#thumbs.rejected.has(fingerprint2)) {
701
+ if (!this.untrustUnknownCertificate) {
702
+ return "Good";
703
+ }
704
+ try {
705
+ exploreCertificateInfo(chain[0]);
706
+ } catch (_err) {
707
+ return "BadCertificateInvalid";
708
+ }
709
+ const filename = path2.join(this.rejectedFolder, `${buildIdealCertificateName(leafCertificate)}.pem`);
710
+ debugLog("certificate has never been seen before and is now rejected (untrusted) ", filename);
711
+ await fsWriteFile(filename, toPem(chain, "CERTIFICATE"));
712
+ this.#thumbs.rejected.set(fingerprint2, { certificate: leafCertificate, filename });
713
+ }
714
+ return "BadCertificateUntrusted";
715
+ } catch (_err) {
716
+ return "BadCertificateInvalid";
695
717
  }
696
- return "BadCertificateUntrusted";
697
718
  }
698
719
  async #innerVerifyCertificateAsync(certificateOrChain, _isIssuer, level, options) {
699
720
  if (level >= 5) {
700
721
  return "BadSecurityChecksFailed" /* BadSecurityChecksFailed */;
701
722
  }
702
- const chain = Array.isArray(certificateOrChain) ? certificateOrChain : [certificateOrChain];
723
+ const chain = coerceCertificateChain(certificateOrChain);
703
724
  debugLog("NB CERTIFICATE IN CHAIN = ", chain.length);
704
725
  const info = exploreCertificate(chain[0]);
705
726
  let hasValidIssuer = false;
@@ -754,7 +775,7 @@ var init_certificate_manager = __esm({
754
775
  return "BadSecurityChecksFailed" /* BadSecurityChecksFailed */;
755
776
  }
756
777
  hasValidIssuer = true;
757
- let revokedStatus = await this.isCertificateRevoked(certificateOrChain);
778
+ let revokedStatus = await this.isCertificateRevoked(chain, issuerCertificate);
758
779
  if (revokedStatus === "BadCertificateRevocationUnknown" /* BadCertificateRevocationUnknown */) {
759
780
  if (options?.ignoreMissingRevocationList) {
760
781
  revokedStatus = "Good" /* Good */;
@@ -779,11 +800,11 @@ var init_certificate_manager = __esm({
779
800
  debugLog("Self-signed Certificate signature is not valid");
780
801
  return "BadSecurityChecksFailed" /* BadSecurityChecksFailed */;
781
802
  }
782
- const revokedStatus = await this.isCertificateRevoked(certificateOrChain);
803
+ const revokedStatus = await this.isCertificateRevoked(chain);
783
804
  debugLog("revokedStatus of self signed certificate:", revokedStatus);
784
805
  }
785
806
  }
786
- const status = await this.#checkRejectedOrTrusted(certificateOrChain);
807
+ const status = await this.#checkRejectedOrTrusted(chain[0]);
787
808
  if (status === "rejected") {
788
809
  if (!(options.acceptCertificateWithValidIssuerChain && hasValidIssuer && hasTrustedIssuer)) {
789
810
  return "BadCertificateUntrusted" /* BadCertificateUntrusted */;
@@ -841,17 +862,15 @@ var init_certificate_manager = __esm({
841
862
  * @returns the verification status code
842
863
  */
843
864
  async verifyCertificateAsync(certificate, options) {
844
- if (!Array.isArray(certificate)) {
865
+ const chain = coerceCertificateChain(certificate);
866
+ for (const element of chain) {
845
867
  try {
846
- const derElements = split_der(certificate);
847
- for (const element of derElements) {
848
- exploreCertificateInfo(element);
849
- }
868
+ exploreCertificateInfo(element);
850
869
  } catch (_err) {
851
870
  return "BadCertificateInvalid" /* BadCertificateInvalid */;
852
871
  }
853
872
  }
854
- const status1 = await this.#innerVerifyCertificateAsync(certificate, false, 0, options);
873
+ const status1 = await this.#innerVerifyCertificateAsync(chain, false, 0, options);
855
874
  return status1;
856
875
  }
857
876
  /**
@@ -1281,7 +1300,7 @@ var init_certificate_manager = __esm({
1281
1300
  async #addTrustedCertificateFromChainImpl(certificateChain) {
1282
1301
  let certificates;
1283
1302
  try {
1284
- certificates = Array.isArray(certificateChain) ? certificateChain : split_der(certificateChain);
1303
+ certificates = coerceCertificateChain(certificateChain);
1285
1304
  } catch (_err) {
1286
1305
  return "BadCertificateInvalid" /* BadCertificateInvalid */;
1287
1306
  }
@@ -1365,7 +1384,7 @@ var init_certificate_manager = __esm({
1365
1384
  return "BadCertificateInvalid" /* BadCertificateInvalid */;
1366
1385
  }
1367
1386
  }
1368
- await this.trustCertificate(leafCertificate);
1387
+ await this.trustCertificate(certificates);
1369
1388
  return "Good" /* Good */;
1370
1389
  }
1371
1390
  /**
@@ -1408,7 +1427,7 @@ var init_certificate_manager = __esm({
1408
1427
  *
1409
1428
  */
1410
1429
  async findIssuerCertificate(certificate) {
1411
- const firstCertificate = Array.isArray(certificate) ? certificate[0] : certificate;
1430
+ const firstCertificate = coerceCertificateChain(certificate)[0];
1412
1431
  const certInfo = exploreCertificate(firstCertificate);
1413
1432
  if (isSelfSigned2(certInfo)) {
1414
1433
  return firstCertificate;
@@ -1445,7 +1464,7 @@ var init_certificate_manager = __esm({
1445
1464
  * @private
1446
1465
  */
1447
1466
  async #checkRejectedOrTrusted(certificate) {
1448
- const firstCertificate = Array.isArray(certificate) ? certificate[0] : certificate;
1467
+ const firstCertificate = coerceCertificateChain(certificate)[0];
1449
1468
  const fingerprint2 = makeFingerprint(firstCertificate);
1450
1469
  debugLog("#checkRejectedOrTrusted fingerprint ", short(fingerprint2));
1451
1470
  await this.#readCertificates();
@@ -1457,12 +1476,14 @@ var init_certificate_manager = __esm({
1457
1476
  }
1458
1477
  return "unknown";
1459
1478
  }
1460
- async #moveCertificate(certificate, newStatus) {
1479
+ async #moveCertificate(certificateOrChain, newStatus) {
1461
1480
  await this.withLock2(async () => {
1481
+ const chain = coerceCertificateChain(certificateOrChain);
1482
+ const certificate = chain[0];
1462
1483
  const fingerprint2 = makeFingerprint(certificate);
1463
1484
  let status = await this.#checkRejectedOrTrusted(certificate);
1464
1485
  if (status === "unknown") {
1465
- const pem = toPem(certificate, "CERTIFICATE");
1486
+ const pem = toPem(chain, "CERTIFICATE");
1466
1487
  const filename = path2.join(this.rejectedFolder, `${buildIdealCertificateName(certificate)}.pem`);
1467
1488
  await fs4.promises.writeFile(filename, pem);
1468
1489
  this.#thumbs.rejected.set(fingerprint2, { certificate, filename });
@@ -1512,13 +1533,17 @@ var init_certificate_manager = __esm({
1512
1533
  * found.
1513
1534
  */
1514
1535
  async isCertificateRevoked(certificate, issuerCertificate) {
1515
- const firstCertificate = Array.isArray(certificate) ? certificate[0] : certificate;
1536
+ const chain = coerceCertificateChain(certificate);
1537
+ const firstCertificate = chain[0];
1516
1538
  if (isSelfSigned3(firstCertificate)) {
1517
1539
  return "Good" /* Good */;
1518
1540
  }
1519
1541
  if (!issuerCertificate) {
1520
1542
  issuerCertificate = await this.findIssuerCertificate(firstCertificate);
1521
1543
  }
1544
+ if (!issuerCertificate) {
1545
+ issuerCertificate = findIssuerCertificateInChain(firstCertificate, chain);
1546
+ }
1522
1547
  if (!issuerCertificate) {
1523
1548
  return "BadCertificateChainIncomplete" /* BadCertificateChainIncomplete */;
1524
1549
  }
@@ -1645,7 +1670,25 @@ var init_certificate_manager = __esm({
1645
1670
  try {
1646
1671
  const stat = await fs4.promises.stat(filename);
1647
1672
  if (!stat.isFile()) continue;
1648
- const certificate = (await readCertificateChainAsync(filename))[0];
1673
+ const certs = await readCertificateChainAsync(filename);
1674
+ if (certs.length === 0) continue;
1675
+ const certificate = certs[0];
1676
+ if (certs.length > 1) {
1677
+ try {
1678
+ await fs4.promises.writeFile(filename, toPem(certs, "CERTIFICATE"), "ascii");
1679
+ } catch (writeErr) {
1680
+ debugLog(`scanCertFolder: could not rewrite legacy PEM ${filename} (read-only fs?)`, writeErr);
1681
+ }
1682
+ for (let i = 1; i < certs.length; i++) {
1683
+ if (isIssuer(certs[i])) {
1684
+ try {
1685
+ await this.addIssuer(certs[i]);
1686
+ } catch (issuerErr) {
1687
+ debugLog(`scanCertFolder: could not auto-register issuer from ${filename}`, issuerErr);
1688
+ }
1689
+ }
1690
+ }
1691
+ }
1649
1692
  const info = exploreCertificate(certificate);
1650
1693
  const fingerprint2 = makeFingerprint(certificate);
1651
1694
  index.set(fingerprint2, { certificate, filename, info });
@@ -2544,7 +2587,7 @@ nsComment = ''OpenSSL Generated Certificate''
2544
2587
  #nsRenewalUrl =
2545
2588
  #nsCaPolicyUrl =
2546
2589
  #nsSslServerName =
2547
- keyUsage = critical, digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment, keyAgreement, keyCertSign
2590
+ keyUsage = critical, digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment, keyAgreement
2548
2591
  extendedKeyUsage = critical,serverAuth ,clientAuth
2549
2592
 
2550
2593
  [ v3_req ]