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.mjs CHANGED
@@ -31,9 +31,17 @@ function adjustDate(params) {
31
31
  assert(params instanceof Object);
32
32
  params.startDate = params.startDate || /* @__PURE__ */ new Date();
33
33
  assert(params.startDate instanceof Date);
34
- params.validity = params.validity || 365;
35
- params.endDate = new Date(params.startDate.getTime());
36
- params.endDate.setDate(params.startDate.getDate() + params.validity);
34
+ if (params.validityMs !== void 0) {
35
+ if (params.validityMs <= 0) {
36
+ throw new RangeError(`validityMs must be > 0 (got ${params.validityMs})`);
37
+ }
38
+ params.endDate = new Date(params.startDate.getTime() + params.validityMs);
39
+ params.validity = Math.ceil(params.validityMs / 864e5);
40
+ } else {
41
+ params.validity = params.validity || 365;
42
+ params.endDate = new Date(params.startDate.getTime());
43
+ params.endDate.setDate(params.startDate.getDate() + params.validity);
44
+ }
37
45
  assert(params.endDate instanceof Date);
38
46
  assert(params.startDate instanceof Date);
39
47
  }
@@ -120,9 +128,15 @@ function setEnv(varName, value) {
120
128
  process.env[varName] = value;
121
129
  }
122
130
  }
131
+ function hasEnv(varName) {
132
+ return Object.prototype.hasOwnProperty.call(exportedEnvVars, varName);
133
+ }
123
134
  function getEnv(varName) {
124
135
  return exportedEnvVars[varName];
125
136
  }
137
+ function unsetEnv(varName) {
138
+ delete exportedEnvVars[varName];
139
+ }
126
140
  function getEnvironmentVarNames() {
127
141
  return Object.keys(exportedEnvVars).map((varName) => {
128
142
  return { key: varName, pattern: `\\$ENV\\:\\:${varName}` };
@@ -639,10 +653,17 @@ function openssl_require2DigitYearInDate() {
639
653
  }
640
654
  g_config.opensslVersion = "";
641
655
  var _counter = 0;
656
+ function stripConditionalBlocks(template) {
657
+ return template.replace(/\{\{#([A-Z_][A-Z0-9_]*)\}\}([\s\S]*?)\{\{\/\1\}\}\r?\n?/g, (_match, key, content) => {
658
+ const keep = hasEnv(key) && getEnv(key) !== "";
659
+ return keep ? content : "";
660
+ });
661
+ }
642
662
  function generateStaticConfig(configPath, options) {
643
663
  const prePath = options?.cwd || "";
644
664
  const originalFilename = !path3.isAbsolute(configPath) ? path3.join(prePath, configPath) : configPath;
645
665
  let staticConfig = fs5.readFileSync(originalFilename, { encoding: "utf8" });
666
+ staticConfig = stripConditionalBlocks(staticConfig);
646
667
  for (const envVar of getEnvironmentVarNames()) {
647
668
  staticConfig = staticConfig.replace(new RegExp(envVar.pattern, "gi"), getEnv(envVar.key));
648
669
  }
@@ -797,7 +818,9 @@ nsComment = ''OpenSSL Generated Certificate''
797
818
  #nsSslServerName =
798
819
  keyUsage = critical, digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment, keyAgreement
799
820
  extendedKeyUsage = critical,serverAuth ,clientAuth
800
-
821
+ {{#CDP_URL}}crlDistributionPoints = URI:$ENV::CDP_URL
822
+ {{/CDP_URL}}{{#AIA_VALUE}}authorityInfoAccess = $ENV::AIA_VALUE
823
+ {{/AIA_VALUE}}
801
824
  [ v3_req ]
802
825
  basicConstraints = critical, CA:FALSE
803
826
  keyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment, keyAgreement
@@ -820,10 +843,9 @@ nsComment = "CA Certificate generated by Node-OPCUA Certificate
820
843
  #nsCertType = sslCA, emailCA
821
844
  #issuerAltName = issuer:copy
822
845
  #obj = DER:02:03
823
- crlDistributionPoints = @crl_info
824
- [ crl_info ]
825
- URI.0 = http://localhost:8900/crl.pem
826
- [ v3_selfsigned]
846
+ {{#CDP_URL}}crlDistributionPoints = URI:$ENV::CDP_URL
847
+ {{/CDP_URL}}{{#AIA_VALUE}}authorityInfoAccess = $ENV::AIA_VALUE
848
+ {{/AIA_VALUE}}[ v3_selfsigned]
827
849
  basicConstraints = critical, CA:FALSE
828
850
  keyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment, keyAgreement
829
851
  extendedKeyUsage = critical,serverAuth ,clientAuth
@@ -906,6 +928,7 @@ async function construct_CertificateAuthority(certificateAuthority) {
906
928
  const subjectOpt = ` -subj "${subject.toString()}" `;
907
929
  const caCommonName = subject.commonName || "NodeOPCUA-CA";
908
930
  setEnv("ALTNAME", `URI:urn:${caCommonName}`);
931
+ certificateAuthority._wireRevocationEnvVars();
909
932
  const options = { cwd: caRootDir };
910
933
  const configFile = generateStaticConfig("conf/caconfig.cnf", options);
911
934
  const configOption = ` -config ${q3(n4(configFile))}`;
@@ -959,6 +982,33 @@ function parseOpenSSLDate(dateStr) {
959
982
  const sec = raw.substring(10, 12);
960
983
  return `${year}-${month}-${day}T${hour}:${min}:${sec}Z`;
961
984
  }
985
+ function validateRevocationUrl(url2, fieldName) {
986
+ if (url2 === void 0) {
987
+ return void 0;
988
+ }
989
+ if (url2 === "") {
990
+ throw new Error(`${fieldName} must not be empty \u2014 pass undefined to disable the extension`);
991
+ }
992
+ let parsed;
993
+ try {
994
+ parsed = new URL(url2);
995
+ } catch {
996
+ throw new Error(`${fieldName} is not a valid URL: ${url2}`);
997
+ }
998
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
999
+ throw new Error(`${fieldName} must use http: or https: (got ${parsed.protocol} in ${url2})`);
1000
+ }
1001
+ if (!parsed.pathname || parsed.pathname === "/") {
1002
+ throw new Error(`${fieldName} must include a path component (got ${url2})`);
1003
+ }
1004
+ const isLoopback = parsed.hostname === "localhost" || parsed.hostname === "::1" || parsed.hostname.startsWith("127.");
1005
+ if (isLoopback) {
1006
+ console.warn(
1007
+ `[node-opcua-pki] ${fieldName} points at loopback (${url2}) \u2014 certificates issued with this URL will be unreachable from any other host.`
1008
+ );
1009
+ }
1010
+ return url2;
1011
+ }
962
1012
  var CertificateAuthority = class {
963
1013
  /** RSA key size used when generating the CA private key. */
964
1014
  keySize;
@@ -968,6 +1018,10 @@ var CertificateAuthority = class {
968
1018
  subject;
969
1019
  /** @internal Parent CA (undefined for root CAs). */
970
1020
  _issuerCA;
1021
+ /** @internal Configured CDP / AIA URLs (US-202). */
1022
+ _crlDistributionUrl;
1023
+ _ocspResponderUrl;
1024
+ _caIssuersUrl;
971
1025
  constructor(options) {
972
1026
  assert7(Object.prototype.hasOwnProperty.call(options, "location"));
973
1027
  assert7(Object.prototype.hasOwnProperty.call(options, "keySize"));
@@ -975,6 +1029,93 @@ var CertificateAuthority = class {
975
1029
  this.keySize = options.keySize || 2048;
976
1030
  this.subject = new Subject2(options.subject || defaultSubject);
977
1031
  this._issuerCA = options.issuerCA;
1032
+ if (options.crlDistributionUrl !== void 0) {
1033
+ this.setCrlDistributionUrl(options.crlDistributionUrl);
1034
+ }
1035
+ if (options.ocspResponderUrl !== void 0) {
1036
+ this.setOcspResponderUrl(options.ocspResponderUrl);
1037
+ }
1038
+ if (options.caIssuersUrl !== void 0) {
1039
+ this.setCaIssuersUrl(options.caIssuersUrl);
1040
+ }
1041
+ }
1042
+ /**
1043
+ * Public URL where the CRL produced by this CA is reachable, or
1044
+ * `undefined` if no CDP extension should be emitted on issued certs.
1045
+ */
1046
+ get crlDistributionUrl() {
1047
+ return this._crlDistributionUrl;
1048
+ }
1049
+ /**
1050
+ * Public URL of the OCSP responder, or `undefined` if no AIA OCSP
1051
+ * leg should be emitted on issued certs.
1052
+ */
1053
+ get ocspResponderUrl() {
1054
+ return this._ocspResponderUrl;
1055
+ }
1056
+ /**
1057
+ * Public URL where the issuer's certificate can be fetched, or
1058
+ * `undefined` if no AIA caIssuers leg should be emitted.
1059
+ */
1060
+ get caIssuersUrl() {
1061
+ return this._caIssuersUrl;
1062
+ }
1063
+ /**
1064
+ * Configure the URL embedded as `crlDistributionPoints` in every
1065
+ * subsequently-issued certificate. Pass `undefined` to disable
1066
+ * the extension entirely. Validated synchronously — throws on
1067
+ * empty string, non-http(s) protocol, missing path. Warns (does
1068
+ * not throw) when the URL points at loopback.
1069
+ *
1070
+ * @see US-202
1071
+ */
1072
+ setCrlDistributionUrl(url2) {
1073
+ this._crlDistributionUrl = validateRevocationUrl(url2, "crlDistributionUrl");
1074
+ }
1075
+ /**
1076
+ * Configure the OCSP responder URL embedded as the `OCSP` leg of
1077
+ * the `authorityInfoAccess` extension on every subsequently-issued
1078
+ * certificate. Pass `undefined` to disable.
1079
+ *
1080
+ * @see US-202
1081
+ */
1082
+ setOcspResponderUrl(url2) {
1083
+ this._ocspResponderUrl = validateRevocationUrl(url2, "ocspResponderUrl");
1084
+ }
1085
+ /**
1086
+ * Configure the caIssuers URL embedded as the `caIssuers` leg of
1087
+ * the `authorityInfoAccess` extension on every subsequently-issued
1088
+ * certificate. Pass `undefined` to disable.
1089
+ *
1090
+ * @see US-202
1091
+ */
1092
+ setCaIssuersUrl(url2) {
1093
+ this._caIssuersUrl = validateRevocationUrl(url2, "caIssuersUrl");
1094
+ }
1095
+ /**
1096
+ * @internal
1097
+ * Populate the OpenSSL config substitution env vars (`CDP_URL` and
1098
+ * `AIA_VALUE`) from the configured URLs, or unset them so the
1099
+ * matching `{{#KEY}}...{{/KEY}}` blocks in the templates are
1100
+ * stripped. MUST be called before every `generateStaticConfig`
1101
+ * invocation that signs a certificate.
1102
+ */
1103
+ _wireRevocationEnvVars() {
1104
+ unsetEnv("CDP_URL");
1105
+ unsetEnv("AIA_VALUE");
1106
+ if (this._crlDistributionUrl) {
1107
+ setEnv("CDP_URL", this._crlDistributionUrl);
1108
+ }
1109
+ const aiaLegs = [];
1110
+ if (this._ocspResponderUrl) {
1111
+ aiaLegs.push(`OCSP;URI:${this._ocspResponderUrl}`);
1112
+ }
1113
+ if (this._caIssuersUrl) {
1114
+ aiaLegs.push(`caIssuers;URI:${this._caIssuersUrl}`);
1115
+ }
1116
+ if (aiaLegs.length > 0) {
1117
+ setEnv("AIA_VALUE", aiaLegs.join(","));
1118
+ }
978
1119
  }
979
1120
  /** Absolute path to the CA root directory (alias for {@link location}). */
980
1121
  get rootDir() {
@@ -1221,14 +1362,15 @@ var CertificateAuthority = class {
1221
1362
  * @returns the signed certificate as a DER-encoded buffer
1222
1363
  */
1223
1364
  async signCertificateRequestFromDER(csrDer, options) {
1224
- const validity = options?.validity ?? 365;
1225
1365
  const tmpDir = await fs7.promises.mkdtemp(path5.join(os3.tmpdir(), "pki-sign-"));
1226
1366
  try {
1227
1367
  const csrFile = path5.join(tmpDir, "request.csr");
1228
1368
  const certFile = path5.join(tmpDir, "certificate.pem");
1229
1369
  const csrPem = toPem(csrDer, "CERTIFICATE REQUEST");
1230
1370
  await fs7.promises.writeFile(csrFile, csrPem, "utf-8");
1231
- const signingParams = { validity };
1371
+ const signingParams = {};
1372
+ if (options?.validityMs !== void 0) signingParams.validityMs = options.validityMs;
1373
+ else signingParams.validity = options?.validity ?? 365;
1232
1374
  if (options?.startDate) signingParams.startDate = options.startDate;
1233
1375
  if (options?.dns) signingParams.dns = options.dns;
1234
1376
  if (options?.ip) signingParams.ip = options.ip;
@@ -1244,6 +1386,35 @@ var CertificateAuthority = class {
1244
1386
  });
1245
1387
  }
1246
1388
  }
1389
+ /**
1390
+ * Advertise the validity limits this CA can honor.
1391
+ *
1392
+ * Consumers (notably the GDS server in [`cert_auth.ts`](https://github.com/sterfive/node-opcua-gds))
1393
+ * clamp a requested validity against these bounds before calling
1394
+ * {@link signCertificateRequestFromDER}, so a misconfigured
1395
+ * `defaultCertValidity` cannot ask the CA for something it cannot
1396
+ * produce.
1397
+ *
1398
+ * Defaults match the OpenSSL-backed implementation:
1399
+ * - `minValidityMs = 60_000` (1 minute) — practical floor; the
1400
+ * X.509 spec floor is 1 second but very short certs are rarely
1401
+ * useful and pathological for any real deployment.
1402
+ * - `maxValidityMs = 10 * 365 * 86_400_000` (≈ 10 years) — long
1403
+ * enough for root CAs.
1404
+ * - `validityGranularityMs = 1_000` (1 second) — RFC 5280 §4.1.2.5
1405
+ * floor on `notBefore` / `notAfter`.
1406
+ * - `nativeUnit = "second"` — what `x509Date()` actually encodes.
1407
+ *
1408
+ * @see US-208 — the consumer-side capability story.
1409
+ */
1410
+ getCapabilities() {
1411
+ return {
1412
+ minValidityMs: 6e4,
1413
+ maxValidityMs: 10 * 365 * 864e5,
1414
+ validityGranularityMs: 1e3,
1415
+ nativeUnit: "second"
1416
+ };
1417
+ }
1247
1418
  /**
1248
1419
  * Generate a new RSA key pair, create an internal CSR, sign it
1249
1420
  * with this CA, and return both the certificate and private key
@@ -1261,7 +1432,6 @@ var CertificateAuthority = class {
1261
1432
  */
1262
1433
  async generateKeyPairAndSignDER(options) {
1263
1434
  const keySize = options.keySize ?? 2048;
1264
- const validity = options.validity ?? 365;
1265
1435
  const startDate = options.startDate ?? /* @__PURE__ */ new Date();
1266
1436
  const tmpDir = await fs7.promises.mkdtemp(path5.join(os3.tmpdir(), "pki-keygen-"));
1267
1437
  try {
@@ -1281,13 +1451,15 @@ var CertificateAuthority = class {
1281
1451
  purpose: CertificatePurpose.ForApplication
1282
1452
  });
1283
1453
  const certFile = path5.join(tmpDir, "certificate.pem");
1284
- await this.signCertificateRequest(certFile, csrFile, {
1454
+ const signingParams = {
1285
1455
  applicationUri: options.applicationUri,
1286
1456
  dns: options.dns,
1287
1457
  ip: options.ip,
1288
- startDate,
1289
- validity
1290
- });
1458
+ startDate
1459
+ };
1460
+ if (options.validityMs !== void 0) signingParams.validityMs = options.validityMs;
1461
+ else signingParams.validity = options.validity ?? 365;
1462
+ await this.signCertificateRequest(certFile, csrFile, signingParams);
1291
1463
  const certPem = readCertificatePEM(certFile);
1292
1464
  const certificateDer = convertPEMtoDER(certPem);
1293
1465
  const privateKey = readPrivateKey(privateKeyFile);
@@ -1312,7 +1484,6 @@ var CertificateAuthority = class {
1312
1484
  */
1313
1485
  async generateKeyPairAndSignPFX(options) {
1314
1486
  const keySize = options.keySize ?? 2048;
1315
- const validity = options.validity ?? 365;
1316
1487
  const startDate = options.startDate ?? /* @__PURE__ */ new Date();
1317
1488
  const passphrase = options.passphrase ?? "";
1318
1489
  const tmpDir = await fs7.promises.mkdtemp(path5.join(os3.tmpdir(), "pki-keygen-pfx-"));
@@ -1333,13 +1504,15 @@ var CertificateAuthority = class {
1333
1504
  purpose: CertificatePurpose.ForApplication
1334
1505
  });
1335
1506
  const certFile = path5.join(tmpDir, "certificate.pem");
1336
- await this.signCertificateRequest(certFile, csrFile, {
1507
+ const signingParams = {
1337
1508
  applicationUri: options.applicationUri,
1338
1509
  dns: options.dns,
1339
1510
  ip: options.ip,
1340
- startDate,
1341
- validity
1342
- });
1511
+ startDate
1512
+ };
1513
+ if (options.validityMs !== void 0) signingParams.validityMs = options.validityMs;
1514
+ else signingParams.validity = options.validity ?? 365;
1515
+ await this.signCertificateRequest(certFile, csrFile, signingParams);
1343
1516
  const pfxFile = path5.join(tmpDir, "bundle.pfx");
1344
1517
  await createPFX({
1345
1518
  certificateFile: certFile,
@@ -1577,6 +1750,7 @@ var CertificateAuthority = class {
1577
1750
  async signCACertificateRequest(certFile, csrFile, params) {
1578
1751
  const caRootDir = path5.resolve(this.rootDir);
1579
1752
  const options = { cwd: caRootDir };
1753
+ this._wireRevocationEnvVars();
1580
1754
  const configFile = generateStaticConfig("conf/caconfig.cnf", options);
1581
1755
  const validity = params.validity ?? 3650;
1582
1756
  await execute_openssl(
@@ -1743,6 +1917,7 @@ var CertificateAuthority = class {
1743
1917
  ip
1744
1918
  };
1745
1919
  processAltNames(params);
1920
+ this._wireRevocationEnvVars();
1746
1921
  const configFile = generateStaticConfig("conf/caconfig.cnf", options);
1747
1922
  displaySubtitle("- then we ask the authority to sign the certificate signing request");
1748
1923
  const configOption = ` -config ${configFile}`;