node-opcua-pki 6.14.0 → 6.16.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.js CHANGED
@@ -77,9 +77,17 @@ function adjustDate(params) {
77
77
  (0, import_node_assert.default)(params instanceof Object);
78
78
  params.startDate = params.startDate || /* @__PURE__ */ new Date();
79
79
  (0, import_node_assert.default)(params.startDate instanceof Date);
80
- params.validity = params.validity || 365;
81
- params.endDate = new Date(params.startDate.getTime());
82
- params.endDate.setDate(params.startDate.getDate() + params.validity);
80
+ if (params.validityMs !== void 0) {
81
+ if (params.validityMs <= 0) {
82
+ throw new RangeError(`validityMs must be > 0 (got ${params.validityMs})`);
83
+ }
84
+ params.endDate = new Date(params.startDate.getTime() + params.validityMs);
85
+ params.validity = Math.ceil(params.validityMs / 864e5);
86
+ } else {
87
+ params.validity = params.validity || 365;
88
+ params.endDate = new Date(params.startDate.getTime());
89
+ params.endDate.setDate(params.startDate.getDate() + params.validity);
90
+ }
83
91
  (0, import_node_assert.default)(params.endDate instanceof Date);
84
92
  (0, import_node_assert.default)(params.startDate instanceof Date);
85
93
  }
@@ -166,9 +174,15 @@ function setEnv(varName, value) {
166
174
  process.env[varName] = value;
167
175
  }
168
176
  }
177
+ function hasEnv(varName) {
178
+ return Object.prototype.hasOwnProperty.call(exportedEnvVars, varName);
179
+ }
169
180
  function getEnv(varName) {
170
181
  return exportedEnvVars[varName];
171
182
  }
183
+ function unsetEnv(varName) {
184
+ delete exportedEnvVars[varName];
185
+ }
172
186
  function getEnvironmentVarNames() {
173
187
  return Object.keys(exportedEnvVars).map((varName) => {
174
188
  return { key: varName, pattern: `\\$ENV\\:\\:${varName}` };
@@ -685,10 +699,17 @@ function openssl_require2DigitYearInDate() {
685
699
  }
686
700
  g_config.opensslVersion = "";
687
701
  var _counter = 0;
702
+ function stripConditionalBlocks(template) {
703
+ return template.replace(/\{\{#([A-Z_][A-Z0-9_]*)\}\}([\s\S]*?)\{\{\/\1\}\}\r?\n?/g, (_match, key, content) => {
704
+ const keep = hasEnv(key) && getEnv(key) !== "";
705
+ return keep ? content : "";
706
+ });
707
+ }
688
708
  function generateStaticConfig(configPath, options) {
689
709
  const prePath = options?.cwd || "";
690
710
  const originalFilename = !import_node_path3.default.isAbsolute(configPath) ? import_node_path3.default.join(prePath, configPath) : configPath;
691
711
  let staticConfig = import_node_fs5.default.readFileSync(originalFilename, { encoding: "utf8" });
712
+ staticConfig = stripConditionalBlocks(staticConfig);
692
713
  for (const envVar of getEnvironmentVarNames()) {
693
714
  staticConfig = staticConfig.replace(new RegExp(envVar.pattern, "gi"), getEnv(envVar.key));
694
715
  }
@@ -843,7 +864,9 @@ nsComment = ''OpenSSL Generated Certificate''
843
864
  #nsSslServerName =
844
865
  keyUsage = critical, digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment, keyAgreement
845
866
  extendedKeyUsage = critical,serverAuth ,clientAuth
846
-
867
+ {{#CDP_URL}}crlDistributionPoints = URI:$ENV::CDP_URL
868
+ {{/CDP_URL}}{{#AIA_VALUE}}authorityInfoAccess = $ENV::AIA_VALUE
869
+ {{/AIA_VALUE}}
847
870
  [ v3_req ]
848
871
  basicConstraints = critical, CA:FALSE
849
872
  keyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment, keyAgreement
@@ -866,10 +889,9 @@ nsComment = "CA Certificate generated by Node-OPCUA Certificate
866
889
  #nsCertType = sslCA, emailCA
867
890
  #issuerAltName = issuer:copy
868
891
  #obj = DER:02:03
869
- crlDistributionPoints = @crl_info
870
- [ crl_info ]
871
- URI.0 = http://localhost:8900/crl.pem
872
- [ v3_selfsigned]
892
+ {{#CDP_URL}}crlDistributionPoints = URI:$ENV::CDP_URL
893
+ {{/CDP_URL}}{{#AIA_VALUE}}authorityInfoAccess = $ENV::AIA_VALUE
894
+ {{/AIA_VALUE}}[ v3_selfsigned]
873
895
  basicConstraints = critical, CA:FALSE
874
896
  keyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment, keyAgreement
875
897
  extendedKeyUsage = critical,serverAuth ,clientAuth
@@ -952,6 +974,7 @@ async function construct_CertificateAuthority(certificateAuthority) {
952
974
  const subjectOpt = ` -subj "${subject.toString()}" `;
953
975
  const caCommonName = subject.commonName || "NodeOPCUA-CA";
954
976
  setEnv("ALTNAME", `URI:urn:${caCommonName}`);
977
+ certificateAuthority._wireRevocationEnvVars();
955
978
  const options = { cwd: caRootDir };
956
979
  const configFile = generateStaticConfig("conf/caconfig.cnf", options);
957
980
  const configOption = ` -config ${q3(n4(configFile))}`;
@@ -1005,6 +1028,33 @@ function parseOpenSSLDate(dateStr) {
1005
1028
  const sec = raw.substring(10, 12);
1006
1029
  return `${year}-${month}-${day}T${hour}:${min}:${sec}Z`;
1007
1030
  }
1031
+ function validateRevocationUrl(url2, fieldName) {
1032
+ if (url2 === void 0) {
1033
+ return void 0;
1034
+ }
1035
+ if (url2 === "") {
1036
+ throw new Error(`${fieldName} must not be empty \u2014 pass undefined to disable the extension`);
1037
+ }
1038
+ let parsed;
1039
+ try {
1040
+ parsed = new URL(url2);
1041
+ } catch {
1042
+ throw new Error(`${fieldName} is not a valid URL: ${url2}`);
1043
+ }
1044
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1045
+ throw new Error(`${fieldName} must use http: or https: (got ${parsed.protocol} in ${url2})`);
1046
+ }
1047
+ if (!parsed.pathname || parsed.pathname === "/") {
1048
+ throw new Error(`${fieldName} must include a path component (got ${url2})`);
1049
+ }
1050
+ const isLoopback = parsed.hostname === "localhost" || parsed.hostname === "::1" || parsed.hostname.startsWith("127.");
1051
+ if (isLoopback) {
1052
+ console.warn(
1053
+ `[node-opcua-pki] ${fieldName} points at loopback (${url2}) \u2014 certificates issued with this URL will be unreachable from any other host.`
1054
+ );
1055
+ }
1056
+ return url2;
1057
+ }
1008
1058
  var CertificateAuthority = class {
1009
1059
  /** RSA key size used when generating the CA private key. */
1010
1060
  keySize;
@@ -1014,6 +1064,10 @@ var CertificateAuthority = class {
1014
1064
  subject;
1015
1065
  /** @internal Parent CA (undefined for root CAs). */
1016
1066
  _issuerCA;
1067
+ /** @internal Configured CDP / AIA URLs (US-202). */
1068
+ _crlDistributionUrl;
1069
+ _ocspResponderUrl;
1070
+ _caIssuersUrl;
1017
1071
  constructor(options) {
1018
1072
  (0, import_node_assert7.default)(Object.prototype.hasOwnProperty.call(options, "location"));
1019
1073
  (0, import_node_assert7.default)(Object.prototype.hasOwnProperty.call(options, "keySize"));
@@ -1021,6 +1075,93 @@ var CertificateAuthority = class {
1021
1075
  this.keySize = options.keySize || 2048;
1022
1076
  this.subject = new import_node_opcua_crypto2.Subject(options.subject || defaultSubject);
1023
1077
  this._issuerCA = options.issuerCA;
1078
+ if (options.crlDistributionUrl !== void 0) {
1079
+ this.setCrlDistributionUrl(options.crlDistributionUrl);
1080
+ }
1081
+ if (options.ocspResponderUrl !== void 0) {
1082
+ this.setOcspResponderUrl(options.ocspResponderUrl);
1083
+ }
1084
+ if (options.caIssuersUrl !== void 0) {
1085
+ this.setCaIssuersUrl(options.caIssuersUrl);
1086
+ }
1087
+ }
1088
+ /**
1089
+ * Public URL where the CRL produced by this CA is reachable, or
1090
+ * `undefined` if no CDP extension should be emitted on issued certs.
1091
+ */
1092
+ get crlDistributionUrl() {
1093
+ return this._crlDistributionUrl;
1094
+ }
1095
+ /**
1096
+ * Public URL of the OCSP responder, or `undefined` if no AIA OCSP
1097
+ * leg should be emitted on issued certs.
1098
+ */
1099
+ get ocspResponderUrl() {
1100
+ return this._ocspResponderUrl;
1101
+ }
1102
+ /**
1103
+ * Public URL where the issuer's certificate can be fetched, or
1104
+ * `undefined` if no AIA caIssuers leg should be emitted.
1105
+ */
1106
+ get caIssuersUrl() {
1107
+ return this._caIssuersUrl;
1108
+ }
1109
+ /**
1110
+ * Configure the URL embedded as `crlDistributionPoints` in every
1111
+ * subsequently-issued certificate. Pass `undefined` to disable
1112
+ * the extension entirely. Validated synchronously — throws on
1113
+ * empty string, non-http(s) protocol, missing path. Warns (does
1114
+ * not throw) when the URL points at loopback.
1115
+ *
1116
+ * @see US-202
1117
+ */
1118
+ setCrlDistributionUrl(url2) {
1119
+ this._crlDistributionUrl = validateRevocationUrl(url2, "crlDistributionUrl");
1120
+ }
1121
+ /**
1122
+ * Configure the OCSP responder URL embedded as the `OCSP` leg of
1123
+ * the `authorityInfoAccess` extension on every subsequently-issued
1124
+ * certificate. Pass `undefined` to disable.
1125
+ *
1126
+ * @see US-202
1127
+ */
1128
+ setOcspResponderUrl(url2) {
1129
+ this._ocspResponderUrl = validateRevocationUrl(url2, "ocspResponderUrl");
1130
+ }
1131
+ /**
1132
+ * Configure the caIssuers URL embedded as the `caIssuers` leg of
1133
+ * the `authorityInfoAccess` extension on every subsequently-issued
1134
+ * certificate. Pass `undefined` to disable.
1135
+ *
1136
+ * @see US-202
1137
+ */
1138
+ setCaIssuersUrl(url2) {
1139
+ this._caIssuersUrl = validateRevocationUrl(url2, "caIssuersUrl");
1140
+ }
1141
+ /**
1142
+ * @internal
1143
+ * Populate the OpenSSL config substitution env vars (`CDP_URL` and
1144
+ * `AIA_VALUE`) from the configured URLs, or unset them so the
1145
+ * matching `{{#KEY}}...{{/KEY}}` blocks in the templates are
1146
+ * stripped. MUST be called before every `generateStaticConfig`
1147
+ * invocation that signs a certificate.
1148
+ */
1149
+ _wireRevocationEnvVars() {
1150
+ unsetEnv("CDP_URL");
1151
+ unsetEnv("AIA_VALUE");
1152
+ if (this._crlDistributionUrl) {
1153
+ setEnv("CDP_URL", this._crlDistributionUrl);
1154
+ }
1155
+ const aiaLegs = [];
1156
+ if (this._ocspResponderUrl) {
1157
+ aiaLegs.push(`OCSP;URI:${this._ocspResponderUrl}`);
1158
+ }
1159
+ if (this._caIssuersUrl) {
1160
+ aiaLegs.push(`caIssuers;URI:${this._caIssuersUrl}`);
1161
+ }
1162
+ if (aiaLegs.length > 0) {
1163
+ setEnv("AIA_VALUE", aiaLegs.join(","));
1164
+ }
1024
1165
  }
1025
1166
  /** Absolute path to the CA root directory (alias for {@link location}). */
1026
1167
  get rootDir() {
@@ -1267,14 +1408,15 @@ var CertificateAuthority = class {
1267
1408
  * @returns the signed certificate as a DER-encoded buffer
1268
1409
  */
1269
1410
  async signCertificateRequestFromDER(csrDer, options) {
1270
- const validity = options?.validity ?? 365;
1271
1411
  const tmpDir = await import_node_fs7.default.promises.mkdtemp(import_node_path5.default.join(import_node_os3.default.tmpdir(), "pki-sign-"));
1272
1412
  try {
1273
1413
  const csrFile = import_node_path5.default.join(tmpDir, "request.csr");
1274
1414
  const certFile = import_node_path5.default.join(tmpDir, "certificate.pem");
1275
1415
  const csrPem = (0, import_node_opcua_crypto2.toPem)(csrDer, "CERTIFICATE REQUEST");
1276
1416
  await import_node_fs7.default.promises.writeFile(csrFile, csrPem, "utf-8");
1277
- const signingParams = { validity };
1417
+ const signingParams = {};
1418
+ if (options?.validityMs !== void 0) signingParams.validityMs = options.validityMs;
1419
+ else signingParams.validity = options?.validity ?? 365;
1278
1420
  if (options?.startDate) signingParams.startDate = options.startDate;
1279
1421
  if (options?.dns) signingParams.dns = options.dns;
1280
1422
  if (options?.ip) signingParams.ip = options.ip;
@@ -1290,6 +1432,35 @@ var CertificateAuthority = class {
1290
1432
  });
1291
1433
  }
1292
1434
  }
1435
+ /**
1436
+ * Advertise the validity limits this CA can honor.
1437
+ *
1438
+ * Consumers (notably the GDS server in [`cert_auth.ts`](https://github.com/sterfive/node-opcua-gds))
1439
+ * clamp a requested validity against these bounds before calling
1440
+ * {@link signCertificateRequestFromDER}, so a misconfigured
1441
+ * `defaultCertValidity` cannot ask the CA for something it cannot
1442
+ * produce.
1443
+ *
1444
+ * Defaults match the OpenSSL-backed implementation:
1445
+ * - `minValidityMs = 60_000` (1 minute) — practical floor; the
1446
+ * X.509 spec floor is 1 second but very short certs are rarely
1447
+ * useful and pathological for any real deployment.
1448
+ * - `maxValidityMs = 10 * 365 * 86_400_000` (≈ 10 years) — long
1449
+ * enough for root CAs.
1450
+ * - `validityGranularityMs = 1_000` (1 second) — RFC 5280 §4.1.2.5
1451
+ * floor on `notBefore` / `notAfter`.
1452
+ * - `nativeUnit = "second"` — what `x509Date()` actually encodes.
1453
+ *
1454
+ * @see US-208 — the consumer-side capability story.
1455
+ */
1456
+ getCapabilities() {
1457
+ return {
1458
+ minValidityMs: 6e4,
1459
+ maxValidityMs: 10 * 365 * 864e5,
1460
+ validityGranularityMs: 1e3,
1461
+ nativeUnit: "second"
1462
+ };
1463
+ }
1293
1464
  /**
1294
1465
  * Generate a new RSA key pair, create an internal CSR, sign it
1295
1466
  * with this CA, and return both the certificate and private key
@@ -1307,7 +1478,6 @@ var CertificateAuthority = class {
1307
1478
  */
1308
1479
  async generateKeyPairAndSignDER(options) {
1309
1480
  const keySize = options.keySize ?? 2048;
1310
- const validity = options.validity ?? 365;
1311
1481
  const startDate = options.startDate ?? /* @__PURE__ */ new Date();
1312
1482
  const tmpDir = await import_node_fs7.default.promises.mkdtemp(import_node_path5.default.join(import_node_os3.default.tmpdir(), "pki-keygen-"));
1313
1483
  try {
@@ -1327,13 +1497,15 @@ var CertificateAuthority = class {
1327
1497
  purpose: import_node_opcua_crypto2.CertificatePurpose.ForApplication
1328
1498
  });
1329
1499
  const certFile = import_node_path5.default.join(tmpDir, "certificate.pem");
1330
- await this.signCertificateRequest(certFile, csrFile, {
1500
+ const signingParams = {
1331
1501
  applicationUri: options.applicationUri,
1332
1502
  dns: options.dns,
1333
1503
  ip: options.ip,
1334
- startDate,
1335
- validity
1336
- });
1504
+ startDate
1505
+ };
1506
+ if (options.validityMs !== void 0) signingParams.validityMs = options.validityMs;
1507
+ else signingParams.validity = options.validity ?? 365;
1508
+ await this.signCertificateRequest(certFile, csrFile, signingParams);
1337
1509
  const certPem = (0, import_node_opcua_crypto2.readCertificatePEM)(certFile);
1338
1510
  const certificateDer = (0, import_node_opcua_crypto2.convertPEMtoDER)(certPem);
1339
1511
  const privateKey = (0, import_node_opcua_crypto2.readPrivateKey)(privateKeyFile);
@@ -1358,7 +1530,6 @@ var CertificateAuthority = class {
1358
1530
  */
1359
1531
  async generateKeyPairAndSignPFX(options) {
1360
1532
  const keySize = options.keySize ?? 2048;
1361
- const validity = options.validity ?? 365;
1362
1533
  const startDate = options.startDate ?? /* @__PURE__ */ new Date();
1363
1534
  const passphrase = options.passphrase ?? "";
1364
1535
  const tmpDir = await import_node_fs7.default.promises.mkdtemp(import_node_path5.default.join(import_node_os3.default.tmpdir(), "pki-keygen-pfx-"));
@@ -1379,13 +1550,15 @@ var CertificateAuthority = class {
1379
1550
  purpose: import_node_opcua_crypto2.CertificatePurpose.ForApplication
1380
1551
  });
1381
1552
  const certFile = import_node_path5.default.join(tmpDir, "certificate.pem");
1382
- await this.signCertificateRequest(certFile, csrFile, {
1553
+ const signingParams = {
1383
1554
  applicationUri: options.applicationUri,
1384
1555
  dns: options.dns,
1385
1556
  ip: options.ip,
1386
- startDate,
1387
- validity
1388
- });
1557
+ startDate
1558
+ };
1559
+ if (options.validityMs !== void 0) signingParams.validityMs = options.validityMs;
1560
+ else signingParams.validity = options.validity ?? 365;
1561
+ await this.signCertificateRequest(certFile, csrFile, signingParams);
1389
1562
  const pfxFile = import_node_path5.default.join(tmpDir, "bundle.pfx");
1390
1563
  await createPFX({
1391
1564
  certificateFile: certFile,
@@ -1623,6 +1796,7 @@ var CertificateAuthority = class {
1623
1796
  async signCACertificateRequest(certFile, csrFile, params) {
1624
1797
  const caRootDir = import_node_path5.default.resolve(this.rootDir);
1625
1798
  const options = { cwd: caRootDir };
1799
+ this._wireRevocationEnvVars();
1626
1800
  const configFile = generateStaticConfig("conf/caconfig.cnf", options);
1627
1801
  const validity = params.validity ?? 3650;
1628
1802
  await execute_openssl(
@@ -1789,6 +1963,7 @@ var CertificateAuthority = class {
1789
1963
  ip
1790
1964
  };
1791
1965
  processAltNames(params);
1966
+ this._wireRevocationEnvVars();
1792
1967
  const configFile = generateStaticConfig("conf/caconfig.cnf", options);
1793
1968
  displaySubtitle("- then we ask the authority to sign the certificate signing request");
1794
1969
  const configOption = ` -config ${configFile}`;