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/bin/pki.mjs CHANGED
@@ -248,9 +248,17 @@ function adjustDate(params) {
248
248
  assert4(params instanceof Object);
249
249
  params.startDate = params.startDate || /* @__PURE__ */ new Date();
250
250
  assert4(params.startDate instanceof Date);
251
- params.validity = params.validity || 365;
252
- params.endDate = new Date(params.startDate.getTime());
253
- params.endDate.setDate(params.startDate.getDate() + params.validity);
251
+ if (params.validityMs !== void 0) {
252
+ if (params.validityMs <= 0) {
253
+ throw new RangeError(`validityMs must be > 0 (got ${params.validityMs})`);
254
+ }
255
+ params.endDate = new Date(params.startDate.getTime() + params.validityMs);
256
+ params.validity = Math.ceil(params.validityMs / 864e5);
257
+ } else {
258
+ params.validity = params.validity || 365;
259
+ params.endDate = new Date(params.startDate.getTime());
260
+ params.endDate.setDate(params.startDate.getDate() + params.validity);
261
+ }
254
262
  assert4(params.endDate instanceof Date);
255
263
  assert4(params.startDate instanceof Date);
256
264
  }
@@ -1944,9 +1952,15 @@ function setEnv(varName, value) {
1944
1952
  process.env[varName] = value;
1945
1953
  }
1946
1954
  }
1955
+ function hasEnv(varName) {
1956
+ return Object.prototype.hasOwnProperty.call(exportedEnvVars, varName);
1957
+ }
1947
1958
  function getEnv(varName) {
1948
1959
  return exportedEnvVars[varName];
1949
1960
  }
1961
+ function unsetEnv(varName) {
1962
+ delete exportedEnvVars[varName];
1963
+ }
1950
1964
  function getEnvironmentVarNames() {
1951
1965
  return Object.keys(exportedEnvVars).map((varName) => {
1952
1966
  return { key: varName, pattern: `\\$ENV\\:\\:${varName}` };
@@ -2416,10 +2430,17 @@ function openssl_require2DigitYearInDate() {
2416
2430
  }
2417
2431
  return g_config.opensslVersion.match(/OpenSSL 0\.9/);
2418
2432
  }
2433
+ function stripConditionalBlocks(template) {
2434
+ return template.replace(/\{\{#([A-Z_][A-Z0-9_]*)\}\}([\s\S]*?)\{\{\/\1\}\}\r?\n?/g, (_match, key, content) => {
2435
+ const keep = hasEnv(key) && getEnv(key) !== "";
2436
+ return keep ? content : "";
2437
+ });
2438
+ }
2419
2439
  function generateStaticConfig(configPath, options) {
2420
2440
  const prePath = options?.cwd || "";
2421
2441
  const originalFilename = !path4.isAbsolute(configPath) ? path4.join(prePath, configPath) : configPath;
2422
2442
  let staticConfig = fs7.readFileSync(originalFilename, { encoding: "utf8" });
2443
+ staticConfig = stripConditionalBlocks(staticConfig);
2423
2444
  for (const envVar of getEnvironmentVarNames()) {
2424
2445
  staticConfig = staticConfig.replace(new RegExp(envVar.pattern, "gi"), getEnv(envVar.key));
2425
2446
  }
@@ -2672,7 +2693,9 @@ nsComment = ''OpenSSL Generated Certificate''
2672
2693
  #nsSslServerName =
2673
2694
  keyUsage = critical, digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment, keyAgreement
2674
2695
  extendedKeyUsage = critical,serverAuth ,clientAuth
2675
-
2696
+ {{#CDP_URL}}crlDistributionPoints = URI:$ENV::CDP_URL
2697
+ {{/CDP_URL}}{{#AIA_VALUE}}authorityInfoAccess = $ENV::AIA_VALUE
2698
+ {{/AIA_VALUE}}
2676
2699
  [ v3_req ]
2677
2700
  basicConstraints = critical, CA:FALSE
2678
2701
  keyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment, keyAgreement
@@ -2695,10 +2718,9 @@ nsComment = "CA Certificate generated by Node-OPCUA Certificate
2695
2718
  #nsCertType = sslCA, emailCA
2696
2719
  #issuerAltName = issuer:copy
2697
2720
  #obj = DER:02:03
2698
- crlDistributionPoints = @crl_info
2699
- [ crl_info ]
2700
- URI.0 = http://localhost:8900/crl.pem
2701
- [ v3_selfsigned]
2721
+ {{#CDP_URL}}crlDistributionPoints = URI:$ENV::CDP_URL
2722
+ {{/CDP_URL}}{{#AIA_VALUE}}authorityInfoAccess = $ENV::AIA_VALUE
2723
+ {{/AIA_VALUE}}[ v3_selfsigned]
2702
2724
  basicConstraints = critical, CA:FALSE
2703
2725
  keyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment, keyAgreement
2704
2726
  extendedKeyUsage = critical,serverAuth ,clientAuth
@@ -2790,6 +2812,7 @@ async function construct_CertificateAuthority(certificateAuthority) {
2790
2812
  const subjectOpt = ` -subj "${subject.toString()}" `;
2791
2813
  const caCommonName = subject.commonName || "NodeOPCUA-CA";
2792
2814
  setEnv("ALTNAME", `URI:urn:${caCommonName}`);
2815
+ certificateAuthority._wireRevocationEnvVars();
2793
2816
  const options = { cwd: caRootDir };
2794
2817
  const configFile = generateStaticConfig("conf/caconfig.cnf", options);
2795
2818
  const configOption = ` -config ${q4(n5(configFile))}`;
@@ -2843,6 +2866,33 @@ function parseOpenSSLDate(dateStr) {
2843
2866
  const sec = raw.substring(10, 12);
2844
2867
  return `${year}-${month}-${day}T${hour}:${min}:${sec}Z`;
2845
2868
  }
2869
+ function validateRevocationUrl(url2, fieldName) {
2870
+ if (url2 === void 0) {
2871
+ return void 0;
2872
+ }
2873
+ if (url2 === "") {
2874
+ throw new Error(`${fieldName} must not be empty \u2014 pass undefined to disable the extension`);
2875
+ }
2876
+ let parsed;
2877
+ try {
2878
+ parsed = new URL(url2);
2879
+ } catch {
2880
+ throw new Error(`${fieldName} is not a valid URL: ${url2}`);
2881
+ }
2882
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
2883
+ throw new Error(`${fieldName} must use http: or https: (got ${parsed.protocol} in ${url2})`);
2884
+ }
2885
+ if (!parsed.pathname || parsed.pathname === "/") {
2886
+ throw new Error(`${fieldName} must include a path component (got ${url2})`);
2887
+ }
2888
+ const isLoopback = parsed.hostname === "localhost" || parsed.hostname === "::1" || parsed.hostname.startsWith("127.");
2889
+ if (isLoopback) {
2890
+ console.warn(
2891
+ `[node-opcua-pki] ${fieldName} points at loopback (${url2}) \u2014 certificates issued with this URL will be unreachable from any other host.`
2892
+ );
2893
+ }
2894
+ return url2;
2895
+ }
2846
2896
  var defaultSubject, configurationFileTemplate, configurationFileSimpleTemplate2, config3, n5, q4, CertificateAuthority;
2847
2897
  var init_certificate_authority = __esm({
2848
2898
  "packages/node-opcua-pki/lib/ca/certificate_authority.ts"() {
@@ -2873,6 +2923,10 @@ var init_certificate_authority = __esm({
2873
2923
  subject;
2874
2924
  /** @internal Parent CA (undefined for root CAs). */
2875
2925
  _issuerCA;
2926
+ /** @internal Configured CDP / AIA URLs (US-202). */
2927
+ _crlDistributionUrl;
2928
+ _ocspResponderUrl;
2929
+ _caIssuersUrl;
2876
2930
  constructor(options) {
2877
2931
  assert10(Object.prototype.hasOwnProperty.call(options, "location"));
2878
2932
  assert10(Object.prototype.hasOwnProperty.call(options, "keySize"));
@@ -2880,6 +2934,93 @@ var init_certificate_authority = __esm({
2880
2934
  this.keySize = options.keySize || 2048;
2881
2935
  this.subject = new Subject4(options.subject || defaultSubject);
2882
2936
  this._issuerCA = options.issuerCA;
2937
+ if (options.crlDistributionUrl !== void 0) {
2938
+ this.setCrlDistributionUrl(options.crlDistributionUrl);
2939
+ }
2940
+ if (options.ocspResponderUrl !== void 0) {
2941
+ this.setOcspResponderUrl(options.ocspResponderUrl);
2942
+ }
2943
+ if (options.caIssuersUrl !== void 0) {
2944
+ this.setCaIssuersUrl(options.caIssuersUrl);
2945
+ }
2946
+ }
2947
+ /**
2948
+ * Public URL where the CRL produced by this CA is reachable, or
2949
+ * `undefined` if no CDP extension should be emitted on issued certs.
2950
+ */
2951
+ get crlDistributionUrl() {
2952
+ return this._crlDistributionUrl;
2953
+ }
2954
+ /**
2955
+ * Public URL of the OCSP responder, or `undefined` if no AIA OCSP
2956
+ * leg should be emitted on issued certs.
2957
+ */
2958
+ get ocspResponderUrl() {
2959
+ return this._ocspResponderUrl;
2960
+ }
2961
+ /**
2962
+ * Public URL where the issuer's certificate can be fetched, or
2963
+ * `undefined` if no AIA caIssuers leg should be emitted.
2964
+ */
2965
+ get caIssuersUrl() {
2966
+ return this._caIssuersUrl;
2967
+ }
2968
+ /**
2969
+ * Configure the URL embedded as `crlDistributionPoints` in every
2970
+ * subsequently-issued certificate. Pass `undefined` to disable
2971
+ * the extension entirely. Validated synchronously — throws on
2972
+ * empty string, non-http(s) protocol, missing path. Warns (does
2973
+ * not throw) when the URL points at loopback.
2974
+ *
2975
+ * @see US-202
2976
+ */
2977
+ setCrlDistributionUrl(url2) {
2978
+ this._crlDistributionUrl = validateRevocationUrl(url2, "crlDistributionUrl");
2979
+ }
2980
+ /**
2981
+ * Configure the OCSP responder URL embedded as the `OCSP` leg of
2982
+ * the `authorityInfoAccess` extension on every subsequently-issued
2983
+ * certificate. Pass `undefined` to disable.
2984
+ *
2985
+ * @see US-202
2986
+ */
2987
+ setOcspResponderUrl(url2) {
2988
+ this._ocspResponderUrl = validateRevocationUrl(url2, "ocspResponderUrl");
2989
+ }
2990
+ /**
2991
+ * Configure the caIssuers URL embedded as the `caIssuers` leg of
2992
+ * the `authorityInfoAccess` extension on every subsequently-issued
2993
+ * certificate. Pass `undefined` to disable.
2994
+ *
2995
+ * @see US-202
2996
+ */
2997
+ setCaIssuersUrl(url2) {
2998
+ this._caIssuersUrl = validateRevocationUrl(url2, "caIssuersUrl");
2999
+ }
3000
+ /**
3001
+ * @internal
3002
+ * Populate the OpenSSL config substitution env vars (`CDP_URL` and
3003
+ * `AIA_VALUE`) from the configured URLs, or unset them so the
3004
+ * matching `{{#KEY}}...{{/KEY}}` blocks in the templates are
3005
+ * stripped. MUST be called before every `generateStaticConfig`
3006
+ * invocation that signs a certificate.
3007
+ */
3008
+ _wireRevocationEnvVars() {
3009
+ unsetEnv("CDP_URL");
3010
+ unsetEnv("AIA_VALUE");
3011
+ if (this._crlDistributionUrl) {
3012
+ setEnv("CDP_URL", this._crlDistributionUrl);
3013
+ }
3014
+ const aiaLegs = [];
3015
+ if (this._ocspResponderUrl) {
3016
+ aiaLegs.push(`OCSP;URI:${this._ocspResponderUrl}`);
3017
+ }
3018
+ if (this._caIssuersUrl) {
3019
+ aiaLegs.push(`caIssuers;URI:${this._caIssuersUrl}`);
3020
+ }
3021
+ if (aiaLegs.length > 0) {
3022
+ setEnv("AIA_VALUE", aiaLegs.join(","));
3023
+ }
2883
3024
  }
2884
3025
  /** Absolute path to the CA root directory (alias for {@link location}). */
2885
3026
  get rootDir() {
@@ -3126,14 +3267,15 @@ var init_certificate_authority = __esm({
3126
3267
  * @returns the signed certificate as a DER-encoded buffer
3127
3268
  */
3128
3269
  async signCertificateRequestFromDER(csrDer, options) {
3129
- const validity = options?.validity ?? 365;
3130
3270
  const tmpDir = await fs10.promises.mkdtemp(path6.join(os4.tmpdir(), "pki-sign-"));
3131
3271
  try {
3132
3272
  const csrFile = path6.join(tmpDir, "request.csr");
3133
3273
  const certFile = path6.join(tmpDir, "certificate.pem");
3134
3274
  const csrPem = toPem2(csrDer, "CERTIFICATE REQUEST");
3135
3275
  await fs10.promises.writeFile(csrFile, csrPem, "utf-8");
3136
- const signingParams = { validity };
3276
+ const signingParams = {};
3277
+ if (options?.validityMs !== void 0) signingParams.validityMs = options.validityMs;
3278
+ else signingParams.validity = options?.validity ?? 365;
3137
3279
  if (options?.startDate) signingParams.startDate = options.startDate;
3138
3280
  if (options?.dns) signingParams.dns = options.dns;
3139
3281
  if (options?.ip) signingParams.ip = options.ip;
@@ -3149,6 +3291,35 @@ var init_certificate_authority = __esm({
3149
3291
  });
3150
3292
  }
3151
3293
  }
3294
+ /**
3295
+ * Advertise the validity limits this CA can honor.
3296
+ *
3297
+ * Consumers (notably the GDS server in [`cert_auth.ts`](https://github.com/sterfive/node-opcua-gds))
3298
+ * clamp a requested validity against these bounds before calling
3299
+ * {@link signCertificateRequestFromDER}, so a misconfigured
3300
+ * `defaultCertValidity` cannot ask the CA for something it cannot
3301
+ * produce.
3302
+ *
3303
+ * Defaults match the OpenSSL-backed implementation:
3304
+ * - `minValidityMs = 60_000` (1 minute) — practical floor; the
3305
+ * X.509 spec floor is 1 second but very short certs are rarely
3306
+ * useful and pathological for any real deployment.
3307
+ * - `maxValidityMs = 10 * 365 * 86_400_000` (≈ 10 years) — long
3308
+ * enough for root CAs.
3309
+ * - `validityGranularityMs = 1_000` (1 second) — RFC 5280 §4.1.2.5
3310
+ * floor on `notBefore` / `notAfter`.
3311
+ * - `nativeUnit = "second"` — what `x509Date()` actually encodes.
3312
+ *
3313
+ * @see US-208 — the consumer-side capability story.
3314
+ */
3315
+ getCapabilities() {
3316
+ return {
3317
+ minValidityMs: 6e4,
3318
+ maxValidityMs: 10 * 365 * 864e5,
3319
+ validityGranularityMs: 1e3,
3320
+ nativeUnit: "second"
3321
+ };
3322
+ }
3152
3323
  /**
3153
3324
  * Generate a new RSA key pair, create an internal CSR, sign it
3154
3325
  * with this CA, and return both the certificate and private key
@@ -3166,7 +3337,6 @@ var init_certificate_authority = __esm({
3166
3337
  */
3167
3338
  async generateKeyPairAndSignDER(options) {
3168
3339
  const keySize = options.keySize ?? 2048;
3169
- const validity = options.validity ?? 365;
3170
3340
  const startDate = options.startDate ?? /* @__PURE__ */ new Date();
3171
3341
  const tmpDir = await fs10.promises.mkdtemp(path6.join(os4.tmpdir(), "pki-keygen-"));
3172
3342
  try {
@@ -3186,13 +3356,15 @@ var init_certificate_authority = __esm({
3186
3356
  purpose: CertificatePurpose2.ForApplication
3187
3357
  });
3188
3358
  const certFile = path6.join(tmpDir, "certificate.pem");
3189
- await this.signCertificateRequest(certFile, csrFile, {
3359
+ const signingParams = {
3190
3360
  applicationUri: options.applicationUri,
3191
3361
  dns: options.dns,
3192
3362
  ip: options.ip,
3193
- startDate,
3194
- validity
3195
- });
3363
+ startDate
3364
+ };
3365
+ if (options.validityMs !== void 0) signingParams.validityMs = options.validityMs;
3366
+ else signingParams.validity = options.validity ?? 365;
3367
+ await this.signCertificateRequest(certFile, csrFile, signingParams);
3196
3368
  const certPem = readCertificatePEM(certFile);
3197
3369
  const certificateDer = convertPEMtoDER(certPem);
3198
3370
  const privateKey = readPrivateKey(privateKeyFile);
@@ -3217,7 +3389,6 @@ var init_certificate_authority = __esm({
3217
3389
  */
3218
3390
  async generateKeyPairAndSignPFX(options) {
3219
3391
  const keySize = options.keySize ?? 2048;
3220
- const validity = options.validity ?? 365;
3221
3392
  const startDate = options.startDate ?? /* @__PURE__ */ new Date();
3222
3393
  const passphrase = options.passphrase ?? "";
3223
3394
  const tmpDir = await fs10.promises.mkdtemp(path6.join(os4.tmpdir(), "pki-keygen-pfx-"));
@@ -3238,13 +3409,15 @@ var init_certificate_authority = __esm({
3238
3409
  purpose: CertificatePurpose2.ForApplication
3239
3410
  });
3240
3411
  const certFile = path6.join(tmpDir, "certificate.pem");
3241
- await this.signCertificateRequest(certFile, csrFile, {
3412
+ const signingParams = {
3242
3413
  applicationUri: options.applicationUri,
3243
3414
  dns: options.dns,
3244
3415
  ip: options.ip,
3245
- startDate,
3246
- validity
3247
- });
3416
+ startDate
3417
+ };
3418
+ if (options.validityMs !== void 0) signingParams.validityMs = options.validityMs;
3419
+ else signingParams.validity = options.validity ?? 365;
3420
+ await this.signCertificateRequest(certFile, csrFile, signingParams);
3248
3421
  const pfxFile = path6.join(tmpDir, "bundle.pfx");
3249
3422
  await createPFX({
3250
3423
  certificateFile: certFile,
@@ -3482,6 +3655,7 @@ var init_certificate_authority = __esm({
3482
3655
  async signCACertificateRequest(certFile, csrFile, params) {
3483
3656
  const caRootDir = path6.resolve(this.rootDir);
3484
3657
  const options = { cwd: caRootDir };
3658
+ this._wireRevocationEnvVars();
3485
3659
  const configFile = generateStaticConfig("conf/caconfig.cnf", options);
3486
3660
  const validity = params.validity ?? 3650;
3487
3661
  await execute_openssl(
@@ -3648,6 +3822,7 @@ var init_certificate_authority = __esm({
3648
3822
  ip
3649
3823
  };
3650
3824
  processAltNames(params);
3825
+ this._wireRevocationEnvVars();
3651
3826
  const configFile = generateStaticConfig("conf/caconfig.cnf", options);
3652
3827
  displaySubtitle("- then we ask the authority to sign the certificate signing request");
3653
3828
  const configOption = ` -config ${configFile}`;