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.
- package/dist/clientTools/certificate_types.d.ts +15 -0
- package/dist/clientTools/certificate_types.js +19 -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 +6 -5
- 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 +59 -81
- 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 +348 -82
- package/dist/server/promote_trust_list.js.map +1 -1
- package/dist/server/push_certificate_manager/apply_changes.d.ts +4 -0
- package/dist/server/push_certificate_manager/apply_changes.js +65 -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 +134 -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 +110 -113
- package/dist/server/push_certificate_manager_helpers.js.map +1 -1
- package/dist/server/push_certificate_manager_server_impl.d.ts +37 -30
- package/dist/server/push_certificate_manager_server_impl.js +58 -438
- 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 +30 -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 +17 -18
- 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 -68
- package/source/server/promote_trust_list.ts +392 -73
- package/source/server/push_certificate_manager/apply_changes.ts +73 -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 +203 -0
- package/source/server/push_certificate_manager/util.ts +145 -0
- package/source/server/push_certificate_manager_helpers.ts +62 -52
- package/source/server/push_certificate_manager_server_impl.ts +133 -552
- 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,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
|
-
|
|
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 {
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
67
|
-
const cm =
|
|
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
|
-
|
|
74
|
-
if (trustListIsAlreadyOpened(trustList)) {
|
|
311
|
+
if (trustListIsAlreadyOpened(trustList) || trustList.$$openedForWrite) {
|
|
75
312
|
return { statusCode: StatusCodes.BadInvalidState };
|
|
76
313
|
}
|
|
77
314
|
|
|
78
|
-
const
|
|
315
|
+
const certificateBuffer: Buffer = inputArguments[0].value as Buffer;
|
|
79
316
|
const isTrustedCertificate: boolean = inputArguments[1].value as boolean;
|
|
80
317
|
|
|
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);
|
|
318
|
+
// OPC UA Spec: "If FALSE Bad_CertificateInvalid is returned."
|
|
319
|
+
if (!isTrustedCertificate) {
|
|
87
320
|
return { statusCode: StatusCodes.BadCertificateInvalid };
|
|
88
321
|
}
|
|
89
322
|
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
|
127
|
-
const _open_asyncExecutionFunction =
|
|
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
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
const
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
464
|
+
const certificateManager = trustListEx.$$certificateManager;
|
|
157
465
|
if (certificateManager) {
|
|
158
466
|
writeTrustList(MemFs as AbstractFs, filename, trustMask, certificateManager)
|
|
159
467
|
.then(() => {
|
|
160
|
-
//
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
526
|
+
fileType.open?.bindMethod(_openCallback);
|
|
208
527
|
fileType.addCertificate.bindMethod(_addCertificate);
|
|
209
528
|
fileType.removeCertificate.bindMethod(_removeCertificate);
|
|
210
|
-
fileType.openWithMasks
|
|
211
|
-
fileType.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
|
+
}
|