node-opcua-pki 6.21.0 → 6.22.1

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
@@ -13,16 +13,20 @@ import {
13
13
  ensurePrivateDirectory,
14
14
  g_config,
15
15
  isEncryptedPrivateKeyFile,
16
+ isOpaqueSigner,
16
17
  makePath,
17
18
  mkdirRecursiveSync,
18
19
  quote,
19
20
  resolvePrivateKeyPassphrase,
20
21
  restrictPrivateFilePermissions,
21
22
  warningLog
22
- } from "./chunk-2HTNH73K.mjs";
23
+ } from "./chunk-EQ4DGI4M.mjs";
23
24
 
24
25
  // packages/node-opcua-pki/lib/index.ts
25
- import { PrivateKeyPassphraseRequiredError } from "node-opcua-crypto";
26
+ import {
27
+ PrivateKeyPassphraseRequiredError,
28
+ PrivateKeyUnavailableError as PrivateKeyUnavailableError2
29
+ } from "node-opcua-crypto";
26
30
 
27
31
  // packages/node-opcua-pki/lib/ca/backends/openssl_ca_backend.ts
28
32
  import fs4 from "fs";
@@ -407,8 +411,32 @@ function passinArg(passphrase = "") {
407
411
  function renderForDisplay(file, args) {
408
412
  return [file, ...args].map((a) => a === "" || /[\s"'`$\\]/.test(a) ? JSON.stringify(a) : a).join(" ");
409
413
  }
414
+ function hasUnsafeArgChar(value) {
415
+ for (const ch of value) {
416
+ const code = ch.codePointAt(0);
417
+ if (code < 32 || code === 127) {
418
+ return true;
419
+ }
420
+ if (code < 128) {
421
+ continue;
422
+ }
423
+ if (/[\p{L}\p{N}\p{M}]/u.test(ch)) {
424
+ continue;
425
+ }
426
+ return true;
427
+ }
428
+ return false;
429
+ }
430
+ function checkArgs(file, args) {
431
+ for (const value of [file, ...args]) {
432
+ if (typeof value === "string" && hasUnsafeArgChar(value)) {
433
+ throw new Error(`execute_openssl: refusing argument with an unexpected character: ${JSON.stringify(value)}`);
434
+ }
435
+ }
436
+ }
410
437
  async function execute2(file, args, options) {
411
438
  const from = new Error();
439
+ checkArgs(file, args);
412
440
  options.cwd = options.cwd || process.cwd();
413
441
  if (!g_config.silent) {
414
442
  warningLog(chalk2.cyan(" CWD "), options.cwd);
@@ -975,11 +1003,14 @@ import { drainPendingLocks, withLock } from "@ster5/global-mutex";
975
1003
  import chalk3 from "chalk";
976
1004
  import chokidar from "chokidar";
977
1005
  import {
1006
+ caSignerFromKeyOperations,
978
1007
  exploreCertificate,
979
1008
  exploreCertificateInfo,
980
1009
  exploreCertificateRevocationList,
981
1010
  generatePrivateKeyFile,
1011
+ keyOperationsFromPrivateKey,
982
1012
  makeSHA1Thumbprint,
1013
+ PrivateKeyUnavailableError,
983
1014
  readCertificateChain,
984
1015
  readCertificateChainAsync,
985
1016
  readCertificateRevocationList,
@@ -1006,8 +1037,9 @@ async function createCertificateSigningRequestAsync(certificateSigningRequestFil
1006
1037
  assert3(typeof certificateSigningRequestFilename === "string");
1007
1038
  const subject = params.subject ? new Subject3(params.subject).toString() : void 0;
1008
1039
  displaySubtitle("- Creating a Certificate Signing Request with subtile");
1009
- const privateKeyPem = typeof params.privateKey === "string" ? await fs5.promises.readFile(params.privateKey, "utf-8") : coercePrivateKeyPem(params.privateKey);
1010
- const privateKey = await pemToPrivateKey(privateKeyPem);
1040
+ const privateKey = isOpaqueSigner(params.privateKey) ? params.privateKey : await pemToPrivateKey(
1041
+ typeof params.privateKey === "string" ? await fs5.promises.readFile(params.privateKey, "utf-8") : coercePrivateKeyPem(params.privateKey)
1042
+ );
1011
1043
  const { csr } = await createCertificateSigningRequest({
1012
1044
  privateKey,
1013
1045
  dns: params.dns,
@@ -1017,7 +1049,9 @@ async function createCertificateSigningRequestAsync(certificateSigningRequestFil
1017
1049
  purpose: params.purpose
1018
1050
  });
1019
1051
  await fs5.promises.writeFile(certificateSigningRequestFilename, csr, "utf-8");
1020
- display(`- privateKey ${typeof params.privateKey === "string" ? params.privateKey : "<in-memory>"}`);
1052
+ display(
1053
+ `- privateKey ${typeof params.privateKey === "string" ? params.privateKey : isOpaqueSigner(params.privateKey) ? "<opaque-signer>" : "<in-memory>"}`
1054
+ );
1021
1055
  display(`- certificateSigningRequestFilename ${certificateSigningRequestFilename}`);
1022
1056
  }
1023
1057
 
@@ -1050,8 +1084,9 @@ async function createSelfSignedCertificateAsync(certificate, params) {
1050
1084
  subject = subject.toString();
1051
1085
  const purpose = params.purpose;
1052
1086
  displayTitle("Generate a certificate request");
1053
- const privateKeyPem = typeof params.privateKey === "string" ? await fs6.promises.readFile(params.privateKey, "utf-8") : coercePrivateKeyPem2(params.privateKey);
1054
- const privateKey = await pemToPrivateKey2(privateKeyPem);
1087
+ const privateKey = isOpaqueSigner(params.privateKey) ? params.privateKey : await pemToPrivateKey2(
1088
+ typeof params.privateKey === "string" ? await fs6.promises.readFile(params.privateKey, "utf-8") : coercePrivateKeyPem2(params.privateKey)
1089
+ );
1055
1090
  const { cert } = await createSelfSignedCertificate1({
1056
1091
  privateKey,
1057
1092
  notBefore: params.startDate,
@@ -1117,18 +1152,31 @@ function makeFingerprint(certificate) {
1117
1152
  function short(stringToShorten) {
1118
1153
  return stringToShorten.substring(0, 10);
1119
1154
  }
1120
- var forbiddenChars = /[\x00-\x1F<>:"/\\|?*]/g;
1155
+ var disallowedInLabel = /[^A-Za-z0-9._ -]/g;
1156
+ function toFilenameLabel(commonName) {
1157
+ const label = (commonName || "").normalize("NFKC").replace(disallowedInLabel, "_").replace(/[^\x20-\x7E]/g, "").slice(0, 64).trim();
1158
+ return label || "certificate";
1159
+ }
1121
1160
  function buildIdealCertificateName(certificate) {
1122
1161
  const chain = coerceCertificateChain(certificate);
1123
1162
  const fingerprint = makeFingerprint(chain);
1124
1163
  try {
1125
1164
  const commonName = exploreCertificate(chain[0]).tbsCertificate.subject.commonName || "";
1126
- const sanitizedCommonName = commonName.replace(forbiddenChars, "_");
1165
+ const sanitizedCommonName = toFilenameLabel(commonName);
1127
1166
  return `${sanitizedCommonName}[${fingerprint}]`;
1128
1167
  } catch (_err) {
1129
1168
  return `invalid_certificate_[${fingerprint}]`;
1130
1169
  }
1131
1170
  }
1171
+ function safeStoreJoin(folder, filename) {
1172
+ const full = path4.resolve(folder, filename);
1173
+ const base = path4.resolve(folder);
1174
+ if (full !== base && !full.startsWith(base + path4.sep)) {
1175
+ warningLog("refusing to write certificate outside its store folder:", filename);
1176
+ throw new Error(`certificate filename escapes its store folder: ${filename}`);
1177
+ }
1178
+ return full;
1179
+ }
1132
1180
  function findMatchingIssuerKey(entries, wantedIssuerKey) {
1133
1181
  return entries.filter((entry) => {
1134
1182
  const info = getOrComputeInfo(entry);
@@ -1356,6 +1404,9 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
1356
1404
  #disableFileWatchers;
1357
1405
  #privateKeyPassphrase;
1358
1406
  #privateKeyProvider;
1407
+ #keyOperations;
1408
+ /** The stable lazy wrap handed out by {@link getKeyOperations} for non-opaque configurations. */
1409
+ #lazyLocalKeyOperations;
1359
1410
  /**
1360
1411
  * The on-disk key, decrypted once and kept for the instance's lifetime,
1361
1412
  * so the passphrase (or its resolver function) is consulted at most
@@ -1399,6 +1450,17 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
1399
1450
  this.#disableFileWatchers = options.disableFileWatchers ?? process.env.OPCUA_PKI_DISABLE_FILE_WATCHERS === "true";
1400
1451
  this.#privateKeyPassphrase = options.privateKeyPassphrase;
1401
1452
  this.#privateKeyProvider = options.privateKeyProvider;
1453
+ this.#keyOperations = options.keyOperations;
1454
+ if (this.#keyOperations && this.#privateKeyProvider) {
1455
+ throw new Error(
1456
+ "CertificateManager: 'keyOperations' and 'privateKeyProvider' are mutually exclusive: one hides the key, the other sources its material"
1457
+ );
1458
+ }
1459
+ if (this.#keyOperations && this.#privateKeyPassphrase) {
1460
+ throw new Error(
1461
+ "CertificateManager: 'privateKeyPassphrase' is meaningless with 'keyOperations': there is no key material for a passphrase to protect"
1462
+ );
1463
+ }
1402
1464
  mkdirRecursiveSync(options.location);
1403
1465
  if (!fs7.existsSync(this.#location)) {
1404
1466
  throw new Error(`CertificateManager cannot access location ${this.#location}`);
@@ -1440,6 +1502,11 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
1440
1502
  * authority on what the current key is.
1441
1503
  */
1442
1504
  async getPrivateKey() {
1505
+ if (this.#keyOperations) {
1506
+ throw new PrivateKeyUnavailableError(
1507
+ "CertificateManager.getPrivateKey is not available when keyOperations is configured: the private key is held by the key-operations provider (HSM/KMS) and cannot be read \u2014 use getKeyOperations() instead"
1508
+ );
1509
+ }
1443
1510
  if (this.#privateKeyProvider) {
1444
1511
  return await this.#privateKeyProvider.getPrivateKey();
1445
1512
  }
@@ -1465,6 +1532,75 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
1465
1532
  }
1466
1533
  /** In-flight first read of the on-disk key, so concurrent callers share one passphrase resolution. */
1467
1534
  #privateKeyPromise;
1535
+ /**
1536
+ * True when this manager's key is opaque — configured through
1537
+ * `keyOperations`, held by an HSM/KMS, never obtainable as material.
1538
+ * When true, {@link getPrivateKey} throws `PrivateKeyUnavailableError`
1539
+ * and {@link getKeyOperations} is the only way to use the key.
1540
+ */
1541
+ isPrivateKeyOpaque() {
1542
+ return !!this.#keyOperations;
1543
+ }
1544
+ /**
1545
+ * The key as an opaque {@link IKeyOperations} — the recommended way to
1546
+ * *use* the private key regardless of where it lives.
1547
+ *
1548
+ * Returns the configured `keyOperations` object when the key is opaque.
1549
+ * Otherwise returns a stable lazy wrap over {@link getPrivateKey}: its
1550
+ * methods resolve the key on first use (disk read, passphrase,
1551
+ * `privateKeyProvider` — all async), so the wrap offers no synchronous
1552
+ * fast path; callers that need one resolve the key themselves and build
1553
+ * a `LocalKeyOperations` over it. The wrap follows key rotation: a
1554
+ * `privateKeyProvider` that starts returning a different key gets a
1555
+ * fresh underlying `LocalKeyOperations`.
1556
+ */
1557
+ getKeyOperations() {
1558
+ if (this.#keyOperations) {
1559
+ return this.#keyOperations;
1560
+ }
1561
+ if (!this.#lazyLocalKeyOperations) {
1562
+ let cached;
1563
+ const resolve = async () => {
1564
+ const key = await this.getPrivateKey();
1565
+ if (!cached || cached.key !== key) {
1566
+ cached = { key, ops: keyOperationsFromPrivateKey(key) };
1567
+ }
1568
+ return cached.ops;
1569
+ };
1570
+ this.#lazyLocalKeyOperations = {
1571
+ sign: async (data, params) => (await resolve()).sign(data, params),
1572
+ decryptBlock: async (block, params) => (await resolve()).decryptBlock(block, params),
1573
+ getKeyMetadata: async () => (await resolve()).getKeyMetadata(),
1574
+ getPublicKey: async () => (await resolve()).getPublicKey()
1575
+ };
1576
+ }
1577
+ return this.#lazyLocalKeyOperations;
1578
+ }
1579
+ /**
1580
+ * Fail closed at `initialize()` time, not on the first certificate
1581
+ * operation, if the key cannot actually be used: wrong/missing
1582
+ * passphrase on an encrypted key, a broken `privateKeyProvider`, or an
1583
+ * unreachable `keyOperations` provider (probed via `getKeyMetadata`).
1584
+ */
1585
+ async #probePrivateKey() {
1586
+ if (this.#keyOperations) {
1587
+ await this.#keyOperations.getKeyMetadata();
1588
+ return;
1589
+ }
1590
+ await this.getPrivateKey();
1591
+ }
1592
+ /**
1593
+ * The key to hand to a certificate-issuance primitive: the raw
1594
+ * {@link PrivateKey} for a local configuration, or a {@link CaSigner}
1595
+ * adapted from `keyOperations` when the key is opaque — which requires
1596
+ * the provider to implement `getPublicKey` (the adapter says so if not).
1597
+ */
1598
+ async #resolveSigningKey() {
1599
+ if (this.#keyOperations) {
1600
+ return caSignerFromKeyOperations(this.#keyOperations);
1601
+ }
1602
+ return await this.getPrivateKey();
1603
+ }
1468
1604
  /**
1469
1605
  * Enable, disable, or rotate the passphrase protecting the on-disk
1470
1606
  * private key: decrypt with `oldPassphrase` (omit if the key is
@@ -1486,6 +1622,11 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
1486
1622
  * disk file for this method to rewrite).
1487
1623
  */
1488
1624
  async reencryptPrivateKey(oldPassphrase, newPassphrase) {
1625
+ if (this.#keyOperations) {
1626
+ throw new PrivateKeyUnavailableError(
1627
+ "reencryptPrivateKey: not supported when keyOperations is configured \u2014 there is no key material to rewrite"
1628
+ );
1629
+ }
1489
1630
  if (this.#privateKeyProvider) {
1490
1631
  throw new Error("reencryptPrivateKey: not supported when a privateKeyProvider is configured");
1491
1632
  }
@@ -1611,7 +1752,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
1611
1752
  } catch (_err) {
1612
1753
  return "BadCertificateInvalid";
1613
1754
  }
1614
- const filename = path4.join(this.rejectedFolder, `${buildIdealCertificateName(leafCertificate)}.pem`);
1755
+ const filename = safeStoreJoin(this.rejectedFolder, `${buildIdealCertificateName(leafCertificate)}.pem`);
1615
1756
  debugLog("certificate has never been seen before and is now rejected (untrusted) ", filename);
1616
1757
  await fsWriteFile(filename, toPem(chain, "CERTIFICATE"));
1617
1758
  this.#thumbs.rejected.set(fingerprint, { certificate: leafCertificate, filename });
@@ -1834,7 +1975,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
1834
1975
  mkdirRecursiveSync(path4.join(pkiDir, "issuers"));
1835
1976
  mkdirRecursiveSync(path4.join(pkiDir, "issuers/certs"));
1836
1977
  mkdirRecursiveSync(path4.join(pkiDir, "issuers/crl"));
1837
- const ownsDiskKey = !this.#privateKeyProvider;
1978
+ const ownsDiskKey = !this.#privateKeyProvider && !this.#keyOperations;
1838
1979
  const needsKeyGeneration = ownsDiskKey && !fs7.existsSync(this.privateKey);
1839
1980
  const needsKeyEncryption = ownsDiskKey && !needsKeyGeneration && this.#privateKeyPassphrase !== void 0 && !isEncryptedPrivateKeyFile(this.privateKey);
1840
1981
  if (!fs7.existsSync(this.configFile) || needsKeyGeneration || needsKeyEncryption) {
@@ -1860,14 +2001,14 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
1860
2001
  if (ownsDiskKey) {
1861
2002
  restrictPrivateFilePermissions(this.privateKey, 384);
1862
2003
  }
1863
- await this.getPrivateKey();
2004
+ await this.#probePrivateKey();
1864
2005
  await this.#readCertificates();
1865
2006
  });
1866
2007
  } else {
1867
2008
  if (ownsDiskKey) {
1868
2009
  restrictPrivateFilePermissions(this.privateKey, 384);
1869
2010
  }
1870
- await this.getPrivateKey();
2011
+ await this.#probePrivateKey();
1871
2012
  await this.#readCertificates();
1872
2013
  }
1873
2014
  }
@@ -1952,7 +2093,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
1952
2093
  if (typeof params.applicationUri !== "string") {
1953
2094
  throw new Error("createSelfSignedCertificate: expecting applicationUri to be a string");
1954
2095
  }
1955
- if (!this.#privateKeyProvider && !fs7.existsSync(this.privateKey)) {
2096
+ if (!this.#privateKeyProvider && !this.#keyOperations && !fs7.existsSync(this.privateKey)) {
1956
2097
  throw new Error(`Cannot find private key ${this.privateKey}`);
1957
2098
  }
1958
2099
  let certificateFilename = path4.join(this.rootDir, "own/certs/self_signed_certificate.pem");
@@ -1961,7 +2102,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
1961
2102
  ...params,
1962
2103
  rootDir: this.rootDir,
1963
2104
  configFile: this.configFile,
1964
- privateKey: await this.getPrivateKey(),
2105
+ privateKey: await this.#resolveSigningKey(),
1965
2106
  subject: params.subject || "CN=FIXME"
1966
2107
  };
1967
2108
  await this.withLock2(async () => {
@@ -1989,7 +2130,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
1989
2130
  ...params,
1990
2131
  rootDir: path4.resolve(this.rootDir),
1991
2132
  configFile: path4.resolve(this.configFile),
1992
- privateKey: await this.getPrivateKey()
2133
+ privateKey: await this.#resolveSigningKey()
1993
2134
  };
1994
2135
  return await this.withLock2(async () => {
1995
2136
  const now = /* @__PURE__ */ new Date();
@@ -2019,7 +2160,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
2019
2160
  if (this.#thumbs.issuers.certs.has(fingerprint)) {
2020
2161
  return "Good" /* Good */;
2021
2162
  }
2022
- const filename = path4.join(this.issuersCertFolder, `issuer_${buildIdealCertificateName(certificate)}.pem`);
2163
+ const filename = safeStoreJoin(this.issuersCertFolder, `issuer_${buildIdealCertificateName(certificate)}.pem`);
2023
2164
  await fs7.promises.writeFile(filename, pemCertificate, "ascii");
2024
2165
  this.#thumbs.issuers.certs.set(fingerprint, { certificate, filename });
2025
2166
  if (addInTrustList) {
@@ -2478,7 +2619,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
2478
2619
  let status = await this.#checkRejectedOrTrusted(certificate);
2479
2620
  if (status === "unknown") {
2480
2621
  const pem = toPem(chain, "CERTIFICATE");
2481
- const filename = path4.join(this.rejectedFolder, `${buildIdealCertificateName(certificate)}.pem`);
2622
+ const filename = safeStoreJoin(this.rejectedFolder, `${buildIdealCertificateName(certificate)}.pem`);
2482
2623
  await fs7.promises.writeFile(filename, pem);
2483
2624
  this.#thumbs.rejected.set(fingerprint, { certificate, filename });
2484
2625
  status = "rejected";
@@ -2495,7 +2636,7 @@ var CertificateManager = class _CertificateManager extends EventEmitter {
2495
2636
  throw new Error(`#moveCertificate: certificate ${fingerprint.substring(0, 10)} not found in ${status} index`);
2496
2637
  }
2497
2638
  const destFolder = newStatus === "trusted" ? this.trustedFolder : this.rejectedFolder;
2498
- const certificateDest = path4.join(destFolder, path4.basename(srcEntry.filename));
2639
+ const certificateDest = safeStoreJoin(destFolder, path4.basename(srcEntry.filename));
2499
2640
  debugLog("#moveCertificate", fingerprint.substring(0, 10), "old name", srcEntry.filename);
2500
2641
  debugLog("#moveCertificate", fingerprint.substring(0, 10), "new name", certificateDest);
2501
2642
  await fs7.promises.rename(srcEntry.filename, certificateDest);
@@ -2976,6 +3117,7 @@ export {
2976
3117
  NativeCaBackend,
2977
3118
  OpenSslCaBackend,
2978
3119
  PrivateKeyPassphraseRequiredError,
3120
+ PrivateKeyUnavailableError2 as PrivateKeyUnavailableError,
2979
3121
  Subject,
2980
3122
  VerificationStatus,
2981
3123
  adjustApplicationUri,
@@ -2992,9 +3134,12 @@ export {
2992
3134
  install_prerequisite,
2993
3135
  isIntermediateIssuer,
2994
3136
  isIssuer,
3137
+ isOpaqueSigner,
2995
3138
  isRootIssuer,
2996
3139
  makeFingerprint,
2997
3140
  quote,
2998
- resolvePrivateKeyPassphrase
3141
+ resolvePrivateKeyPassphrase,
3142
+ safeStoreJoin,
3143
+ toFilenameLabel
2999
3144
  };
3000
3145
  //# sourceMappingURL=index.mjs.map