node-opcua-server-configuration 2.167.0 → 2.169.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/push_certificate_management_client.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/server/file_transaction_manager.d.ts +10 -0
- package/dist/server/file_transaction_manager.js +23 -0
- package/dist/server/file_transaction_manager.js.map +1 -1
- package/dist/server/{install_push_certitifate_management.d.ts → install_push_certificate_management.d.ts} +3 -2
- package/dist/server/install_push_certificate_management.js +263 -0
- package/dist/server/install_push_certificate_management.js.map +1 -0
- package/dist/server/promote_trust_list.js +154 -3
- package/dist/server/promote_trust_list.js.map +1 -1
- package/dist/server/push_certificate_manager/create_signing_request.js +19 -13
- package/dist/server/push_certificate_manager/create_signing_request.js.map +1 -1
- package/dist/server/push_certificate_manager/update_certificate.js +21 -9
- package/dist/server/push_certificate_manager/update_certificate.js.map +1 -1
- package/dist/server/push_certificate_manager_helpers.js.map +1 -1
- package/dist/server/push_certificate_manager_server_impl.js.map +1 -1
- package/dist/server/trust_list_server.js +5 -0
- package/dist/server/trust_list_server.js.map +1 -1
- package/package.json +24 -26
- package/source/clientTools/push_certificate_management_client.ts +4 -8
- package/source/index.ts +2 -1
- package/source/server/file_transaction_manager.ts +25 -0
- package/source/server/install_push_certificate_management.ts +332 -0
- package/source/server/promote_trust_list.ts +185 -9
- package/source/server/push_certificate_manager/create_signing_request.ts +27 -17
- package/source/server/push_certificate_manager/update_certificate.ts +25 -8
- package/source/server/push_certificate_manager_helpers.ts +1 -1
- package/source/server/push_certificate_manager_server_impl.ts +3 -9
- package/source/server/trust_list_server.ts +7 -2
- package/dist/server/install_push_certitifate_management.js +0 -144
- package/dist/server/install_push_certitifate_management.js.map +0 -1
- package/source/server/install_push_certitifate_management.ts +0 -193
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* @module node-opcua-server-configuration
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
5
8
|
import { fs as MemFs } from "memfs";
|
|
6
9
|
|
|
7
10
|
import type {
|
|
@@ -25,11 +28,10 @@ import { VerificationStatus } from "node-opcua-pki";
|
|
|
25
28
|
import { type CallbackT, type StatusCode, StatusCodes } from "node-opcua-status-code";
|
|
26
29
|
import { type CallMethodResultOptions, TrustListDataType } from "node-opcua-types";
|
|
27
30
|
import { DataType, Variant } from "node-opcua-variant";
|
|
31
|
+
import type { PushCertificateManagerServerImpl } from "./push_certificate_manager_server_impl.js";
|
|
28
32
|
import { rolePermissionAdminOnly } from "./roles_and_permissions.js";
|
|
29
|
-
|
|
30
33
|
import { hasEncryptedChannel, hasExpectedUserAccess } from "./tools.js";
|
|
31
34
|
import { TrustListMasks, writeTrustList } from "./trust_list_server.js";
|
|
32
|
-
import type { PushCertificateManagerServerImpl } from "./push_certificate_manager_server_impl.js";
|
|
33
35
|
|
|
34
36
|
const debugLog = make_debugLog("ServerConfiguration");
|
|
35
37
|
const doDebug = checkDebugFlag("ServerConfiguration");
|
|
@@ -46,13 +48,14 @@ function emitTrustListUpdated(trustList: UATrustList): void {
|
|
|
46
48
|
const certificateGroup = trustList.parent;
|
|
47
49
|
const groupName = certificateGroup?.browseName?.name ?? "Unknown";
|
|
48
50
|
|
|
49
|
-
const serverConfiguration = trustList.addressSpace.rootFolder
|
|
50
|
-
.objects.server.getChildByName("ServerConfiguration");
|
|
51
|
+
const serverConfiguration = trustList.addressSpace.rootFolder.objects.server.getChildByName("ServerConfiguration");
|
|
51
52
|
if (!serverConfiguration) return;
|
|
52
53
|
|
|
53
|
-
const pushManager = (
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
const pushManager = (
|
|
55
|
+
serverConfiguration as unknown as {
|
|
56
|
+
$pushCertificateManager?: PushCertificateManagerServerImpl;
|
|
57
|
+
}
|
|
58
|
+
).$pushCertificateManager;
|
|
56
59
|
|
|
57
60
|
if (pushManager) {
|
|
58
61
|
pushManager.emit("trustListUpdated", groupName);
|
|
@@ -97,6 +100,151 @@ function updateLastUpdateTime(trustList: UATrustList): void {
|
|
|
97
100
|
}
|
|
98
101
|
}
|
|
99
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Scan the PKI store folders (trusted/certs, trusted/crl, issuers/certs,
|
|
105
|
+
* issuers/crl) and return the most recent modification time across all
|
|
106
|
+
* files. Returns null if no files are found.
|
|
107
|
+
*
|
|
108
|
+
* Uses async fs.promises to avoid blocking the event loop on startup
|
|
109
|
+
* when PKI directories are large or on slow filesystems.
|
|
110
|
+
*/
|
|
111
|
+
async function getNewestMtimeFromPkiStore(
|
|
112
|
+
cm: OPCUACertificateManager,
|
|
113
|
+
isAborted?: () => boolean
|
|
114
|
+
): Promise<Date | null> {
|
|
115
|
+
const dirs = [cm.trustedFolder, cm.crlFolder, cm.issuersCertFolder, cm.issuersCrlFolder];
|
|
116
|
+
let newest: Date | null = null;
|
|
117
|
+
|
|
118
|
+
for (const dir of dirs) {
|
|
119
|
+
if (isAborted?.()) break;
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
await fs.promises.access(dir);
|
|
123
|
+
} catch {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
let entries: string[];
|
|
127
|
+
try {
|
|
128
|
+
entries = await fs.promises.readdir(dir);
|
|
129
|
+
} catch {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Process stats sequentially to avoid threadpool exhaustion
|
|
134
|
+
// and event-loop lag when directories have thousands of files.
|
|
135
|
+
for (const entry of entries) {
|
|
136
|
+
if (isAborted?.()) return null;
|
|
137
|
+
try {
|
|
138
|
+
const stat = await fs.promises.stat(path.join(dir, entry));
|
|
139
|
+
if (stat.isFile() && (!newest || stat.mtime > newest)) {
|
|
140
|
+
newest = stat.mtime;
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
// skip unreadable entries
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return newest;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Initialize the LastUpdateTime property from the PKI store's
|
|
152
|
+
* filesystem timestamps. This avoids displaying MinDate
|
|
153
|
+
* (0001-01-01T00:00:00Z) when the trust store already contains
|
|
154
|
+
* certificates or CRLs (e.g. populated by selfOnboard or addIssuer).
|
|
155
|
+
*
|
|
156
|
+
* Also subscribes to the CertificateManager's filesystem watcher
|
|
157
|
+
* events (certificateAdded, certificateRemoved, certificateChange,
|
|
158
|
+
* crlAdded, crlRemoved) so that LastUpdateTime stays current even
|
|
159
|
+
* when the trust store is modified externally (e.g. manual file
|
|
160
|
+
* copy, programmatic addIssuer/trustCertificate calls).
|
|
161
|
+
*
|
|
162
|
+
* Listeners are installed at most once per TrustList node
|
|
163
|
+
* (guarded by $$listenersInstalled) and are removed via
|
|
164
|
+
* addressSpace.registerShutdownTask to prevent leaks.
|
|
165
|
+
*/
|
|
166
|
+
async function _initializeLastUpdateTimeFromFilesystem(trustList: UATrustListEx): Promise<void> {
|
|
167
|
+
const cm = trustList.$$certificateManager;
|
|
168
|
+
if (!cm) return;
|
|
169
|
+
|
|
170
|
+
if (trustList.$$initaliseMTimePromise) {
|
|
171
|
+
return await trustList.$$initaliseMTimePromise;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
trustList.$$initaliseMTimePromise = (async () => {
|
|
175
|
+
const startTime = Date.now();
|
|
176
|
+
let isAborted = false;
|
|
177
|
+
|
|
178
|
+
// Note: Removed abortHandler from registerShutdownTask because AddressSpace
|
|
179
|
+
// does not have an unregister mechanism, which causes `_shutdownTasks` to leak
|
|
180
|
+
// continuously if promoteTrustList is called multiple times.
|
|
181
|
+
// Sequential scanning is fast enough that it won't block shutdown significantly.
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const lastUpdateTimeNode = trustList.lastUpdateTime;
|
|
185
|
+
if (!lastUpdateTimeNode) return;
|
|
186
|
+
|
|
187
|
+
// Seed the initial timestamp from the filesystem only when
|
|
188
|
+
// the current value is still unset (MinDate). The event
|
|
189
|
+
// listeners below must always be installed regardless, so we
|
|
190
|
+
// must NOT return early here.
|
|
191
|
+
const currentValue = lastUpdateTimeNode.readValue().value.value as Date | undefined;
|
|
192
|
+
if (!currentValue || currentValue.getTime() <= 0) {
|
|
193
|
+
const newest = await getNewestMtimeFromPkiStore(cm, () => isAborted);
|
|
194
|
+
if (isAborted) {
|
|
195
|
+
console.log(`[node-opcua] _initializeLastUpdateTimeFromFilesystem aborted for ${trustList.browseName.toString()}`);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (newest) {
|
|
199
|
+
lastUpdateTimeNode.setValueFromSource({
|
|
200
|
+
dataType: DataType.DateTime,
|
|
201
|
+
value: newest
|
|
202
|
+
});
|
|
203
|
+
doDebug && debugLog("Initialized LastUpdateTime from filesystem:", newest.toISOString());
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Guard: install listeners at most once per TrustList node
|
|
208
|
+
// to prevent duplicate handler invocation on re-promotion.
|
|
209
|
+
if (trustList.$$listenersInstalled) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
trustList.$$listenersInstalled = true;
|
|
213
|
+
|
|
214
|
+
// Subscribe to CertificateManager filesystem watcher events
|
|
215
|
+
// so LastUpdateTime stays current when the store is modified
|
|
216
|
+
// outside of OPC UA methods (e.g. addIssuer, trustCertificate,
|
|
217
|
+
// or manual file operations).
|
|
218
|
+
const _updateNow = () => {
|
|
219
|
+
updateLastUpdateTime(trustList);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const events = ["certificateAdded", "certificateRemoved", "certificateChange", "crlAdded", "crlRemoved"] as const;
|
|
223
|
+
for (const event of events) {
|
|
224
|
+
cm.on(event, _updateNow);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Clean up listeners when the address space shuts down
|
|
228
|
+
// to avoid MaxListenersExceededWarning and memory leaks.
|
|
229
|
+
// Guard: addressSpace may be null if dispose() was called
|
|
230
|
+
// concurrently (e.g. fast test teardown).
|
|
231
|
+
if (trustList.addressSpace) {
|
|
232
|
+
trustList.addressSpace.registerShutdownTask(() => {
|
|
233
|
+
for (const event of events) {
|
|
234
|
+
cm.removeListener(event, _updateNow);
|
|
235
|
+
}
|
|
236
|
+
trustList.$$listenersInstalled = false;
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
console.log(`[node-opcua] _initializeLastUpdateTimeFromFilesystem took ${Date.now() - startTime}ms for ${trustList.browseName.toString()}`);
|
|
241
|
+
} finally {
|
|
242
|
+
trustList.$$initaliseMTimePromise = undefined;
|
|
243
|
+
}
|
|
244
|
+
})();
|
|
245
|
+
return await trustList.$$initaliseMTimePromise;
|
|
246
|
+
}
|
|
247
|
+
|
|
100
248
|
interface UAMethodEx extends UAMethod {
|
|
101
249
|
_asyncExecutionFunction?: MethodFunctor;
|
|
102
250
|
}
|
|
@@ -104,6 +252,8 @@ interface UATrustListEx extends UATrustList {
|
|
|
104
252
|
$$certificateManager: OPCUACertificateManager;
|
|
105
253
|
$$filename: string;
|
|
106
254
|
$$openedForWrite: boolean;
|
|
255
|
+
$$listenersInstalled: boolean;
|
|
256
|
+
$$initaliseMTimePromise?: Promise<void>;
|
|
107
257
|
}
|
|
108
258
|
|
|
109
259
|
async function applyTrustListChanges(cm: OPCUACertificateManager, trustListData: TrustListDataType): Promise<StatusCode> {
|
|
@@ -445,10 +595,18 @@ async function _removeCertificate(
|
|
|
445
595
|
let counter = 0;
|
|
446
596
|
|
|
447
597
|
export async function promoteTrustList(trustList: UATrustList) {
|
|
598
|
+
const trustListEx = trustList as UATrustListEx & { $$promoted?: boolean };
|
|
599
|
+
|
|
600
|
+
// Prevent double-binding if called multiple times testing
|
|
601
|
+
if (trustListEx.$$promoted) {
|
|
602
|
+
await _initializeLastUpdateTimeFromFilesystem(trustListEx);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
trustListEx.$$promoted = true;
|
|
606
|
+
|
|
448
607
|
const filename = `/tmpFile${counter}`;
|
|
449
608
|
counter += 1;
|
|
450
609
|
|
|
451
|
-
const trustListEx = trustList as UATrustListEx;
|
|
452
610
|
// Store filename for use in _closeAndUpdate
|
|
453
611
|
trustListEx.$$filename = filename;
|
|
454
612
|
// Initialize write lock flag
|
|
@@ -507,7 +665,9 @@ export async function promoteTrustList(trustList: UATrustList) {
|
|
|
507
665
|
callback(err, { statusCode: StatusCodes.BadInternalError });
|
|
508
666
|
});
|
|
509
667
|
} else {
|
|
510
|
-
warningLog(
|
|
668
|
+
warningLog(
|
|
669
|
+
"certificateManager is not defined on trustlist do something to update the trust list document before we open it"
|
|
670
|
+
);
|
|
511
671
|
return _open_asyncExecutionFunction.call(this, inputArgs, context, callback);
|
|
512
672
|
}
|
|
513
673
|
}
|
|
@@ -539,6 +699,17 @@ export async function promoteTrustList(trustList: UATrustList) {
|
|
|
539
699
|
addCertificate.bindMethod(_addCertificate);
|
|
540
700
|
removeCertificate.bindMethod(_removeCertificate);
|
|
541
701
|
|
|
702
|
+
function _closeCallback(
|
|
703
|
+
this: UAMethod,
|
|
704
|
+
inputArgs: Variant[],
|
|
705
|
+
context: ISessionContext,
|
|
706
|
+
callback: CallbackT<CallMethodResultOptions>
|
|
707
|
+
) {
|
|
708
|
+
trustListEx.$$openedForWrite = false;
|
|
709
|
+
_close_asyncExecutionFunction.call(this, inputArgs, context, callback);
|
|
710
|
+
}
|
|
711
|
+
close.bindMethod(_closeCallback);
|
|
712
|
+
|
|
542
713
|
// Wrapper to pass the underlying close method to _closeAndUpdate
|
|
543
714
|
closeAndUpdate?.bindMethod(async function (
|
|
544
715
|
this: UAMethod,
|
|
@@ -554,12 +725,17 @@ export async function promoteTrustList(trustList: UATrustList) {
|
|
|
554
725
|
return;
|
|
555
726
|
}
|
|
556
727
|
fileType.open?.bindMethod(_openCallback);
|
|
728
|
+
fileType.close?.bindMethod(_closeCallback);
|
|
557
729
|
fileType.addCertificate.bindMethod(_addCertificate);
|
|
558
730
|
fileType.removeCertificate.bindMethod(_removeCertificate);
|
|
559
731
|
fileType.openWithMasks?.bindMethod(_openWithMaskCallback);
|
|
560
732
|
fileType.closeAndUpdate?.bindMethod(_closeAndUpdate);
|
|
561
733
|
}
|
|
562
734
|
install_method_handle_on_TrustListType(trustList.addressSpace);
|
|
735
|
+
|
|
736
|
+
// Initialize LastUpdateTime from the PKI store's filesystem timestamps
|
|
737
|
+
// so it doesn't show MinDate (0001-01-01) when files already exist.
|
|
738
|
+
await _initializeLastUpdateTimeFromFilesystem(trustListEx);
|
|
563
739
|
}
|
|
564
740
|
|
|
565
741
|
export function installAccessRestrictionOnTrustList(trustList: UAVariable | UAObject) {
|
|
@@ -2,7 +2,7 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { CertificateManager } from "node-opcua-certificate-manager";
|
|
5
|
-
import { convertPEMtoDER, exploreCertificate, readCertificateChain
|
|
5
|
+
import { convertPEMtoDER, type DirectoryName, exploreCertificate, readCertificateChain } from "node-opcua-crypto";
|
|
6
6
|
import { checkDebugFlag, make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
|
|
7
7
|
import { NodeId, resolveNodeId, sameNodeId } from "node-opcua-nodeid";
|
|
8
8
|
import type { SubjectOptions } from "node-opcua-pki";
|
|
@@ -118,20 +118,30 @@ export async function executeCreateSigningRequest(
|
|
|
118
118
|
});
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
121
|
+
try {
|
|
122
|
+
const options = {
|
|
123
|
+
applicationUri: serverImpl.applicationUri,
|
|
124
|
+
subject: subjectName
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const activeCertificateManager = serverImpl.tmpCertificateManager || certificateManager;
|
|
128
|
+
|
|
129
|
+
await activeCertificateManager.initialize();
|
|
130
|
+
const csrFile = await activeCertificateManager.createCertificateRequest(options);
|
|
131
|
+
const csrPEM = await fs.promises.readFile(csrFile, "utf8");
|
|
132
|
+
const certificateSigningRequest = convertPEMtoDER(csrPEM);
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
certificateSigningRequest,
|
|
136
|
+
statusCode: StatusCodes.Good
|
|
137
|
+
};
|
|
138
|
+
} catch (err) {
|
|
139
|
+
errorLog(
|
|
140
|
+
"CreateSigningRequest failed during CSR generation:",
|
|
141
|
+
(err as Error).message,
|
|
142
|
+
"subject=", subjectName,
|
|
143
|
+
"applicationUri=", serverImpl.applicationUri
|
|
144
|
+
);
|
|
145
|
+
return { statusCode: StatusCodes.BadInternalError };
|
|
146
|
+
}
|
|
137
147
|
}
|
|
@@ -3,11 +3,13 @@ import path from "node:path";
|
|
|
3
3
|
import { assert } from "node-opcua-assert";
|
|
4
4
|
import type { ByteString } from "node-opcua-basic-types";
|
|
5
5
|
import type { CertificateManager, OPCUACertificateManager } from "node-opcua-certificate-manager";
|
|
6
|
-
import {
|
|
6
|
+
import { exploreCertificate, readPrivateKey } from "node-opcua-crypto";
|
|
7
7
|
import {
|
|
8
8
|
certificateMatchesPrivateKey,
|
|
9
|
+
type Certificate,
|
|
9
10
|
coercePEMorDerToPrivateKey,
|
|
10
11
|
coercePrivateKeyPem,
|
|
12
|
+
combine_der,
|
|
11
13
|
makeSHA1Thumbprint,
|
|
12
14
|
type PrivateKey,
|
|
13
15
|
toPem
|
|
@@ -19,7 +21,7 @@ import { StatusCodes } from "node-opcua-status-code";
|
|
|
19
21
|
import type { UpdateCertificateResult } from "../../push_certificate_manager.js";
|
|
20
22
|
import { validateCertificateAndChain } from "../certificate_validation.js";
|
|
21
23
|
import type { PushCertificateManagerInternalContext } from "./internal_context.js";
|
|
22
|
-
import { resolveCertificateGroupContext, validateCertificateType
|
|
24
|
+
import { findCertificateGroupName, resolveCertificateGroupContext, validateCertificateType } from "./util.js";
|
|
23
25
|
|
|
24
26
|
const warningLog = make_warningLog("ServerConfiguration");
|
|
25
27
|
const debugLog = make_debugLog("ServerConfiguration");
|
|
@@ -52,18 +54,33 @@ async function preInstallIssuerCertificates(
|
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
// Helper: Stage main certificate to temporary files
|
|
57
|
+
//
|
|
58
|
+
// The PEM file contains the full chain (leaf + issuer CAs) so that
|
|
59
|
+
// getCertificateChain() returns the complete chain. This allows
|
|
60
|
+
// OpenSecureChannel and CreateSession to present the full chain to
|
|
61
|
+
// connecting clients, enabling them to build and verify the trust
|
|
62
|
+
// path without needing all CA certificates pre-installed.
|
|
55
63
|
async function preInstallCertificate(
|
|
56
64
|
serverImpl: PushCertificateManagerInternalContext,
|
|
57
65
|
certificateManager: CertificateManager,
|
|
58
|
-
certificate:
|
|
66
|
+
certificate: Certificate,
|
|
67
|
+
issuerCertificates?: Certificate[]
|
|
59
68
|
): Promise<void> {
|
|
60
69
|
const certFolder = certificateManager.ownCertFolder;
|
|
61
|
-
const destDER = path.join(certFolder, "certificate.der");
|
|
62
70
|
const destPEM = path.join(certFolder, "certificate.pem");
|
|
63
|
-
const certificatePEM = toPem(certificate, "CERTIFICATE");
|
|
64
71
|
|
|
65
|
-
|
|
72
|
+
// Build the full chain: leaf first, then issuer CAs
|
|
73
|
+
const certificateChain: Certificate[] = [certificate, ...(issuerCertificates ?? [])];
|
|
74
|
+
const certificatePEM = toPem(combine_der(certificateChain), "CERTIFICATE");
|
|
75
|
+
|
|
66
76
|
await serverImpl.fileTransactionManager.stageFile(destPEM, certificatePEM, "utf-8");
|
|
77
|
+
|
|
78
|
+
// Clean up any leftover certificate.der from previous installations.
|
|
79
|
+
// Since DER is no longer written, a stale file would be misleading.
|
|
80
|
+
const legacyDER = path.join(certFolder, "certificate.der");
|
|
81
|
+
if (fs.existsSync(legacyDER)) {
|
|
82
|
+
serverImpl.fileTransactionManager.stageFileRemoval(legacyDER);
|
|
83
|
+
}
|
|
67
84
|
}
|
|
68
85
|
|
|
69
86
|
// Helper: Stage private key to temporary file
|
|
@@ -164,7 +181,7 @@ export async function executeUpdateCertificate(
|
|
|
164
181
|
}
|
|
165
182
|
|
|
166
183
|
await preInstallIssuerCertificates(serverImpl, certificateManager, issuerCertificates);
|
|
167
|
-
await preInstallCertificate(serverImpl, certificateManager, certificate);
|
|
184
|
+
await preInstallCertificate(serverImpl, certificateManager, certificate, issuerCertificates);
|
|
168
185
|
serverImpl.emit("certificateUpdated", certificateGroupId, certificate);
|
|
169
186
|
return { statusCode: StatusCodes.Good, applyChangesRequired: true };
|
|
170
187
|
} else {
|
|
@@ -203,7 +220,7 @@ export async function executeUpdateCertificate(
|
|
|
203
220
|
|
|
204
221
|
await preInstallPrivateKey(serverImpl, certificateManager, privateKeyFormat, tempPrivateKey);
|
|
205
222
|
await preInstallIssuerCertificates(serverImpl, certificateManager, issuerCertificates);
|
|
206
|
-
await preInstallCertificate(serverImpl, certificateManager, certificate);
|
|
223
|
+
await preInstallCertificate(serverImpl, certificateManager, certificate, issuerCertificates);
|
|
207
224
|
|
|
208
225
|
serverImpl.emit("certificateUpdated", certificateGroupId, certificate);
|
|
209
226
|
return { statusCode: StatusCodes.Good, applyChangesRequired: true };
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
import { EventNotifierFlags, type UAObject, type UAVariable } from "node-opcua-address-space-base";
|
|
18
18
|
import type { ByteString, UAString } from "node-opcua-basic-types";
|
|
19
19
|
import { ObjectIds, ObjectTypeIds } from "node-opcua-constants";
|
|
20
|
-
import {
|
|
20
|
+
import { type Certificate, readCertificateChainAsync } from "node-opcua-crypto";
|
|
21
21
|
import { AccessRestrictionsFlag, BrowseDirection, coerceQualifiedName, NodeClass } from "node-opcua-data-model";
|
|
22
22
|
import { make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
|
|
23
23
|
import { NodeId, resolveNodeId } from "node-opcua-nodeid";
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* @module node-opcua-server-configuration-server
|
|
3
3
|
*/
|
|
4
4
|
import { EventEmitter } from "node:events";
|
|
5
|
-
import { assert } from "node-opcua-assert";
|
|
6
5
|
import type { ISessionContext } from "node-opcua-address-space-base";
|
|
6
|
+
import { assert } from "node-opcua-assert";
|
|
7
7
|
import type { ByteString } from "node-opcua-basic-types";
|
|
8
8
|
import { CertificateManager } from "node-opcua-certificate-manager";
|
|
9
9
|
import { make_errorLog } from "node-opcua-debug";
|
|
@@ -70,10 +70,7 @@ export class PushCertificateManagerServerImpl extends EventEmitter implements Pu
|
|
|
70
70
|
public applicationUri: string;
|
|
71
71
|
|
|
72
72
|
// ── typed event helpers ──────────────────────────────────────────
|
|
73
|
-
public on<K extends keyof PushCertificateManagerEvents>(
|
|
74
|
-
event: K,
|
|
75
|
-
listener: PushCertificateManagerEvents[K]
|
|
76
|
-
): this;
|
|
73
|
+
public on<K extends keyof PushCertificateManagerEvents>(event: K, listener: PushCertificateManagerEvents[K]): this;
|
|
77
74
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
78
75
|
public on(event: string | symbol, listener: (...args: any[]) => void): this;
|
|
79
76
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -81,10 +78,7 @@ export class PushCertificateManagerServerImpl extends EventEmitter implements Pu
|
|
|
81
78
|
return super.on(event, listener);
|
|
82
79
|
}
|
|
83
80
|
|
|
84
|
-
public once<K extends keyof PushCertificateManagerEvents>(
|
|
85
|
-
event: K,
|
|
86
|
-
listener: PushCertificateManagerEvents[K]
|
|
87
|
-
): this;
|
|
81
|
+
public once<K extends keyof PushCertificateManagerEvents>(event: K, listener: PushCertificateManagerEvents[K]): this;
|
|
88
82
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
89
83
|
public once(event: string | symbol, listener: (...args: any[]) => void): this;
|
|
90
84
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -11,8 +11,8 @@ const errorLog = make_errorLog("TrustListServer");
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Read all certificate (chains) and CRLs in a folder
|
|
14
|
-
* @param folder
|
|
15
|
-
* @returns
|
|
14
|
+
* @param folder
|
|
15
|
+
* @returns
|
|
16
16
|
*/
|
|
17
17
|
async function readAll(folder: string): Promise<Buffer[]> {
|
|
18
18
|
const results: Buffer[] = [];
|
|
@@ -22,6 +22,11 @@ async function readAll(folder: string): Promise<Buffer[]> {
|
|
|
22
22
|
const ext = path.extname(file);
|
|
23
23
|
if (ext === ".der" || ext === ".pem") {
|
|
24
24
|
const chain = await readCertificateChainAsync(file);
|
|
25
|
+
// Return the full chain as a single ByteString.
|
|
26
|
+
// When addTrustedCertificateFromChain stores a chain-on-disk
|
|
27
|
+
// (leaf + issuer CAs in a single PEM), we preserve that
|
|
28
|
+
// chain so clients reading the TrustList can use it for
|
|
29
|
+
// chain-building.
|
|
25
30
|
const concatenated = Buffer.concat(chain);
|
|
26
31
|
results.push(concatenated);
|
|
27
32
|
} else if (ext === ".crl") {
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module node-opcua-server-configuration-server
|
|
3
|
-
*/
|
|
4
|
-
import fs from "node:fs";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import chalk from "chalk";
|
|
7
|
-
import { assert } from "node-opcua-assert";
|
|
8
|
-
import { invalidateCachedSecrets } from "node-opcua-common";
|
|
9
|
-
import { readPrivateKey } from "node-opcua-crypto";
|
|
10
|
-
import { combine_der, convertPEMtoDER, split_der } from "node-opcua-crypto/web";
|
|
11
|
-
import { checkDebugFlag, make_debugLog, make_errorLog } from "node-opcua-debug";
|
|
12
|
-
import { getFullyQualifiedDomainName, getIpAddresses } from "node-opcua-hostname";
|
|
13
|
-
import { installPushCertificateManagement } from "./push_certificate_manager_helpers.js";
|
|
14
|
-
const debugLog = make_debugLog("ServerConfiguration");
|
|
15
|
-
const errorLog = make_errorLog("ServerConfiguration");
|
|
16
|
-
const doDebug = checkDebugFlag("ServerConfiguration");
|
|
17
|
-
function getCertificateChainEP() {
|
|
18
|
-
const certificateFile = path.join(this.certificateManager.rootDir, "own/certs/certificate.pem");
|
|
19
|
-
const certificatePEM = fs.readFileSync(certificateFile, "utf8");
|
|
20
|
-
return split_der(convertPEMtoDER(certificatePEM));
|
|
21
|
-
}
|
|
22
|
-
function getPrivateKeyEP() {
|
|
23
|
-
return readPrivateKey(this.certificateManager.privateKey);
|
|
24
|
-
}
|
|
25
|
-
async function onCertificateAboutToChange(server) {
|
|
26
|
-
doDebug && debugLog(chalk.yellow(" onCertificateAboutToChange => Suspending End points"));
|
|
27
|
-
await server.suspendEndPoints();
|
|
28
|
-
doDebug && debugLog(chalk.yellow(" onCertificateAboutToChange => End points suspended"));
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* onCertificateChange is called when the serverConfiguration notifies
|
|
32
|
-
* that the server certificate and/or private key has changed.
|
|
33
|
-
*
|
|
34
|
-
* This function invalidates the cached secrets so that the next
|
|
35
|
-
* getCertificate() / getPrivateKey() call re-reads from disk,
|
|
36
|
-
* then shuts down all channels and resumes endpoints.
|
|
37
|
-
*
|
|
38
|
-
* @param server
|
|
39
|
-
*/
|
|
40
|
-
async function onCertificateChange(server) {
|
|
41
|
-
doDebug && debugLog("on CertificateChanged");
|
|
42
|
-
// Invalidate the cached certificate chain and private key.
|
|
43
|
-
// The SecretHolder will re-read from disk on next access.
|
|
44
|
-
invalidateCachedSecrets(server);
|
|
45
|
-
setTimeout(async () => {
|
|
46
|
-
try {
|
|
47
|
-
doDebug && debugLog(chalk.yellow(" onCertificateChange => shutting down channels"));
|
|
48
|
-
await server.shutdownChannels();
|
|
49
|
-
doDebug && debugLog(chalk.yellow(" onCertificateChange => channels shut down"));
|
|
50
|
-
doDebug && debugLog(chalk.yellow(" onCertificateChange => resuming end points"));
|
|
51
|
-
await server.resumeEndPoints();
|
|
52
|
-
doDebug && debugLog(chalk.yellow(" onCertificateChange => end points resumed"));
|
|
53
|
-
debugLog(chalk.yellow("channels have been closed -> client should reconnect "));
|
|
54
|
-
}
|
|
55
|
-
catch (err) {
|
|
56
|
-
errorLog("Error in CertificateChanged handler ", err.message);
|
|
57
|
-
debugLog("err = ", err);
|
|
58
|
-
}
|
|
59
|
-
}, 2000);
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Install push certificate management on the server.
|
|
63
|
-
*
|
|
64
|
-
* This redirects `getCertificate`, `getCertificateChain` and
|
|
65
|
-
* `getPrivateKey` to read from the serverCertificateManager's
|
|
66
|
-
* PEM files, and wires up the push certificate management
|
|
67
|
-
* address-space nodes.
|
|
68
|
-
*/
|
|
69
|
-
async function install() {
|
|
70
|
-
doDebug && debugLog("install push certificate management", this.serverCertificateManager.rootDir);
|
|
71
|
-
Object.defineProperty(this, "privateKeyFile", {
|
|
72
|
-
get: () => this.serverCertificateManager.privateKey,
|
|
73
|
-
configurable: true
|
|
74
|
-
});
|
|
75
|
-
Object.defineProperty(this, "certificateFile", {
|
|
76
|
-
get: () => path.join(this.serverCertificateManager.rootDir, "own/certs/certificate.pem"),
|
|
77
|
-
configurable: true
|
|
78
|
-
});
|
|
79
|
-
const certificateFile = this.certificateFile;
|
|
80
|
-
if (!fs.existsSync(certificateFile)) {
|
|
81
|
-
// this is the first time server is launched
|
|
82
|
-
// let's create a default self signed certificate with limited validity
|
|
83
|
-
const fqdn = getFullyQualifiedDomainName();
|
|
84
|
-
const ipAddresses = getIpAddresses();
|
|
85
|
-
const applicationUri = (this.serverInfo ? this.serverInfo.applicationUri : null) || "uri:MISSING";
|
|
86
|
-
const options = {
|
|
87
|
-
applicationUri,
|
|
88
|
-
dns: [fqdn],
|
|
89
|
-
ip: ipAddresses,
|
|
90
|
-
subject: `/CN=${applicationUri};/L=Paris`,
|
|
91
|
-
startDate: new Date(),
|
|
92
|
-
validity: 365 * 5, // five years
|
|
93
|
-
outputFile: certificateFile
|
|
94
|
-
};
|
|
95
|
-
doDebug && debugLog("creating self signed certificate", options);
|
|
96
|
-
await this.serverCertificateManager.createSelfSignedCertificate(options);
|
|
97
|
-
}
|
|
98
|
-
// Invalidate any previously cached secrets so that
|
|
99
|
-
// getCertificateChain() / getPrivateKey() will re-read from disk.
|
|
100
|
-
invalidateCachedSecrets(this);
|
|
101
|
-
}
|
|
102
|
-
export async function installPushCertificateManagementOnServer(server) {
|
|
103
|
-
if (!server.engine || !server.engine.addressSpace) {
|
|
104
|
-
throw new Error("Server must have a valid address space." +
|
|
105
|
-
"you need to call installPushCertificateManagementOnServer after server has been initialized");
|
|
106
|
-
}
|
|
107
|
-
await install.call(server);
|
|
108
|
-
for (const endpoint of server.endpoints) {
|
|
109
|
-
const endpointPriv = endpoint;
|
|
110
|
-
endpointPriv._certificateChain = null;
|
|
111
|
-
endpointPriv._privateKey = null;
|
|
112
|
-
endpoint.getCertificateChain = getCertificateChainEP;
|
|
113
|
-
endpoint.getPrivateKey = getPrivateKeyEP;
|
|
114
|
-
for (const e of endpoint.endpointDescriptions()) {
|
|
115
|
-
Object.defineProperty(e, "serverCertificate", {
|
|
116
|
-
get: () => combine_der(endpoint.getCertificateChain()),
|
|
117
|
-
configurable: true
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
await installPushCertificateManagement(server.engine.addressSpace, {
|
|
122
|
-
applicationGroup: server.serverCertificateManager,
|
|
123
|
-
userTokenGroup: server.userCertificateManager,
|
|
124
|
-
applicationUri: server.serverInfo.applicationUri || "InvalidURI"
|
|
125
|
-
});
|
|
126
|
-
const serverConfiguration = server.engine.addressSpace.rootFolder.objects.server.getChildByName("ServerConfiguration");
|
|
127
|
-
const serverConfigurationPriv = serverConfiguration;
|
|
128
|
-
assert(serverConfigurationPriv.$pushCertificateManager);
|
|
129
|
-
serverConfigurationPriv.$pushCertificateManager.on("CertificateAboutToChange", (actionQueue) => {
|
|
130
|
-
actionQueue.push(async () => {
|
|
131
|
-
doDebug && debugLog("CertificateAboutToChange Event received");
|
|
132
|
-
await onCertificateAboutToChange(server);
|
|
133
|
-
doDebug && debugLog("CertificateAboutToChange Event processed");
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
serverConfigurationPriv.$pushCertificateManager.on("CertificateChanged", (actionQueue) => {
|
|
137
|
-
actionQueue.push(async () => {
|
|
138
|
-
doDebug && debugLog("CertificateChanged Event received");
|
|
139
|
-
await onCertificateChange(server);
|
|
140
|
-
doDebug && debugLog("CertificateChanged Event processed");
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
//# sourceMappingURL=install_push_certitifate_management.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"install_push_certitifate_management.js","sourceRoot":"","sources":["../../source/server/install_push_certitifate_management.ts"],"names":[],"mappings":"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"}
|