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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-opcua-server-configuration",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.169.0",
|
|
4
4
|
"description": "pure nodejs OPCUA SDK - module server-configuration",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build": "tsc -b",
|
|
@@ -15,36 +15,34 @@
|
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"chalk": "4.1.2",
|
|
17
17
|
"memfs": "^4.57.1",
|
|
18
|
-
"node-opcua-address-space": "2.
|
|
19
|
-
"node-opcua-address-space-base": "2.
|
|
18
|
+
"node-opcua-address-space": "2.169.0",
|
|
19
|
+
"node-opcua-address-space-base": "2.169.0",
|
|
20
20
|
"node-opcua-assert": "2.164.0",
|
|
21
|
-
"node-opcua-basic-types": "2.
|
|
22
|
-
"node-opcua-binary-stream": "2.
|
|
23
|
-
"node-opcua-certificate-manager": "2.
|
|
24
|
-
"node-opcua-common": "2.
|
|
21
|
+
"node-opcua-basic-types": "2.169.0",
|
|
22
|
+
"node-opcua-binary-stream": "2.169.0",
|
|
23
|
+
"node-opcua-certificate-manager": "2.169.0",
|
|
24
|
+
"node-opcua-common": "2.169.0",
|
|
25
25
|
"node-opcua-constants": "2.157.0",
|
|
26
|
-
"node-opcua-crypto": "5.3.
|
|
27
|
-
"node-opcua-data-model": "2.
|
|
28
|
-
"node-opcua-debug": "2.
|
|
29
|
-
"node-opcua-file-transfer": "2.
|
|
26
|
+
"node-opcua-crypto": "5.3.5",
|
|
27
|
+
"node-opcua-data-model": "2.169.0",
|
|
28
|
+
"node-opcua-debug": "2.168.0",
|
|
29
|
+
"node-opcua-file-transfer": "2.169.0",
|
|
30
30
|
"node-opcua-hostname": "2.167.0",
|
|
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.
|
|
37
|
-
"node-opcua-status-code": "2.
|
|
38
|
-
"node-opcua-types": "2.
|
|
39
|
-
"node-opcua-variant": "2.
|
|
31
|
+
"node-opcua-nodeid": "2.169.0",
|
|
32
|
+
"node-opcua-pki": "6.13.0",
|
|
33
|
+
"node-opcua-pseudo-session": "2.169.0",
|
|
34
|
+
"node-opcua-secure-channel": "2.169.0",
|
|
35
|
+
"node-opcua-server": "2.169.0",
|
|
36
|
+
"node-opcua-service-translate-browse-path": "2.169.0",
|
|
37
|
+
"node-opcua-status-code": "2.169.0",
|
|
38
|
+
"node-opcua-types": "2.169.0",
|
|
39
|
+
"node-opcua-variant": "2.169.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@types/mocha": "^10.0.10",
|
|
43
42
|
"bcryptjs": "3.0.3",
|
|
44
|
-
"
|
|
45
|
-
"node-opcua-
|
|
46
|
-
"node-opcua-
|
|
47
|
-
"node-opcua-leak-detector": "2.165.0",
|
|
43
|
+
"node-opcua-client": "2.169.0",
|
|
44
|
+
"node-opcua-data-value": "2.169.0",
|
|
45
|
+
"node-opcua-leak-detector": "2.169.0",
|
|
48
46
|
"node-opcua-nodesets": "2.163.1"
|
|
49
47
|
},
|
|
50
48
|
"author": "Etienne Rossignon",
|
|
@@ -62,7 +60,7 @@
|
|
|
62
60
|
"internet of things"
|
|
63
61
|
],
|
|
64
62
|
"homepage": "http://node-opcua.github.io/",
|
|
65
|
-
"gitHead": "
|
|
63
|
+
"gitHead": "82d570d3e95bea689cbbe30096279885c5282245",
|
|
66
64
|
"files": [
|
|
67
65
|
"dist",
|
|
68
66
|
"source"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { ByteString } from "node-opcua-basic-types";
|
|
5
5
|
import { BinaryStream } from "node-opcua-binary-stream";
|
|
6
|
-
import {
|
|
6
|
+
import { type Certificate, combine_der } from "node-opcua-crypto/web";
|
|
7
7
|
import { AttributeIds, coerceQualifiedName, type QualifiedNameLike } from "node-opcua-data-model";
|
|
8
8
|
import { ClientFile, OpenFileMode } from "node-opcua-file-transfer";
|
|
9
9
|
import { NodeId, resolveNodeId } from "node-opcua-nodeid";
|
|
@@ -141,10 +141,7 @@ export class TrustListClient extends ClientFile implements ITrustList {
|
|
|
141
141
|
return callMethodResult.outputArguments?.[0].value as boolean;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
async addCertificate(
|
|
145
|
-
certificateChain: Certificate | Certificate[],
|
|
146
|
-
isTrustedCertificate: boolean
|
|
147
|
-
): Promise<StatusCode> {
|
|
144
|
+
async addCertificate(certificateChain: Certificate | Certificate[], isTrustedCertificate: boolean): Promise<StatusCode> {
|
|
148
145
|
await this.ensureInitialized();
|
|
149
146
|
|
|
150
147
|
if (Array.isArray(certificateChain)) {
|
|
@@ -153,8 +150,7 @@ export class TrustListClient extends ClientFile implements ITrustList {
|
|
|
153
150
|
}
|
|
154
151
|
}
|
|
155
152
|
|
|
156
|
-
const compositeCertificate: Buffer =
|
|
157
|
-
Array.isArray(certificateChain) ? combine_der(certificateChain) : certificateChain;
|
|
153
|
+
const compositeCertificate: Buffer = Array.isArray(certificateChain) ? combine_der(certificateChain) : certificateChain;
|
|
158
154
|
|
|
159
155
|
const inputArguments: VariantLike[] = [
|
|
160
156
|
{ dataType: DataType.ByteString, value: compositeCertificate },
|
|
@@ -223,7 +219,7 @@ export class CertificateGroup {
|
|
|
223
219
|
constructor(
|
|
224
220
|
public session: IBasicSessionAsync,
|
|
225
221
|
public nodeId: NodeId
|
|
226
|
-
) {
|
|
222
|
+
) {}
|
|
227
223
|
async getCertificateTypes(): Promise<NodeId[]> {
|
|
228
224
|
const browsePathResult = await this.session.translateBrowsePath(makeBrowsePath(this.nodeId, "/CertificateTypes"));
|
|
229
225
|
if (browsePathResult.statusCode.isNotGood()) {
|
package/source/index.ts
CHANGED
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
export * from "./clientTools/certificate_types.js";
|
|
7
7
|
export * from "./clientTools/push_certificate_management_client.js";
|
|
8
8
|
export * from "./push_certificate_manager.js";
|
|
9
|
-
export * from "./server/
|
|
9
|
+
export * from "./server/install_push_certificate_management.js";
|
|
10
10
|
export * from "./server/promote_trust_list.js";
|
|
11
11
|
export * from "./server/push_certificate_manager/subject_to_string.js";
|
|
12
12
|
export * from "./server/push_certificate_manager_helpers.js";
|
|
13
13
|
export * from "./server/push_certificate_manager_server_impl.js";
|
|
14
|
+
export * from "./server/trust_list_server.js";
|
|
14
15
|
export * from "./standard_certificate_types.js";
|
|
@@ -93,6 +93,31 @@ export class FileTransactionManager {
|
|
|
93
93
|
this.addFileOp(() => this.#moveFileWithBackupTracked(tempFilePath, destinationPath));
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Stages a file for deletion during the transaction.
|
|
98
|
+
*
|
|
99
|
+
* The file is backed up before removal so it can be restored
|
|
100
|
+
* if the transaction is rolled back. If the file does not
|
|
101
|
+
* exist at apply time the operation is silently skipped.
|
|
102
|
+
*
|
|
103
|
+
* @param filePath - absolute path of the file to remove
|
|
104
|
+
*/
|
|
105
|
+
public stageFileRemoval(filePath: string): void {
|
|
106
|
+
this.addFileOp(async () => {
|
|
107
|
+
if (!fs.existsSync(filePath)) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// Create a backup before deleting so rollback can restore it
|
|
111
|
+
const tmpDir = await this.getTmpDir();
|
|
112
|
+
const uniqueFileName = `${crypto.randomBytes(16).toString("hex")}_backup.tmp`;
|
|
113
|
+
const backupPath = path.join(tmpDir, uniqueFileName);
|
|
114
|
+
this.#backupFiles.set(filePath, backupPath);
|
|
115
|
+
|
|
116
|
+
await _copyFile(filePath, backupPath);
|
|
117
|
+
await _deleteFile(filePath);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
96
121
|
public addFileOp(functor: Functor): void {
|
|
97
122
|
this.#pendingFileOps.push(functor);
|
|
98
123
|
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module node-opcua-server-configuration-server
|
|
3
|
+
*/
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
|
|
8
|
+
import type { AddressSpace, UAServerConfiguration } from "node-opcua-address-space";
|
|
9
|
+
import { assert } from "node-opcua-assert";
|
|
10
|
+
import type { OPCUACertificateManager } from "node-opcua-certificate-manager";
|
|
11
|
+
import { type ICertificateKeyPairProvider, invalidateCachedSecrets } from "node-opcua-common";
|
|
12
|
+
import { type Certificate, split_der, exploreCertificateInfo } from "node-opcua-crypto/web";
|
|
13
|
+
import { checkDebugFlag, make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
|
|
14
|
+
import { invalidateServerCertificateCache, type OPCUAServer, type OPCUAServerEndPoint } from "node-opcua-server";
|
|
15
|
+
import { type StatusCode, StatusCodes } from "node-opcua-status-code";
|
|
16
|
+
import { type ApplicationDescriptionOptions, ServerState } from "node-opcua-types";
|
|
17
|
+
|
|
18
|
+
import { installPushCertificateManagement } from "./push_certificate_manager_helpers.js";
|
|
19
|
+
import type { ActionQueue, PushCertificateManagerServerImpl } from "./push_certificate_manager_server_impl.js";
|
|
20
|
+
|
|
21
|
+
const debugLog = make_debugLog("ServerConfiguration");
|
|
22
|
+
const doDebug = checkDebugFlag("ServerConfiguration");
|
|
23
|
+
const errorLog = make_errorLog("ServerConfiguration");
|
|
24
|
+
const warningLog = make_warningLog("ServerConfiguration");
|
|
25
|
+
|
|
26
|
+
/** Relative path from cert manager root to the leaf certificate PEM. */
|
|
27
|
+
const CERT_PEM_RELATIVE_PATH = "own/certs/certificate.pem";
|
|
28
|
+
|
|
29
|
+
export interface OPCUAServerPartial extends ICertificateKeyPairProvider {
|
|
30
|
+
serverInfo?: ApplicationDescriptionOptions;
|
|
31
|
+
serverCertificateManager: OPCUACertificateManager;
|
|
32
|
+
privateKeyFile: string;
|
|
33
|
+
certificateFile: string;
|
|
34
|
+
engine: { addressSpace?: AddressSpace };
|
|
35
|
+
createDefaultCertificate(): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function onCertificateAboutToChange(server: OPCUAServer) {
|
|
39
|
+
doDebug && debugLog(chalk.yellow(" onCertificateAboutToChange => Suspending End points"));
|
|
40
|
+
await server.suspendEndPoints();
|
|
41
|
+
doDebug && debugLog(chalk.yellow(" onCertificateAboutToChange => End points suspended"));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* onCertificateChange is called when the serverConfiguration notifies
|
|
46
|
+
* that the server certificate and/or private key has changed.
|
|
47
|
+
*
|
|
48
|
+
* This function invalidates all cached certificates so that new
|
|
49
|
+
* connections immediately serve the updated certificate.
|
|
50
|
+
*
|
|
51
|
+
* Channel teardown is deferred to the `applyChangesCompleted` event
|
|
52
|
+
* so the ApplyChanges OPC-UA method can return its response before
|
|
53
|
+
* the admin client's own channel is destroyed.
|
|
54
|
+
*/
|
|
55
|
+
async function onCertificateChange(server: OPCUAServer) {
|
|
56
|
+
doDebug && debugLog("on CertificateChanged");
|
|
57
|
+
invalidateServerCertificateCache(server);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Deferred channel restart: called after the ApplyChanges method
|
|
62
|
+
* response has been sent. Shuts down all existing secure channels
|
|
63
|
+
* (which forces clients to reconnect with the new cert) and resumes
|
|
64
|
+
* the endpoints.
|
|
65
|
+
*/
|
|
66
|
+
async function onApplyChangesCompleted(server: OPCUAServer) {
|
|
67
|
+
doDebug && debugLog(chalk.yellow(" onApplyChangesCompleted => shutting down channels"));
|
|
68
|
+
await server.shutdownChannels();
|
|
69
|
+
doDebug && debugLog(chalk.yellow(" onApplyChangesCompleted => channels shut down"));
|
|
70
|
+
|
|
71
|
+
doDebug && debugLog(chalk.yellow(" onApplyChangesCompleted => resuming end points"));
|
|
72
|
+
await server.resumeEndPoints();
|
|
73
|
+
doDebug && debugLog(chalk.yellow(" onApplyChangesCompleted => end points resumed"));
|
|
74
|
+
|
|
75
|
+
debugLog(chalk.yellow("channels have been closed -> client should reconnect "));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Redirect the server's `certificateFile` and `privateKeyFile`
|
|
80
|
+
* properties to the cert manager's paths, create a default
|
|
81
|
+
* certificate if none exists, and invalidate cached secrets.
|
|
82
|
+
*/
|
|
83
|
+
async function install(this: OPCUAServerPartial): Promise<void> {
|
|
84
|
+
doDebug && debugLog("install push certificate management", this.serverCertificateManager.rootDir);
|
|
85
|
+
|
|
86
|
+
Object.defineProperty(this, "privateKeyFile", {
|
|
87
|
+
get: () => this.serverCertificateManager.privateKey,
|
|
88
|
+
configurable: true,
|
|
89
|
+
enumerable: true
|
|
90
|
+
});
|
|
91
|
+
Object.defineProperty(this, "certificateFile", {
|
|
92
|
+
get: () => path.join(this.serverCertificateManager.rootDir, CERT_PEM_RELATIVE_PATH),
|
|
93
|
+
configurable: true,
|
|
94
|
+
enumerable: true
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Delegate to the base server's createDefaultCertificate() which
|
|
98
|
+
// handles DNS (fqdn + hostname + configured), IPs (auto + configured),
|
|
99
|
+
// proper subject via makeSubject(), mutex locking, and file checks.
|
|
100
|
+
await this.createDefaultCertificate();
|
|
101
|
+
|
|
102
|
+
// Invalidate any previously cached secrets so that
|
|
103
|
+
// getCertificateChain() / getPrivateKey() will re-read from disk.
|
|
104
|
+
invalidateCachedSecrets(this);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface UAServerConfigurationEx extends UAServerConfiguration {
|
|
108
|
+
$pushCertificateManager: PushCertificateManagerServerImpl;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function installPushCertificateManagementOnServer(server: OPCUAServer): Promise<void> {
|
|
112
|
+
if (!server.engine || !server.engine.addressSpace) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
"Server must have a valid address space. " +
|
|
115
|
+
"You need to call installPushCertificateManagementOnServer after server has been initialized"
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
await install.call(server as unknown as OPCUAServerPartial);
|
|
119
|
+
|
|
120
|
+
// After install() redirected certificateFile / privateKeyFile,
|
|
121
|
+
// the SecretHolder(this) in each endpoint already follows the
|
|
122
|
+
// new paths. Just invalidate their cached values so the next
|
|
123
|
+
// access re-reads from the cert manager's files.
|
|
124
|
+
invalidateServerCertificateCache(server);
|
|
125
|
+
|
|
126
|
+
await installPushCertificateManagement(server.engine.addressSpace, {
|
|
127
|
+
applicationGroup: server.serverCertificateManager,
|
|
128
|
+
userTokenGroup: server.userCertificateManager,
|
|
129
|
+
|
|
130
|
+
applicationUri: server.serverInfo.applicationUri || "InvalidURI"
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const serverConfiguration = server.engine.addressSpace.rootFolder.objects.server.getChildByName("ServerConfiguration");
|
|
134
|
+
const serverConfigurationPriv = serverConfiguration as UAServerConfigurationEx;
|
|
135
|
+
assert(serverConfigurationPriv.$pushCertificateManager);
|
|
136
|
+
|
|
137
|
+
serverConfigurationPriv.$pushCertificateManager.on("CertificateAboutToChange", (actionQueue: ActionQueue) => {
|
|
138
|
+
actionQueue.push(async (): Promise<void> => {
|
|
139
|
+
doDebug && debugLog("CertificateAboutToChange Event received");
|
|
140
|
+
await onCertificateAboutToChange(server);
|
|
141
|
+
doDebug && debugLog("CertificateAboutToChange Event processed");
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
serverConfigurationPriv.$pushCertificateManager.on("CertificateChanged", (actionQueue: ActionQueue) => {
|
|
145
|
+
actionQueue.push(async (): Promise<void> => {
|
|
146
|
+
doDebug && debugLog("CertificateChanged Event received");
|
|
147
|
+
await onCertificateChange(server);
|
|
148
|
+
doDebug && debugLog("CertificateChanged Event processed");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
serverConfigurationPriv.$pushCertificateManager.on("applyChangesCompleted", () => {
|
|
153
|
+
// Fire-and-forget: schedule channel teardown + endpoint
|
|
154
|
+
// resumption AFTER the method response is sent.
|
|
155
|
+
setImmediate(async () => {
|
|
156
|
+
try {
|
|
157
|
+
await onApplyChangesCompleted(server);
|
|
158
|
+
} catch (err) {
|
|
159
|
+
errorLog("onApplyChangesCompleted error:", (err as Error).message);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ── Install NoConfiguration certificate relaxation ─────────
|
|
165
|
+
//
|
|
166
|
+
// When the server is in NoConfiguration state (awaiting GDS
|
|
167
|
+
// provisioning), relax certain certificate trust/CRL errors
|
|
168
|
+
// so that the GDS client can connect and provision the server.
|
|
169
|
+
//
|
|
170
|
+
// This hook is ONLY installed when push certificate management
|
|
171
|
+
// is active — bare servers are completely unaffected.
|
|
172
|
+
installCertificateRelaxationHook(server);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── Certificate relaxation for NoConfiguration state ──────────
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Status codes that can be relaxed during NoConfiguration state.
|
|
179
|
+
*
|
|
180
|
+
* These represent "trust infrastructure not yet set up" situations
|
|
181
|
+
* (missing issuers, missing CRLs) — NOT security violations
|
|
182
|
+
* (revoked, expired, invalid).
|
|
183
|
+
*
|
|
184
|
+
* ### Why `BadCertificateUntrusted` MUST be included (SECURITY NOTE)
|
|
185
|
+
*
|
|
186
|
+
* In `NoConfiguration` the server's trust store is **empty** — no
|
|
187
|
+
* trusted certificates and no CRLs exist yet. A GDS client that
|
|
188
|
+
* connects to provision the server will inevitably present a
|
|
189
|
+
* certificate that is "untrusted" simply because nothing is trusted
|
|
190
|
+
* yet. Removing `BadCertificateUntrusted` from this list would make
|
|
191
|
+
* it impossible for the GDS to connect, breaking the entire push
|
|
192
|
+
* certificate management provisioning workflow (chicken-and-egg).
|
|
193
|
+
*
|
|
194
|
+
* **Security boundary:** this relaxation is ONLY active while the
|
|
195
|
+
* server state is `ServerState.NoConfiguration`. Once the server
|
|
196
|
+
* transitions to `Running` (after successful provisioning) the
|
|
197
|
+
* relaxation hook returns the original status code unchanged and
|
|
198
|
+
* normal strict certificate validation applies. The accepted
|
|
199
|
+
* certificate is auto-trusted (see `autoTrustCertificateChain`)
|
|
200
|
+
* so that subsequent connections succeed under normal validation.
|
|
201
|
+
*
|
|
202
|
+
* Errors that indicate an active security violation (revoked,
|
|
203
|
+
* expired, invalid signature, wrong usage) are **never** relaxed,
|
|
204
|
+
* even in `NoConfiguration`.
|
|
205
|
+
*/
|
|
206
|
+
function isRelaxableCertificateError(statusCode: StatusCode): boolean {
|
|
207
|
+
return (
|
|
208
|
+
StatusCodes.BadCertificateUntrusted.equals(statusCode) ||
|
|
209
|
+
StatusCodes.BadCertificateRevocationUnknown.equals(statusCode) ||
|
|
210
|
+
StatusCodes.BadCertificateIssuerRevocationUnknown.equals(statusCode) ||
|
|
211
|
+
StatusCodes.BadCertificateChainIncomplete.equals(statusCode)
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Auto-trust the client's leaf certificate during NoConfiguration.
|
|
217
|
+
*
|
|
218
|
+
* Only the **leaf** (chain[0]) is placed in the trusted store —
|
|
219
|
+
* this is sufficient for the GDS client to reconnect after the
|
|
220
|
+
* server transitions to Running (the PKI verifier short-circuits
|
|
221
|
+
* to `Good` when the leaf itself is explicitly trusted).
|
|
222
|
+
*
|
|
223
|
+
* Issuer (CA) certificates from the chain are added to the
|
|
224
|
+
* **issuers/** store for chain-building purposes, but they do
|
|
225
|
+
* NOT become trust anchors. This prevents a single provisioning
|
|
226
|
+
* connection from unintentionally granting trust to every
|
|
227
|
+
* certificate signed by the same CA.
|
|
228
|
+
*/
|
|
229
|
+
async function autoTrustCertificateChain(
|
|
230
|
+
server: OPCUAServer,
|
|
231
|
+
certificate: Certificate
|
|
232
|
+
): Promise<void> {
|
|
233
|
+
let chain: Certificate[];
|
|
234
|
+
try {
|
|
235
|
+
chain = split_der(certificate);
|
|
236
|
+
} catch (err) {
|
|
237
|
+
warningLog(
|
|
238
|
+
"[NoConfiguration] Cannot parse certificate chain for auto-trust:",
|
|
239
|
+
(err as Error).message
|
|
240
|
+
);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const cm = server.serverCertificateManager;
|
|
245
|
+
|
|
246
|
+
for (let i = 0; i < chain.length; i++) {
|
|
247
|
+
const cert = chain[i];
|
|
248
|
+
// Validate the DER structure before persisting.
|
|
249
|
+
// Garbage data (e.g. zero-filled buffers) parses into tiny blobs
|
|
250
|
+
// that are not valid X.509 certificates.
|
|
251
|
+
try {
|
|
252
|
+
exploreCertificateInfo(cert);
|
|
253
|
+
} catch (err) {
|
|
254
|
+
warningLog(
|
|
255
|
+
"[NoConfiguration] Skipping invalid certificate in chain for auto-trust:",
|
|
256
|
+
(err as Error).message
|
|
257
|
+
);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (i === 0) {
|
|
262
|
+
// Leaf certificate → trust explicitly
|
|
263
|
+
try {
|
|
264
|
+
await cm.trustCertificate(cert);
|
|
265
|
+
} catch (err) {
|
|
266
|
+
// ENOENT can happen if another concurrent call already
|
|
267
|
+
// moved the cert from rejected to trusted.
|
|
268
|
+
if ((err as Error & { code?: string }).code !== "ENOENT") {
|
|
269
|
+
warningLog(
|
|
270
|
+
"[NoConfiguration] Failed to auto-trust leaf certificate:",
|
|
271
|
+
(err as Error).message
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
// Issuer CA certificate → add to issuers/ (chain-building
|
|
277
|
+
// only, does NOT establish a trust anchor)
|
|
278
|
+
try {
|
|
279
|
+
await cm.addIssuer(cert);
|
|
280
|
+
} catch (err) {
|
|
281
|
+
warningLog(
|
|
282
|
+
"[NoConfiguration] Failed to add issuer certificate:",
|
|
283
|
+
(err as Error).message
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Install the `onAdjustCertificateStatus` hook on every endpoint.
|
|
292
|
+
*
|
|
293
|
+
* When the server is in `ServerState.NoConfiguration`, the hook
|
|
294
|
+
* relaxes trust/CRL errors so that a GDS client with a valid
|
|
295
|
+
* full-chain certificate can connect and provision the server.
|
|
296
|
+
*
|
|
297
|
+
* The leaf certificate is auto-trusted so that after the server
|
|
298
|
+
* transitions to Running, the same client is accepted by normal
|
|
299
|
+
* validation. Issuer CAs are placed in `issuers/` for chain
|
|
300
|
+
* building but do not become trust anchors.
|
|
301
|
+
*/
|
|
302
|
+
function installCertificateRelaxationHook(server: OPCUAServer): void {
|
|
303
|
+
const adjustCertificateStatus = async (
|
|
304
|
+
statusCode: StatusCode,
|
|
305
|
+
certificate: Certificate
|
|
306
|
+
): Promise<StatusCode> => {
|
|
307
|
+
// Only relax in NoConfiguration state
|
|
308
|
+
if (server.engine.getServerState() !== ServerState.NoConfiguration) {
|
|
309
|
+
return statusCode;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Only relax trust-infrastructure errors, NOT security errors
|
|
313
|
+
if (!isRelaxableCertificateError(statusCode)) {
|
|
314
|
+
return statusCode;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
doDebug && warningLog(
|
|
318
|
+
`[NoConfiguration] Relaxing certificate check:`,
|
|
319
|
+
`${statusCode.toString()} → Good`,
|
|
320
|
+
"(server is awaiting GDS provisioning)"
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
// Auto-trust the leaf certificate; issuer CAs go to issuers/
|
|
324
|
+
await autoTrustCertificateChain(server, certificate);
|
|
325
|
+
|
|
326
|
+
return StatusCodes.Good;
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
for (const endpoint of server.endpoints) {
|
|
330
|
+
(endpoint as OPCUAServerEndPoint).onAdjustCertificateStatus = adjustCertificateStatus;
|
|
331
|
+
}
|
|
332
|
+
}
|