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
@@ -1,27 +1,26 @@
1
1
  /**
2
2
  * @module node-opcua-server-configuration.client
3
3
  */
4
- import { ByteString } from "node-opcua-basic-types";
5
- import { NodeId, resolveNodeId } from "node-opcua-nodeid";
6
- import { CallMethodRequestLike, IBasicSessionAsync } from "node-opcua-pseudo-session";
7
- import { StatusCode, StatusCodes } from "node-opcua-status-code";
8
- import { DataType, VariantArrayType, VariantLike } from "node-opcua-variant";
4
+ import type { ByteString } from "node-opcua-basic-types";
5
+ import { BinaryStream } from "node-opcua-binary-stream";
6
+ import type { Certificate } from "node-opcua-crypto/web";
7
+ import { AttributeIds, coerceQualifiedName, type QualifiedNameLike } from "node-opcua-data-model";
9
8
  import { ClientFile, OpenFileMode } from "node-opcua-file-transfer";
10
- import { AttributeIds, QualifiedNameLike, coerceQualifiedName } from "node-opcua-data-model";
9
+ import { NodeId, resolveNodeId } from "node-opcua-nodeid";
10
+ import type { CallMethodRequestLike, IBasicSessionAsync } from "node-opcua-pseudo-session";
11
11
  import { makeBrowsePath } from "node-opcua-service-translate-browse-path";
12
- import { Certificate } from "node-opcua-crypto/web";
12
+ import { type StatusCode, StatusCodes } from "node-opcua-status-code";
13
13
  import { TrustListDataType } from "node-opcua-types";
14
- import { BinaryStream } from "node-opcua-binary-stream";
14
+ import { DataType, VariantArrayType, type VariantLike } from "node-opcua-variant";
15
15
 
16
- import {
16
+ import type {
17
17
  CreateSigningRequestResult,
18
18
  GetRejectedListResult,
19
19
  PushCertificateManager,
20
20
  UpdateCertificateResult
21
- } from "../push_certificate_manager";
22
-
23
- import { ITrustList } from "../trust_list";
24
- import { TrustListMasks } from "../server/trust_list_server";
21
+ } from "../push_certificate_manager.js";
22
+ import type { TrustListMasks } from "../server/trust_list_server.js";
23
+ import type { ITrustList } from "../trust_list.js";
25
24
 
26
25
  const serverConfigurationNodeId = resolveNodeId("ServerConfiguration");
27
26
  const createSigningRequestMethod = resolveNodeId("ServerConfiguration_CreateSigningRequest");
@@ -34,7 +33,6 @@ const defaultApplicationGroup = resolveNodeId("ServerConfiguration_CertificateGr
34
33
  const defaultHttpsGroup = resolveNodeId("ServerConfiguration_CertificateGroups_DefaultHttpsGroup");
35
34
  const defaultUserTokenGroup = resolveNodeId("ServerConfiguration_CertificateGroups_DefaultUserTokenGroup");
36
35
 
37
-
38
36
  function findCertificateGroupNodeId(certificateGroup: NodeId | string): NodeId {
39
37
  if (certificateGroup instanceof NodeId) {
40
38
  return certificateGroup;
@@ -64,7 +62,10 @@ export class TrustListClient extends ClientFile implements ITrustList {
64
62
  private removeCertificateNodeId?: NodeId;
65
63
  private openWithMasksNodeId?: NodeId;
66
64
 
67
- constructor(session: IBasicSessionAsync, public nodeId: NodeId) {
65
+ constructor(
66
+ session: IBasicSessionAsync,
67
+ public nodeId: NodeId
68
+ ) {
68
69
  super(session, nodeId);
69
70
  }
70
71
  /**
@@ -78,12 +79,12 @@ export class TrustListClient extends ClientFile implements ITrustList {
78
79
  makeBrowsePath(this.nodeId, "/OpenWithMasks") // OpenWithMasks Mandatory
79
80
  ]);
80
81
 
81
- this.closeAndUpdateNodeId = browseResults[0].targets![0].targetId;
82
- this.addCertificateNodeId = browseResults[1].targets![0].targetId;
83
- this.removeCertificateNodeId = browseResults[2].targets![0].targetId;
84
- this.openWithMasksNodeId = browseResults[3].targets![0].targetId;
82
+ this.closeAndUpdateNodeId = browseResults[0].targets?.[0].targetId;
83
+ this.addCertificateNodeId = browseResults[1].targets?.[0].targetId;
84
+ this.removeCertificateNodeId = browseResults[2].targets?.[0].targetId;
85
+ this.openWithMasksNodeId = browseResults[3].targets?.[0].targetId;
85
86
 
86
- // istanbul ignore next
87
+ // c8 ignore next
87
88
  if (!this.openWithMasksNodeId || this.openWithMasksNodeId.isEmpty()) {
88
89
  throw new Error("Cannot find mandatory method OpenWithMask on object");
89
90
  }
@@ -95,12 +96,12 @@ export class TrustListClient extends ClientFile implements ITrustList {
95
96
  }
96
97
 
97
98
  async openWithMasks(trustListMask: TrustListMasks): Promise<number> {
98
- // istanbul ignore next
99
+ // c8 ignore next
99
100
  if (this.fileHandle) {
100
101
  throw new Error("File has already be opened");
101
102
  }
102
103
  await this.ensureInitialized();
103
- // istanbul ignore next
104
+ // c8 ignore next
104
105
  if (!this.openWithMasksNodeId) {
105
106
  throw new Error("OpenWithMasks doesn't exist");
106
107
  }
@@ -114,11 +115,11 @@ export class TrustListClient extends ClientFile implements ITrustList {
114
115
  if (callMethodResult.statusCode.isNotGood()) {
115
116
  throw new Error(callMethodResult.statusCode.name);
116
117
  }
117
- this.fileHandle = callMethodResult.outputArguments![0].value as number;
118
+ this.fileHandle = callMethodResult.outputArguments?.[0].value as number;
118
119
  return this.fileHandle;
119
120
  }
120
121
 
121
- async closeAndUpdate(applyChangesRequired: boolean): Promise<boolean> {
122
+ async closeAndUpdate(): Promise<boolean> {
122
123
  if (!this.fileHandle) {
123
124
  throw new Error("File has node been opened yet");
124
125
  }
@@ -126,10 +127,7 @@ export class TrustListClient extends ClientFile implements ITrustList {
126
127
  if (!this.closeAndUpdateNodeId) {
127
128
  throw new Error("CloseAndUpdateMethod doesn't exist");
128
129
  }
129
- const inputArguments = [
130
- { dataType: DataType.UInt32, value: this.fileHandle },
131
- { dataType: DataType.Boolean, value: !!applyChangesRequired }
132
- ];
130
+ const inputArguments = [{ dataType: DataType.UInt32, value: this.fileHandle }];
133
131
  const methodToCall: CallMethodRequestLike = {
134
132
  inputArguments,
135
133
  methodId: this.closeAndUpdateNodeId,
@@ -139,7 +137,8 @@ export class TrustListClient extends ClientFile implements ITrustList {
139
137
  if (callMethodResult.statusCode.isNotGood()) {
140
138
  throw new Error(callMethodResult.statusCode.name);
141
139
  }
142
- return callMethodResult.outputArguments![0].value as boolean;
140
+ this.fileHandle = 0;
141
+ return callMethodResult.outputArguments?.[0].value as boolean;
143
142
  }
144
143
 
145
144
  async addCertificate(certificate: Certificate, isTrustedCertificate: boolean): Promise<StatusCode> {
@@ -200,21 +199,25 @@ export class TrustListClient extends ClientFile implements ITrustList {
200
199
  }
201
200
 
202
201
  async writeTrustedCertificateList(trustedList: TrustListDataType): Promise<boolean> {
203
- await this.open(OpenFileMode.Write);
202
+ await this.open(OpenFileMode.WriteEraseExisting);
204
203
  const s = trustedList.binaryStoreSize();
205
204
  const stream = new BinaryStream(s);
206
205
  trustedList.encode(stream);
207
- return await this.closeAndUpdate(true);
206
+ await this.write(stream.buffer);
207
+ return await this.closeAndUpdate();
208
208
  }
209
209
  }
210
210
  export class CertificateGroup {
211
- constructor(public session: IBasicSessionAsync, public nodeId: NodeId) {}
211
+ constructor(
212
+ public session: IBasicSessionAsync,
213
+ public nodeId: NodeId
214
+ ) {}
212
215
  async getCertificateTypes(): Promise<NodeId[]> {
213
216
  const browsePathResult = await this.session.translateBrowsePath(makeBrowsePath(this.nodeId, "/CertificateTypes"));
214
217
  if (browsePathResult.statusCode.isNotGood()) {
215
218
  throw new Error(browsePathResult.statusCode.name);
216
219
  }
217
- const certificateTypesNodeId = browsePathResult.targets![0].targetId;
220
+ const certificateTypesNodeId = browsePathResult.targets?.[0].targetId;
218
221
  const dataValue = await this.session.read({ nodeId: certificateTypesNodeId, attributeId: AttributeIds.Value });
219
222
  if (dataValue.statusCode.isNotGood()) {
220
223
  throw new Error(browsePathResult.statusCode.name);
@@ -226,7 +229,10 @@ export class CertificateGroup {
226
229
  if (browsePathResult.statusCode.isNotGood()) {
227
230
  throw new Error(browsePathResult.statusCode.name);
228
231
  }
229
- const trustListNodeId = browsePathResult.targets![0].targetId;
232
+ const trustListNodeId = browsePathResult.targets?.[0].targetId;
233
+ if (!trustListNodeId) {
234
+ throw new Error("TrustList node not found");
235
+ }
230
236
  return new TrustListClient(this.session, trustListNodeId);
231
237
  }
232
238
  }
@@ -302,7 +308,7 @@ export class ClientPushCertificateManagement implements PushCertificateManager {
302
308
 
303
309
  if (callMethodResult.statusCode.isGood()) {
304
310
  return {
305
- certificateSigningRequest: callMethodResult.outputArguments![0].value,
311
+ certificateSigningRequest: callMethodResult.outputArguments?.[0].value,
306
312
  statusCode: callMethodResult.statusCode
307
313
  };
308
314
  } else {
@@ -330,11 +336,11 @@ export class ClientPushCertificateManagement implements PushCertificateManager {
330
336
  };
331
337
  const callMethodResult = await this.session.call(methodToCall);
332
338
  if (callMethodResult.statusCode.isGood()) {
333
- if (callMethodResult.outputArguments![0].dataType !== DataType.ByteString) {
339
+ if (callMethodResult.outputArguments?.[0].dataType !== DataType.ByteString) {
334
340
  return { statusCode: StatusCodes.BadInvalidArgument };
335
341
  }
336
342
  return {
337
- certificates: callMethodResult.outputArguments![0].value,
343
+ certificates: callMethodResult.outputArguments?.[0].value,
338
344
  statusCode: callMethodResult.statusCode
339
345
  };
340
346
  } else {
@@ -422,22 +428,21 @@ export class ClientPushCertificateManagement implements PushCertificateManager {
422
428
  };
423
429
  const callMethodResult = await this.session.call(methodToCall);
424
430
  if (callMethodResult.statusCode.isGood()) {
425
- if (!callMethodResult.outputArguments || callMethodResult.outputArguments!.length !== 1) {
431
+ if (!callMethodResult.outputArguments || callMethodResult.outputArguments.length !== 1) {
426
432
  return {
427
433
  statusCode: StatusCodes.BadInternalError,
428
- applyChangesRequired: false,
434
+ applyChangesRequired: false
429
435
  };
430
436
  // throw Error("Internal Error, expecting 1 output result");
431
437
  }
432
438
  return {
433
- applyChangesRequired: !!callMethodResult.outputArguments![0].value,
439
+ applyChangesRequired: !!callMethodResult.outputArguments?.[0].value,
434
440
  statusCode: callMethodResult.statusCode
435
441
  };
436
442
  } else {
437
- return {
443
+ return {
438
444
  statusCode: callMethodResult.statusCode,
439
445
  applyChangesRequired: false
440
-
441
446
  };
442
447
  }
443
448
  }
@@ -471,7 +476,7 @@ export class ClientPushCertificateManagement implements PushCertificateManager {
471
476
  };
472
477
  const callMethodResult = await this.session.call(methodToCall);
473
478
 
474
- if (callMethodResult.outputArguments && callMethodResult.outputArguments.length) {
479
+ if (callMethodResult.outputArguments?.length) {
475
480
  throw new Error("Invalid output arguments");
476
481
  }
477
482
  return callMethodResult.statusCode;
@@ -507,7 +512,7 @@ export class ClientPushCertificateManagement implements PushCertificateManager {
507
512
  if (browseName.toString() === "DefaultUserTokenGroup") {
508
513
  return new CertificateGroup(this.session, defaultUserTokenGroup);
509
514
  }
510
- // istanbul ignore next
515
+ // c8 ignore next
511
516
  throw new Error("Not Implemented yet");
512
517
  }
513
518
  public async getApplicationGroup(): Promise<CertificateGroup> {
package/source/index.ts CHANGED
@@ -2,11 +2,13 @@
2
2
  * @module node-opcua-server-configuration
3
3
  */
4
4
  // export * from "./trust_list_impl";
5
- export * from "./push_certificate_manager";
6
- export * from "./clientTools/push_certificate_management_client";
7
- export * from "./standard_certificate_types";
8
5
 
9
- export * from "./server/install_push_certitifate_management";
10
- export * from "./server/push_certificate_manager_server_impl";
11
- export * from "./server/push_certificate_manager_helpers";
12
- export * from "./server/promote_trust_list";
6
+ export * from "./clientTools/certificate_types.js";
7
+ export * from "./clientTools/push_certificate_management_client.js";
8
+ export * from "./push_certificate_manager.js";
9
+ export * from "./server/install_push_certitifate_management.js";
10
+ export * from "./server/promote_trust_list.js";
11
+ export * from "./server/push_certificate_manager/subject_to_string.js";
12
+ export * from "./server/push_certificate_manager_helpers.js";
13
+ export * from "./server/push_certificate_manager_server_impl.js";
14
+ export * from "./standard_certificate_types.js";
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * @module node-opcua-server-configuration
3
3
  */
4
- import { ByteString, UAString } from "node-opcua-basic-types";
5
- import { NodeId } from "node-opcua-nodeid";
6
- import { StatusCode } from "node-opcua-status-code";
4
+ import type { ByteString, UAString } from "node-opcua-basic-types";
5
+ import type { NodeId } from "node-opcua-nodeid";
6
+ import type { StatusCode } from "node-opcua-status-code";
7
7
 
8
8
  export interface CreateSigningRequestResult {
9
9
  statusCode: StatusCode;
@@ -21,7 +21,6 @@ export interface UpdateCertificateResult {
21
21
  }
22
22
 
23
23
  export interface PushCertificateManager {
24
-
25
24
  /**
26
25
  * The SupportedPrivateKeyFormats specifies the private key formats supported by the Server.
27
26
  * Possible values include “PEM” (see RFC 5958) or “PFX” (see PKCS #12). The array is empty
@@ -65,12 +64,12 @@ export interface PushCertificateManager {
65
64
  *
66
65
  */
67
66
  updateCertificate(
68
- certificateGroupId: NodeId | string,
69
- certificateTypeId: NodeId | string,
70
- certificate: ByteString,
71
- issuerCertificates: ByteString[],
72
- privateKeyFormat: UAString,
73
- privateKey: ByteString
67
+ certificateGroupId: NodeId | string,
68
+ certificateTypeId: NodeId | string,
69
+ certificate: ByteString,
70
+ issuerCertificates: ByteString[],
71
+ privateKeyFormat?: UAString,
72
+ privateKey?: ByteString
74
73
  ): Promise<UpdateCertificateResult>;
75
74
 
76
75
  /**
@@ -136,11 +135,11 @@ export interface PushCertificateManager {
136
135
  * Bad_UserAccessDenied The current user does not have the rights required.
137
136
  */
138
137
  createSigningRequest(
139
- certificateGroupId: NodeId | string,
140
- certificateTypeId: NodeId | string,
141
- subjectName: string | null,
142
- regeneratePrivateKey?: boolean,
143
- nonce?: ByteString
138
+ certificateGroupId: NodeId | string,
139
+ certificateTypeId: NodeId | string,
140
+ subjectName: string | null,
141
+ regeneratePrivateKey?: boolean,
142
+ nonce?: ByteString
144
143
  ): Promise<CreateSigningRequestResult>;
145
144
 
146
145
  /**
@@ -159,6 +158,5 @@ export interface PushCertificateManager {
159
158
  * Bad_UserAccessDenied The current user does not have the rights required
160
159
  */
161
160
 
162
- getRejectedList(): Promise</*certificates*/GetRejectedListResult>;
163
-
161
+ getRejectedList(): Promise</*certificates*/ GetRejectedListResult>;
164
162
  }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * @module node-opcua-server-configuration-server
3
+ */
4
+ import type { ByteString } from "node-opcua-basic-types";
5
+ import type { OPCUACertificateManager } from "node-opcua-certificate-manager";
6
+ import { type CertificateInternals, exploreCertificate, verifyCertificateChain } from "node-opcua-crypto/web";
7
+ import { make_errorLog, make_warningLog } from "node-opcua-debug";
8
+ import { type StatusCode, StatusCodes } from "node-opcua-status-code";
9
+
10
+ const warningLog = make_warningLog("ServerConfiguration");
11
+ const errorLog = make_errorLog("ServerConfiguration");
12
+
13
+ /**
14
+ * Validates the certificate and its issuer chain, including parsing, date validity,
15
+ * chain verification, and trust verification.
16
+ * @returns Object with statusCode. If Good, also contains the concatenated certificateChain.
17
+ */
18
+ export async function validateCertificateAndChain(
19
+ certificateManager: OPCUACertificateManager,
20
+ isApplicationGroup: boolean,
21
+ certificate: Buffer,
22
+ issuerCertificates: ByteString[] | null | undefined
23
+ ): Promise<{ statusCode: StatusCode; certificateChain?: Buffer }> {
24
+ let certInfo: CertificateInternals;
25
+ try {
26
+ certInfo = exploreCertificate(certificate);
27
+ } catch (err) {
28
+ errorLog("Cannot parse certificate:", (err as Error).message);
29
+ return { statusCode: StatusCodes.BadCertificateInvalid };
30
+ }
31
+
32
+ const issuerCertBuffers = (issuerCertificates || []).filter((cert): cert is Buffer => {
33
+ return Buffer.isBuffer(cert) && cert.length > 0;
34
+ });
35
+
36
+ if ((issuerCertificates || []).length !== issuerCertBuffers.length) {
37
+ warningLog("issuerCertificates contains invalid entries");
38
+ return { statusCode: StatusCodes.BadCertificateInvalid };
39
+ }
40
+
41
+ for (const issuerCert of issuerCertBuffers) {
42
+ try {
43
+ const issuerInfo = exploreCertificate(issuerCert);
44
+ const nowIssuer = new Date();
45
+ if (issuerInfo.tbsCertificate.validity.notBefore.getTime() > nowIssuer.getTime()) {
46
+ warningLog("Issuer certificate is not yet valid");
47
+ return { statusCode: StatusCodes.BadSecurityChecksFailed };
48
+ }
49
+ if (issuerInfo.tbsCertificate.validity.notAfter.getTime() < nowIssuer.getTime()) {
50
+ warningLog("Issuer certificate is out of date");
51
+ return { statusCode: StatusCodes.BadSecurityChecksFailed };
52
+ }
53
+ } catch (err) {
54
+ errorLog("Cannot parse issuer certificate:", (err as Error).message);
55
+ return { statusCode: StatusCodes.BadCertificateInvalid };
56
+ }
57
+ }
58
+
59
+ if (issuerCertBuffers.length > 0) {
60
+ const chainCheck = await verifyCertificateChain([certificate, ...issuerCertBuffers]);
61
+ if (chainCheck.status !== "Good") {
62
+ warningLog("Issuer chain validation failed:", chainCheck.status, chainCheck.reason);
63
+ return { statusCode: StatusCodes.BadSecurityChecksFailed };
64
+ }
65
+ }
66
+
67
+ const certificateChain = Buffer.concat([certificate, ...issuerCertBuffers]);
68
+
69
+ // Trust validation is only relevant for client certificates, not the server's own certificate
70
+ if (!isApplicationGroup) {
71
+ if (certificateManager.verifyCertificate) {
72
+ const status = await certificateManager.verifyCertificate(certificateChain, {
73
+ acceptCertificateWithValidIssuerChain: true
74
+ });
75
+ if (status !== "Good") {
76
+ warningLog("Certificate trust validation failed:", status);
77
+ return { statusCode: StatusCodes.BadSecurityChecksFailed };
78
+ }
79
+ }
80
+ }
81
+
82
+ const now = new Date();
83
+ if (certInfo.tbsCertificate.validity.notBefore.getTime() > now.getTime()) {
84
+ warningLog(
85
+ "Certificate is not yet valid : not before ",
86
+ certInfo.tbsCertificate.validity.notBefore.toISOString(),
87
+ "now = ",
88
+ now.toISOString()
89
+ );
90
+ return { statusCode: StatusCodes.BadSecurityChecksFailed };
91
+ }
92
+ if (certInfo.tbsCertificate.validity.notAfter.getTime() < now.getTime()) {
93
+ warningLog(
94
+ "Certificate is already out of date : not after ",
95
+ certInfo.tbsCertificate.validity.notAfter.toISOString(),
96
+ "now = ",
97
+ now.toISOString()
98
+ );
99
+ return { statusCode: StatusCodes.BadSecurityChecksFailed };
100
+ }
101
+
102
+ return { statusCode: StatusCodes.Good, certificateChain };
103
+ }
@@ -0,0 +1,253 @@
1
+ /**
2
+ * @module node-opcua-server-configuration-server
3
+ */
4
+
5
+ import crypto from "node:crypto";
6
+ import fs from "node:fs";
7
+ import os from "node:os";
8
+ import path from "node:path";
9
+ import { make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
10
+
11
+ const debugLog = make_debugLog("ServerConfiguration");
12
+ const errorLog = make_errorLog("ServerConfiguration");
13
+ const warningLog = make_warningLog("ServerConfiguration");
14
+
15
+ type Functor = () => Promise<void>;
16
+
17
+ async function _copyFile(source: string, dest: string): Promise<void> {
18
+ try {
19
+ debugLog("copying file \n source ", source, "\n =>\n dest ", dest);
20
+ const sourceExist = fs.existsSync(source);
21
+ if (sourceExist) {
22
+ await fs.promises.copyFile(source, dest);
23
+ }
24
+ } catch (err) {
25
+ errorLog(err);
26
+ }
27
+ }
28
+
29
+ async function _deleteFile(file: string): Promise<void> {
30
+ try {
31
+ const exists = fs.existsSync(file);
32
+ if (exists) {
33
+ debugLog("deleting file ", file);
34
+ await fs.promises.unlink(file);
35
+ }
36
+ } catch (err) {
37
+ errorLog(err);
38
+ }
39
+ }
40
+
41
+ async function _moveFile(source: string, dest: string): Promise<void> {
42
+ debugLog("moving file file \n source ", source, "\n =>\n dest ", dest);
43
+ try {
44
+ await _copyFile(source, dest);
45
+ await _deleteFile(source);
46
+ } catch (err) {
47
+ errorLog(err);
48
+ }
49
+ }
50
+
51
+ async function _moveFileWithBackup(source: string, dest: string, backupPath: string): Promise<void> {
52
+ // let make a copy of the destination file
53
+ debugLog("moveFileWithBackup file \n source ", source, "\n =>\n dest ", dest);
54
+ await _copyFile(dest, backupPath);
55
+ await _moveFile(source, dest);
56
+ }
57
+
58
+ export class FileTransactionManager {
59
+ readonly #pendingFileOps: Functor[] = [];
60
+ readonly #cleanupTasks: Functor[] = [];
61
+ readonly #backupFiles: Map<string, string> = new Map();
62
+ #tmpdir?: string;
63
+
64
+ /**
65
+ * Gets or initializes the underlying temporary directory for the transaction.
66
+ */
67
+ public async getTmpDir(): Promise<string> {
68
+ if (!this.#tmpdir) {
69
+ const tempBase = path.join(os.tmpdir(), "node-opcua-tx-");
70
+ this.#tmpdir = await fs.promises.mkdtemp(tempBase);
71
+ }
72
+ return this.#tmpdir;
73
+ }
74
+
75
+ /**
76
+ * Stages a file for writing during the transaction.
77
+ * Writes the content to a temporary location and registers
78
+ * a move operation to atomically place it at destinationPath upon applyFileOps().
79
+ */
80
+ public async stageFile(destinationPath: string, content: Buffer | string, encoding?: BufferEncoding): Promise<void> {
81
+ // ensure tmpdir exists
82
+ const tmpDir = await this.getTmpDir();
83
+
84
+ const uniqueFileName = `${crypto.randomBytes(16).toString("hex")}.tmp`;
85
+ const tempFilePath = path.join(tmpDir, uniqueFileName);
86
+
87
+ if (encoding) {
88
+ await fs.promises.writeFile(tempFilePath, content as string, encoding);
89
+ } else {
90
+ await fs.promises.writeFile(tempFilePath, content);
91
+ }
92
+
93
+ this.addFileOp(() => this.#moveFileWithBackupTracked(tempFilePath, destinationPath));
94
+ }
95
+
96
+ public addFileOp(functor: Functor): void {
97
+ this.#pendingFileOps.push(functor);
98
+ }
99
+
100
+ public addCleanupTask(functor: Functor): void {
101
+ this.#cleanupTasks.push(functor);
102
+ }
103
+
104
+ public get pendingTasksCount(): number {
105
+ return this.#pendingFileOps.length;
106
+ }
107
+
108
+ /**
109
+ * Abort the current transaction by clearing pending file operations
110
+ * and deleting the temporary staging folder.
111
+ */
112
+ public async abortTransaction(): Promise<void> {
113
+ this.#pendingFileOps.length = 0;
114
+ await this.#executeCleanupTasks();
115
+ await this.#cleanupTempFolder();
116
+ }
117
+
118
+ /**
119
+ * Move file with backup and track the backup for potential rollback.
120
+ * This method creates a backup of the destination file and tracks it
121
+ * so it can be restored if the transaction fails.
122
+ */
123
+ async #moveFileWithBackupTracked(source: string, dest: string): Promise<void> {
124
+ const tmpDir = await this.getTmpDir();
125
+ const uniqueFileName = `${crypto.randomBytes(16).toString("hex")}_backup.tmp`;
126
+ const backupPath = path.join(tmpDir, uniqueFileName);
127
+
128
+ // Track the backup before creating it
129
+ this.#backupFiles.set(dest, backupPath);
130
+
131
+ // Perform the actual move with backup
132
+ await _moveFileWithBackup(source, dest, backupPath);
133
+ }
134
+
135
+ /**
136
+ * Commit the transaction by executing all pending file operations.
137
+ */
138
+ public async applyFileOps(): Promise<void> {
139
+ debugLog("start applyFileOps");
140
+ const fileOperation = this.#pendingFileOps.splice(0);
141
+
142
+ try {
143
+ while (fileOperation.length) {
144
+ const op = fileOperation.shift();
145
+ await op?.();
146
+ }
147
+ debugLog("end applyFileOps");
148
+
149
+ // Transaction successful - clean up backup files
150
+ await this.#cleanupBackupFiles();
151
+ await this.#executeCleanupTasks();
152
+ await this.#cleanupTempFolder();
153
+ } catch (err) {
154
+ errorLog("Error during applyFileOps:", (err as Error).message);
155
+ errorLog("Rolling back transaction to restore previous certificate state");
156
+
157
+ // Rollback: restore all backup files to their original locations
158
+ try {
159
+ await this.#rollbackTransaction();
160
+ debugLog("Transaction rollback successful");
161
+ } catch (rollbackErr) {
162
+ errorLog("Critical: Rollback failed:", (rollbackErr as Error).message);
163
+ errorLog("Certificate state may be inconsistent - manual intervention required");
164
+ }
165
+
166
+ // Clear backup tracking after rollback
167
+ this.#backupFiles.clear();
168
+ await this.#executeCleanupTasks();
169
+ await this.#cleanupTempFolder();
170
+
171
+ throw err;
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Rollback the transaction by restoring all backup files.
177
+ * This restores files from their *_old backups to recover the previous state.
178
+ */
179
+ async #rollbackTransaction(): Promise<void> {
180
+ debugLog("Rolling back transaction, restoring", this.#backupFiles.size, "backup files");
181
+
182
+ const rollbackPromises: Promise<void>[] = [];
183
+
184
+ for (const [dest, backupPath] of this.#backupFiles.entries()) {
185
+ rollbackPromises.push(
186
+ (async () => {
187
+ try {
188
+ // Check if backup exists before trying to restore
189
+ if (fs.existsSync(backupPath)) {
190
+ debugLog("Restoring backup:", backupPath, "to", dest);
191
+ await _copyFile(backupPath, dest);
192
+ // Delete backup immediately after restoration
193
+ await _deleteFile(backupPath);
194
+ }
195
+ } catch (err) {
196
+ errorLog("Error restoring backup file", backupPath, "to", dest, ":", (err as Error).message);
197
+ }
198
+ })()
199
+ );
200
+ }
201
+
202
+ await Promise.all(rollbackPromises);
203
+ debugLog("Transaction rollback completed");
204
+ }
205
+
206
+ /**
207
+ * Clean up backup files after successful transaction.
208
+ * Removes all *_old backup files that were created during the transaction.
209
+ */
210
+ async #cleanupBackupFiles(): Promise<void> {
211
+ debugLog("Cleaning up", this.#backupFiles.size, "backup files");
212
+
213
+ const cleanupPromises: Promise<void>[] = [];
214
+
215
+ for (const backupPath of this.#backupFiles.values()) {
216
+ cleanupPromises.push(
217
+ _deleteFile(backupPath).catch((err) => {
218
+ warningLog("Failed to delete backup file", backupPath, ":", err);
219
+ })
220
+ );
221
+ }
222
+
223
+ await Promise.all(cleanupPromises);
224
+ this.#backupFiles.clear();
225
+ }
226
+
227
+ /**
228
+ * Clean up the temporary transaction folder.
229
+ */
230
+ async #cleanupTempFolder(): Promise<void> {
231
+ if (this.#tmpdir) {
232
+ try {
233
+ await fs.promises.rm(this.#tmpdir, { recursive: true, force: true });
234
+ } catch (err) {
235
+ warningLog("Failed to delete temporary transaction folder", this.#tmpdir, ":", err);
236
+ } finally {
237
+ this.#tmpdir = undefined;
238
+ }
239
+ }
240
+ }
241
+
242
+ async #executeCleanupTasks(): Promise<void> {
243
+ debugLog("Executing cleanup tasks");
244
+ const tasks = this.#cleanupTasks.splice(0);
245
+ for (const task of tasks) {
246
+ try {
247
+ await task();
248
+ } catch (err) {
249
+ errorLog("Error during cleanup task:", (err as Error).message);
250
+ }
251
+ }
252
+ }
253
+ }