node-opcua-pki 6.21.0 → 6.22.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
@@ -93,6 +93,9 @@ var init_hostname = __esm({
93
93
 
94
94
  // packages/node-opcua-pki/lib/toolbox/common.ts
95
95
  import assert2 from "assert";
96
+ function isOpaqueSigner(privateKey) {
97
+ return typeof privateKey !== "string" && typeof privateKey.sign === "function";
98
+ }
96
99
  async function resolvePrivateKeyPassphrase(passphrase) {
97
100
  if (passphrase === void 0) {
98
101
  return void 0;
@@ -276,8 +279,9 @@ async function createCertificateSigningRequestAsync(certificateSigningRequestFil
276
279
  assert4(typeof certificateSigningRequestFilename === "string");
277
280
  const subject = params.subject ? new Subject(params.subject).toString() : void 0;
278
281
  displaySubtitle("- Creating a Certificate Signing Request with subtile");
279
- const privateKeyPem = typeof params.privateKey === "string" ? await fs2.promises.readFile(params.privateKey, "utf-8") : coercePrivateKeyPem(params.privateKey);
280
- const privateKey = await pemToPrivateKey(privateKeyPem);
282
+ const privateKey = isOpaqueSigner(params.privateKey) ? params.privateKey : await pemToPrivateKey(
283
+ typeof params.privateKey === "string" ? await fs2.promises.readFile(params.privateKey, "utf-8") : coercePrivateKeyPem(params.privateKey)
284
+ );
281
285
  const { csr } = await createCertificateSigningRequest({
282
286
  privateKey,
283
287
  dns: params.dns,
@@ -287,13 +291,16 @@ async function createCertificateSigningRequestAsync(certificateSigningRequestFil
287
291
  purpose: params.purpose
288
292
  });
289
293
  await fs2.promises.writeFile(certificateSigningRequestFilename, csr, "utf-8");
290
- display(`- privateKey ${typeof params.privateKey === "string" ? params.privateKey : "<in-memory>"}`);
294
+ display(
295
+ `- privateKey ${typeof params.privateKey === "string" ? params.privateKey : isOpaqueSigner(params.privateKey) ? "<opaque-signer>" : "<in-memory>"}`
296
+ );
291
297
  display(`- certificateSigningRequestFilename ${certificateSigningRequestFilename}`);
292
298
  }
293
299
  var init_create_certificate_signing_request = __esm({
294
300
  "packages/node-opcua-pki/lib/toolbox/without_openssl/create_certificate_signing_request.ts"() {
295
301
  "use strict";
296
302
  init_esm_shims();
303
+ init_common();
297
304
  init_display();
298
305
  }
299
306
  });
@@ -327,8 +334,9 @@ async function createSelfSignedCertificateAsync(certificate, params) {
327
334
  subject = subject.toString();
328
335
  const purpose = params.purpose;
329
336
  displayTitle("Generate a certificate request");
330
- const privateKeyPem = typeof params.privateKey === "string" ? await fs3.promises.readFile(params.privateKey, "utf-8") : coercePrivateKeyPem2(params.privateKey);
331
- const privateKey = await pemToPrivateKey2(privateKeyPem);
337
+ const privateKey = isOpaqueSigner(params.privateKey) ? params.privateKey : await pemToPrivateKey2(
338
+ typeof params.privateKey === "string" ? await fs3.promises.readFile(params.privateKey, "utf-8") : coercePrivateKeyPem2(params.privateKey)
339
+ );
332
340
  const { cert } = await createSelfSignedCertificate1({
333
341
  privateKey,
334
342
  notBefore: params.startDate,
@@ -383,11 +391,14 @@ import { drainPendingLocks, withLock } from "@ster5/global-mutex";
383
391
  import chalk3 from "chalk";
384
392
  import chokidar from "chokidar";
385
393
  import {
394
+ caSignerFromKeyOperations,
386
395
  exploreCertificate,
387
396
  exploreCertificateInfo,
388
397
  exploreCertificateRevocationList,
389
398
  generatePrivateKeyFile,
399
+ keyOperationsFromPrivateKey,
390
400
  makeSHA1Thumbprint,
401
+ PrivateKeyUnavailableError,
391
402
  readCertificateChain,
392
403
  readCertificateChainAsync,
393
404
  readCertificateRevocationList,
@@ -640,6 +651,9 @@ var init_certificate_manager = __esm({
640
651
  #disableFileWatchers;
641
652
  #privateKeyPassphrase;
642
653
  #privateKeyProvider;
654
+ #keyOperations;
655
+ /** The stable lazy wrap handed out by {@link getKeyOperations} for non-opaque configurations. */
656
+ #lazyLocalKeyOperations;
643
657
  /**
644
658
  * The on-disk key, decrypted once and kept for the instance's lifetime,
645
659
  * so the passphrase (or its resolver function) is consulted at most
@@ -683,6 +697,17 @@ var init_certificate_manager = __esm({
683
697
  this.#disableFileWatchers = options.disableFileWatchers ?? process.env.OPCUA_PKI_DISABLE_FILE_WATCHERS === "true";
684
698
  this.#privateKeyPassphrase = options.privateKeyPassphrase;
685
699
  this.#privateKeyProvider = options.privateKeyProvider;
700
+ this.#keyOperations = options.keyOperations;
701
+ if (this.#keyOperations && this.#privateKeyProvider) {
702
+ throw new Error(
703
+ "CertificateManager: 'keyOperations' and 'privateKeyProvider' are mutually exclusive: one hides the key, the other sources its material"
704
+ );
705
+ }
706
+ if (this.#keyOperations && this.#privateKeyPassphrase) {
707
+ throw new Error(
708
+ "CertificateManager: 'privateKeyPassphrase' is meaningless with 'keyOperations': there is no key material for a passphrase to protect"
709
+ );
710
+ }
686
711
  mkdirRecursiveSync(options.location);
687
712
  if (!fs4.existsSync(this.#location)) {
688
713
  throw new Error(`CertificateManager cannot access location ${this.#location}`);
@@ -724,6 +749,11 @@ var init_certificate_manager = __esm({
724
749
  * authority on what the current key is.
725
750
  */
726
751
  async getPrivateKey() {
752
+ if (this.#keyOperations) {
753
+ throw new PrivateKeyUnavailableError(
754
+ "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"
755
+ );
756
+ }
727
757
  if (this.#privateKeyProvider) {
728
758
  return await this.#privateKeyProvider.getPrivateKey();
729
759
  }
@@ -749,6 +779,75 @@ var init_certificate_manager = __esm({
749
779
  }
750
780
  /** In-flight first read of the on-disk key, so concurrent callers share one passphrase resolution. */
751
781
  #privateKeyPromise;
782
+ /**
783
+ * True when this manager's key is opaque — configured through
784
+ * `keyOperations`, held by an HSM/KMS, never obtainable as material.
785
+ * When true, {@link getPrivateKey} throws `PrivateKeyUnavailableError`
786
+ * and {@link getKeyOperations} is the only way to use the key.
787
+ */
788
+ isPrivateKeyOpaque() {
789
+ return !!this.#keyOperations;
790
+ }
791
+ /**
792
+ * The key as an opaque {@link IKeyOperations} — the recommended way to
793
+ * *use* the private key regardless of where it lives.
794
+ *
795
+ * Returns the configured `keyOperations` object when the key is opaque.
796
+ * Otherwise returns a stable lazy wrap over {@link getPrivateKey}: its
797
+ * methods resolve the key on first use (disk read, passphrase,
798
+ * `privateKeyProvider` — all async), so the wrap offers no synchronous
799
+ * fast path; callers that need one resolve the key themselves and build
800
+ * a `LocalKeyOperations` over it. The wrap follows key rotation: a
801
+ * `privateKeyProvider` that starts returning a different key gets a
802
+ * fresh underlying `LocalKeyOperations`.
803
+ */
804
+ getKeyOperations() {
805
+ if (this.#keyOperations) {
806
+ return this.#keyOperations;
807
+ }
808
+ if (!this.#lazyLocalKeyOperations) {
809
+ let cached;
810
+ const resolve = async () => {
811
+ const key = await this.getPrivateKey();
812
+ if (!cached || cached.key !== key) {
813
+ cached = { key, ops: keyOperationsFromPrivateKey(key) };
814
+ }
815
+ return cached.ops;
816
+ };
817
+ this.#lazyLocalKeyOperations = {
818
+ sign: async (data, params) => (await resolve()).sign(data, params),
819
+ decryptBlock: async (block, params) => (await resolve()).decryptBlock(block, params),
820
+ getKeyMetadata: async () => (await resolve()).getKeyMetadata(),
821
+ getPublicKey: async () => (await resolve()).getPublicKey()
822
+ };
823
+ }
824
+ return this.#lazyLocalKeyOperations;
825
+ }
826
+ /**
827
+ * Fail closed at `initialize()` time, not on the first certificate
828
+ * operation, if the key cannot actually be used: wrong/missing
829
+ * passphrase on an encrypted key, a broken `privateKeyProvider`, or an
830
+ * unreachable `keyOperations` provider (probed via `getKeyMetadata`).
831
+ */
832
+ async #probePrivateKey() {
833
+ if (this.#keyOperations) {
834
+ await this.#keyOperations.getKeyMetadata();
835
+ return;
836
+ }
837
+ await this.getPrivateKey();
838
+ }
839
+ /**
840
+ * The key to hand to a certificate-issuance primitive: the raw
841
+ * {@link PrivateKey} for a local configuration, or a {@link CaSigner}
842
+ * adapted from `keyOperations` when the key is opaque — which requires
843
+ * the provider to implement `getPublicKey` (the adapter says so if not).
844
+ */
845
+ async #resolveSigningKey() {
846
+ if (this.#keyOperations) {
847
+ return caSignerFromKeyOperations(this.#keyOperations);
848
+ }
849
+ return await this.getPrivateKey();
850
+ }
752
851
  /**
753
852
  * Enable, disable, or rotate the passphrase protecting the on-disk
754
853
  * private key: decrypt with `oldPassphrase` (omit if the key is
@@ -770,6 +869,11 @@ var init_certificate_manager = __esm({
770
869
  * disk file for this method to rewrite).
771
870
  */
772
871
  async reencryptPrivateKey(oldPassphrase, newPassphrase) {
872
+ if (this.#keyOperations) {
873
+ throw new PrivateKeyUnavailableError(
874
+ "reencryptPrivateKey: not supported when keyOperations is configured \u2014 there is no key material to rewrite"
875
+ );
876
+ }
773
877
  if (this.#privateKeyProvider) {
774
878
  throw new Error("reencryptPrivateKey: not supported when a privateKeyProvider is configured");
775
879
  }
@@ -1118,7 +1222,7 @@ var init_certificate_manager = __esm({
1118
1222
  mkdirRecursiveSync(path2.join(pkiDir, "issuers"));
1119
1223
  mkdirRecursiveSync(path2.join(pkiDir, "issuers/certs"));
1120
1224
  mkdirRecursiveSync(path2.join(pkiDir, "issuers/crl"));
1121
- const ownsDiskKey = !this.#privateKeyProvider;
1225
+ const ownsDiskKey = !this.#privateKeyProvider && !this.#keyOperations;
1122
1226
  const needsKeyGeneration = ownsDiskKey && !fs4.existsSync(this.privateKey);
1123
1227
  const needsKeyEncryption = ownsDiskKey && !needsKeyGeneration && this.#privateKeyPassphrase !== void 0 && !isEncryptedPrivateKeyFile(this.privateKey);
1124
1228
  if (!fs4.existsSync(this.configFile) || needsKeyGeneration || needsKeyEncryption) {
@@ -1144,14 +1248,14 @@ var init_certificate_manager = __esm({
1144
1248
  if (ownsDiskKey) {
1145
1249
  restrictPrivateFilePermissions(this.privateKey, 384);
1146
1250
  }
1147
- await this.getPrivateKey();
1251
+ await this.#probePrivateKey();
1148
1252
  await this.#readCertificates();
1149
1253
  });
1150
1254
  } else {
1151
1255
  if (ownsDiskKey) {
1152
1256
  restrictPrivateFilePermissions(this.privateKey, 384);
1153
1257
  }
1154
- await this.getPrivateKey();
1258
+ await this.#probePrivateKey();
1155
1259
  await this.#readCertificates();
1156
1260
  }
1157
1261
  }
@@ -1236,7 +1340,7 @@ var init_certificate_manager = __esm({
1236
1340
  if (typeof params.applicationUri !== "string") {
1237
1341
  throw new Error("createSelfSignedCertificate: expecting applicationUri to be a string");
1238
1342
  }
1239
- if (!this.#privateKeyProvider && !fs4.existsSync(this.privateKey)) {
1343
+ if (!this.#privateKeyProvider && !this.#keyOperations && !fs4.existsSync(this.privateKey)) {
1240
1344
  throw new Error(`Cannot find private key ${this.privateKey}`);
1241
1345
  }
1242
1346
  let certificateFilename = path2.join(this.rootDir, "own/certs/self_signed_certificate.pem");
@@ -1245,7 +1349,7 @@ var init_certificate_manager = __esm({
1245
1349
  ...params,
1246
1350
  rootDir: this.rootDir,
1247
1351
  configFile: this.configFile,
1248
- privateKey: await this.getPrivateKey(),
1352
+ privateKey: await this.#resolveSigningKey(),
1249
1353
  subject: params.subject || "CN=FIXME"
1250
1354
  };
1251
1355
  await this.withLock2(async () => {
@@ -1273,7 +1377,7 @@ var init_certificate_manager = __esm({
1273
1377
  ...params,
1274
1378
  rootDir: path2.resolve(this.rootDir),
1275
1379
  configFile: path2.resolve(this.configFile),
1276
- privateKey: await this.getPrivateKey()
1380
+ privateKey: await this.#resolveSigningKey()
1277
1381
  };
1278
1382
  return await this.withLock2(async () => {
1279
1383
  const now = /* @__PURE__ */ new Date();