Skip to content

Commit 855998c

Browse files
Controller Api MQTT: add TLS support (#2459)
Co-authored-by: Stefan Feilmeier <[email protected]>
1 parent 4aa1268 commit 855998c

File tree

10 files changed

+211
-10
lines changed

10 files changed

+211
-10
lines changed

cnf/pom.xml

+6
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,12 @@
250250
<artifactId>org.apache.servicemix.bundles.junit</artifactId>
251251
<version>4.13.2_1</version>
252252
</dependency>
253+
<dependency>
254+
<!-- Bouncycastle for Eclipse Paho MQTTv5 Client -->
255+
<groupId>org.bouncycastle</groupId>
256+
<artifactId>bcpkix-jdk15on</artifactId>
257+
<version>1.70</version>
258+
</dependency>
253259
<dependency>
254260
<groupId>org.dhatim</groupId>
255261
<artifactId>fastexcel</artifactId>

doc/modules/ROOT/pages/gettingstarted.adoc

+1-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ image::eclipse-io.openems.edge.application.png[io.openems.edge.application proje
8585
+
8686
NOTE: Instead of navigating through the projects tree, you can simply use the keyboard shortcut btn:[Ctrl] + btn:[Shift] + btn:[R] to start the "Open Resource" dialog. Enter "EdgeApp.bndrun" there and press btn:[Enter] to open the file.
8787
+
88-
The `EdgeApp.bndrun` file declares all the bundles and runtime properties. For now it should not be necessary to edit it, but it hides some useful settings unter the btn:[Source] tab:
88+
The `EdgeApp.bndrun` file declares all the bundles and runtime properties. For now it should not be necessary to edit it, but it hides some useful settings under the btn:[Source] tab:
8989
+
9090
- `org.osgi.service.http.port=8080`: start the Apache Felix Web Console on port `8080`
9191
- `felix.cm.dir=c:/openems/config`: persist configurations in the folder `c:/openems/config`. Adjust this if you are working on Linux to keep your configurations after restart

io.openems.edge.application/EdgeApp.bndrun

+3
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,9 @@
185185

186186
-runbundles: \
187187
Java-WebSocket;version='[1.5.4,1.5.5)',\
188+
bcpkix;version='[1.70.0,1.70.1)',\
189+
bcprov;version='[1.70.0,1.70.1)',\
190+
bcutil;version='[1.70.0,1.70.1)',\
188191
com.fazecast.jSerialComm;version='[2.5.1,2.5.2)',\
189192
com.ghgande.j2mod;version='[2.5.5,2.5.6)',\
190193
com.google.gson;version='[2.10.1,2.10.2)',\

io.openems.edge.controller.api.mqtt/bnd.bnd

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ Bundle-Version: 1.0.0.${tstamp}
55

66
-buildpath: \
77
${buildpath},\
8+
bcpkix;version='1.70',\
9+
bcprov;version='1.70',\
810
io.openems.common,\
911
io.openems.edge.common,\
1012
io.openems.edge.controller.api,\

io.openems.edge.controller.api.mqtt/src/io/openems/edge/controller/api/mqtt/Config.java

+9
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@
3232
@AttributeDefinition(name = "Uri", description = "The connection Uri to MQTT broker.")
3333
String uri() default "tcp://localhost:1883";
3434

35+
@AttributeDefinition(name = "Certificate", description = "The client certificate in PEM format")
36+
String certPem();
37+
38+
@AttributeDefinition(name = "Private Key", description = "The private key in PEM format")
39+
String privateKeyPem();
40+
41+
@AttributeDefinition(name = "Trust Store", description = "The trust store in PEM format")
42+
String trustStorePem();
43+
3544
@AttributeDefinition(name = "Persistence Priority", description = "Send only Channels with a Persistence Priority greater-or-equals this.")
3645
PersistencePriority persistencePriority() default PersistencePriority.VERY_LOW;
3746

io.openems.edge.controller.api.mqtt/src/io/openems/edge/controller/api/mqtt/ControllerApiMqttImpl.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ private void activate(ComponentContext context, Config config) throws Exception
7777
this.topicPrefix = String.format(ControllerApiMqtt.TOPIC_PREFIX, config.clientId());
7878

7979
super.activate(context, config.id(), config.alias(), config.enabled());
80-
this.mqttConnector.connect(config.uri(), config.clientId(), config.username(), config.password())
81-
.thenAccept(client -> {
80+
this.mqttConnector.connect(config.uri(), config.clientId(), config.username(), config.password(),
81+
config.certPem(), config.privateKeyPem(), config.trustStorePem()).thenAccept(client -> {
8282
this.mqttClient = client;
8383
this.logInfo(this.log, "Connected to MQTT Broker [" + config.uri() + "]");
8484
});
@@ -174,4 +174,4 @@ protected boolean publish(String subTopic, String message, int qos, boolean reta
174174
var msg = new MqttMessage(message.getBytes(StandardCharsets.UTF_8), qos, retained, properties);
175175
return this.publish(subTopic, msg);
176176
}
177-
}
177+
}

io.openems.edge.controller.api.mqtt/src/io/openems/edge/controller/api/mqtt/MqttConnector.java

+15-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.openems.edge.controller.api.mqtt;
22

3+
import static io.openems.edge.controller.api.mqtt.MqttUtils.createSslSocketFactory;
4+
35
import java.nio.charset.StandardCharsets;
46
import java.util.Date;
57
import java.util.concurrent.CompletableFuture;
@@ -66,26 +68,34 @@ protected synchronized void deactivate() {
6668
}
6769

6870
protected synchronized CompletableFuture<IMqttClient> connect(String serverUri, String clientId, String username,
69-
String password) throws IllegalArgumentException, MqttException {
70-
return this.connect(serverUri, clientId, username, password, null);
71+
String password, String certPem, String privateKeyPem, String trustStorePem)
72+
throws IllegalArgumentException, MqttException {
73+
return this.connect(serverUri, clientId, username, password, certPem, privateKeyPem, trustStorePem, null);
7174
}
7275

7376
protected synchronized CompletableFuture<IMqttClient> connect(String serverUri, String clientId, String username,
74-
String password, MqttCallback callback) throws IllegalArgumentException, MqttException {
77+
String password, String certPem, String privateKeyPem, String trustStorePem, MqttCallback callback)
78+
throws IllegalArgumentException, MqttException {
7579
IMqttClient client = new MqttClient(serverUri, clientId);
7680
if (callback != null) {
7781
client.setCallback(callback);
7882
}
7983

8084
var options = new MqttConnectionOptions();
8185
options.setUserName(username);
82-
if (password != null) {
86+
if (password != null && !password.isBlank()) {
8387
options.setPassword(password.getBytes(StandardCharsets.UTF_8));
8488
}
8589
options.setAutomaticReconnect(true);
8690
options.setCleanStart(true);
8791
options.setConnectionTimeout(10);
8892

93+
if (certPem != null && !certPem.isBlank() //
94+
&& privateKeyPem != null && !privateKeyPem.isBlank() //
95+
&& trustStorePem != null && !trustStorePem.isBlank()) {
96+
options.setSocketFactory(createSslSocketFactory(certPem, privateKeyPem, trustStorePem));
97+
}
98+
8999
this.connector = new MyConnector(client, options);
90100

91101
this.executor.schedule(this.connector, 0 /* immediately */, TimeUnit.SECONDS);
@@ -97,4 +107,4 @@ private void waitAndRetry() {
97107
this.executor.schedule(this.connector, this.waitSeconds.get(), TimeUnit.SECONDS);
98108
}
99109

100-
}
110+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package io.openems.edge.controller.api.mqtt;
2+
3+
import java.io.ByteArrayInputStream;
4+
import java.io.IOException;
5+
import java.io.InputStream;
6+
import java.io.InputStreamReader;
7+
import java.security.KeyStore;
8+
import java.security.NoSuchAlgorithmException;
9+
import java.security.PrivateKey;
10+
import java.security.SecureRandom;
11+
import java.security.Security;
12+
import java.security.cert.CertificateException;
13+
import java.security.cert.CertificateFactory;
14+
import java.security.cert.X509Certificate;
15+
import java.security.spec.InvalidKeySpecException;
16+
17+
import javax.net.ssl.KeyManagerFactory;
18+
import javax.net.ssl.SSLContext;
19+
import javax.net.ssl.SSLSocketFactory;
20+
import javax.net.ssl.TrustManagerFactory;
21+
22+
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
23+
import org.bouncycastle.jce.provider.BouncyCastleProvider;
24+
import org.bouncycastle.openssl.PEMKeyPair;
25+
import org.bouncycastle.openssl.PEMParser;
26+
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
27+
28+
/**
29+
* This Utility class provides methods for handling MQTT-related operations.
30+
*/
31+
public class MqttUtils {
32+
/**
33+
* Creates and returns an SSLSocketFactory for establishing secure connections
34+
* in MQTT.
35+
*
36+
* <p>
37+
* This method initializes an SSL context using the provided certificate,
38+
* private key and server truststore information, allowing the creation of an
39+
* SSLSocketFactory with the configured security settings.
40+
*
41+
* @param cert The client's certificate as String.
42+
* @param privateKey The private key as String.
43+
* @param trustStore The server's trust store as String.
44+
* @return An SSLSocketFactory configured with the specified security settings.
45+
* @throws RuntimeException If there is an error during the creation of the
46+
* SSLSocketFactory.
47+
*/
48+
49+
public static SSLSocketFactory createSslSocketFactory(String cert, String privateKey, String trustStore) {
50+
try {
51+
Security.addProvider(new BouncyCastleProvider());
52+
53+
// Load client certificate
54+
X509Certificate clientCert = loadCertificate(cert);
55+
56+
// Load client private key
57+
PrivateKey clientKey = loadPrivateKey(privateKey);
58+
59+
// Load CA certificate
60+
X509Certificate caCert = loadCertificate(trustStore);
61+
62+
// Create a KeyStore and add the CA certificate, client certificate, and private
63+
// key
64+
KeyStore keyStore = KeyStore.getInstance("PKCS12");
65+
keyStore.load(null, null);
66+
keyStore.setCertificateEntry("caCert", caCert);
67+
keyStore.setCertificateEntry("clientCert", clientCert);
68+
keyStore.setKeyEntry("clientKey", clientKey, new char[0], new X509Certificate[] { clientCert });
69+
70+
// Create a TrustManager that trusts the CA certificate
71+
TrustManagerFactory trustManagerFactory = TrustManagerFactory
72+
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
73+
trustManagerFactory.init(keyStore);
74+
75+
// Create a KeyManager that uses the client certificate and private key
76+
KeyManagerFactory keyManagerFactory = KeyManagerFactory
77+
.getInstance(KeyManagerFactory.getDefaultAlgorithm());
78+
keyManagerFactory.init(keyStore, new char[0]);
79+
80+
// Create an SSLContext with the TrustManager and KeyManager
81+
SSLContext sslContext = SSLContext.getInstance("TLS");
82+
sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(),
83+
new SecureRandom());
84+
85+
return sslContext.getSocketFactory();
86+
} catch (Exception e) {
87+
throw new RuntimeException("Error creating SSLSocketFactory", e);
88+
}
89+
}
90+
91+
/**
92+
* Loads an X.509 certificate from a PEM-encoded string.
93+
*
94+
* @param cert The PEM-encoded certificate string.
95+
* @return The X.509 certificate.
96+
* @throws IOException If an I/O error occurs.
97+
* @throws CertificateException If an error occurs while processing the
98+
* certificate.
99+
*/
100+
private static X509Certificate loadCertificate(String cert) throws IOException, CertificateException {
101+
CertificateFactory cf = CertificateFactory.getInstance("X.509");
102+
try (InputStream is = new ByteArrayInputStream(cert.getBytes())) {
103+
return (X509Certificate) cf.generateCertificate(is);
104+
}
105+
}
106+
107+
/**
108+
* Loads a private key from a PEM-encoded string.
109+
*
110+
* @param privateKey The PEM-encoded private key string.
111+
* @return The private key.
112+
* @throws IOException If an I/O error occurs.
113+
* @throws NoSuchAlgorithmException If the specified algorithm is not available.
114+
* @throws InvalidKeySpecException If the private key cannot be generated.
115+
*/
116+
private static PrivateKey loadPrivateKey(String privateKey)
117+
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
118+
try (PEMParser pemParser = new PEMParser(
119+
new InputStreamReader(new ByteArrayInputStream(privateKey.getBytes())))) {
120+
Object obj = pemParser.readObject();
121+
if (obj instanceof PEMKeyPair) {
122+
// Handle RSA private key
123+
PEMKeyPair pemKeyPair = (PEMKeyPair) obj;
124+
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
125+
return converter.getPrivateKey(pemKeyPair.getPrivateKeyInfo());
126+
} else if (obj instanceof PrivateKeyInfo) {
127+
// Handle other private key formats
128+
PrivateKeyInfo privateKeyInfo = (PrivateKeyInfo) obj;
129+
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
130+
return converter.getPrivateKey(privateKeyInfo);
131+
} else {
132+
throw new InvalidKeySpecException("Invalid private key format");
133+
}
134+
}
135+
}
136+
}

io.openems.edge.controller.api.mqtt/test/io/openems/edge/controller/api/mqtt/ControllerApiMqttImplTest.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ public void test() throws Exception {
3030
.setUri("ws://localhost:1883") //
3131
.setPersistencePriority(PersistencePriority.VERY_LOW) //
3232
.setDebugMode(true) //
33+
.setCertPem("") //
34+
.setPrivateKeyPem("") //
35+
.setTrustStorePath("") //
3336
.build());
3437
}
3538

36-
}
39+
}

io.openems.edge.controller.api.mqtt/test/io/openems/edge/controller/api/mqtt/MyConfig.java

+32
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ protected static class Builder {
1414
private String clientId;
1515
private String username;
1616
private String password;
17+
private String certPem;
18+
private String privateKeyPem;
19+
private String trustStorePem;
1720

1821
private Builder() {
1922
}
@@ -43,6 +46,21 @@ public Builder setPassword(String password) {
4346
return this;
4447
}
4548

49+
public Builder setCertPem(String certPem) {
50+
this.certPem = certPem;
51+
return this;
52+
}
53+
54+
public Builder setPrivateKeyPem(String privateKeyPem) {
55+
this.privateKeyPem = privateKeyPem;
56+
return this;
57+
}
58+
59+
public Builder setTrustStorePath(String trustStorePem) {
60+
this.trustStorePem = trustStorePem;
61+
return this;
62+
}
63+
4664
public Builder setPersistencePriority(PersistencePriority persistencePriority) {
4765
this.persistencePriority = persistencePriority;
4866
return this;
@@ -104,4 +122,18 @@ public String password() {
104122
return this.builder.password;
105123
}
106124

125+
@Override
126+
public String certPem() {
127+
return this.builder.certPem;
128+
}
129+
130+
@Override
131+
public String privateKeyPem() {
132+
return this.builder.privateKeyPem;
133+
}
134+
135+
@Override
136+
public String trustStorePem() {
137+
return this.builder.trustStorePem;
138+
}
107139
}

0 commit comments

Comments
 (0)