node-opcua-server-configuration 2.177.0 → 2.179.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.
Files changed (32) hide show
  1. package/dist/clientTools/push_certificate_management_client.js +1 -1
  2. package/dist/clientTools/push_certificate_management_client.js.map +1 -1
  3. package/dist/server/file_transaction_manager.js +26 -13
  4. package/dist/server/file_transaction_manager.js.map +1 -1
  5. package/dist/server/install_certificate_file_watcher.js +4 -2
  6. package/dist/server/install_certificate_file_watcher.js.map +1 -1
  7. package/dist/server/install_push_certificate_management.d.ts +35 -3
  8. package/dist/server/install_push_certificate_management.js +119 -27
  9. package/dist/server/install_push_certificate_management.js.map +1 -1
  10. package/dist/server/promote_trust_list.js +6 -6
  11. package/dist/server/promote_trust_list.js.map +1 -1
  12. package/dist/server/push_certificate_manager/create_signing_request.js.map +1 -1
  13. package/dist/server/push_certificate_manager/update_certificate.js +34 -4
  14. package/dist/server/push_certificate_manager/update_certificate.js.map +1 -1
  15. package/dist/server/push_certificate_manager_helpers.js +8 -4
  16. package/dist/server/push_certificate_manager_helpers.js.map +1 -1
  17. package/dist/server/push_certificate_manager_server_impl.d.ts +4 -2
  18. package/dist/server/push_certificate_manager_server_impl.js +0 -3
  19. package/dist/server/push_certificate_manager_server_impl.js.map +1 -1
  20. package/dist/server/tools.js +1 -1
  21. package/dist/server/tools.js.map +1 -1
  22. package/package.json +30 -30
  23. package/source/clientTools/push_certificate_management_client.ts +1 -1
  24. package/source/server/file_transaction_manager.ts +26 -13
  25. package/source/server/install_certificate_file_watcher.ts +5 -4
  26. package/source/server/install_push_certificate_management.ts +175 -58
  27. package/source/server/promote_trust_list.ts +16 -15
  28. package/source/server/push_certificate_manager/create_signing_request.ts +4 -2
  29. package/source/server/push_certificate_manager/update_certificate.ts +36 -7
  30. package/source/server/push_certificate_manager_helpers.ts +9 -5
  31. package/source/server/push_certificate_manager_server_impl.ts +6 -9
  32. package/source/server/tools.ts +1 -1
@@ -3,10 +3,10 @@ import path from "node:path";
3
3
  import { assert } from "node-opcua-assert";
4
4
  import type { ByteString } from "node-opcua-basic-types";
5
5
  import type { CertificateManager, OPCUACertificateManager } from "node-opcua-certificate-manager";
6
- import { exploreCertificate, readPrivateKey } from "node-opcua-crypto";
6
+ import { _toExportableKeyObject, exploreCertificate } from "node-opcua-crypto";
7
7
  import {
8
- certificateMatchesPrivateKey,
9
8
  type Certificate,
9
+ certificateMatchesPrivateKey,
10
10
  coercePEMorDerToPrivateKey,
11
11
  coercePrivateKeyPem,
12
12
  combine_der,
@@ -83,6 +83,22 @@ async function preInstallCertificate(
83
83
  }
84
84
  }
85
85
 
86
+ /**
87
+ * Encode `privateKeyObj` as a PKCS#8 PEM string, encrypted with `passphrase`
88
+ * (aes-256-cbc — same cipher `writePrivateKeyFile` uses) when given, plaintext
89
+ * otherwise. Never touches disk — the caller stages the resulting string via
90
+ * `FileTransactionManager.stageFile()`, so an encrypted key is the only form
91
+ * that is ever written, whether to the staging temp file or the final
92
+ * destination.
93
+ */
94
+ function encodePrivateKeyPem(privateKeyObj: PrivateKey, passphrase: string | undefined): string {
95
+ if (!passphrase) {
96
+ return coercePrivateKeyPem(privateKeyObj);
97
+ }
98
+ const keyObject = _toExportableKeyObject(privateKeyObj);
99
+ return keyObject.export({ type: "pkcs8", format: "pem", cipher: "aes-256-cbc", passphrase }) as string;
100
+ }
101
+
86
102
  // Helper: Stage private key to temporary file
87
103
  async function preInstallPrivateKey(
88
104
  serverImpl: PushCertificateManagerInternalContext,
@@ -94,7 +110,11 @@ async function preInstallPrivateKey(
94
110
 
95
111
  if (privateKey) {
96
112
  const privateKeyObj = coercePEMorDerToPrivateKey(privateKey as string | Buffer);
97
- const privateKeyPEM = coercePrivateKeyPem(privateKeyObj);
113
+ // If the certificate manager is configured with a privateKeyPassphrase,
114
+ // the staged (and later applied) key must be encrypted with it — never
115
+ // written back to disk in clear.
116
+ const passphrase = await (certificateManager as OPCUACertificateManager).getPrivateKeyPassphrase?.();
117
+ const privateKeyPEM = encodePrivateKeyPem(privateKeyObj, passphrase);
98
118
  await serverImpl.fileTransactionManager.stageFile(certificateManager.privateKey, privateKeyPEM, "utf-8");
99
119
  }
100
120
  }
@@ -117,7 +137,8 @@ export async function executeUpdateCertificate(
117
137
  try {
118
138
  const context = resolveCertificateGroupContext(serverImpl, certificateGroupId);
119
139
  if (context.statusCode.isNotGood() || !context.certificateManager) {
120
- debugLog(" cannot find group ", certificateGroupId);
140
+ // c8 ignore next
141
+ doDebug && debugLog(" cannot find group ", certificateGroupId);
121
142
  return { statusCode: StatusCodes.BadInvalidArgument, applyChangesRequired: false };
122
143
  }
123
144
 
@@ -170,9 +191,17 @@ export async function executeUpdateCertificate(
170
191
  }
171
192
 
172
193
  if (!hasPrivateKeyFormat && !hasPrivateKey) {
173
- const privateKeyObj = readPrivateKey(
174
- serverImpl.tmpCertificateManager ? serverImpl.tmpCertificateManager.privateKey : certificateManager.privateKey
175
- );
194
+ // Resolve via getPrivateKey(), for two independent reasons:
195
+ // - CertificateManager.getPrivateKey() caches the decoded key for the lifetime of the
196
+ // instance (see node-opcua-pki). createCertificateRequest()/createSelfSignedCertificate()
197
+ // both go through that cache, so the CSR that produced `certificate` was necessarily
198
+ // signed with the *cached* key. Comparing against a fresh disk read (readPrivateKey
199
+ // on the file path) would compare against whatever is currently on disk instead — which
200
+ // can differ right after a previous updateCertificate() call replaced the private key
201
+ // file directly (bypassing the cache), causing a spurious BadSecurityChecksFailed.
202
+ // - the on-disk file may be an encrypted PKCS#8 key (privateKeyPassphrase); a raw
203
+ // readPrivateKey without the manager's passphrase would fail on it.
204
+ const privateKeyObj = await (serverImpl.tmpCertificateManager ?? certificateManager).getPrivateKey();
176
205
 
177
206
  if (!certificateMatchesPrivateKey(certificate, privateKeyObj)) {
178
207
  warningLog("certificate doesn't match privateKey");
@@ -16,12 +16,12 @@ import {
16
16
  } from "node-opcua-address-space";
17
17
  import { EventNotifierFlags, type UAObject, type UAVariable } from "node-opcua-address-space-base";
18
18
  import type { ByteString, UAString } from "node-opcua-basic-types";
19
+ import type { CertificateManager } from "node-opcua-certificate-manager";
19
20
  import { ObjectIds, ObjectTypeIds } from "node-opcua-constants";
20
21
  import { type Certificate, readCertificateChainAsync } from "node-opcua-crypto";
21
22
  import { AccessRestrictionsFlag, BrowseDirection, coerceQualifiedName, NodeClass } from "node-opcua-data-model";
22
- import { make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
23
+ import { checkDebugFlag, make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
23
24
  import { NodeId, resolveNodeId } from "node-opcua-nodeid";
24
- import type { CertificateManager } from "node-opcua-certificate-manager";
25
25
  import { StatusCodes } from "node-opcua-status-code";
26
26
  import type { CallMethodResultOptions } from "node-opcua-types";
27
27
  import { DataType, type Variant, VariantArrayType } from "node-opcua-variant";
@@ -37,6 +37,7 @@ import { rolePermissionAdminOnly, rolePermissionRestricted } from "./roles_and_p
37
37
  import { hasEncryptedChannel, hasExpectedUserAccess } from "./tools.js";
38
38
 
39
39
  const debugLog = make_debugLog("ServerConfiguration");
40
+ const doDebug = checkDebugFlag("ServerConfiguration");
40
41
  const warningLog = make_warningLog("ServerConfiguration");
41
42
  const errorLog = make_errorLog("ServerConfiguration");
42
43
 
@@ -273,14 +274,16 @@ function bindCertificateGroup(certificateGroup: UACertificateGroup, certificateM
273
274
  const certificateFile = getCertificateFilename(certificateManager);
274
275
  const changeDetector = installCertificateFileWatcher(certificateGroup, certificateFile);
275
276
  changeDetector.on("certificateChange", () => {
276
- debugLog("detecting certificate change", certificateFile);
277
+ // c8 ignore next
278
+ doDebug && debugLog("detecting certificate change", certificateFile);
277
279
  updateCertificateAlarm();
278
280
  });
279
281
  }
280
282
 
281
283
  async function updateCertificateAlarm() {
282
284
  try {
283
- debugLog("updateCertificateAlarm", certificateGroup.browseName.toString());
285
+ // c8 ignore next
286
+ doDebug && debugLog("updateCertificateAlarm", certificateGroup.browseName.toString());
284
287
  const certificateExpired = certificateGroup.getComponentByName("CertificateExpired");
285
288
  if (certificateExpired && certificateManager) {
286
289
  const certificateExpiredEx = certificateExpired as unknown as UACertificateExpirationAlarmEx;
@@ -393,7 +396,8 @@ export async function installPushCertificateManagement(
393
396
 
394
397
  const serverConfigurationPriv = serverConfiguration as UAServerConfigurationPriv;
395
398
  if (serverConfigurationPriv.$pushCertificateManager) {
396
- debugLog("PushCertificateManagement has already been installed");
399
+ // c8 ignore next
400
+ doDebug && debugLog("PushCertificateManagement has already been installed");
397
401
  return;
398
402
  }
399
403
 
@@ -39,6 +39,8 @@ export interface PushCertificateManagerEvents {
39
39
  applyChangesCompleted: (context?: ISessionContext) => void;
40
40
  certificateUpdated: (certificateGroupId: NodeId | string, certificate: Buffer) => void;
41
41
  trustListUpdated: (certificateGroupId: string) => void;
42
+ CertificateAboutToChange: (actionQueue: ActionQueue) => void;
43
+ CertificateChanged: (actionQueue: ActionQueue) => void;
42
44
  }
43
45
 
44
46
  export interface PushCertificateManagerServerOptions {
@@ -71,18 +73,14 @@ export class PushCertificateManagerServerImpl extends EventEmitter implements Pu
71
73
 
72
74
  // ── typed event helpers ──────────────────────────────────────────
73
75
  public on<K extends keyof PushCertificateManagerEvents>(event: K, listener: PushCertificateManagerEvents[K]): this;
74
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
- public on(event: string | symbol, listener: (...args: any[]) => void): this;
76
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
77
- public on(event: string | symbol, listener: (...args: any[]) => void): this {
76
+ public on(event: string | symbol, listener: (...args: unknown[]) => void): this;
77
+ public on(event: string | symbol, listener: (...args: unknown[]) => void): this {
78
78
  return super.on(event, listener);
79
79
  }
80
80
 
81
81
  public once<K extends keyof PushCertificateManagerEvents>(event: K, listener: PushCertificateManagerEvents[K]): this;
82
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
83
- public once(event: string | symbol, listener: (...args: any[]) => void): this;
84
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
85
- public once(event: string | symbol, listener: (...args: any[]) => void): this {
82
+ public once(event: string | symbol, listener: (...args: unknown[]) => void): this;
83
+ public once(event: string | symbol, listener: (...args: unknown[]) => void): this {
86
84
  return super.once(event, listener);
87
85
  }
88
86
 
@@ -175,7 +173,6 @@ export class PushCertificateManagerServerImpl extends EventEmitter implements Pu
175
173
  return await executeGetRejectedList(this._context);
176
174
  }
177
175
 
178
- // eslint-disable-next-line max-statements
179
176
  public async updateCertificate(
180
177
  certificateGroupId: NodeId | string,
181
178
  certificateTypeId: NodeId | string,
@@ -2,7 +2,7 @@ import { type ISessionContext, WellKnownRoles } from "node-opcua-address-space";
2
2
  import { MessageSecurityMode } from "node-opcua-secure-channel";
3
3
 
4
4
  export function hasExpectedUserAccess(context: ISessionContext) {
5
- if (!context || !context.session || !context.session.userIdentityToken) {
5
+ if (!context?.session?.userIdentityToken) {
6
6
  return false;
7
7
  }
8
8
  return context.currentUserHasRole(WellKnownRoles.SecurityAdmin);