11/********************************************************************************
2- * Copyright (c) 2022 Contributors to the Eclipse Foundation
2+ * Copyright (c) 2025 Contributors to the Eclipse Foundation
33 *
44 * See the NOTICE file(s) distributed with this work for additional
55 * information regarding copyright ownership.
1616// node-wot implementation of W3C WoT Servient
1717
1818import { expect } from "chai" ;
19- import path from "path" ;
19+ import path , { resolve } from "path" ;
2020import {
2121 OPCUACUserNameAuthenticationScheme ,
2222 OPCUACertificateAuthenticationScheme ,
@@ -26,14 +26,27 @@ import {
2626} from "@node-wot/core" ;
2727import { InteractionOptions } from "wot-typescript-definitions" ;
2828
29- import { OPCUAClient , OPCUAServer } from "node-opcua" ;
29+ import {
30+ IBasicSessionCallAsync ,
31+ MessageSecurityMode ,
32+ ObjectIds ,
33+ OPCUAClient ,
34+ OPCUAServer ,
35+ SecurityPolicy ,
36+ } from "node-opcua" ;
3037import { coercePrivateKeyPem , readCertificate , readCertificatePEM , readPrivateKey } from "node-opcua-crypto" ;
3138import { OPCUAClientFactory , OPCUAProtocolClient } from "../src" ;
3239import { startServer } from "./fixture/basic-opcua-server" ;
3340const endpoint = "opc.tcp://localhost:7890" ;
3441
3542const { debug } = createLoggers ( "binding-opcua" , "full-opcua-thing-test" ) ;
3643
44+ interface WhoAmI {
45+ UserName : string | null ;
46+ UserIdentityTokenType : string | null ;
47+ ChannelSecurityMode : string | null ;
48+ ChannelSecurityPolicyUri : string | null ;
49+ }
3750const thingDescription : WoT . ThingDescription = {
3851 "@context" : "https://www.w3.org/2019/wot/td/v1" ,
3952 "@type" : [ "Thing" ] ,
@@ -48,8 +61,14 @@ const thingDescription: WoT.ThingDescription = {
4861 messageMode : "sign_encrypt" ,
4962 policy : "Basic256Sha256" , // deprecated
5063 } ,
64+ // Aes128_Sha256_RsaOaep
65+ "c:sign-encrypt_aes128Sha256RsaOaep" : < OPCUAChannelSecurityScheme > {
66+ scheme : "opcua-channel-security" ,
67+ messageMode : "sign_encrypt" ,
68+ policy : "Aes128_Sha256_RsaOaep" ,
69+ } ,
5170
52- "c:sign " : < OPCUAChannelSecurityScheme > {
71+ "c:sign_basic256Sha256 " : < OPCUAChannelSecurityScheme > {
5372 scheme : "opcua-channel-security" ,
5473 messageMode : "sign" ,
5574 policy : "Basic256Sha256" ,
@@ -89,13 +108,13 @@ const thingDescription: WoT.ThingDescription = {
89108 privateKey : undefined ,
90109 } ,
91110 // compbo
92- "c:sign -a:username-password" : {
111+ "c:sign_basic256Sha256 -a:username-password" : {
93112 scheme : "combo" ,
94- allOf : [ "c:sign " , "a:username-password" ] ,
113+ allOf : [ "c:sign_basic256Sha256 " , "a:username-password" ] ,
95114 } ,
96- "c:sign -a:username-invalid-password" : {
115+ "c:sign_basic256Sha256 -a:username-invalid-password" : {
97116 scheme : "combo" ,
98- allOf : [ "c:sign " , "a:username-invalid-password" ] ,
117+ allOf : [ "c:sign_basic256Sha256 " , "a:username-invalid-password" ] ,
99118 } ,
100119 "c:sign-encrypt_basic256Sha256-a:username-password" : {
101120 scheme : "combo" ,
@@ -146,10 +165,157 @@ const thingDescription: WoT.ThingDescription = {
146165 ] ,
147166 } ,
148167 } ,
168+ actions : {
169+ whoAmI : {
170+ forms : [
171+ {
172+ type : "object" ,
173+ href : "/" ,
174+ op : [ "invokeaction" ] ,
175+ "opcua:nodeId" : { root : "i=84" , path : "/Objects/Server" } ,
176+ "opcua:method" : { root : "i=84" , path : "/Objects/Server/1:WhoAmI" } ,
177+ } ,
178+ ] ,
179+ description : "query information about the log in user and current channel security mode" ,
180+ // see https://www.w3.org/TR/wot-thing-description11/#action-serialization-sample
181+ input : {
182+ type : "object" ,
183+ properties : { } ,
184+ required : [ ] ,
185+ } ,
186+ output : {
187+ type : "object" ,
188+ properties : {
189+ UserName : {
190+ type : "string" ,
191+ title : "the current user name" ,
192+ } ,
193+ UserIdentityTokenType : {
194+ type : "string" ,
195+ title : "the current user identity token type" ,
196+ } ,
197+ ChannelSecurityMode : {
198+ type : "string" ,
199+ title : "the current security mode" ,
200+ } ,
201+ ChannelSecurityPolicyUri : {
202+ type : "string" ,
203+ title : "the current security policy" ,
204+ } ,
205+ } ,
206+ required : [ "UserName" , "UserIdentityTokenType" , "ChannelSecurityMode" , "ChannelSecurityPolicyUri" ] ,
207+ } ,
208+ } ,
209+ } ,
149210} ;
150211
212+ function inferExpectedSecurityMode ( security : string ) : WhoAmI {
213+ const expected : WhoAmI = {
214+ UserName : null ,
215+ UserIdentityTokenType : "AnonymousIdentityToken" ,
216+ ChannelSecurityMode : MessageSecurityMode [ MessageSecurityMode . None ] ,
217+ ChannelSecurityPolicyUri : SecurityPolicy . None ,
218+ } ;
219+
220+ if ( security . match ( / a : u s e r n a m e - p a s s w o r d / ) ) {
221+ expected . UserName = "joe" ;
222+ expected . UserIdentityTokenType = "UserNameIdentityToken" ;
223+ } else if ( security . match ( / c e r t i f i c a t e / ) ) {
224+ expected . UserName = null ;
225+ expected . UserIdentityTokenType = "X509IdentityToken" ;
226+ } else {
227+ expected . UserName = null ;
228+ expected . UserIdentityTokenType = "AnonymousIdentityToken" ;
229+ }
230+
231+ //
232+ if ( security . match ( / c : s i g n - e n c r y p t / ) ) {
233+ expected . ChannelSecurityMode = "SignAndEncrypt" ;
234+ } else if ( security . match ( / c : s i g n / ) ) {
235+ expected . ChannelSecurityMode = "Sign" ;
236+ } else if ( security . match ( / c : n o _ s e c u r i t y / ) ) {
237+ expected . ChannelSecurityMode = "None" ;
238+ expected . ChannelSecurityPolicyUri = "http://opcfoundation.org/UA/SecurityPolicy#None" ;
239+ }
240+
241+ if ( security . match ( / b a s i c 2 5 6 S h a 2 5 6 / ) ) {
242+ expected . ChannelSecurityPolicyUri = "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256" ;
243+ } else if ( security . match ( / a e s 1 2 8 S h a 2 5 6 R s a O a e p / ) ) {
244+ expected . ChannelSecurityPolicyUri = "http://opcfoundation.org/UA/SecurityPolicy#Aes128_Sha256_RsaOaep" ;
245+ }
246+ return expected ;
247+ }
248+
249+ describe ( "Testing OPCUA Expected Value inference" , ( ) => {
250+ it ( "should infer expected values from security string" , ( ) => {
251+ let expected = inferExpectedSecurityMode ( "c:sign-encrypt_basic256Sha256-a:username-password" ) ;
252+ expect ( expected . UserName ) . to . eql ( "joe" ) ;
253+ expect ( expected . UserIdentityTokenType ) . to . eql ( "UserNameIdentityToken" ) ;
254+ expect ( expected . ChannelSecurityMode ) . to . eql ( "SignAndEncrypt" ) ;
255+ expect ( expected . ChannelSecurityPolicyUri ) . to . eql ( "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256" ) ;
256+
257+ expected = inferExpectedSecurityMode ( "c:sign_basic256Sha256-a:username-password" ) ;
258+ expect ( expected . UserName ) . to . eql ( "joe" ) ;
259+ expect ( expected . UserIdentityTokenType ) . to . eql ( "UserNameIdentityToken" ) ;
260+ expect ( expected . ChannelSecurityMode ) . to . eql ( "Sign" ) ;
261+ expect ( expected . ChannelSecurityPolicyUri ) . to . eql ( "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256" ) ;
262+
263+ expected = inferExpectedSecurityMode ( "c:no_security" ) ;
264+ expect ( expected . UserName ) . to . eql ( null ) ;
265+ expect ( expected . UserIdentityTokenType ) . to . eql ( "AnonymousIdentityToken" ) ;
266+ expect ( expected . ChannelSecurityMode ) . to . eql ( "None" ) ;
267+ expect ( expected . ChannelSecurityPolicyUri ) . to . eql ( "http://opcfoundation.org/UA/SecurityPolicy#None" ) ;
268+
269+ expected = inferExpectedSecurityMode ( "c:sign-encrypt_aes128Sha256RsaOaep-a:x509-certificate" ) ;
270+ expect ( expected . UserName ) . to . eql ( null ) ;
271+ expect ( expected . UserIdentityTokenType ) . to . eql ( "X509IdentityToken" ) ;
272+ expect ( expected . ChannelSecurityMode ) . to . eql ( "SignAndEncrypt" ) ;
273+ expect ( expected . ChannelSecurityPolicyUri ) . to . eql (
274+ "http://opcfoundation.org/UA/SecurityPolicy#Aes128_Sha256_RsaOaep"
275+ ) ;
276+ } ) ;
277+ } ) ;
278+
151279const possibleSecurityMode = Object . keys ( thingDescription . securityDefinitions ) . filter ( ( s ) => ! s . match ( / i n v a l i d / ) ) ;
152280const possibleInvalidSecurityMode = Object . keys ( thingDescription . securityDefinitions ) . filter ( ( s ) => s . match ( / i n v a l i d / ) ) ;
281+ describe ( "verify test securityDefinitions" , ( ) => {
282+ it ( "should have a coherent security definitions" , ( ) => {
283+ expect ( thingDescription ) . to . be . an ( "object" ) ;
284+ const definitions = thingDescription . securityDefinitions ;
285+ expect ( definitions ) . to . be . an ( "object" ) ;
286+ expect ( Object . keys ( definitions ) . length ) . to . be . greaterThan ( 0 ) ;
287+ for ( const key of Object . keys ( definitions ) ) {
288+ const def = definitions [ key ] ;
289+ expect ( def ) . to . have . property ( "scheme" ) ;
290+ if ( def . scheme === "nosec" ) {
291+ continue ;
292+ }
293+ if ( def . scheme === "combo" ) {
294+ const comboDef = def as { scheme : string ; allOf : string [ ] } ;
295+ expect ( comboDef . allOf ) . to . be . an ( "array" ) ;
296+ expect ( comboDef . allOf . length ) . to . be . greaterThan ( 0 ) ;
297+ for ( const subKey of comboDef . allOf ) {
298+ expect ( definitions ) . to . have . property ( subKey ) ;
299+ }
300+ } else if ( def . scheme === "opcua-channel-security" ) {
301+ const channelDef = def as OPCUAChannelSecurityScheme ;
302+ expect ( channelDef ) . to . have . property ( "messageMode" ) ;
303+ expect ( [ "none" , "sign" , "sign_encrypt" ] ) . to . include ( channelDef . messageMode ) ;
304+ // policy is optional
305+ } else if ( def . scheme === "opcua-authentication" ) {
306+ const authDef = def as OPCUACertificateAuthenticationScheme | OPCUACUserNameAuthenticationScheme ;
307+ expect ( authDef ) . to . have . property ( "tokenType" ) ;
308+ if ( authDef . tokenType === "username" ) {
309+ expect ( authDef ) . to . have . property ( "userName" ) ;
310+ expect ( authDef ) . to . have . property ( "password" ) ;
311+ } else if ( authDef . tokenType === "certificate" ) {
312+ expect ( authDef ) . to . have . property ( "certificate" ) ;
313+ expect ( authDef ) . to . have . property ( "privateKey" ) ;
314+ }
315+ }
316+ }
317+ } ) ;
318+ } ) ;
153319
154320describe ( "Testing OPCUA Security Combination" , ( ) => {
155321 let opcuaServer : OPCUAServer ;
@@ -238,15 +404,20 @@ describe("Testing OPCUA Security Combination", () => {
238404
239405 async function doTest (
240406 thing : WoT . ConsumedThing ,
241- propertyName : string ,
242407 localOptions : InteractionOptions
243- ) : Promise < { value ?: number ; err ?: Error } > {
408+ ) : Promise < { value ?: number ; whoAmI ?: WhoAmI ; err ?: Error } > {
244409 debug ( "------------------------------------------------------" ) ;
245410 try {
246- const content = await thing . readProperty ( propertyName , localOptions ) ;
411+ const propertyName = "temperature" ;
247412
413+ const content = await thing . readProperty ( propertyName , localOptions ) ;
248414 const value = ( await content . value ( ) ) as number ;
249- return { value } ;
415+
416+ const result = await thing . invokeAction ( "whoAmI" , { } , localOptions ) ;
417+ const whoAmI = ( await result ?. value ( ) ) as WhoAmI ;
418+
419+ debug ( `whoAmI = ${ JSON . stringify ( whoAmI ) } ` ) ;
420+ return { value, whoAmI } ;
250421 } catch ( e ) {
251422 debug ( `${ e } ` ) ;
252423 return { err : e as Error } ;
@@ -258,10 +429,13 @@ describe("Testing OPCUA Security Combination", () => {
258429 const localOptions = { } ;
259430 const { thing, servient } = await makeThing ( security ) ;
260431 try {
261- const propertyName = "temperature" ;
262- const { value, err } = await doTest ( thing , propertyName , localOptions ) ;
432+ const { value, whoAmI, err } = await doTest ( thing , localOptions ) ;
263433 expect ( err ) . to . eql ( undefined ) ;
264434 expect ( value ) . to . eql ( 25 ) ;
435+
436+ const expected = inferExpectedSecurityMode ( security ) ;
437+ expect ( whoAmI ) . to . eql ( expected ) ;
438+ // infer expected result from security string
265439 } finally {
266440 await servient . shutdown ( ) ;
267441 }
@@ -273,8 +447,7 @@ describe("Testing OPCUA Security Combination", () => {
273447 const localOptions = { } ;
274448 const { thing, servient } = await makeThing ( security ) ;
275449 try {
276- const propertyName = "temperature" ;
277- const { err } = await doTest ( thing , propertyName , localOptions ) ;
450+ const { err } = await doTest ( thing , localOptions ) ;
278451 expect ( err ) . to . not . eql ( undefined ) ;
279452 } finally {
280453 await servient . shutdown ( ) ;
0 commit comments