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.
- package/dist/clientTools/certificate_types.d.ts +17 -0
- package/dist/clientTools/certificate_types.js +20 -0
- package/dist/clientTools/certificate_types.js.map +1 -0
- package/dist/clientTools/get_certificate_key_type.d.ts +6 -0
- package/dist/clientTools/get_certificate_key_type.js +55 -0
- package/dist/clientTools/get_certificate_key_type.js.map +1 -0
- package/dist/clientTools/index.d.ts +2 -1
- package/dist/clientTools/index.js +2 -17
- package/dist/clientTools/index.js.map +1 -1
- package/dist/clientTools/push_certificate_management_client.d.ts +10 -10
- package/dist/clientTools/push_certificate_management_client.js +85 -89
- package/dist/clientTools/push_certificate_management_client.js.map +1 -1
- package/dist/index.d.ts +9 -7
- package/dist/index.js +9 -23
- package/dist/index.js.map +1 -1
- package/dist/push_certificate_manager.d.ts +4 -4
- package/dist/push_certificate_manager.js +1 -2
- package/dist/server/certificate_validation.d.ts +15 -0
- package/dist/server/certificate_validation.js +76 -0
- package/dist/server/certificate_validation.js.map +1 -0
- package/dist/server/file_transaction_manager.d.ts +30 -0
- package/dist/server/file_transaction_manager.js +223 -0
- package/dist/server/file_transaction_manager.js.map +1 -0
- package/dist/server/install_certificate_file_watcher.d.ts +1 -1
- package/dist/server/install_certificate_file_watcher.js +8 -14
- package/dist/server/install_certificate_file_watcher.js.map +1 -1
- package/dist/server/install_push_certitifate_management.d.ts +6 -6
- package/dist/server/install_push_certitifate_management.js +61 -65
- package/dist/server/install_push_certitifate_management.js.map +1 -1
- package/dist/server/promote_trust_list.d.ts +1 -1
- package/dist/server/promote_trust_list.js +323 -82
- package/dist/server/promote_trust_list.js.map +1 -1
- package/dist/server/push_certificate_manager/apply_changes.d.ts +3 -0
- package/dist/server/push_certificate_manager/apply_changes.js +59 -0
- package/dist/server/push_certificate_manager/apply_changes.js.map +1 -0
- package/dist/server/push_certificate_manager/create_signing_request.d.ts +5 -0
- package/dist/server/push_certificate_manager/create_signing_request.js +108 -0
- package/dist/server/push_certificate_manager/create_signing_request.js.map +1 -0
- package/dist/server/push_certificate_manager/get_rejected_list.d.ts +3 -0
- package/dist/server/push_certificate_manager/get_rejected_list.js +46 -0
- package/dist/server/push_certificate_manager/get_rejected_list.js.map +1 -0
- package/dist/server/push_certificate_manager/internal_context.d.ts +35 -0
- package/dist/server/push_certificate_manager/internal_context.js +45 -0
- package/dist/server/push_certificate_manager/internal_context.js.map +1 -0
- package/dist/server/push_certificate_manager/subject_to_string.d.ts +3 -0
- package/dist/server/push_certificate_manager/subject_to_string.js +27 -0
- package/dist/server/push_certificate_manager/subject_to_string.js.map +1 -0
- package/dist/server/push_certificate_manager/update_certificate.d.ts +5 -0
- package/dist/server/push_certificate_manager/update_certificate.js +132 -0
- package/dist/server/push_certificate_manager/update_certificate.js.map +1 -0
- package/dist/server/push_certificate_manager/util.d.ts +29 -0
- package/dist/server/push_certificate_manager/util.js +117 -0
- package/dist/server/push_certificate_manager/util.js.map +1 -0
- package/dist/server/push_certificate_manager_helpers.d.ts +5 -2
- package/dist/server/push_certificate_manager_helpers.js +109 -112
- package/dist/server/push_certificate_manager_helpers.js.map +1 -1
- package/dist/server/push_certificate_manager_server_impl.d.ts +16 -29
- package/dist/server/push_certificate_manager_server_impl.js +49 -437
- package/dist/server/push_certificate_manager_server_impl.js.map +1 -1
- package/dist/server/roles_and_permissions.d.ts +1 -1
- package/dist/server/roles_and_permissions.js +24 -27
- package/dist/server/roles_and_permissions.js.map +1 -1
- package/dist/server/tools.d.ts +1 -1
- package/dist/server/tools.js +7 -13
- package/dist/server/tools.js.map +1 -1
- package/dist/server/trust_list_server.d.ts +2 -2
- package/dist/server/trust_list_server.js +40 -29
- package/dist/server/trust_list_server.js.map +1 -1
- package/dist/standard_certificate_types.js +6 -9
- package/dist/standard_certificate_types.js.map +1 -1
- package/dist/trust_list.d.ts +2 -2
- package/dist/trust_list.js +1 -2
- package/dist/trust_list_impl.js +1 -2
- package/dist/trust_list_impl.js.map +1 -1
- package/package.json +29 -30
- package/source/clientTools/certificate_types.ts +21 -0
- package/source/clientTools/get_certificate_key_type.ts +73 -0
- package/source/clientTools/index.ts +2 -1
- package/source/clientTools/push_certificate_management_client.ts +49 -44
- package/source/index.ts +9 -7
- package/source/push_certificate_manager.ts +15 -17
- package/source/server/certificate_validation.ts +103 -0
- package/source/server/file_transaction_manager.ts +253 -0
- package/source/server/install_certificate_file_watcher.ts +15 -11
- package/source/server/install_push_certitifate_management.ts +52 -51
- package/source/server/promote_trust_list.ts +362 -73
- package/source/server/push_certificate_manager/apply_changes.ts +63 -0
- package/source/server/push_certificate_manager/create_signing_request.ts +137 -0
- package/source/server/push_certificate_manager/get_rejected_list.ts +63 -0
- package/source/server/push_certificate_manager/internal_context.ts +63 -0
- package/source/server/push_certificate_manager/subject_to_string.ts +25 -0
- package/source/server/push_certificate_manager/update_certificate.ts +201 -0
- package/source/server/push_certificate_manager/util.ts +145 -0
- package/source/server/push_certificate_manager_helpers.ts +61 -51
- package/source/server/push_certificate_manager_server_impl.ts +94 -553
- package/source/server/roles_and_permissions.ts +7 -8
- package/source/server/tools.ts +2 -5
- package/source/server/trust_list_server.ts +24 -9
- package/source/standard_certificate_types.ts +2 -3
- 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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
UAObjectType,
|
|
14
|
+
UATrustList,
|
|
15
|
+
UATrustList_Base,
|
|
16
|
+
UAVariable
|
|
15
17
|
} from "node-opcua-address-space";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
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 {
|
|
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 =
|
|
36
|
+
const errorLog = make_errorLog("ServerConfiguration");
|
|
36
37
|
|
|
37
38
|
function trustListIsAlreadyOpened(trustList: UATrustList): boolean {
|
|
38
|
-
|
|
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
|
-
|
|
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
|
|
67
|
-
const cm =
|
|
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
|
-
|
|
74
|
-
if (trustListIsAlreadyOpened(trustList)) {
|
|
283
|
+
if (trustListIsAlreadyOpened(trustList) || trustList.$$openedForWrite) {
|
|
75
284
|
return { statusCode: StatusCodes.BadInvalidState };
|
|
76
285
|
}
|
|
77
286
|
|
|
78
|
-
const
|
|
287
|
+
const certificateBuffer: Buffer = inputArguments[0].value as Buffer;
|
|
79
288
|
const isTrustedCertificate: boolean = inputArguments[1].value as boolean;
|
|
80
289
|
|
|
81
|
-
|
|
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
|
-
|
|
91
|
-
const
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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
|
|
127
|
-
const _open_asyncExecutionFunction =
|
|
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
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
const
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
434
|
+
const certificateManager = trustListEx.$$certificateManager;
|
|
157
435
|
if (certificateManager) {
|
|
158
436
|
writeTrustList(MemFs as AbstractFs, filename, trustMask, certificateManager)
|
|
159
437
|
.then(() => {
|
|
160
|
-
//
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
496
|
+
fileType.open?.bindMethod(_openCallback);
|
|
208
497
|
fileType.addCertificate.bindMethod(_addCertificate);
|
|
209
498
|
fileType.removeCertificate.bindMethod(_removeCertificate);
|
|
210
|
-
fileType.openWithMasks
|
|
211
|
-
fileType.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
|
+
}
|