node-opcua-pki 6.13.0 → 6.15.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/index.mjs CHANGED
@@ -120,9 +120,15 @@ function setEnv(varName, value) {
120
120
  process.env[varName] = value;
121
121
  }
122
122
  }
123
+ function hasEnv(varName) {
124
+ return Object.prototype.hasOwnProperty.call(exportedEnvVars, varName);
125
+ }
123
126
  function getEnv(varName) {
124
127
  return exportedEnvVars[varName];
125
128
  }
129
+ function unsetEnv(varName) {
130
+ delete exportedEnvVars[varName];
131
+ }
126
132
  function getEnvironmentVarNames() {
127
133
  return Object.keys(exportedEnvVars).map((varName) => {
128
134
  return { key: varName, pattern: `\\$ENV\\:\\:${varName}` };
@@ -639,10 +645,17 @@ function openssl_require2DigitYearInDate() {
639
645
  }
640
646
  g_config.opensslVersion = "";
641
647
  var _counter = 0;
648
+ function stripConditionalBlocks(template) {
649
+ return template.replace(/\{\{#([A-Z_][A-Z0-9_]*)\}\}([\s\S]*?)\{\{\/\1\}\}\r?\n?/g, (_match, key, content) => {
650
+ const keep = hasEnv(key) && getEnv(key) !== "";
651
+ return keep ? content : "";
652
+ });
653
+ }
642
654
  function generateStaticConfig(configPath, options) {
643
655
  const prePath = options?.cwd || "";
644
656
  const originalFilename = !path3.isAbsolute(configPath) ? path3.join(prePath, configPath) : configPath;
645
657
  let staticConfig = fs5.readFileSync(originalFilename, { encoding: "utf8" });
658
+ staticConfig = stripConditionalBlocks(staticConfig);
646
659
  for (const envVar of getEnvironmentVarNames()) {
647
660
  staticConfig = staticConfig.replace(new RegExp(envVar.pattern, "gi"), getEnv(envVar.key));
648
661
  }
@@ -797,7 +810,9 @@ nsComment = ''OpenSSL Generated Certificate''
797
810
  #nsSslServerName =
798
811
  keyUsage = critical, digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment, keyAgreement
799
812
  extendedKeyUsage = critical,serverAuth ,clientAuth
800
-
813
+ {{#CDP_URL}}crlDistributionPoints = URI:$ENV::CDP_URL
814
+ {{/CDP_URL}}{{#AIA_VALUE}}authorityInfoAccess = $ENV::AIA_VALUE
815
+ {{/AIA_VALUE}}
801
816
  [ v3_req ]
802
817
  basicConstraints = critical, CA:FALSE
803
818
  keyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment, keyAgreement
@@ -808,21 +823,21 @@ nsComment = "CA Generated by Node-OPCUA Certificate utility usin
808
823
  subjectKeyIdentifier = hash
809
824
  basicConstraints = CA:TRUE
810
825
  keyUsage = critical, cRLSign, keyCertSign
826
+ subjectAltName = $ENV::ALTNAME
811
827
  nsComment = "CA CSR generated by Node-OPCUA Certificate utility using openssl"
812
828
  [ v3_ca ]
813
829
  subjectKeyIdentifier = hash
814
830
  authorityKeyIdentifier = keyid:always,issuer:always
815
831
  basicConstraints = CA:TRUE
816
832
  keyUsage = critical, cRLSign, keyCertSign
833
+ subjectAltName = $ENV::ALTNAME
817
834
  nsComment = "CA Certificate generated by Node-OPCUA Certificate utility using openssl"
818
835
  #nsCertType = sslCA, emailCA
819
- #subjectAltName = email:copy
820
836
  #issuerAltName = issuer:copy
821
837
  #obj = DER:02:03
822
- crlDistributionPoints = @crl_info
823
- [ crl_info ]
824
- URI.0 = http://localhost:8900/crl.pem
825
- [ v3_selfsigned]
838
+ {{#CDP_URL}}crlDistributionPoints = URI:$ENV::CDP_URL
839
+ {{/CDP_URL}}{{#AIA_VALUE}}authorityInfoAccess = $ENV::AIA_VALUE
840
+ {{/AIA_VALUE}}[ v3_selfsigned]
826
841
  basicConstraints = critical, CA:FALSE
827
842
  keyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment, keyAgreement
828
843
  extendedKeyUsage = critical,serverAuth ,clientAuth
@@ -903,7 +918,9 @@ async function construct_CertificateAuthority(certificateAuthority) {
903
918
  await fs7.promises.writeFile(caConfigFile, data);
904
919
  }
905
920
  const subjectOpt = ` -subj "${subject.toString()}" `;
906
- processAltNames({});
921
+ const caCommonName = subject.commonName || "NodeOPCUA-CA";
922
+ setEnv("ALTNAME", `URI:urn:${caCommonName}`);
923
+ certificateAuthority._wireRevocationEnvVars();
907
924
  const options = { cwd: caRootDir };
908
925
  const configFile = generateStaticConfig("conf/caconfig.cnf", options);
909
926
  const configOption = ` -config ${q3(n4(configFile))}`;
@@ -957,6 +974,33 @@ function parseOpenSSLDate(dateStr) {
957
974
  const sec = raw.substring(10, 12);
958
975
  return `${year}-${month}-${day}T${hour}:${min}:${sec}Z`;
959
976
  }
977
+ function validateRevocationUrl(url2, fieldName) {
978
+ if (url2 === void 0) {
979
+ return void 0;
980
+ }
981
+ if (url2 === "") {
982
+ throw new Error(`${fieldName} must not be empty \u2014 pass undefined to disable the extension`);
983
+ }
984
+ let parsed;
985
+ try {
986
+ parsed = new URL(url2);
987
+ } catch {
988
+ throw new Error(`${fieldName} is not a valid URL: ${url2}`);
989
+ }
990
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
991
+ throw new Error(`${fieldName} must use http: or https: (got ${parsed.protocol} in ${url2})`);
992
+ }
993
+ if (!parsed.pathname || parsed.pathname === "/") {
994
+ throw new Error(`${fieldName} must include a path component (got ${url2})`);
995
+ }
996
+ const isLoopback = parsed.hostname === "localhost" || parsed.hostname === "::1" || parsed.hostname.startsWith("127.");
997
+ if (isLoopback) {
998
+ console.warn(
999
+ `[node-opcua-pki] ${fieldName} points at loopback (${url2}) \u2014 certificates issued with this URL will be unreachable from any other host.`
1000
+ );
1001
+ }
1002
+ return url2;
1003
+ }
960
1004
  var CertificateAuthority = class {
961
1005
  /** RSA key size used when generating the CA private key. */
962
1006
  keySize;
@@ -966,6 +1010,10 @@ var CertificateAuthority = class {
966
1010
  subject;
967
1011
  /** @internal Parent CA (undefined for root CAs). */
968
1012
  _issuerCA;
1013
+ /** @internal Configured CDP / AIA URLs (US-202). */
1014
+ _crlDistributionUrl;
1015
+ _ocspResponderUrl;
1016
+ _caIssuersUrl;
969
1017
  constructor(options) {
970
1018
  assert7(Object.prototype.hasOwnProperty.call(options, "location"));
971
1019
  assert7(Object.prototype.hasOwnProperty.call(options, "keySize"));
@@ -973,6 +1021,93 @@ var CertificateAuthority = class {
973
1021
  this.keySize = options.keySize || 2048;
974
1022
  this.subject = new Subject2(options.subject || defaultSubject);
975
1023
  this._issuerCA = options.issuerCA;
1024
+ if (options.crlDistributionUrl !== void 0) {
1025
+ this.setCrlDistributionUrl(options.crlDistributionUrl);
1026
+ }
1027
+ if (options.ocspResponderUrl !== void 0) {
1028
+ this.setOcspResponderUrl(options.ocspResponderUrl);
1029
+ }
1030
+ if (options.caIssuersUrl !== void 0) {
1031
+ this.setCaIssuersUrl(options.caIssuersUrl);
1032
+ }
1033
+ }
1034
+ /**
1035
+ * Public URL where the CRL produced by this CA is reachable, or
1036
+ * `undefined` if no CDP extension should be emitted on issued certs.
1037
+ */
1038
+ get crlDistributionUrl() {
1039
+ return this._crlDistributionUrl;
1040
+ }
1041
+ /**
1042
+ * Public URL of the OCSP responder, or `undefined` if no AIA OCSP
1043
+ * leg should be emitted on issued certs.
1044
+ */
1045
+ get ocspResponderUrl() {
1046
+ return this._ocspResponderUrl;
1047
+ }
1048
+ /**
1049
+ * Public URL where the issuer's certificate can be fetched, or
1050
+ * `undefined` if no AIA caIssuers leg should be emitted.
1051
+ */
1052
+ get caIssuersUrl() {
1053
+ return this._caIssuersUrl;
1054
+ }
1055
+ /**
1056
+ * Configure the URL embedded as `crlDistributionPoints` in every
1057
+ * subsequently-issued certificate. Pass `undefined` to disable
1058
+ * the extension entirely. Validated synchronously — throws on
1059
+ * empty string, non-http(s) protocol, missing path. Warns (does
1060
+ * not throw) when the URL points at loopback.
1061
+ *
1062
+ * @see US-202
1063
+ */
1064
+ setCrlDistributionUrl(url2) {
1065
+ this._crlDistributionUrl = validateRevocationUrl(url2, "crlDistributionUrl");
1066
+ }
1067
+ /**
1068
+ * Configure the OCSP responder URL embedded as the `OCSP` leg of
1069
+ * the `authorityInfoAccess` extension on every subsequently-issued
1070
+ * certificate. Pass `undefined` to disable.
1071
+ *
1072
+ * @see US-202
1073
+ */
1074
+ setOcspResponderUrl(url2) {
1075
+ this._ocspResponderUrl = validateRevocationUrl(url2, "ocspResponderUrl");
1076
+ }
1077
+ /**
1078
+ * Configure the caIssuers URL embedded as the `caIssuers` leg of
1079
+ * the `authorityInfoAccess` extension on every subsequently-issued
1080
+ * certificate. Pass `undefined` to disable.
1081
+ *
1082
+ * @see US-202
1083
+ */
1084
+ setCaIssuersUrl(url2) {
1085
+ this._caIssuersUrl = validateRevocationUrl(url2, "caIssuersUrl");
1086
+ }
1087
+ /**
1088
+ * @internal
1089
+ * Populate the OpenSSL config substitution env vars (`CDP_URL` and
1090
+ * `AIA_VALUE`) from the configured URLs, or unset them so the
1091
+ * matching `{{#KEY}}...{{/KEY}}` blocks in the templates are
1092
+ * stripped. MUST be called before every `generateStaticConfig`
1093
+ * invocation that signs a certificate.
1094
+ */
1095
+ _wireRevocationEnvVars() {
1096
+ unsetEnv("CDP_URL");
1097
+ unsetEnv("AIA_VALUE");
1098
+ if (this._crlDistributionUrl) {
1099
+ setEnv("CDP_URL", this._crlDistributionUrl);
1100
+ }
1101
+ const aiaLegs = [];
1102
+ if (this._ocspResponderUrl) {
1103
+ aiaLegs.push(`OCSP;URI:${this._ocspResponderUrl}`);
1104
+ }
1105
+ if (this._caIssuersUrl) {
1106
+ aiaLegs.push(`caIssuers;URI:${this._caIssuersUrl}`);
1107
+ }
1108
+ if (aiaLegs.length > 0) {
1109
+ setEnv("AIA_VALUE", aiaLegs.join(","));
1110
+ }
976
1111
  }
977
1112
  /** Absolute path to the CA root directory (alias for {@link location}). */
978
1113
  get rootDir() {
@@ -1575,6 +1710,7 @@ var CertificateAuthority = class {
1575
1710
  async signCACertificateRequest(certFile, csrFile, params) {
1576
1711
  const caRootDir = path5.resolve(this.rootDir);
1577
1712
  const options = { cwd: caRootDir };
1713
+ this._wireRevocationEnvVars();
1578
1714
  const configFile = generateStaticConfig("conf/caconfig.cnf", options);
1579
1715
  const validity = params.validity ?? 3650;
1580
1716
  await execute_openssl(
@@ -1741,6 +1877,7 @@ var CertificateAuthority = class {
1741
1877
  ip
1742
1878
  };
1743
1879
  processAltNames(params);
1880
+ this._wireRevocationEnvVars();
1744
1881
  const configFile = generateStaticConfig("conf/caconfig.cnf", options);
1745
1882
  displaySubtitle("- then we ask the authority to sign the certificate signing request");
1746
1883
  const configOption = ` -config ${configFile}`;