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