Skip to content

Commit a070497

Browse files
authored
Feature: add auto secure scheme support in binding-opcua (#1425)
* feat(binding-opcua): implement Auto Security Scheme for OPC UA #1401 * chore(opcua-binding): fix file naming inconsistency #1423
1 parent 71de51c commit a070497

File tree

7 files changed

+151
-3
lines changed

7 files changed

+151
-3
lines changed
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/********************************************************************************
2+
* Copyright (c) 2025 Contributors to the Eclipse Foundation
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information regarding copyright ownership.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License v. 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and
10+
* Document License (2015-05-13) which is available at
11+
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document.
12+
*
13+
* SPDX-License-Identifier: EPL-2.0 OR W3C-20150513
14+
********************************************************************************/
15+
16+
import { MessageSecurityMode, OPCUAClient, SecurityPolicy } from "node-opcua-client";
17+
18+
function getPriority(securityPolicy: string | null, securityMode: MessageSecurityMode): number {
19+
const encryptWeight = securityMode === MessageSecurityMode.SignAndEncrypt ? 100 : 0;
20+
21+
switch (securityPolicy) {
22+
case null:
23+
case "":
24+
case "http://opcfoundation.org/UA/SecurityPolicy#None":
25+
return 0;
26+
case "http://opcfoundation.org/UA/SecurityPolicy#Basic128":
27+
return 1 + encryptWeight;
28+
case "http://opcfoundation.org/UA/SecurityPolicy#Basic192":
29+
return 2 + encryptWeight;
30+
case "http://opcfoundation.org/UA/SecurityPolicy#Basic192Rsa15":
31+
return 3 + encryptWeight;
32+
case "http://opcfoundation.org/UA/SecurityPolicy#Basic256":
33+
return 4 + encryptWeight;
34+
case "http://opcfoundation.org/UA/SecurityPolicy#Basic256Rsa15":
35+
return 5 + encryptWeight;
36+
case "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256":
37+
return 6 + encryptWeight;
38+
case "http://opcfoundation.org/UA/SecurityPolicy#Aes128_Sha256_RsaOaep":
39+
return 7 + encryptWeight;
40+
case "http://opcfoundation.org/UA/SecurityPolicy#Aes256_Sha256_RsaPss":
41+
return 8 + encryptWeight;
42+
default:
43+
return -100;
44+
}
45+
}
46+
47+
function coerceSecurityPolicyUri(securityPolicyUri: string | null): SecurityPolicy {
48+
switch (securityPolicyUri) {
49+
case null:
50+
case "":
51+
case "http://opcfoundation.org/UA/SecurityPolicy#None":
52+
return SecurityPolicy.None;
53+
case "http://opcfoundation.org/UA/SecurityPolicy#Basic128":
54+
return SecurityPolicy.Basic128;
55+
case "http://opcfoundation.org/UA/SecurityPolicy#Basic192":
56+
return SecurityPolicy.Basic192;
57+
case "http://opcfoundation.org/UA/SecurityPolicy#Basic192Rsa15":
58+
return SecurityPolicy.Basic192Rsa15;
59+
case "http://opcfoundation.org/UA/SecurityPolicy#Basic256":
60+
return SecurityPolicy.Basic256;
61+
case "http://opcfoundation.org/UA/SecurityPolicy#Basic256Rsa15":
62+
return SecurityPolicy.Basic256Rsa15;
63+
case "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256":
64+
return SecurityPolicy.Basic256Sha256;
65+
case "http://opcfoundation.org/UA/SecurityPolicy#Aes128_Sha256_RsaOaep":
66+
return SecurityPolicy.Aes128_Sha256_RsaOaep;
67+
case "http://opcfoundation.org/UA/SecurityPolicy#Aes256_Sha256_RsaPss":
68+
return SecurityPolicy.Aes256_Sha256_RsaPss;
69+
default:
70+
return SecurityPolicy.Invalid;
71+
}
72+
}
73+
74+
interface EndpointDescriptionMini {
75+
endpointUrl: string | null;
76+
securityMode: MessageSecurityMode;
77+
securityPolicyUri: string | null;
78+
}
79+
80+
const defaultEndpoint: EndpointDescriptionMini = {
81+
endpointUrl: null,
82+
securityMode: MessageSecurityMode.None,
83+
securityPolicyUri: SecurityPolicy.None,
84+
};
85+
86+
async function findMostSecureChannelInternal(client: OPCUAClient): Promise<EndpointDescriptionMini> {
87+
let endpoints = await client.getEndpoints();
88+
89+
// sort in descending order of security level
90+
endpoints = endpoints.sort((a, b) => {
91+
const securityLevelA = getPriority(a.securityPolicyUri, a.securityMode);
92+
const securityLevelB = getPriority(b.securityPolicyUri, b.securityMode);
93+
return securityLevelB - securityLevelA;
94+
});
95+
return endpoints.length > 0 ? endpoints[0] : defaultEndpoint;
96+
}
97+
98+
export async function findMostSecureChannel(
99+
endpointUrl: string
100+
): Promise<{ messageSecurityMode: MessageSecurityMode; securityPolicy: SecurityPolicy }> {
101+
const client = OPCUAClient.create({
102+
endpointMustExist: false,
103+
securityMode: MessageSecurityMode.None,
104+
securityPolicy: SecurityPolicy.None,
105+
});
106+
try {
107+
await client.connect(endpointUrl);
108+
const endpoint = await findMostSecureChannelInternal(client);
109+
const messageSecurityMode = endpoint.securityMode;
110+
const securityPolicy = coerceSecurityPolicyUri(endpoint.securityPolicyUri);
111+
return { messageSecurityMode, securityPolicy };
112+
} finally {
113+
await client.disconnect();
114+
}
115+
}

packages/binding-opcua/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@
1616
export * from "./factory";
1717
export * from "./codec";
1818
export * from "./opcua-protocol-client";
19-
export * from "./security_scheme";
19+
export * from "./security-scheme";
2020
// no protocol_client here => get access from factor

packages/binding-opcua/src/opcua-protocol-client.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,10 @@ import { Argument, BrowseDescription, BrowseResult, MessageSecurityMode, UserTok
6363
import { isGoodish2, ReferenceTypeIds } from "node-opcua";
6464

6565
import { schemaDataValue } from "./codec";
66-
import { OPCUACAuthenticationScheme, OPCUAChannelSecurityScheme } from "./security_scheme";
66+
import { OPCUACAuthenticationScheme, OPCUAChannelSecurityScheme } from "./security-scheme";
6767
import { CertificateManagerSingleton } from "./certificate-manager-singleton";
6868
import { resolveChannelSecurity, resolvedUserIdentity } from "./opcua-security-resolver";
69+
import { findMostSecureChannel } from "./find-most-secure-channel";
6970

7071
const { debug } = createLoggers("binding-opcua", "opcua-protocol-client");
7172

@@ -161,6 +162,7 @@ export class OPCUAProtocolClient implements ProtocolClient {
161162

162163
private _securityMode: MessageSecurityMode = MessageSecurityMode.None;
163164
private _securityPolicy: SecurityPolicy = SecurityPolicy.None;
165+
private _useAutoChannel: boolean = false;
164166
private _userIdentity: UserIdentityInfo = <AnonymousIdentity>{ type: UserTokenType.Anonymous };
165167

166168
private async _withConnection<T>(form: OPCUAForm, next: (connection: OPCUAConnection) => Promise<T>): Promise<T> {
@@ -173,6 +175,14 @@ export class OPCUAProtocolClient implements ProtocolClient {
173175
let c: OPCUAConnectionEx | undefined = this._connections.get(endpoint);
174176
if (!c) {
175177
const clientCertificateManager = await CertificateManagerSingleton.getCertificateManager();
178+
179+
if (this._useAutoChannel) {
180+
if (this._securityMode === MessageSecurityMode.Invalid) {
181+
const { messageSecurityMode, securityPolicy } = await findMostSecureChannel(endpoint);
182+
this._securityMode = messageSecurityMode;
183+
this._securityPolicy = securityPolicy;
184+
}
185+
}
176186
const client = OPCUAClient.create({
177187
endpointMustExist: false,
178188
connectionStrategy: {
@@ -517,6 +527,7 @@ export class OPCUAProtocolClient implements ProtocolClient {
517527
const { messageSecurityMode, securityPolicy } = resolveChannelSecurity(security);
518528
this._securityMode = messageSecurityMode;
519529
this._securityPolicy = securityPolicy;
530+
this._useAutoChannel = false;
520531
return true;
521532
}
522533

@@ -529,6 +540,20 @@ export class OPCUAProtocolClient implements ProtocolClient {
529540
for (const securityScheme of securitySchemes) {
530541
let success = true;
531542
switch (securityScheme.scheme) {
543+
case "auto": {
544+
//
545+
// Security scheme = auto instruct the client to automatically
546+
// determine the most secure channel to use based on server Endpoints.
547+
//
548+
this._useAutoChannel = true;
549+
// Invalid + _useAutoChannel=true indicates that the
550+
// Channel Security Settings need to be resolved at runtime
551+
// based on server Endpoints.
552+
this._securityMode = MessageSecurityMode.Invalid;
553+
this._securityPolicy = SecurityPolicy.None;
554+
success = true;
555+
break;
556+
}
532557
case "uav:channel-security":
533558
success = this.#setChannelSecurity(securityScheme as OPCUAChannelSecurityScheme);
534559
break;

packages/binding-opcua/src/opcua-security-resolver.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
UserTokenType,
2222
} from "node-opcua-client";
2323
import { convertPEMtoDER } from "node-opcua-crypto";
24-
import { OPCUACAuthenticationScheme, OPCUAChannelSecurityScheme } from "./security_scheme";
24+
import { OPCUACAuthenticationScheme, OPCUAChannelSecurityScheme } from "./security-scheme";
2525

2626
export interface OPCUAChannelSecuritySettings {
2727
securityPolicy: SecurityPolicy;

packages/binding-opcua/test/opcua-security-test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ const thingDescription: WoT.ThingDescription = {
126126
scheme: "combo",
127127
allOf: ["c:sign-encrypt_basic256Sha256"],
128128
},
129+
"c:auto_a:anonymous": {
130+
scheme: "auto",
131+
},
129132
},
130133

131134
security: "no_security", // by default,
@@ -230,6 +233,11 @@ function inferExpectedSecurityMode(security: string): WhoAmI {
230233
} else if (security.match(/c:no_security/)) {
231234
expected.ChannelSecurityMode = "None";
232235
expected.ChannelSecurityPolicyUri = "http://opcfoundation.org/UA/SecurityPolicy#None";
236+
} else if (security.match(/c:auto/)) {
237+
// the most secure that the server supports, by client looking-up at the server endpoints
238+
expected.ChannelSecurityMode = "SignAndEncrypt";
239+
expected.ChannelSecurityPolicyUri = "http://opcfoundation.org/UA/SecurityPolicy#Aes256_Sha256_RsaPss";
240+
return expected;
233241
}
234242

235243
if (security.match(/basic256Sha256/)) {

0 commit comments

Comments
 (0)