node-opcua-server-configuration 2.176.1 → 2.178.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/server/install_push_certificate_management.d.ts +35 -3
- package/dist/server/install_push_certificate_management.js +118 -27
- package/dist/server/install_push_certificate_management.js.map +1 -1
- package/dist/server/push_certificate_manager/update_certificate.js +32 -3
- package/dist/server/push_certificate_manager/update_certificate.js.map +1 -1
- package/package.json +21 -21
- package/source/server/install_push_certificate_management.ts +174 -58
- package/source/server/push_certificate_manager/update_certificate.ts +34 -6
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AddressSpace } from "node-opcua-address-space";
|
|
2
2
|
import { OPCUACertificateManager } from "node-opcua-certificate-manager";
|
|
3
|
-
import { type ICertificateKeyPairProvider } from "node-opcua-common";
|
|
4
|
-
import {
|
|
3
|
+
import { type ICertificateChainProvider, type ICertificateKeyPairProvider } from "node-opcua-common";
|
|
4
|
+
import type { OPCUAServer } from "node-opcua-server";
|
|
5
5
|
import { type ApplicationDescriptionOptions } from "node-opcua-types";
|
|
6
6
|
export interface OPCUAServerPartial extends ICertificateKeyPairProvider {
|
|
7
7
|
serverInfo?: ApplicationDescriptionOptions;
|
|
@@ -14,5 +14,37 @@ export interface OPCUAServerPartial extends ICertificateKeyPairProvider {
|
|
|
14
14
|
createDefaultCertificate(): Promise<void>;
|
|
15
15
|
setProvider(provider: ICertificateKeyPairProvider): void;
|
|
16
16
|
invalidateCachedCertificates(): void;
|
|
17
|
+
getCertificateChainProvider(): ICertificateChainProvider;
|
|
17
18
|
}
|
|
18
|
-
export
|
|
19
|
+
export interface InstallPushCertificateManagementOnServerOptions {
|
|
20
|
+
/**
|
|
21
|
+
* What happens to secure channels that are already open when
|
|
22
|
+
* `ApplyChanges` installs a new server certificate (and possibly a new
|
|
23
|
+
* private key).
|
|
24
|
+
*
|
|
25
|
+
* - `true` (default): every existing channel is shut down as soon as
|
|
26
|
+
* the ApplyChanges response has been sent. All connected clients
|
|
27
|
+
* reconnect immediately with the new certificate. This is the safe
|
|
28
|
+
* choice when the rotation is a response to a **compromised or
|
|
29
|
+
* revoked** key: nothing keeps running under the old one.
|
|
30
|
+
*
|
|
31
|
+
* - `false`: existing channels are left open and the rotation is
|
|
32
|
+
* **seamless** for them. Each server channel is bound to the
|
|
33
|
+
* certificate/key pair it was created with (OPC UA Part 6 §6.7.2
|
|
34
|
+
* defines the thumbprint check against "the Certificate it is using
|
|
35
|
+
* for the SecureChannel", and §6.7.4 requires the client to close the
|
|
36
|
+
* channel if a response is signed with a different certificate than
|
|
37
|
+
* the request was encrypted to — so the old pair is the only one a
|
|
38
|
+
* Renew on an old channel can conformantly use). Connected clients
|
|
39
|
+
* keep renewing their security token under the old pair, unmodified
|
|
40
|
+
* third-party clients included; new connections use the new pair.
|
|
41
|
+
* Choose this for a **planned** rotation. The old pair remains in use
|
|
42
|
+
* by those channels until they close naturally — which is exactly why
|
|
43
|
+
* `true` stays the default: when the rotation answers a **compromised
|
|
44
|
+
* or revoked** key, nothing must keep running under it.
|
|
45
|
+
*
|
|
46
|
+
* @defaultValue true
|
|
47
|
+
*/
|
|
48
|
+
closeChannelsOnApplyChanges?: boolean;
|
|
49
|
+
}
|
|
50
|
+
export declare function installPushCertificateManagementOnServer(server: OPCUAServer, options?: InstallPushCertificateManagementOnServerOptions): Promise<void>;
|
|
@@ -5,10 +5,10 @@ import path from "node:path";
|
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
import { assert } from "node-opcua-assert";
|
|
7
7
|
import { OPCUACertificateManager } from "node-opcua-certificate-manager";
|
|
8
|
-
import { DiskCertificateKeyPairProvider } from "node-opcua-common";
|
|
9
|
-
import {
|
|
8
|
+
import { DiskCertificateKeyPairProvider, ResolvedCertificateKeyPairProvider, resolvePrivateKeyProviderIfNeeded } from "node-opcua-common";
|
|
9
|
+
import { readPrivateKey } from "node-opcua-crypto";
|
|
10
|
+
import { exploreCertificateInfo, split_der } from "node-opcua-crypto/web";
|
|
10
11
|
import { checkDebugFlag, make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
|
|
11
|
-
import { invalidateServerCertificateCache } from "node-opcua-server";
|
|
12
12
|
import { StatusCodes } from "node-opcua-status-code";
|
|
13
13
|
import { ServerState } from "node-opcua-types";
|
|
14
14
|
import { installPushCertificateManagement } from "./push_certificate_manager_helpers.js";
|
|
@@ -36,22 +36,102 @@ async function onCertificateAboutToChange(server) {
|
|
|
36
36
|
*/
|
|
37
37
|
async function onCertificateChange(server) {
|
|
38
38
|
doDebug && debugLog("on CertificateChanged");
|
|
39
|
-
|
|
39
|
+
await reresolveServerCertificateProvider(server);
|
|
40
40
|
}
|
|
41
41
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* the
|
|
42
|
+
* Resolve the private key immediately after a rotation, bypassing
|
|
43
|
+
* `OPCUACertificateManager.getPrivateKey()`'s cache.
|
|
44
|
+
*
|
|
45
|
+
* `getPrivateKey()` decrypts the on-disk key *once* and caches it for the
|
|
46
|
+
* manager instance's whole lifetime (pki's documented behavior — see
|
|
47
|
+
* `CertificateManager.getPrivateKey`). That's exactly right for a normal
|
|
48
|
+
* startup resolve (`resolvePrivateKeyProviderIfNeeded`, used by
|
|
49
|
+
* `initializeCM()`), but wrong here: `UpdateCertificate`/`ApplyChanges` just
|
|
50
|
+
* replaced the on-disk key file underneath this *same, long-lived*
|
|
51
|
+
* `certificateManager` instance, so a cached call would silently keep
|
|
52
|
+
* returning the pre-rotation key — certificate and key would then belong to
|
|
53
|
+
* different key pairs, and every subsequent handshake fails signature
|
|
54
|
+
* verification.
|
|
55
|
+
*
|
|
56
|
+
* `privateKeyProvider` mode has no such cache — pki consults the injected
|
|
57
|
+
* provider on every call — so this only special-cases `privateKeyPassphrase`
|
|
58
|
+
* (reading the file directly, with the manager's own configured passphrase,
|
|
59
|
+
* instead of going through the memoized `getPrivateKey()`).
|
|
60
|
+
*/
|
|
61
|
+
async function resolvePrivateKeyAfterRotation(certificateManager, privateKeyFile) {
|
|
62
|
+
const passphrase = await certificateManager.getPrivateKeyPassphrase();
|
|
63
|
+
if (passphrase !== undefined) {
|
|
64
|
+
return readPrivateKey(privateKeyFile, passphrase);
|
|
65
|
+
}
|
|
66
|
+
return certificateManager.getPrivateKey();
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Refresh the server's certificate/private-key provider after a rotation,
|
|
70
|
+
* and push the result to every endpoint.
|
|
71
|
+
*
|
|
72
|
+
* When the certificate manager is passphrase-/provider-managed, a
|
|
73
|
+
* synchronous "invalidate, let it lazily re-read the file" (the old
|
|
74
|
+
* behavior) cannot decrypt the key — so the key is instead resolved here,
|
|
75
|
+
* eagerly and asynchronously (see `resolvePrivateKeyAfterRotation`), and the
|
|
76
|
+
* resulting provider handed out explicitly to the server and every
|
|
77
|
+
* endpoint.
|
|
78
|
+
*
|
|
79
|
+
* For a plain (unmanaged) certificate manager, this falls back to exactly
|
|
80
|
+
* the previous behavior: invalidate the server's and every endpoint's
|
|
81
|
+
* cached certificate/key so the next access re-reads the (plaintext) files
|
|
82
|
+
* from disk.
|
|
83
|
+
*/
|
|
84
|
+
async function reresolveServerCertificateProvider(server) {
|
|
85
|
+
const certificateManager = server.serverCertificateManager;
|
|
86
|
+
const isManaged = certificateManager instanceof OPCUACertificateManager && certificateManager.isPrivateKeyManaged();
|
|
87
|
+
if (isManaged) {
|
|
88
|
+
const privateKey = await resolvePrivateKeyAfterRotation(certificateManager, server.privateKeyFile);
|
|
89
|
+
server.setProvider(new ResolvedCertificateKeyPairProvider(server.certificateFile, server.privateKeyFile, privateKey));
|
|
90
|
+
const provider = server.getCertificateChainProvider();
|
|
91
|
+
for (const endpoint of server.endpoints) {
|
|
92
|
+
endpoint.setCertificateProvider(provider);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
server.invalidateCachedCertificates();
|
|
97
|
+
for (const endpoint of server.endpoints) {
|
|
98
|
+
endpoint.invalidateCertificates();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Deferred step, called after the ApplyChanges method response has been
|
|
104
|
+
* sent (so the admin client gets its answer before its own channel is
|
|
105
|
+
* affected).
|
|
106
|
+
*
|
|
107
|
+
* With `closeChannelsOnApplyChanges` (the default) every existing secure
|
|
108
|
+
* channel is shut down immediately, forcing all connected clients to
|
|
109
|
+
* reconnect with the new certificate right away. Without it, existing
|
|
110
|
+
* channels are left alone and continue — including OpenSecureChannel
|
|
111
|
+
* *Renews* — under the certificate/key pair each channel is bound to,
|
|
112
|
+
* while new connections use the new pair. See
|
|
113
|
+
* {@link InstallPushCertificateManagementOnServerOptions.closeChannelsOnApplyChanges}
|
|
114
|
+
* for when each is appropriate.
|
|
115
|
+
*
|
|
116
|
+
* Either way the endpoints, suspended in `onCertificateAboutToChange`,
|
|
117
|
+
* are resumed here.
|
|
46
118
|
*/
|
|
47
|
-
async function onApplyChangesCompleted(server) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
119
|
+
async function onApplyChangesCompleted(server, closeChannels) {
|
|
120
|
+
if (closeChannels) {
|
|
121
|
+
doDebug && debugLog(chalk.yellow(" onApplyChangesCompleted => shutting down channels"));
|
|
122
|
+
await server.shutdownChannels();
|
|
123
|
+
doDebug && debugLog(chalk.yellow(" onApplyChangesCompleted => channels shut down"));
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
doDebug &&
|
|
127
|
+
debugLog(chalk.yellow(" onApplyChangesCompleted => leaving existing channels open (bound to their original pair)"));
|
|
128
|
+
}
|
|
51
129
|
doDebug && debugLog(chalk.yellow(" onApplyChangesCompleted => resuming end points"));
|
|
52
130
|
await server.resumeEndPoints();
|
|
53
131
|
doDebug && debugLog(chalk.yellow(" onApplyChangesCompleted => end points resumed"));
|
|
54
|
-
|
|
132
|
+
if (closeChannels) {
|
|
133
|
+
debugLog(chalk.yellow("channels have been closed -> client should reconnect "));
|
|
134
|
+
}
|
|
55
135
|
}
|
|
56
136
|
/**
|
|
57
137
|
* Redirect the server's certificate provider to the cert manager's
|
|
@@ -62,20 +142,26 @@ async function install() {
|
|
|
62
142
|
doDebug && debugLog("install push certificate management", this.serverCertificateManager.rootDir);
|
|
63
143
|
const certFile = path.join(this.serverCertificateManager.rootDir, CERT_PEM_RELATIVE_PATH);
|
|
64
144
|
const keyFile = this.serverCertificateManager.privateKey;
|
|
65
|
-
// Inject a new disk provider pointing at the cert manager's
|
|
66
|
-
//
|
|
67
|
-
//
|
|
145
|
+
// Inject a new disk provider pointing at the cert manager's paths. The
|
|
146
|
+
// server's certificateFile/privateKeyFile getters now automatically
|
|
147
|
+
// return the new paths. If the key turns out to be passphrase-encrypted,
|
|
148
|
+
// resolvePrivateKeyProviderIfNeeded() below immediately replaces this
|
|
149
|
+
// with a resolved, in-memory provider.
|
|
68
150
|
this.setProvider(new DiskCertificateKeyPairProvider(certFile, keyFile));
|
|
69
151
|
// Delegate to the base server's createDefaultCertificate() which
|
|
70
152
|
// handles DNS (fqdn + hostname + configured), IPs (auto + configured),
|
|
71
153
|
// proper subject via makeSubject(), mutex locking, and file checks.
|
|
72
154
|
await this.createDefaultCertificate();
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
|
|
155
|
+
// Resolve the (possibly passphrase-encrypted) private key once,
|
|
156
|
+
// asynchronously, and install the resolved provider — a synchronous
|
|
157
|
+
// disk re-read (the old invalidateCachedCertificates() behavior) cannot
|
|
158
|
+
// decrypt an encrypted key. For a plaintext key this just replaces the
|
|
159
|
+
// disk provider above with an equivalent resolved one.
|
|
160
|
+
await resolvePrivateKeyProviderIfNeeded(this, this.serverCertificateManager, false);
|
|
76
161
|
}
|
|
77
|
-
export async function installPushCertificateManagementOnServer(server) {
|
|
78
|
-
|
|
162
|
+
export async function installPushCertificateManagementOnServer(server, options) {
|
|
163
|
+
const closeChannelsOnApplyChanges = options?.closeChannelsOnApplyChanges ?? true;
|
|
164
|
+
if (!server.engine?.addressSpace) {
|
|
79
165
|
throw new Error("Server must have a valid address space. " +
|
|
80
166
|
"You need to call installPushCertificateManagementOnServer after server has been initialized");
|
|
81
167
|
}
|
|
@@ -91,10 +177,14 @@ export async function installPushCertificateManagementOnServer(server) {
|
|
|
91
177
|
" serverCertificateManager");
|
|
92
178
|
}
|
|
93
179
|
const cm = server.serverCertificateManager;
|
|
94
|
-
|
|
95
|
-
|
|
180
|
+
// Reuse the server's own provider (installed by install() above, and
|
|
181
|
+
// already resolved if the key is passphrase-encrypted) rather than
|
|
182
|
+
// constructing a fresh DiskCertificateKeyPairProvider from the file
|
|
183
|
+
// paths — a plain disk provider would re-read the file and fail
|
|
184
|
+
// synchronously on an encrypted key.
|
|
185
|
+
const provider = server.getCertificateChainProvider();
|
|
96
186
|
for (const endpoint of server.endpoints) {
|
|
97
|
-
endpoint.setCertificateProvider(
|
|
187
|
+
endpoint.setCertificateProvider(provider);
|
|
98
188
|
}
|
|
99
189
|
await installPushCertificateManagement(server.engine.addressSpace, {
|
|
100
190
|
applicationGroup: cm,
|
|
@@ -119,11 +209,11 @@ export async function installPushCertificateManagementOnServer(server) {
|
|
|
119
209
|
});
|
|
120
210
|
});
|
|
121
211
|
serverConfigurationPriv.$pushCertificateManager.on("applyChangesCompleted", () => {
|
|
122
|
-
// Fire-and-forget: schedule channel teardown + endpoint
|
|
212
|
+
// Fire-and-forget: schedule (optional) channel teardown + endpoint
|
|
123
213
|
// resumption AFTER the method response is sent.
|
|
124
214
|
setImmediate(async () => {
|
|
125
215
|
try {
|
|
126
|
-
await onApplyChangesCompleted(server);
|
|
216
|
+
await onApplyChangesCompleted(server, closeChannelsOnApplyChanges);
|
|
127
217
|
}
|
|
128
218
|
catch (err) {
|
|
129
219
|
errorLog("onApplyChangesCompleted error:", err.message);
|
|
@@ -259,7 +349,8 @@ function installCertificateRelaxationHook(server) {
|
|
|
259
349
|
if (!isRelaxableCertificateError(statusCode)) {
|
|
260
350
|
return statusCode;
|
|
261
351
|
}
|
|
262
|
-
doDebug &&
|
|
352
|
+
doDebug &&
|
|
353
|
+
warningLog(`[NoConfiguration] Relaxing certificate check:`, `${statusCode.toString()} → Good`, "(server is awaiting GDS provisioning)");
|
|
263
354
|
// Auto-trust the leaf certificate; issuer CAs go to issuers/
|
|
264
355
|
await autoTrustCertificateChain(server, certificate);
|
|
265
356
|
return StatusCodes.Good;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"install_push_certificate_management.js","sourceRoot":"","sources":["../../source/server/install_push_certificate_management.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,
|
|
1
|
+
{"version":3,"file":"install_push_certificate_management.js","sourceRoot":"","sources":["../../source/server/install_push_certificate_management.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { assert } from "node-opcua-assert";
|
|
4
|
-
import {
|
|
4
|
+
import { _toExportableKeyObject, exploreCertificate } from "node-opcua-crypto";
|
|
5
5
|
import { certificateMatchesPrivateKey, coercePEMorDerToPrivateKey, coercePrivateKeyPem, combine_der, makeSHA1Thumbprint, toPem } from "node-opcua-crypto/web";
|
|
6
6
|
import { checkDebugFlag, make_debugLog, make_warningLog } from "node-opcua-debug";
|
|
7
7
|
import { StatusCodes } from "node-opcua-status-code";
|
|
@@ -48,12 +48,31 @@ async function preInstallCertificate(serverImpl, certificateManager, certificate
|
|
|
48
48
|
serverImpl.fileTransactionManager.stageFileRemoval(legacyDER);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Encode `privateKeyObj` as a PKCS#8 PEM string, encrypted with `passphrase`
|
|
53
|
+
* (aes-256-cbc — same cipher `writePrivateKeyFile` uses) when given, plaintext
|
|
54
|
+
* otherwise. Never touches disk — the caller stages the resulting string via
|
|
55
|
+
* `FileTransactionManager.stageFile()`, so an encrypted key is the only form
|
|
56
|
+
* that is ever written, whether to the staging temp file or the final
|
|
57
|
+
* destination.
|
|
58
|
+
*/
|
|
59
|
+
function encodePrivateKeyPem(privateKeyObj, passphrase) {
|
|
60
|
+
if (!passphrase) {
|
|
61
|
+
return coercePrivateKeyPem(privateKeyObj);
|
|
62
|
+
}
|
|
63
|
+
const keyObject = _toExportableKeyObject(privateKeyObj);
|
|
64
|
+
return keyObject.export({ type: "pkcs8", format: "pem", cipher: "aes-256-cbc", passphrase });
|
|
65
|
+
}
|
|
51
66
|
// Helper: Stage private key to temporary file
|
|
52
67
|
async function preInstallPrivateKey(serverImpl, certificateManager, privateKeyFormat, privateKey) {
|
|
53
68
|
assert(privateKeyFormat.toUpperCase() === "PEM");
|
|
54
69
|
if (privateKey) {
|
|
55
70
|
const privateKeyObj = coercePEMorDerToPrivateKey(privateKey);
|
|
56
|
-
|
|
71
|
+
// If the certificate manager is configured with a privateKeyPassphrase,
|
|
72
|
+
// the staged (and later applied) key must be encrypted with it — never
|
|
73
|
+
// written back to disk in clear.
|
|
74
|
+
const passphrase = await certificateManager.getPrivateKeyPassphrase?.();
|
|
75
|
+
const privateKeyPEM = encodePrivateKeyPem(privateKeyObj, passphrase);
|
|
57
76
|
await serverImpl.fileTransactionManager.stageFile(certificateManager.privateKey, privateKeyPEM, "utf-8");
|
|
58
77
|
}
|
|
59
78
|
}
|
|
@@ -101,7 +120,17 @@ export async function executeUpdateCertificate(serverImpl, certificateGroupId, c
|
|
|
101
120
|
return { statusCode: StatusCodes.BadInvalidArgument, applyChangesRequired: false };
|
|
102
121
|
}
|
|
103
122
|
if (!hasPrivateKeyFormat && !hasPrivateKey) {
|
|
104
|
-
|
|
123
|
+
// Resolve via getPrivateKey(), for two independent reasons:
|
|
124
|
+
// - CertificateManager.getPrivateKey() caches the decoded key for the lifetime of the
|
|
125
|
+
// instance (see node-opcua-pki). createCertificateRequest()/createSelfSignedCertificate()
|
|
126
|
+
// both go through that cache, so the CSR that produced `certificate` was necessarily
|
|
127
|
+
// signed with the *cached* key. Comparing against a fresh disk read (readPrivateKey
|
|
128
|
+
// on the file path) would compare against whatever is currently on disk instead — which
|
|
129
|
+
// can differ right after a previous updateCertificate() call replaced the private key
|
|
130
|
+
// file directly (bypassing the cache), causing a spurious BadSecurityChecksFailed.
|
|
131
|
+
// - the on-disk file may be an encrypted PKCS#8 key (privateKeyPassphrase); a raw
|
|
132
|
+
// readPrivateKey without the manager's passphrase would fail on it.
|
|
133
|
+
const privateKeyObj = await (serverImpl.tmpCertificateManager ?? certificateManager).getPrivateKey();
|
|
105
134
|
if (!certificateMatchesPrivateKey(certificate, privateKeyObj)) {
|
|
106
135
|
warningLog("certificate doesn't match privateKey");
|
|
107
136
|
await serverImpl.fileTransactionManager.abortTransaction();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update_certificate.js","sourceRoot":"","sources":["../../../source/server/push_certificate_manager/update_certificate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAG3C,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"update_certificate.js","sourceRoot":"","sources":["../../../source/server/push_certificate_manager/update_certificate.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-opcua-server-configuration",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.178.0",
|
|
4
4
|
"description": "pure nodejs OPCUA SDK - module server-configuration",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build": "tsc -b",
|
|
@@ -14,35 +14,35 @@
|
|
|
14
14
|
"type": "module",
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"chalk": "4.1.2",
|
|
17
|
-
"memfs": "^4.
|
|
18
|
-
"node-opcua-address-space": "2.
|
|
19
|
-
"node-opcua-address-space-base": "2.
|
|
17
|
+
"memfs": "^4.68.1",
|
|
18
|
+
"node-opcua-address-space": "2.178.0",
|
|
19
|
+
"node-opcua-address-space-base": "2.178.0",
|
|
20
20
|
"node-opcua-assert": "2.175.6",
|
|
21
|
-
"node-opcua-basic-types": "2.
|
|
21
|
+
"node-opcua-basic-types": "2.177.0",
|
|
22
22
|
"node-opcua-binary-stream": "2.176.0",
|
|
23
|
-
"node-opcua-certificate-manager": "2.
|
|
24
|
-
"node-opcua-common": "2.
|
|
23
|
+
"node-opcua-certificate-manager": "2.178.0",
|
|
24
|
+
"node-opcua-common": "2.178.0",
|
|
25
25
|
"node-opcua-constants": "2.176.0",
|
|
26
|
-
"node-opcua-crypto": "5.
|
|
27
|
-
"node-opcua-data-model": "2.
|
|
26
|
+
"node-opcua-crypto": "5.6.0",
|
|
27
|
+
"node-opcua-data-model": "2.177.0",
|
|
28
28
|
"node-opcua-debug": "2.175.6",
|
|
29
|
-
"node-opcua-file-transfer": "2.
|
|
29
|
+
"node-opcua-file-transfer": "2.178.0",
|
|
30
30
|
"node-opcua-hostname": "2.175.1",
|
|
31
|
-
"node-opcua-nodeid": "2.
|
|
32
|
-
"node-opcua-pki": "6.
|
|
33
|
-
"node-opcua-pseudo-session": "2.
|
|
34
|
-
"node-opcua-secure-channel": "2.
|
|
35
|
-
"node-opcua-server": "2.
|
|
36
|
-
"node-opcua-service-translate-browse-path": "2.
|
|
31
|
+
"node-opcua-nodeid": "2.177.0",
|
|
32
|
+
"node-opcua-pki": "6.20.0",
|
|
33
|
+
"node-opcua-pseudo-session": "2.178.0",
|
|
34
|
+
"node-opcua-secure-channel": "2.178.0",
|
|
35
|
+
"node-opcua-server": "2.178.0",
|
|
36
|
+
"node-opcua-service-translate-browse-path": "2.177.0",
|
|
37
37
|
"node-opcua-status-code": "2.176.0",
|
|
38
|
-
"node-opcua-types": "2.
|
|
38
|
+
"node-opcua-types": "2.177.0",
|
|
39
39
|
"node-opcua-utils": "2.176.0",
|
|
40
|
-
"node-opcua-variant": "2.
|
|
40
|
+
"node-opcua-variant": "2.177.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"bcryptjs": "3.0.3",
|
|
44
|
-
"node-opcua-client": "2.
|
|
45
|
-
"node-opcua-data-value": "2.
|
|
44
|
+
"node-opcua-client": "2.178.0",
|
|
45
|
+
"node-opcua-data-value": "2.177.0",
|
|
46
46
|
"node-opcua-leak-detector": "2.175.6",
|
|
47
47
|
"node-opcua-nodesets": "2.175.3"
|
|
48
48
|
},
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"internet of things"
|
|
62
62
|
],
|
|
63
63
|
"homepage": "http://node-opcua.github.io/",
|
|
64
|
-
"gitHead": "
|
|
64
|
+
"gitHead": "d0e850e5f7df80e14ceb6d8e092e793472965301",
|
|
65
65
|
"files": [
|
|
66
66
|
"dist",
|
|
67
67
|
"source"
|
|
@@ -8,10 +8,17 @@ import chalk from "chalk";
|
|
|
8
8
|
import type { AddressSpace, UAServerConfiguration } from "node-opcua-address-space";
|
|
9
9
|
import { assert } from "node-opcua-assert";
|
|
10
10
|
import { OPCUACertificateManager } from "node-opcua-certificate-manager";
|
|
11
|
-
import {
|
|
12
|
-
|
|
11
|
+
import {
|
|
12
|
+
DiskCertificateKeyPairProvider,
|
|
13
|
+
type ICertificateChainProvider,
|
|
14
|
+
type ICertificateKeyPairProvider,
|
|
15
|
+
ResolvedCertificateKeyPairProvider,
|
|
16
|
+
resolvePrivateKeyProviderIfNeeded
|
|
17
|
+
} from "node-opcua-common";
|
|
18
|
+
import { readPrivateKey } from "node-opcua-crypto";
|
|
19
|
+
import { type Certificate, exploreCertificateInfo, split_der } from "node-opcua-crypto/web";
|
|
13
20
|
import { checkDebugFlag, make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
|
|
14
|
-
import {
|
|
21
|
+
import type { OPCUAServer, OPCUAServerEndPoint } from "node-opcua-server";
|
|
15
22
|
import { type StatusCode, StatusCodes } from "node-opcua-status-code";
|
|
16
23
|
import { type ApplicationDescriptionOptions, ServerState } from "node-opcua-types";
|
|
17
24
|
|
|
@@ -35,6 +42,7 @@ export interface OPCUAServerPartial extends ICertificateKeyPairProvider {
|
|
|
35
42
|
createDefaultCertificate(): Promise<void>;
|
|
36
43
|
setProvider(provider: ICertificateKeyPairProvider): void;
|
|
37
44
|
invalidateCachedCertificates(): void;
|
|
45
|
+
getCertificateChainProvider(): ICertificateChainProvider;
|
|
38
46
|
}
|
|
39
47
|
|
|
40
48
|
async function onCertificateAboutToChange(server: OPCUAServer) {
|
|
@@ -56,25 +64,105 @@ async function onCertificateAboutToChange(server: OPCUAServer) {
|
|
|
56
64
|
*/
|
|
57
65
|
async function onCertificateChange(server: OPCUAServer) {
|
|
58
66
|
doDebug && debugLog("on CertificateChanged");
|
|
59
|
-
|
|
67
|
+
await reresolveServerCertificateProvider(server);
|
|
60
68
|
}
|
|
61
69
|
|
|
62
70
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* the
|
|
71
|
+
* Resolve the private key immediately after a rotation, bypassing
|
|
72
|
+
* `OPCUACertificateManager.getPrivateKey()`'s cache.
|
|
73
|
+
*
|
|
74
|
+
* `getPrivateKey()` decrypts the on-disk key *once* and caches it for the
|
|
75
|
+
* manager instance's whole lifetime (pki's documented behavior — see
|
|
76
|
+
* `CertificateManager.getPrivateKey`). That's exactly right for a normal
|
|
77
|
+
* startup resolve (`resolvePrivateKeyProviderIfNeeded`, used by
|
|
78
|
+
* `initializeCM()`), but wrong here: `UpdateCertificate`/`ApplyChanges` just
|
|
79
|
+
* replaced the on-disk key file underneath this *same, long-lived*
|
|
80
|
+
* `certificateManager` instance, so a cached call would silently keep
|
|
81
|
+
* returning the pre-rotation key — certificate and key would then belong to
|
|
82
|
+
* different key pairs, and every subsequent handshake fails signature
|
|
83
|
+
* verification.
|
|
84
|
+
*
|
|
85
|
+
* `privateKeyProvider` mode has no such cache — pki consults the injected
|
|
86
|
+
* provider on every call — so this only special-cases `privateKeyPassphrase`
|
|
87
|
+
* (reading the file directly, with the manager's own configured passphrase,
|
|
88
|
+
* instead of going through the memoized `getPrivateKey()`).
|
|
89
|
+
*/
|
|
90
|
+
async function resolvePrivateKeyAfterRotation(certificateManager: OPCUACertificateManager, privateKeyFile: string) {
|
|
91
|
+
const passphrase = await certificateManager.getPrivateKeyPassphrase();
|
|
92
|
+
if (passphrase !== undefined) {
|
|
93
|
+
return readPrivateKey(privateKeyFile, passphrase);
|
|
94
|
+
}
|
|
95
|
+
return certificateManager.getPrivateKey();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Refresh the server's certificate/private-key provider after a rotation,
|
|
100
|
+
* and push the result to every endpoint.
|
|
101
|
+
*
|
|
102
|
+
* When the certificate manager is passphrase-/provider-managed, a
|
|
103
|
+
* synchronous "invalidate, let it lazily re-read the file" (the old
|
|
104
|
+
* behavior) cannot decrypt the key — so the key is instead resolved here,
|
|
105
|
+
* eagerly and asynchronously (see `resolvePrivateKeyAfterRotation`), and the
|
|
106
|
+
* resulting provider handed out explicitly to the server and every
|
|
107
|
+
* endpoint.
|
|
108
|
+
*
|
|
109
|
+
* For a plain (unmanaged) certificate manager, this falls back to exactly
|
|
110
|
+
* the previous behavior: invalidate the server's and every endpoint's
|
|
111
|
+
* cached certificate/key so the next access re-reads the (plaintext) files
|
|
112
|
+
* from disk.
|
|
113
|
+
*/
|
|
114
|
+
async function reresolveServerCertificateProvider(server: OPCUAServer): Promise<void> {
|
|
115
|
+
const certificateManager = server.serverCertificateManager;
|
|
116
|
+
const isManaged = certificateManager instanceof OPCUACertificateManager && certificateManager.isPrivateKeyManaged();
|
|
117
|
+
if (isManaged) {
|
|
118
|
+
const privateKey = await resolvePrivateKeyAfterRotation(certificateManager, server.privateKeyFile);
|
|
119
|
+
server.setProvider(new ResolvedCertificateKeyPairProvider(server.certificateFile, server.privateKeyFile, privateKey));
|
|
120
|
+
const provider = server.getCertificateChainProvider();
|
|
121
|
+
for (const endpoint of server.endpoints) {
|
|
122
|
+
endpoint.setCertificateProvider(provider);
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
server.invalidateCachedCertificates();
|
|
126
|
+
for (const endpoint of server.endpoints) {
|
|
127
|
+
endpoint.invalidateCertificates();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Deferred step, called after the ApplyChanges method response has been
|
|
134
|
+
* sent (so the admin client gets its answer before its own channel is
|
|
135
|
+
* affected).
|
|
136
|
+
*
|
|
137
|
+
* With `closeChannelsOnApplyChanges` (the default) every existing secure
|
|
138
|
+
* channel is shut down immediately, forcing all connected clients to
|
|
139
|
+
* reconnect with the new certificate right away. Without it, existing
|
|
140
|
+
* channels are left alone and continue — including OpenSecureChannel
|
|
141
|
+
* *Renews* — under the certificate/key pair each channel is bound to,
|
|
142
|
+
* while new connections use the new pair. See
|
|
143
|
+
* {@link InstallPushCertificateManagementOnServerOptions.closeChannelsOnApplyChanges}
|
|
144
|
+
* for when each is appropriate.
|
|
145
|
+
*
|
|
146
|
+
* Either way the endpoints, suspended in `onCertificateAboutToChange`,
|
|
147
|
+
* are resumed here.
|
|
67
148
|
*/
|
|
68
|
-
async function onApplyChangesCompleted(server: OPCUAServer) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
149
|
+
async function onApplyChangesCompleted(server: OPCUAServer, closeChannels: boolean) {
|
|
150
|
+
if (closeChannels) {
|
|
151
|
+
doDebug && debugLog(chalk.yellow(" onApplyChangesCompleted => shutting down channels"));
|
|
152
|
+
await server.shutdownChannels();
|
|
153
|
+
doDebug && debugLog(chalk.yellow(" onApplyChangesCompleted => channels shut down"));
|
|
154
|
+
} else {
|
|
155
|
+
doDebug &&
|
|
156
|
+
debugLog(chalk.yellow(" onApplyChangesCompleted => leaving existing channels open (bound to their original pair)"));
|
|
157
|
+
}
|
|
72
158
|
|
|
73
159
|
doDebug && debugLog(chalk.yellow(" onApplyChangesCompleted => resuming end points"));
|
|
74
160
|
await server.resumeEndPoints();
|
|
75
161
|
doDebug && debugLog(chalk.yellow(" onApplyChangesCompleted => end points resumed"));
|
|
76
162
|
|
|
77
|
-
|
|
163
|
+
if (closeChannels) {
|
|
164
|
+
debugLog(chalk.yellow("channels have been closed -> client should reconnect "));
|
|
165
|
+
}
|
|
78
166
|
}
|
|
79
167
|
|
|
80
168
|
/**
|
|
@@ -88,9 +176,11 @@ async function install(this: OPCUAServerPartial): Promise<void> {
|
|
|
88
176
|
const certFile = path.join(this.serverCertificateManager.rootDir, CERT_PEM_RELATIVE_PATH);
|
|
89
177
|
const keyFile = this.serverCertificateManager.privateKey;
|
|
90
178
|
|
|
91
|
-
// Inject a new disk provider pointing at the cert manager's
|
|
92
|
-
//
|
|
93
|
-
//
|
|
179
|
+
// Inject a new disk provider pointing at the cert manager's paths. The
|
|
180
|
+
// server's certificateFile/privateKeyFile getters now automatically
|
|
181
|
+
// return the new paths. If the key turns out to be passphrase-encrypted,
|
|
182
|
+
// resolvePrivateKeyProviderIfNeeded() below immediately replaces this
|
|
183
|
+
// with a resolved, in-memory provider.
|
|
94
184
|
this.setProvider(new DiskCertificateKeyPairProvider(certFile, keyFile));
|
|
95
185
|
|
|
96
186
|
// Delegate to the base server's createDefaultCertificate() which
|
|
@@ -98,20 +188,59 @@ async function install(this: OPCUAServerPartial): Promise<void> {
|
|
|
98
188
|
// proper subject via makeSubject(), mutex locking, and file checks.
|
|
99
189
|
await this.createDefaultCertificate();
|
|
100
190
|
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
|
|
191
|
+
// Resolve the (possibly passphrase-encrypted) private key once,
|
|
192
|
+
// asynchronously, and install the resolved provider — a synchronous
|
|
193
|
+
// disk re-read (the old invalidateCachedCertificates() behavior) cannot
|
|
194
|
+
// decrypt an encrypted key. For a plaintext key this just replaces the
|
|
195
|
+
// disk provider above with an equivalent resolved one.
|
|
196
|
+
await resolvePrivateKeyProviderIfNeeded(this, this.serverCertificateManager, false);
|
|
104
197
|
}
|
|
105
198
|
|
|
106
199
|
interface UAServerConfigurationEx extends UAServerConfiguration {
|
|
107
200
|
$pushCertificateManager: PushCertificateManagerServerImpl;
|
|
108
201
|
}
|
|
109
202
|
|
|
110
|
-
export
|
|
111
|
-
|
|
203
|
+
export interface InstallPushCertificateManagementOnServerOptions {
|
|
204
|
+
/**
|
|
205
|
+
* What happens to secure channels that are already open when
|
|
206
|
+
* `ApplyChanges` installs a new server certificate (and possibly a new
|
|
207
|
+
* private key).
|
|
208
|
+
*
|
|
209
|
+
* - `true` (default): every existing channel is shut down as soon as
|
|
210
|
+
* the ApplyChanges response has been sent. All connected clients
|
|
211
|
+
* reconnect immediately with the new certificate. This is the safe
|
|
212
|
+
* choice when the rotation is a response to a **compromised or
|
|
213
|
+
* revoked** key: nothing keeps running under the old one.
|
|
214
|
+
*
|
|
215
|
+
* - `false`: existing channels are left open and the rotation is
|
|
216
|
+
* **seamless** for them. Each server channel is bound to the
|
|
217
|
+
* certificate/key pair it was created with (OPC UA Part 6 §6.7.2
|
|
218
|
+
* defines the thumbprint check against "the Certificate it is using
|
|
219
|
+
* for the SecureChannel", and §6.7.4 requires the client to close the
|
|
220
|
+
* channel if a response is signed with a different certificate than
|
|
221
|
+
* the request was encrypted to — so the old pair is the only one a
|
|
222
|
+
* Renew on an old channel can conformantly use). Connected clients
|
|
223
|
+
* keep renewing their security token under the old pair, unmodified
|
|
224
|
+
* third-party clients included; new connections use the new pair.
|
|
225
|
+
* Choose this for a **planned** rotation. The old pair remains in use
|
|
226
|
+
* by those channels until they close naturally — which is exactly why
|
|
227
|
+
* `true` stays the default: when the rotation answers a **compromised
|
|
228
|
+
* or revoked** key, nothing must keep running under it.
|
|
229
|
+
*
|
|
230
|
+
* @defaultValue true
|
|
231
|
+
*/
|
|
232
|
+
closeChannelsOnApplyChanges?: boolean;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function installPushCertificateManagementOnServer(
|
|
236
|
+
server: OPCUAServer,
|
|
237
|
+
options?: InstallPushCertificateManagementOnServerOptions
|
|
238
|
+
): Promise<void> {
|
|
239
|
+
const closeChannelsOnApplyChanges = options?.closeChannelsOnApplyChanges ?? true;
|
|
240
|
+
if (!server.engine?.addressSpace) {
|
|
112
241
|
throw new Error(
|
|
113
242
|
"Server must have a valid address space. " +
|
|
114
|
-
|
|
243
|
+
"You need to call installPushCertificateManagementOnServer after server has been initialized"
|
|
115
244
|
);
|
|
116
245
|
}
|
|
117
246
|
await install.call(server as unknown as OPCUAServerPartial);
|
|
@@ -124,15 +253,19 @@ export async function installPushCertificateManagementOnServer(server: OPCUAServ
|
|
|
124
253
|
if (!(server.serverCertificateManager instanceof OPCUACertificateManager)) {
|
|
125
254
|
throw new Error(
|
|
126
255
|
"installPushCertificateManagementOnServer requires a" +
|
|
127
|
-
|
|
128
|
-
|
|
256
|
+
" disk-based OPCUACertificateManager as" +
|
|
257
|
+
" serverCertificateManager"
|
|
129
258
|
);
|
|
130
259
|
}
|
|
131
260
|
const cm = server.serverCertificateManager;
|
|
132
|
-
|
|
133
|
-
|
|
261
|
+
// Reuse the server's own provider (installed by install() above, and
|
|
262
|
+
// already resolved if the key is passphrase-encrypted) rather than
|
|
263
|
+
// constructing a fresh DiskCertificateKeyPairProvider from the file
|
|
264
|
+
// paths — a plain disk provider would re-read the file and fail
|
|
265
|
+
// synchronously on an encrypted key.
|
|
266
|
+
const provider = server.getCertificateChainProvider();
|
|
134
267
|
for (const endpoint of server.endpoints) {
|
|
135
|
-
endpoint.setCertificateProvider(
|
|
268
|
+
endpoint.setCertificateProvider(provider);
|
|
136
269
|
}
|
|
137
270
|
|
|
138
271
|
await installPushCertificateManagement(server.engine.addressSpace, {
|
|
@@ -162,11 +295,11 @@ export async function installPushCertificateManagementOnServer(server: OPCUAServ
|
|
|
162
295
|
});
|
|
163
296
|
|
|
164
297
|
serverConfigurationPriv.$pushCertificateManager.on("applyChangesCompleted", () => {
|
|
165
|
-
// Fire-and-forget: schedule channel teardown + endpoint
|
|
298
|
+
// Fire-and-forget: schedule (optional) channel teardown + endpoint
|
|
166
299
|
// resumption AFTER the method response is sent.
|
|
167
300
|
setImmediate(async () => {
|
|
168
301
|
try {
|
|
169
|
-
await onApplyChangesCompleted(server);
|
|
302
|
+
await onApplyChangesCompleted(server, closeChannelsOnApplyChanges);
|
|
170
303
|
} catch (err) {
|
|
171
304
|
errorLog("onApplyChangesCompleted error:", (err as Error).message);
|
|
172
305
|
}
|
|
@@ -238,18 +371,12 @@ function isRelaxableCertificateError(statusCode: StatusCode): boolean {
|
|
|
238
371
|
* connection from unintentionally granting trust to every
|
|
239
372
|
* certificate signed by the same CA.
|
|
240
373
|
*/
|
|
241
|
-
async function autoTrustCertificateChain(
|
|
242
|
-
server: OPCUAServer,
|
|
243
|
-
certificate: Certificate
|
|
244
|
-
): Promise<void> {
|
|
374
|
+
async function autoTrustCertificateChain(server: OPCUAServer, certificate: Certificate): Promise<void> {
|
|
245
375
|
let chain: Certificate[];
|
|
246
376
|
try {
|
|
247
377
|
chain = split_der(certificate);
|
|
248
378
|
} catch (err) {
|
|
249
|
-
warningLog(
|
|
250
|
-
"[NoConfiguration] Cannot parse certificate chain for auto-trust:",
|
|
251
|
-
(err as Error).message
|
|
252
|
-
);
|
|
379
|
+
warningLog("[NoConfiguration] Cannot parse certificate chain for auto-trust:", (err as Error).message);
|
|
253
380
|
return;
|
|
254
381
|
}
|
|
255
382
|
|
|
@@ -263,10 +390,7 @@ async function autoTrustCertificateChain(
|
|
|
263
390
|
try {
|
|
264
391
|
exploreCertificateInfo(cert);
|
|
265
392
|
} catch (err) {
|
|
266
|
-
warningLog(
|
|
267
|
-
"[NoConfiguration] Skipping invalid certificate in chain for auto-trust:",
|
|
268
|
-
(err as Error).message
|
|
269
|
-
);
|
|
393
|
+
warningLog("[NoConfiguration] Skipping invalid certificate in chain for auto-trust:", (err as Error).message);
|
|
270
394
|
continue;
|
|
271
395
|
}
|
|
272
396
|
|
|
@@ -278,10 +402,7 @@ async function autoTrustCertificateChain(
|
|
|
278
402
|
// ENOENT can happen if another concurrent call already
|
|
279
403
|
// moved the cert from rejected to trusted.
|
|
280
404
|
if ((err as Error & { code?: string }).code !== "ENOENT") {
|
|
281
|
-
warningLog(
|
|
282
|
-
"[NoConfiguration] Failed to auto-trust leaf certificate:",
|
|
283
|
-
(err as Error).message
|
|
284
|
-
);
|
|
405
|
+
warningLog("[NoConfiguration] Failed to auto-trust leaf certificate:", (err as Error).message);
|
|
285
406
|
}
|
|
286
407
|
}
|
|
287
408
|
} else {
|
|
@@ -290,10 +411,7 @@ async function autoTrustCertificateChain(
|
|
|
290
411
|
try {
|
|
291
412
|
await cm.addIssuer(cert);
|
|
292
413
|
} catch (err) {
|
|
293
|
-
warningLog(
|
|
294
|
-
"[NoConfiguration] Failed to add issuer certificate:",
|
|
295
|
-
(err as Error).message
|
|
296
|
-
);
|
|
414
|
+
warningLog("[NoConfiguration] Failed to add issuer certificate:", (err as Error).message);
|
|
297
415
|
}
|
|
298
416
|
}
|
|
299
417
|
}
|
|
@@ -312,10 +430,7 @@ async function autoTrustCertificateChain(
|
|
|
312
430
|
* building but do not become trust anchors.
|
|
313
431
|
*/
|
|
314
432
|
function installCertificateRelaxationHook(server: OPCUAServer): void {
|
|
315
|
-
const adjustCertificateStatus = async (
|
|
316
|
-
statusCode: StatusCode,
|
|
317
|
-
certificate: Certificate
|
|
318
|
-
): Promise<StatusCode> => {
|
|
433
|
+
const adjustCertificateStatus = async (statusCode: StatusCode, certificate: Certificate): Promise<StatusCode> => {
|
|
319
434
|
// Only relax in NoConfiguration state
|
|
320
435
|
if (server.engine.getServerState() !== ServerState.NoConfiguration) {
|
|
321
436
|
return statusCode;
|
|
@@ -326,11 +441,12 @@ function installCertificateRelaxationHook(server: OPCUAServer): void {
|
|
|
326
441
|
return statusCode;
|
|
327
442
|
}
|
|
328
443
|
|
|
329
|
-
doDebug &&
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
444
|
+
doDebug &&
|
|
445
|
+
warningLog(
|
|
446
|
+
`[NoConfiguration] Relaxing certificate check:`,
|
|
447
|
+
`${statusCode.toString()} → Good`,
|
|
448
|
+
"(server is awaiting GDS provisioning)"
|
|
449
|
+
);
|
|
334
450
|
|
|
335
451
|
// Auto-trust the leaf certificate; issuer CAs go to issuers/
|
|
336
452
|
await autoTrustCertificateChain(server, certificate);
|
|
@@ -3,10 +3,10 @@ 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 { _toExportableKeyObject, exploreCertificate } from "node-opcua-crypto";
|
|
7
7
|
import {
|
|
8
|
-
certificateMatchesPrivateKey,
|
|
9
8
|
type Certificate,
|
|
9
|
+
certificateMatchesPrivateKey,
|
|
10
10
|
coercePEMorDerToPrivateKey,
|
|
11
11
|
coercePrivateKeyPem,
|
|
12
12
|
combine_der,
|
|
@@ -83,6 +83,22 @@ async function preInstallCertificate(
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Encode `privateKeyObj` as a PKCS#8 PEM string, encrypted with `passphrase`
|
|
88
|
+
* (aes-256-cbc — same cipher `writePrivateKeyFile` uses) when given, plaintext
|
|
89
|
+
* otherwise. Never touches disk — the caller stages the resulting string via
|
|
90
|
+
* `FileTransactionManager.stageFile()`, so an encrypted key is the only form
|
|
91
|
+
* that is ever written, whether to the staging temp file or the final
|
|
92
|
+
* destination.
|
|
93
|
+
*/
|
|
94
|
+
function encodePrivateKeyPem(privateKeyObj: PrivateKey, passphrase: string | undefined): string {
|
|
95
|
+
if (!passphrase) {
|
|
96
|
+
return coercePrivateKeyPem(privateKeyObj);
|
|
97
|
+
}
|
|
98
|
+
const keyObject = _toExportableKeyObject(privateKeyObj);
|
|
99
|
+
return keyObject.export({ type: "pkcs8", format: "pem", cipher: "aes-256-cbc", passphrase }) as string;
|
|
100
|
+
}
|
|
101
|
+
|
|
86
102
|
// Helper: Stage private key to temporary file
|
|
87
103
|
async function preInstallPrivateKey(
|
|
88
104
|
serverImpl: PushCertificateManagerInternalContext,
|
|
@@ -94,7 +110,11 @@ async function preInstallPrivateKey(
|
|
|
94
110
|
|
|
95
111
|
if (privateKey) {
|
|
96
112
|
const privateKeyObj = coercePEMorDerToPrivateKey(privateKey as string | Buffer);
|
|
97
|
-
|
|
113
|
+
// If the certificate manager is configured with a privateKeyPassphrase,
|
|
114
|
+
// the staged (and later applied) key must be encrypted with it — never
|
|
115
|
+
// written back to disk in clear.
|
|
116
|
+
const passphrase = await (certificateManager as OPCUACertificateManager).getPrivateKeyPassphrase?.();
|
|
117
|
+
const privateKeyPEM = encodePrivateKeyPem(privateKeyObj, passphrase);
|
|
98
118
|
await serverImpl.fileTransactionManager.stageFile(certificateManager.privateKey, privateKeyPEM, "utf-8");
|
|
99
119
|
}
|
|
100
120
|
}
|
|
@@ -170,9 +190,17 @@ export async function executeUpdateCertificate(
|
|
|
170
190
|
}
|
|
171
191
|
|
|
172
192
|
if (!hasPrivateKeyFormat && !hasPrivateKey) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
)
|
|
193
|
+
// Resolve via getPrivateKey(), for two independent reasons:
|
|
194
|
+
// - CertificateManager.getPrivateKey() caches the decoded key for the lifetime of the
|
|
195
|
+
// instance (see node-opcua-pki). createCertificateRequest()/createSelfSignedCertificate()
|
|
196
|
+
// both go through that cache, so the CSR that produced `certificate` was necessarily
|
|
197
|
+
// signed with the *cached* key. Comparing against a fresh disk read (readPrivateKey
|
|
198
|
+
// on the file path) would compare against whatever is currently on disk instead — which
|
|
199
|
+
// can differ right after a previous updateCertificate() call replaced the private key
|
|
200
|
+
// file directly (bypassing the cache), causing a spurious BadSecurityChecksFailed.
|
|
201
|
+
// - the on-disk file may be an encrypted PKCS#8 key (privateKeyPassphrase); a raw
|
|
202
|
+
// readPrivateKey without the manager's passphrase would fail on it.
|
|
203
|
+
const privateKeyObj = await (serverImpl.tmpCertificateManager ?? certificateManager).getPrivateKey();
|
|
176
204
|
|
|
177
205
|
if (!certificateMatchesPrivateKey(certificate, privateKeyObj)) {
|
|
178
206
|
warningLog("certificate doesn't match privateKey");
|