node-opcua-server-configuration 2.163.0 → 2.164.2

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 (100) hide show
  1. package/dist/clientTools/certificate_types.d.ts +17 -0
  2. package/dist/clientTools/certificate_types.js +20 -0
  3. package/dist/clientTools/certificate_types.js.map +1 -0
  4. package/dist/clientTools/get_certificate_key_type.d.ts +6 -0
  5. package/dist/clientTools/get_certificate_key_type.js +55 -0
  6. package/dist/clientTools/get_certificate_key_type.js.map +1 -0
  7. package/dist/clientTools/index.d.ts +2 -1
  8. package/dist/clientTools/index.js +2 -17
  9. package/dist/clientTools/index.js.map +1 -1
  10. package/dist/clientTools/push_certificate_management_client.d.ts +10 -10
  11. package/dist/clientTools/push_certificate_management_client.js +85 -89
  12. package/dist/clientTools/push_certificate_management_client.js.map +1 -1
  13. package/dist/index.d.ts +9 -7
  14. package/dist/index.js +9 -23
  15. package/dist/index.js.map +1 -1
  16. package/dist/push_certificate_manager.d.ts +4 -4
  17. package/dist/push_certificate_manager.js +1 -2
  18. package/dist/server/certificate_validation.d.ts +15 -0
  19. package/dist/server/certificate_validation.js +76 -0
  20. package/dist/server/certificate_validation.js.map +1 -0
  21. package/dist/server/file_transaction_manager.d.ts +30 -0
  22. package/dist/server/file_transaction_manager.js +223 -0
  23. package/dist/server/file_transaction_manager.js.map +1 -0
  24. package/dist/server/install_certificate_file_watcher.d.ts +1 -1
  25. package/dist/server/install_certificate_file_watcher.js +8 -14
  26. package/dist/server/install_certificate_file_watcher.js.map +1 -1
  27. package/dist/server/install_push_certitifate_management.d.ts +6 -6
  28. package/dist/server/install_push_certitifate_management.js +61 -65
  29. package/dist/server/install_push_certitifate_management.js.map +1 -1
  30. package/dist/server/promote_trust_list.d.ts +1 -1
  31. package/dist/server/promote_trust_list.js +323 -82
  32. package/dist/server/promote_trust_list.js.map +1 -1
  33. package/dist/server/push_certificate_manager/apply_changes.d.ts +3 -0
  34. package/dist/server/push_certificate_manager/apply_changes.js +59 -0
  35. package/dist/server/push_certificate_manager/apply_changes.js.map +1 -0
  36. package/dist/server/push_certificate_manager/create_signing_request.d.ts +5 -0
  37. package/dist/server/push_certificate_manager/create_signing_request.js +108 -0
  38. package/dist/server/push_certificate_manager/create_signing_request.js.map +1 -0
  39. package/dist/server/push_certificate_manager/get_rejected_list.d.ts +3 -0
  40. package/dist/server/push_certificate_manager/get_rejected_list.js +46 -0
  41. package/dist/server/push_certificate_manager/get_rejected_list.js.map +1 -0
  42. package/dist/server/push_certificate_manager/internal_context.d.ts +35 -0
  43. package/dist/server/push_certificate_manager/internal_context.js +45 -0
  44. package/dist/server/push_certificate_manager/internal_context.js.map +1 -0
  45. package/dist/server/push_certificate_manager/subject_to_string.d.ts +3 -0
  46. package/dist/server/push_certificate_manager/subject_to_string.js +27 -0
  47. package/dist/server/push_certificate_manager/subject_to_string.js.map +1 -0
  48. package/dist/server/push_certificate_manager/update_certificate.d.ts +5 -0
  49. package/dist/server/push_certificate_manager/update_certificate.js +132 -0
  50. package/dist/server/push_certificate_manager/update_certificate.js.map +1 -0
  51. package/dist/server/push_certificate_manager/util.d.ts +29 -0
  52. package/dist/server/push_certificate_manager/util.js +117 -0
  53. package/dist/server/push_certificate_manager/util.js.map +1 -0
  54. package/dist/server/push_certificate_manager_helpers.d.ts +5 -2
  55. package/dist/server/push_certificate_manager_helpers.js +109 -112
  56. package/dist/server/push_certificate_manager_helpers.js.map +1 -1
  57. package/dist/server/push_certificate_manager_server_impl.d.ts +16 -29
  58. package/dist/server/push_certificate_manager_server_impl.js +49 -437
  59. package/dist/server/push_certificate_manager_server_impl.js.map +1 -1
  60. package/dist/server/roles_and_permissions.d.ts +1 -1
  61. package/dist/server/roles_and_permissions.js +24 -27
  62. package/dist/server/roles_and_permissions.js.map +1 -1
  63. package/dist/server/tools.d.ts +1 -1
  64. package/dist/server/tools.js +7 -13
  65. package/dist/server/tools.js.map +1 -1
  66. package/dist/server/trust_list_server.d.ts +2 -2
  67. package/dist/server/trust_list_server.js +40 -29
  68. package/dist/server/trust_list_server.js.map +1 -1
  69. package/dist/standard_certificate_types.js +6 -9
  70. package/dist/standard_certificate_types.js.map +1 -1
  71. package/dist/trust_list.d.ts +2 -2
  72. package/dist/trust_list.js +1 -2
  73. package/dist/trust_list_impl.js +1 -2
  74. package/dist/trust_list_impl.js.map +1 -1
  75. package/package.json +29 -30
  76. package/source/clientTools/certificate_types.ts +21 -0
  77. package/source/clientTools/get_certificate_key_type.ts +73 -0
  78. package/source/clientTools/index.ts +2 -1
  79. package/source/clientTools/push_certificate_management_client.ts +49 -44
  80. package/source/index.ts +9 -7
  81. package/source/push_certificate_manager.ts +15 -17
  82. package/source/server/certificate_validation.ts +103 -0
  83. package/source/server/file_transaction_manager.ts +253 -0
  84. package/source/server/install_certificate_file_watcher.ts +15 -11
  85. package/source/server/install_push_certitifate_management.ts +52 -51
  86. package/source/server/promote_trust_list.ts +362 -73
  87. package/source/server/push_certificate_manager/apply_changes.ts +63 -0
  88. package/source/server/push_certificate_manager/create_signing_request.ts +137 -0
  89. package/source/server/push_certificate_manager/get_rejected_list.ts +63 -0
  90. package/source/server/push_certificate_manager/internal_context.ts +63 -0
  91. package/source/server/push_certificate_manager/subject_to_string.ts +25 -0
  92. package/source/server/push_certificate_manager/update_certificate.ts +201 -0
  93. package/source/server/push_certificate_manager/util.ts +145 -0
  94. package/source/server/push_certificate_manager_helpers.ts +61 -51
  95. package/source/server/push_certificate_manager_server_impl.ts +94 -553
  96. package/source/server/roles_and_permissions.ts +7 -8
  97. package/source/server/tools.ts +2 -5
  98. package/source/server/trust_list_server.ts +24 -9
  99. package/source/standard_certificate_types.ts +2 -3
  100. package/source/trust_list.ts +26 -33
@@ -4,46 +4,261 @@
4
4
 
5
5
  import { fs as MemFs } from "memfs";
6
6
 
7
- import {
7
+ import type {
8
+ IAddressSpace,
9
+ ISessionContext,
8
10
  MethodFunctor,
9
11
  UAMethod,
10
- UATrustList,
11
12
  UAObject,
12
- UAVariable,
13
- ISessionContext,
14
- IAddressSpace
13
+ UAObjectType,
14
+ UATrustList,
15
+ UATrustList_Base,
16
+ UAVariable
15
17
  } from "node-opcua-address-space";
16
- import { checkDebugFlag, make_debugLog, make_warningLog } from "node-opcua-debug";
17
- import { CallbackT, StatusCodes } from "node-opcua-status-code";
18
- import { CallMethodResultOptions } from "node-opcua-types";
19
- import { DataType, Variant } from "node-opcua-variant";
20
- import { AccessRestrictionsFlag } from "node-opcua-data-model";
21
- import { CertificateManager } from "node-opcua-pki";
22
- import { AbstractFs, installFileType, OpenFileMode } from "node-opcua-file-transfer";
23
- import { OPCUACertificateManager } from "node-opcua-certificate-manager";
18
+ import { BinaryStream } from "node-opcua-binary-stream";
19
+ import type { OPCUACertificateManager } from "node-opcua-certificate-manager";
24
20
  import { split_der, verifyCertificateChain } from "node-opcua-crypto/web";
21
+ import { AccessRestrictionsFlag } from "node-opcua-data-model";
22
+ import { checkDebugFlag, make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
23
+ import { type AbstractFs, installFileType, OpenFileMode } from "node-opcua-file-transfer";
24
+ import { VerificationStatus } from "node-opcua-pki";
25
+ import { type CallbackT, type StatusCode, StatusCodes } from "node-opcua-status-code";
26
+ import { type CallMethodResultOptions, TrustListDataType } from "node-opcua-types";
27
+ import { DataType, Variant } from "node-opcua-variant";
28
+ import { rolePermissionAdminOnly } from "./roles_and_permissions.js";
25
29
 
26
- import { TrustListMasks, writeTrustList } from "./trust_list_server";
27
-
28
- import { hasEncryptedChannel, hasExpectedUserAccess } from "./tools";
29
- import { rolePermissionAdminOnly } from "./roles_and_permissions";
30
+ import { hasEncryptedChannel, hasExpectedUserAccess } from "./tools.js";
31
+ import { TrustListMasks, writeTrustList } from "./trust_list_server.js";
30
32
 
31
33
  const debugLog = make_debugLog("ServerConfiguration");
32
34
  const doDebug = checkDebugFlag("ServerConfiguration");
33
- doDebug;
34
35
  const warningLog = make_warningLog("ServerConfiguration");
35
- const errorLog = debugLog;
36
+ const errorLog = make_errorLog("ServerConfiguration");
36
37
 
37
38
  function trustListIsAlreadyOpened(trustList: UATrustList): boolean {
38
- return false; // to do...
39
+ // TrustList extends FileType, which has an openCount property tracking how many handles are open
40
+ const openCountNode = trustList.openCount;
41
+ if (!openCountNode) {
42
+ return false;
43
+ }
44
+ const dataValue = openCountNode.readValue();
45
+ if (!dataValue || !dataValue.value) {
46
+ return false;
47
+ }
48
+ const openCount = dataValue.value.value as number;
49
+ return openCount > 0;
50
+ }
51
+
52
+ /**
53
+ * Update the mandatory LastUpdateTime property whenever the trust list is modified.
54
+ * Per OPC UA Part 12 spec, this must be updated after AddCertificate, RemoveCertificate, or CloseAndUpdate.
55
+ */
56
+ function updateLastUpdateTime(trustList: UATrustList): void {
57
+ try {
58
+ const lastUpdateTimeNode = trustList.lastUpdateTime;
59
+ if (lastUpdateTimeNode) {
60
+ lastUpdateTimeNode.setValueFromSource({
61
+ dataType: DataType.DateTime,
62
+ value: new Date()
63
+ });
64
+ doDebug && debugLog("Updated LastUpdateTime to", new Date().toISOString());
65
+ } else {
66
+ warningLog("LastUpdateTime property not found on TrustList");
67
+ }
68
+ } catch (err) {
69
+ errorLog("Error updating LastUpdateTime:", err);
70
+ }
71
+ }
72
+
73
+ interface UAMethodEx extends UAMethod {
74
+ _asyncExecutionFunction?: MethodFunctor;
75
+ }
76
+ interface UATrustListEx extends UATrustList {
77
+ $$certificateManager: OPCUACertificateManager;
78
+ $$filename: string;
79
+ $$openedForWrite: boolean;
80
+ }
81
+
82
+ async function applyTrustListChanges(cm: OPCUACertificateManager, trustListData: TrustListDataType): Promise<StatusCode> {
83
+ try {
84
+ // Automatically update specifiedLists mask
85
+ if (trustListData.issuerCrls && trustListData.issuerCrls.length > 0) {
86
+ trustListData.specifiedLists |= TrustListMasks.IssuerCrls;
87
+ }
88
+ if (trustListData.trustedCrls && trustListData.trustedCrls.length > 0) {
89
+ trustListData.specifiedLists |= TrustListMasks.TrustedCrls;
90
+ }
91
+ if (trustListData.trustedCertificates && trustListData.trustedCertificates.length > 0) {
92
+ trustListData.specifiedLists |= TrustListMasks.TrustedCertificates;
93
+ }
94
+ if (trustListData.issuerCertificates && trustListData.issuerCertificates.length > 0) {
95
+ trustListData.specifiedLists |= TrustListMasks.IssuerCertificates;
96
+ }
97
+
98
+ // Process CRLs
99
+ if ((trustListData.specifiedLists & TrustListMasks.IssuerCrls) === TrustListMasks.IssuerCrls) {
100
+ doDebug && debugLog("Processing issuer CRLs");
101
+ await cm.clearRevocationLists("issuers");
102
+ if (trustListData.issuerCrls && trustListData.issuerCrls.length > 0) {
103
+ doDebug && debugLog(` Writing ${trustListData.issuerCrls.length} issuer CRLs`);
104
+ for (const crl of trustListData.issuerCrls) {
105
+ await cm.addRevocationList(crl, "issuers");
106
+ }
107
+ }
108
+ }
109
+
110
+ if ((trustListData.specifiedLists & TrustListMasks.TrustedCrls) === TrustListMasks.TrustedCrls) {
111
+ doDebug && debugLog("Processing trusted CRLs");
112
+ await cm.clearRevocationLists("trusted");
113
+ if (trustListData.trustedCrls && trustListData.trustedCrls.length > 0) {
114
+ doDebug && debugLog(` Writing ${trustListData.trustedCrls.length} trusted CRLs`);
115
+ for (const crl of trustListData.trustedCrls) {
116
+ await cm.addRevocationList(crl, "trusted");
117
+ }
118
+ }
119
+ }
120
+
121
+ // Validate all trusted certificates
122
+ if (
123
+ (trustListData.specifiedLists & TrustListMasks.TrustedCertificates) === TrustListMasks.TrustedCertificates &&
124
+ trustListData.trustedCertificates
125
+ ) {
126
+ for (const cert of trustListData.trustedCertificates) {
127
+ try {
128
+ const certs = split_der(cert);
129
+ const validationResult = await verifyCertificateChain([certs[0]]);
130
+ if (validationResult.status !== "Good") {
131
+ warningLog("Invalid certificate in trust list:", validationResult.status, validationResult.reason);
132
+ return StatusCodes.BadCertificateInvalid;
133
+ }
134
+ } catch (validationErr) {
135
+ errorLog("Certificate validation failed:", validationErr);
136
+ return StatusCodes.BadCertificateInvalid;
137
+ }
138
+ }
139
+ }
140
+
141
+ // Validate all issuer certificates
142
+ if (
143
+ (trustListData.specifiedLists & TrustListMasks.IssuerCertificates) === TrustListMasks.IssuerCertificates &&
144
+ trustListData.issuerCertificates
145
+ ) {
146
+ for (const cert of trustListData.issuerCertificates) {
147
+ try {
148
+ const certs = split_der(cert);
149
+ const validationResult = await verifyCertificateChain([certs[0]]);
150
+ if (validationResult.status !== "Good") {
151
+ warningLog("Invalid issuer certificate in trust list:", validationResult.status, validationResult.reason);
152
+ return StatusCodes.BadCertificateInvalid;
153
+ }
154
+ } catch (validationErr) {
155
+ errorLog("Issuer certificate validation failed:", validationErr);
156
+ return StatusCodes.BadCertificateInvalid;
157
+ }
158
+ }
159
+ }
160
+
161
+ // Update certificates
162
+ if (
163
+ (trustListData.specifiedLists & TrustListMasks.TrustedCertificates) === TrustListMasks.TrustedCertificates &&
164
+ trustListData.trustedCertificates
165
+ ) {
166
+ for (const cert of trustListData.trustedCertificates) {
167
+ await cm.trustCertificate(cert);
168
+ }
169
+ }
170
+ if (
171
+ (trustListData.specifiedLists & TrustListMasks.IssuerCertificates) === TrustListMasks.IssuerCertificates &&
172
+ trustListData.issuerCertificates
173
+ ) {
174
+ for (const cert of trustListData.issuerCertificates) {
175
+ await cm.addIssuer(cert);
176
+ }
177
+ }
178
+
179
+ return StatusCodes.Good;
180
+ } catch (err) {
181
+ errorLog("Error in applyTrustListChanges:", err);
182
+ return StatusCodes.BadInternalError;
183
+ }
39
184
  }
40
185
 
41
186
  async function _closeAndUpdate(
42
187
  this: UAMethod,
43
188
  inputArguments: Variant[],
44
- context: ISessionContext
189
+ context: ISessionContext,
190
+ _close_method?: MethodFunctor
45
191
  ): Promise<CallMethodResultOptions> {
46
- return { statusCode: StatusCodes.Good };
192
+ const trustList = context.object as UATrustListEx;
193
+ const cm = trustList.$$certificateManager;
194
+ const filename = trustList.$$filename;
195
+
196
+ // Clear the write lock when closing
197
+ trustList.$$openedForWrite = false;
198
+
199
+ // Get the close method if not provided
200
+ if (!_close_method) {
201
+ const closeMethod = trustList.getChildByName("Close") as UAMethodEx;
202
+ if (closeMethod) {
203
+ _close_method = closeMethod._asyncExecutionFunction;
204
+ }
205
+ }
206
+
207
+ if (!cm || !filename) {
208
+ return { statusCode: StatusCodes.BadInternalError };
209
+ }
210
+
211
+ let processStatusCode: StatusCode = StatusCodes.Good;
212
+
213
+ try {
214
+ if (MemFs.existsSync(filename)) {
215
+ const data = await new Promise<Buffer>((resolve, reject) => {
216
+ MemFs.readFile(filename, (err, data) => {
217
+ if (err) reject(err);
218
+ else resolve(data as Buffer);
219
+ });
220
+ });
221
+
222
+ // Decode the TrustListDataType
223
+ const stream = new BinaryStream(data);
224
+ const trustListData = new TrustListDataType();
225
+ trustListData.decode(stream);
226
+
227
+ processStatusCode = await applyTrustListChanges(cm, trustListData);
228
+
229
+ if (processStatusCode === StatusCodes.Good) {
230
+ // OPC UA Spec: Update LastUpdateTime after successful trust list update
231
+ updateLastUpdateTime(trustList);
232
+ }
233
+ }
234
+ } catch (err) {
235
+ errorLog("Error in _closeAndUpdate:", err);
236
+ processStatusCode = StatusCodes.BadInternalError;
237
+ }
238
+
239
+ // Close the underlying file to decrement openCount
240
+ // OPC UA spec: "This Method closes the file and applies the changes."
241
+ if (_close_method) {
242
+ return new Promise<CallMethodResultOptions>((resolve, reject) => {
243
+ _close_method.call(this, inputArguments, context, (err, result) => {
244
+ if (err) {
245
+ reject(err);
246
+ } else {
247
+ // Override the output argument to match CloseAndUpdate signature
248
+ resolve({
249
+ statusCode:
250
+ processStatusCode === StatusCodes.Good ? result?.statusCode || StatusCodes.Good : processStatusCode,
251
+ outputArguments: [new Variant({ dataType: DataType.Boolean, value: false })]
252
+ });
253
+ }
254
+ });
255
+ });
256
+ }
257
+
258
+ return {
259
+ statusCode: processStatusCode,
260
+ outputArguments: [new Variant({ dataType: DataType.Boolean, value: false })]
261
+ };
47
262
  }
48
263
 
49
264
  // in TrustList
@@ -52,10 +267,6 @@ async function _addCertificate(
52
267
  inputArguments: Variant[],
53
268
  context: ISessionContext
54
269
  ): Promise<CallMethodResultOptions> {
55
- // If the Certificate is issued by a CA then the Client shall provide the entire
56
- // chain in the certificate argument (see OPC 10000-6). After validating the Certificate,
57
- // the Server shall add the CA Certificates to the Issuers list in the Trust List.
58
- // The leaf Certificate is added to the list specified by the isTrustedCertificate argument.
59
270
  if (!hasEncryptedChannel(context)) {
60
271
  return { statusCode: StatusCodes.BadSecurityModeInsufficient };
61
272
  }
@@ -63,40 +274,46 @@ async function _addCertificate(
63
274
  return { statusCode: StatusCodes.BadUserAccessDenied };
64
275
  }
65
276
 
66
- const trustList = context.object as UATrustList;
67
- const cm = ((trustList as any).$$certificateManager as CertificateManager) || null;
277
+ const trustList = context.object as UATrustListEx;
278
+ const cm = trustList.$$certificateManager;
68
279
 
69
- // The trust list must have been bound
70
280
  if (!cm) {
71
281
  return { statusCode: StatusCodes.BadInternalError };
72
282
  }
73
- // This method cannot be called if the file object is open.
74
- if (trustListIsAlreadyOpened(trustList)) {
283
+ if (trustListIsAlreadyOpened(trustList) || trustList.$$openedForWrite) {
75
284
  return { statusCode: StatusCodes.BadInvalidState };
76
285
  }
77
286
 
78
- const certificateChain: Buffer = inputArguments[0].value as Buffer;
287
+ const certificateBuffer: Buffer = inputArguments[0].value as Buffer;
79
288
  const isTrustedCertificate: boolean = inputArguments[1].value as boolean;
80
289
 
81
- const certificates = split_der(certificateChain);
82
-
83
- // validate certificate first
84
- const r = await verifyCertificateChain(certificates);
85
- if (r.status !== "Good") {
86
- warningLog("Invalid certificate ", r.status, r.reason);
290
+ // OPC UA Spec: "If FALSE Bad_CertificateInvalid is returned."
291
+ if (!isTrustedCertificate) {
87
292
  return { statusCode: StatusCodes.BadCertificateInvalid };
88
293
  }
89
294
 
90
- for (let i = 0; i < certificates.length; i++) {
91
- const certificate = certificates[i];
92
- if (i === certificates.length - 1 && isTrustedCertificate) {
93
- await cm.trustCertificate(certificate);
94
- } else {
95
- await cm.addIssuer(certificate);
295
+ try {
296
+ const certificates = split_der(certificateBuffer);
297
+ if (certificates.length > 1) {
298
+ warningLog("AddCertificate received a certificate chain. Only the leaf certificate will be added.");
299
+ warningLog("Issuer certificates must be added using the Write/CloseAndUpdate methods.");
300
+ }
301
+
302
+ const status = await cm.addTrustedCertificateFromChain(certificateBuffer);
303
+
304
+ if (status !== VerificationStatus.Good) {
305
+ warningLog("Certificate validation failed:", status);
306
+ return { statusCode: StatusCodes.BadCertificateInvalid };
96
307
  }
308
+
309
+ updateLastUpdateTime(trustList);
310
+
311
+ doDebug && debugLog("_addCertificate - done, leaf certificate has been added to trustedCertificates");
312
+ return { statusCode: StatusCodes.Good };
313
+ } catch (err) {
314
+ errorLog("Error in _addCertificate:", err);
315
+ return { statusCode: StatusCodes.BadCertificateInvalid };
97
316
  }
98
- debugLog("_addCertificate - done isTrustedCertificate= ", isTrustedCertificate);
99
- return { statusCode: StatusCodes.Good };
100
317
  }
101
318
  async function _removeCertificate(
102
319
  this: UAMethod,
@@ -111,7 +328,58 @@ async function _removeCertificate(
111
328
  return { statusCode: StatusCodes.BadUserAccessDenied };
112
329
  }
113
330
 
114
- return { statusCode: StatusCodes.Good };
331
+ const trustList = context.object as UATrustListEx;
332
+ const cm = trustList.$$certificateManager;
333
+
334
+ if (!cm) {
335
+ return { statusCode: StatusCodes.BadInternalError };
336
+ }
337
+
338
+ if (trustListIsAlreadyOpened(trustList) || trustList.$$openedForWrite) {
339
+ return { statusCode: StatusCodes.BadInvalidState };
340
+ }
341
+
342
+ const thumbprint: string = inputArguments[0].value as string;
343
+ const isTrustedCertificate: boolean = inputArguments[1].value as boolean;
344
+
345
+ try {
346
+ // Normalize thumbprint - remove "NodeOPCUA[" prefix if present
347
+ const normalizedThumbprint = thumbprint.replace(/^NodeOPCUA\[|\]$/g, "").toLowerCase();
348
+
349
+ if (isTrustedCertificate) {
350
+ // Remove from trusted store
351
+ const removed = await cm.removeTrustedCertificate(normalizedThumbprint);
352
+ if (!removed) {
353
+ return { statusCode: StatusCodes.BadInvalidArgument };
354
+ }
355
+ } else {
356
+ // Removing an issuer certificate — first check if it's still
357
+ // needed by any trusted certificate
358
+ const issuerCert = await cm.removeIssuer(normalizedThumbprint);
359
+ if (!issuerCert) {
360
+ return { statusCode: StatusCodes.BadInvalidArgument };
361
+ }
362
+
363
+ // Check dependency: is any trusted cert signed by this issuer?
364
+ if (await cm.isIssuerInUseByTrustedCertificate(issuerCert)) {
365
+ // Re-add the issuer since we can't remove it
366
+ await cm.addIssuer(issuerCert);
367
+ warningLog("Certificate is needed for chain validation");
368
+ return { statusCode: StatusCodes.BadCertificateChainIncomplete };
369
+ }
370
+
371
+ // Also remove CRLs issued by this CA
372
+ await cm.removeRevocationListsForIssuer(issuerCert, "issuers");
373
+ }
374
+
375
+ updateLastUpdateTime(trustList);
376
+
377
+ doDebug && debugLog("_removeCertificate - done, isTrustedCertificate=", isTrustedCertificate);
378
+ return { statusCode: StatusCodes.Good };
379
+ } catch (err) {
380
+ errorLog("Error in _removeCertificate:", err);
381
+ return { statusCode: StatusCodes.BadInternalError };
382
+ }
115
383
  }
116
384
 
117
385
  let counter = 0;
@@ -120,49 +388,62 @@ export async function promoteTrustList(trustList: UATrustList) {
120
388
  const filename = `/tmpFile${counter}`;
121
389
  counter += 1;
122
390
 
391
+ const trustListEx = trustList as UATrustListEx;
392
+ // Store filename for use in _closeAndUpdate
393
+ trustListEx.$$filename = filename;
394
+ // Initialize write lock flag
395
+ trustListEx.$$openedForWrite = false;
396
+
123
397
  installFileType(trustList, { filename, fileSystem: MemFs as AbstractFs });
124
398
 
125
399
  // we need to change the default open method
126
- const open = trustList.getChildByName("Open") as UAMethod;
127
- const _open_asyncExecutionFunction = (open as any)._asyncExecutionFunction as MethodFunctor;
400
+ const open = trustList.getChildByName("Open") as UAMethodEx;
401
+ const _open_asyncExecutionFunction = open._asyncExecutionFunction as MethodFunctor;
128
402
 
129
403
  // ... and bind the extended methods as well.
130
- const closeAndUpdate = trustList.getChildByName("CloseAndUpdate") as UAMethod;
131
- const openWithMasks = trustList.getChildByName("OpenWithMasks") as UAMethod;
132
- const addCertificate = trustList.getChildByName("AddCertificate") as UAMethod;
133
- const removeCertificate = trustList.getChildByName("RemoveCertificate") as UAMethod;
404
+ const close = trustList.getChildByName("Close") as UAMethodEx;
405
+ const _close_asyncExecutionFunction = close._asyncExecutionFunction as MethodFunctor;
406
+
407
+ const closeAndUpdate = trustList.getChildByName("CloseAndUpdate") as UAMethodEx;
408
+ const openWithMasks = trustList.getChildByName("OpenWithMasks") as UAMethodEx;
409
+ const addCertificate = trustList.getChildByName("AddCertificate") as UAMethodEx;
410
+ const removeCertificate = trustList.getChildByName("RemoveCertificate") as UAMethodEx;
134
411
 
135
412
  function _openTrustList(
136
- this: any,
413
+ this: UAMethod,
137
414
  trustMask: TrustListMasks,
138
415
  inputArgs: Variant[],
139
416
  context: ISessionContext,
140
417
  callback: CallbackT<CallMethodResultOptions>
141
418
  ) {
142
- if (trustListIsAlreadyOpened(trustList)) {
143
- return callback(null, { statusCode: StatusCodes.BadInvalidState });
144
- }
145
- // if (trustList.isOpened) {
146
- // warningLog("TrustList is already opened")
147
- // return { statusCode: StatusCodes.BadInvalidState};
148
- // }
149
-
150
419
  // The Open Method shall not support modes other than Read (0x01) and the Write + EraseExisting (0x06).
151
420
  const openMask = inputArgs[0].value as number;
152
421
  if (openMask !== OpenFileMode.Read && openMask !== OpenFileMode.WriteEraseExisting) {
153
- return callback(null, { statusCode: StatusCodes.BadInvalidArgument });
422
+ // OPC UA Spec: "If other modes are requested the return code is Bad_NotSupported."
423
+ return callback(null, { statusCode: StatusCodes.BadNotSupported });
424
+ }
425
+
426
+ // OPC UA Spec: BadInvalidState - The Open Method was called with write access
427
+ // and the CloseAndUpdate Method has not been called.
428
+ // If already opened for write, no subsequent opens (read or write) are allowed.
429
+ const isOpenedForWrite = trustListEx.$$openedForWrite;
430
+ if (isOpenedForWrite) {
431
+ return callback(null, { statusCode: StatusCodes.BadInvalidState });
154
432
  }
155
433
  // possible statusCode: Bad_UserAccessDenied The current user does not have the rights required.
156
- const certificateManager = ((trustList as any).$$certificateManager as OPCUACertificateManager) || undefined;
434
+ const certificateManager = trustListEx.$$certificateManager;
157
435
  if (certificateManager) {
158
436
  writeTrustList(MemFs as AbstractFs, filename, trustMask, certificateManager)
159
437
  .then(() => {
160
- // trustList.isOpened = true;
438
+ // Track if opened for write to enforce BadInvalidState on subsequent opens
439
+ if (openMask === OpenFileMode.WriteEraseExisting) {
440
+ trustListEx.$$openedForWrite = true;
441
+ }
161
442
 
162
443
  _open_asyncExecutionFunction.call(this, inputArgs, context, callback);
163
444
  })
164
445
  .catch((err) => {
165
- errorLog(err);
446
+ errorLog((err as Error).message);
166
447
  callback(err, { statusCode: StatusCodes.BadInternalError });
167
448
  });
168
449
  } else {
@@ -172,7 +453,7 @@ export async function promoteTrustList(trustList: UATrustList) {
172
453
  }
173
454
 
174
455
  function _openCallback(
175
- this: any,
456
+ this: UAMethod,
176
457
  inputArgs: Variant[],
177
458
  context: ISessionContext,
178
459
  callback: CallbackT<CallMethodResultOptions>
@@ -183,7 +464,7 @@ export async function promoteTrustList(trustList: UATrustList) {
183
464
  open.bindMethod(_openCallback);
184
465
 
185
466
  function _openWithMaskCallback(
186
- this: any,
467
+ this: UAMethod,
187
468
  inputArgs: Variant[],
188
469
  context: ISessionContext,
189
470
  callback: CallbackT<CallMethodResultOptions>
@@ -197,18 +478,26 @@ export async function promoteTrustList(trustList: UATrustList) {
197
478
  openWithMasks.bindMethod(_openWithMaskCallback);
198
479
  addCertificate.bindMethod(_addCertificate);
199
480
  removeCertificate.bindMethod(_removeCertificate);
200
- closeAndUpdate?.bindMethod(_closeAndUpdate);
481
+
482
+ // Wrapper to pass the underlying close method to _closeAndUpdate
483
+ closeAndUpdate?.bindMethod(async function (
484
+ this: UAMethod,
485
+ inputArguments: Variant[],
486
+ context: ISessionContext
487
+ ): Promise<CallMethodResultOptions> {
488
+ return _closeAndUpdate.call(this, inputArguments, context, _close_asyncExecutionFunction);
489
+ });
201
490
 
202
491
  function install_method_handle_on_TrustListType(addressSpace: IAddressSpace): void {
203
- const fileType = addressSpace.findObjectType("TrustListType") as any;
492
+ const fileType = addressSpace.findObjectType("TrustListType") as (UAObjectType & UATrustList_Base) | null;
204
493
  if (!fileType || fileType.addCertificate.isBound()) {
205
494
  return;
206
495
  }
207
- fileType.open && fileType.open.bindMethod(_openCallback);
496
+ fileType.open?.bindMethod(_openCallback);
208
497
  fileType.addCertificate.bindMethod(_addCertificate);
209
498
  fileType.removeCertificate.bindMethod(_removeCertificate);
210
- fileType.openWithMasks && fileType.openWithMasks.bindMethod(_openWithMaskCallback);
211
- fileType.closeAndUpdate && fileType.closeAndUpdate.bindMethod(_closeAndUpdate);
499
+ fileType.openWithMasks?.bindMethod(_openWithMaskCallback);
500
+ fileType.closeAndUpdate?.bindMethod(_closeAndUpdate);
212
501
  }
213
502
  install_method_handle_on_TrustListType(trustList.addressSpace);
214
503
  }
@@ -0,0 +1,63 @@
1
+ import { make_errorLog, make_warningLog } from "node-opcua-debug";
2
+ import { type StatusCode, StatusCodes } from "node-opcua-status-code";
3
+ import type { PushCertificateManagerInternalContext } from "./internal_context.js";
4
+
5
+ const errorLog = make_errorLog("ServerConfiguration");
6
+ const warningLog = make_warningLog("ServerConfiguration");
7
+
8
+ // Helper: Flush action queue
9
+ async function flushActionQueue(serverImpl: PushCertificateManagerInternalContext): Promise<void> {
10
+ while (serverImpl.actionQueue.length) {
11
+ const first = serverImpl.actionQueue.pop();
12
+ await first?.();
13
+ }
14
+ }
15
+
16
+ export async function executeApplyChanges(serverImpl: PushCertificateManagerInternalContext): Promise<StatusCode> {
17
+ // ApplyChanges is used to tell the Server to apply any security changes.
18
+ // This Method should only be called if a previous call to a Method that changed the
19
+ // configuration returns applyChangesRequired=true.
20
+
21
+ if (serverImpl.operationInProgress) {
22
+ return StatusCodes.BadTooManyOperations;
23
+ }
24
+
25
+ // Check if there are any pending tasks
26
+ if (serverImpl.fileTransactionManager.pendingTasksCount === 0 && serverImpl.actionQueue.length === 0) {
27
+ // If ApplyChanges is called and there is no active transaction then return Bad_NothingToDo
28
+ return StatusCodes.BadNothingToDo;
29
+ }
30
+
31
+ serverImpl.operationInProgress = true;
32
+ try {
33
+ try {
34
+ serverImpl.emit("CertificateAboutToChange", serverImpl.actionQueue);
35
+ } catch (err) {
36
+ errorLog("Event listener error:", (err as Error).message);
37
+ }
38
+ await flushActionQueue(serverImpl);
39
+
40
+ try {
41
+ await serverImpl.fileTransactionManager.applyFileOps();
42
+ } catch (err) {
43
+ await serverImpl.fileTransactionManager.abortTransaction();
44
+ warningLog("err ", (err as Error).message);
45
+ return StatusCodes.BadInternalError;
46
+ }
47
+ try {
48
+ serverImpl.emit("CertificateChanged", serverImpl.actionQueue);
49
+ } catch (err) {
50
+ errorLog("Event listener error:", (err as Error).message);
51
+ }
52
+ await flushActionQueue(serverImpl);
53
+
54
+ // Dispose and clear temporary certificate manager after applying changes
55
+ if (serverImpl.tmpCertificateManager) {
56
+ await serverImpl.tmpCertificateManager.dispose();
57
+ }
58
+ serverImpl.tmpCertificateManager = undefined;
59
+ return StatusCodes.Good;
60
+ } finally {
61
+ serverImpl.operationInProgress = false;
62
+ }
63
+ }