Skip to content

Commit 269ea39

Browse files
committed
refactor(binding-opcua): move cert mgt and security resolution
move certificate management and OPCUA security resolution to own files, for clarity. improve OCPUA Certificate manager singleton lifecycle
1 parent 17f9447 commit 269ea39

File tree

5 files changed

+174
-92
lines changed

5 files changed

+174
-92
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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+
import path from "node:path";
16+
import { OPCUACertificateManager } from "node-opcua-certificate-manager";
17+
import envPath from "env-paths";
18+
import { createLoggers } from "@node-wot/core";
19+
20+
const { debug } = createLoggers("binding-opcua", "opcua-protocol-client");
21+
22+
const env = envPath("binding-opcua", { suffix: "node-wot" });
23+
24+
/**
25+
* Certificate Manager Singleton for OPCUA Binding in the WoT context.
26+
*
27+
*/
28+
export class CertificateManagerSingleton {
29+
private static _certificateManager: OPCUACertificateManager | null = null;
30+
31+
public static async getCertificateManager(): Promise<OPCUACertificateManager> {
32+
if (CertificateManagerSingleton._certificateManager) {
33+
return CertificateManagerSingleton._certificateManager;
34+
}
35+
const rootFolder = path.join(env.config, "PKI");
36+
debug("OPCUA PKI folder", rootFolder);
37+
const certificateManager = new OPCUACertificateManager({
38+
rootFolder,
39+
});
40+
await certificateManager.initialize();
41+
certificateManager.referenceCounter++;
42+
CertificateManagerSingleton._certificateManager = certificateManager;
43+
return certificateManager;
44+
}
45+
46+
public static releaseCertificateManager(): void {
47+
if (CertificateManagerSingleton._certificateManager) {
48+
CertificateManagerSingleton._certificateManager.referenceCounter--;
49+
// dispose is degined to free resources if referenceCounter==0;
50+
CertificateManagerSingleton._certificateManager.dispose();
51+
CertificateManagerSingleton._certificateManager = null;
52+
}
53+
}
54+
}

packages/binding-opcua/src/factory.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
import { ProtocolClientFactory, ProtocolClient, ContentSerdes, createLoggers } from "@node-wot/core";
1717
import { OpcuaJSONCodec, OpcuaBinaryCodec } from "./codec";
1818
import { OPCUAProtocolClient } from "./opcua-protocol-client";
19-
import { OPCUACertificateManager } from "node-opcua-certificate-manager";
2019

2120
const { debug } = createLoggers("binding-opcua", "factory");
2221

@@ -56,12 +55,6 @@ export class OPCUAClientFactory implements ProtocolClientFactory {
5655
await client.stop();
5756
}
5857
})();
59-
60-
OPCUAProtocolClient.releaseCertificateManager();
6158
return true;
6259
}
63-
64-
async getCertificateManager(): Promise<OPCUACertificateManager> {
65-
return await OPCUAProtocolClient.getCertificateManager();
66-
}
6760
}

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

Lines changed: 14 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@ import { Subscription } from "rxjs/Subscription";
1717
import { promisify } from "util";
1818
import { Readable } from "stream";
1919
import { URL } from "url";
20-
import path from "path";
21-
import envPath from "env-paths";
2220
import {
2321
ProtocolClient,
2422
Content,
@@ -54,25 +52,23 @@ import {
5452
getBuiltInDataType,
5553
readNamespaceArray,
5654
UserIdentityInfo,
57-
UserIdentityInfoUserName,
58-
UserIdentityInfoX509,
5955
} from "node-opcua-pseudo-session";
6056
import { makeNodeId, NodeId, NodeIdLike, NodeIdType, resolveNodeId } from "node-opcua-nodeid";
6157
import { AttributeIds, BrowseDirection, makeResultMask } from "node-opcua-data-model";
6258
import { makeBrowsePath } from "node-opcua-service-translate-browse-path";
6359
import { StatusCodes } from "node-opcua-status-code";
64-
import { coercePrivateKeyPem, convertPEMtoDER, readPrivateKey } from "node-opcua-crypto";
60+
import { coercePrivateKeyPem, readPrivateKey } from "node-opcua-crypto";
6561
import { opcuaJsonEncodeVariant } from "node-opcua-json";
6662
import { Argument, BrowseDescription, BrowseResult, MessageSecurityMode, UserTokenType } from "node-opcua-types";
67-
import { isGoodish2, OPCUACertificateManager, ReferenceTypeIds } from "node-opcua";
63+
import { isGoodish2, ReferenceTypeIds } from "node-opcua";
6864

6965
import { schemaDataValue } from "./codec";
7066
import { OPCUACAuthenticationScheme, OPCUAChannelSecurityScheme } from "./security_scheme";
67+
import { CertificateManagerSingleton } from "./certificate-manager-singleton";
68+
import { resolveChannelSecurity, resolvedUserIdentity } from "./opcua-security-resolver";
7169

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

74-
const env = envPath("binding-opcua", { suffix: "node-wot" });
75-
7672
export type Command = "Read" | "Write" | "Subscribe";
7773

7874
export interface NodeByBrowsePath {
@@ -167,32 +163,6 @@ export class OPCUAProtocolClient implements ProtocolClient {
167163
private _securityPolicy: SecurityPolicy = SecurityPolicy.None;
168164
private _userIdentity: UserIdentityInfo = <AnonymousIdentity>{ type: UserTokenType.Anonymous };
169165

170-
private static _certificateManager: OPCUACertificateManager | null = null;
171-
172-
public static async getCertificateManager(): Promise<OPCUACertificateManager> {
173-
if (OPCUAProtocolClient._certificateManager) {
174-
return OPCUAProtocolClient._certificateManager;
175-
}
176-
const rootFolder = path.join(env.config, "PKI");
177-
debug("OPCUA PKI folder", rootFolder);
178-
const certificateManager = new OPCUACertificateManager({
179-
rootFolder,
180-
});
181-
await certificateManager.initialize();
182-
certificateManager.referenceCounter++;
183-
OPCUAProtocolClient._certificateManager = certificateManager;
184-
return certificateManager;
185-
}
186-
187-
public static releaseCertificateManager(): void {
188-
if (OPCUAProtocolClient._certificateManager) {
189-
OPCUAProtocolClient._certificateManager.referenceCounter--;
190-
// dispose is degined to free resources if referenceCounter==0;
191-
OPCUAProtocolClient._certificateManager.dispose();
192-
OPCUAProtocolClient._certificateManager = null;
193-
}
194-
}
195-
196166
private async _withConnection<T>(form: OPCUAForm, next: (connection: OPCUAConnection) => Promise<T>): Promise<T> {
197167
const endpoint = form.href;
198168
const matchesScheme: boolean = endpoint?.match(/^opc.tcp:\/\//) != null;
@@ -202,7 +172,7 @@ export class OPCUAProtocolClient implements ProtocolClient {
202172
}
203173
let c: OPCUAConnectionEx | undefined = this._connections.get(endpoint);
204174
if (!c) {
205-
const clientCertificateManager = await OPCUAProtocolClient.getCertificateManager();
175+
const clientCertificateManager = await CertificateManagerSingleton.getCertificateManager();
206176
const client = OPCUAClient.create({
207177
endpointMustExist: false,
208178
connectionStrategy: {
@@ -540,57 +510,18 @@ export class OPCUAProtocolClient implements ProtocolClient {
540510
await connection.session.close();
541511
await connection.client.disconnect();
542512
}
543-
await OPCUAProtocolClient._certificateManager?.dispose();
513+
CertificateManagerSingleton.releaseCertificateManager();
544514
}
545515

546-
private setChannelSecurity(security: OPCUAChannelSecurityScheme): boolean {
547-
const foundSecurity = SecurityPolicy[security.policy as keyof typeof SecurityPolicy];
548-
549-
if (foundSecurity === undefined) {
550-
return false;
551-
}
552-
553-
this._securityPolicy = foundSecurity;
554-
555-
switch (security.messageMode) {
556-
case "sign":
557-
this._securityMode = MessageSecurityMode.Sign;
558-
break;
559-
case "sign_encrypt":
560-
this._securityMode = MessageSecurityMode.SignAndEncrypt;
561-
break;
562-
default:
563-
this._securityMode = MessageSecurityMode.None;
564-
break;
565-
}
566-
516+
#setChannelSecurity(security: OPCUAChannelSecurityScheme): boolean {
517+
const { messageSecurityMode, securityPolicy } = resolveChannelSecurity(security);
518+
this._securityMode = messageSecurityMode;
519+
this._securityPolicy = securityPolicy;
567520
return true;
568521
}
569522

570-
private setAuthentication(security: OPCUACAuthenticationScheme): boolean {
571-
switch (security.tokenType) {
572-
case "username":
573-
this._userIdentity = <UserIdentityInfoUserName>{
574-
type: UserTokenType.UserName,
575-
password: security.password,
576-
userName: security.userName,
577-
};
578-
break;
579-
case "certificate":
580-
this._userIdentity = <UserIdentityInfoX509>{
581-
type: UserTokenType.Certificate,
582-
certificateData: convertPEMtoDER(security.certificate),
583-
privateKey: security.privateKey,
584-
};
585-
break;
586-
case "anonymous":
587-
this._userIdentity = <UserIdentityInfo>{
588-
type: UserTokenType.Anonymous,
589-
};
590-
break;
591-
default:
592-
return false;
593-
}
523+
#setAuthentication(security: OPCUACAuthenticationScheme): boolean {
524+
this._userIdentity = resolvedUserIdentity(security);
594525
return true;
595526
}
596527

@@ -599,10 +530,10 @@ export class OPCUAProtocolClient implements ProtocolClient {
599530
let success = true;
600531
switch (securityScheme.scheme) {
601532
case "uav:channel-security":
602-
success = this.setChannelSecurity(securityScheme as OPCUAChannelSecurityScheme);
533+
success = this.#setChannelSecurity(securityScheme as OPCUAChannelSecurityScheme);
603534
break;
604535
case "uav:authentication":
605-
success = this.setAuthentication(securityScheme as OPCUACAuthenticationScheme);
536+
success = this.#setAuthentication(securityScheme as OPCUACAuthenticationScheme);
606537
break;
607538
case "combo": {
608539
const combo = securityScheme as AllOfSecurityScheme | OneOfSecurityScheme;
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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+
import {
16+
MessageSecurityMode,
17+
SecurityPolicy,
18+
UserIdentityInfo,
19+
UserIdentityInfoUserName,
20+
UserIdentityInfoX509,
21+
UserTokenType,
22+
} from "node-opcua-client";
23+
import { convertPEMtoDER } from "node-opcua-crypto";
24+
import { OPCUACAuthenticationScheme, OPCUAChannelSecurityScheme } from "./security_scheme";
25+
26+
export interface OPCUAChannelSecuritySettings {
27+
securityPolicy: SecurityPolicy;
28+
messageSecurityMode: MessageSecurityMode;
29+
}
30+
31+
/**
32+
* Resolves the channel security settings from the given security scheme.
33+
* Will throw an error if the policy or message mode is invalid.
34+
* @param security The OPC UA channel security scheme.
35+
* @returns The resolved channel security settings.
36+
*/
37+
export function resolveChannelSecurity(security: OPCUAChannelSecurityScheme): OPCUAChannelSecuritySettings {
38+
if (security.scheme === "uav:channel-security" && security.messageMode !== "none") {
39+
const securityPolicy: SecurityPolicy = SecurityPolicy[security.policy as keyof typeof SecurityPolicy];
40+
41+
if (securityPolicy === undefined) {
42+
throw new Error(`Invalid security policy '${security.policy}'`);
43+
}
44+
45+
let messageSecurityMode: MessageSecurityMode = MessageSecurityMode.Invalid;
46+
switch (security.messageMode) {
47+
case "sign":
48+
messageSecurityMode = MessageSecurityMode.Sign;
49+
break;
50+
case "sign_encrypt":
51+
messageSecurityMode = MessageSecurityMode.SignAndEncrypt;
52+
break;
53+
default:
54+
messageSecurityMode = MessageSecurityMode.None;
55+
break;
56+
}
57+
58+
return {
59+
securityPolicy,
60+
messageSecurityMode,
61+
};
62+
} else {
63+
return {
64+
securityPolicy: SecurityPolicy.None,
65+
messageSecurityMode: MessageSecurityMode.None,
66+
};
67+
}
68+
}
69+
70+
/**
71+
* Resolves the user identity information from the given authentication scheme.
72+
* Will throw an error if the token type is invalid.
73+
* @param security The OPC UA authentication scheme.
74+
* @returns The resolved user identity information.
75+
*/
76+
export function resolvedUserIdentity(security: OPCUACAuthenticationScheme) {
77+
let userIdentity: UserIdentityInfo;
78+
switch (security.tokenType) {
79+
case "username":
80+
userIdentity = <UserIdentityInfoUserName>{
81+
type: UserTokenType.UserName,
82+
password: security.password,
83+
userName: security.userName,
84+
};
85+
break;
86+
case "certificate":
87+
userIdentity = <UserIdentityInfoX509>{
88+
type: UserTokenType.Certificate,
89+
certificateData: convertPEMtoDER(security.certificate),
90+
privateKey: security.privateKey,
91+
};
92+
break;
93+
case "anonymous":
94+
default:
95+
// it is OK to use anonymous as default,
96+
// as it provides the lowest privileges
97+
userIdentity = <UserIdentityInfo>{
98+
type: UserTokenType.Anonymous,
99+
};
100+
break;
101+
}
102+
103+
return userIdentity;
104+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@ import { MessageSecurityMode, OPCUAClient, OPCUAServer, SecurityPolicy } from "n
2424
import { coercePrivateKeyPem, readCertificate, readCertificatePEM, readPrivateKey } from "node-opcua-crypto";
2525
import {
2626
OPCUAClientFactory,
27-
OPCUAProtocolClient,
2827
OPCUACUserNameAuthenticationScheme,
2928
OPCUACertificateAuthenticationScheme,
3029
OPCUAChannelSecurityScheme,
3130
} from "../src";
3231

3332
import { startServer } from "./fixture/basic-opcua-server";
33+
import { CertificateManagerSingleton } from "../src/certificate-manager-singleton";
3434
const endpoint = "opc.tcp://localhost:7890";
3535

3636
const { debug } = createLoggers("binding-opcua", "full-opcua-thing-test");
@@ -325,7 +325,7 @@ describe("Testing OPCUA Security Combination", () => {
325325

326326
// exchnage certificate
327327
const serverCertificateManager = opcuaServer.serverCertificateManager;
328-
const clientCertificateManager = await OPCUAProtocolClient.getCertificateManager();
328+
const clientCertificateManager = await CertificateManagerSingleton.getCertificateManager();
329329

330330
// Client should trust client certificate
331331
const serverCertificate = opcuaServer.getCertificate();

0 commit comments

Comments
 (0)