Skip to content

Commit dcbe2d8

Browse files
authored
Merge pull request #1 from inomotech-foss/feature/cert_as_text
Feature/cert as text
2 parents 35830c2 + 99a9884 commit dcbe2d8

8 files changed

Lines changed: 129 additions & 151 deletions

File tree

io.openems.edge.application/EdgeApp.bndrun

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,4 +403,7 @@
403403
org.osgi.util.promise;version='[1.3.0,1.3.1)',\
404404
org.owasp.encoder;version='[1.2.3,1.2.4)',\
405405
reactive-streams;version='[1.0.4,1.0.5)',\
406-
rrd4j;version='[3.9.0,3.9.1)'
406+
rrd4j;version='[3.9.0,3.9.1)',\
407+
bcpkix;version='[1.65.0,1.65.1)',\
408+
bcprov;version='[1.65.0,1.65.1)'
409+
-resolve: auto

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ Bundle-Version: 1.0.0.${tstamp}
1313
io.openems.wrapper.paho-mqttv5,\
1414
org.eclipse.paho.mqttv5.client;version='1.2',\
1515
org.ops4j.pax.logging.pax-logging-api,\
16-
16+
bcprov,\
17+
bcpkix
18+
1719
-testpath: \
1820
${testpath}

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

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,14 @@
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 path to the client certificate")
36-
String certPath();
35+
@AttributeDefinition(name = "Certificate", description = "The client certificate in PEM format")
36+
String certPem();
3737

38-
@AttributeDefinition(name = "Private Key", description = "The path to the private key")
39-
String privateKeyPath();
38+
@AttributeDefinition(name = "Private Key", description = "The private key in PEM format")
39+
String privateKeyPem();
4040

41-
@AttributeDefinition(name = "Trust Store", description = "The path to the trust store")
42-
String trustStorePath();
43-
44-
@AttributeDefinition(name = "Trust Store Password", description = "Password to access trust store")
45-
String trustStorePassword() default "";
41+
@AttributeDefinition(name = "Trust Store", description = "The trust store in PEM format")
42+
String trustStorePem();
4643

4744
@AttributeDefinition(name = "Persistence Priority", description = "Send only Channels with a Persistence Priority greater-or-equals this.")
4845
PersistencePriority persistencePriority() default PersistencePriority.VERY_LOW;

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

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +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
81-
.connect(config.uri(), config.clientId(), config.username(), config.password(), config.certPath(),
82-
config.privateKeyPath(), config.trustStorePath(), config.trustStorePassword())
83-
.thenAccept(client -> {
80+
this.mqttConnector.connect(config.uri(), config.clientId(), config.username(), config.password(),
81+
config.certPem(), config.privateKeyPem(), config.trustStorePem()).thenAccept(client -> {
8482
this.mqttClient = client;
8583
this.logInfo(this.log, "Connected to MQTT Broker [" + config.uri() + "]");
8684
});
@@ -176,4 +174,4 @@ protected boolean publish(String subTopic, String message, int qos, boolean reta
176174
var msg = new MqttMessage(message.getBytes(StandardCharsets.UTF_8), qos, retained, properties);
177175
return this.publish(subTopic, msg);
178176
}
179-
}
177+
}

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

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,14 @@ protected synchronized void deactivate() {
6666
}
6767

6868
protected synchronized CompletableFuture<IMqttClient> connect(String serverUri, String clientId, String username,
69-
String password, String certPath, String privateKeyPath, String trustStorePath, String trustStorePassword)
69+
String password, String certPem, String privateKeyPem, String trustStorePem)
7070
throws IllegalArgumentException, MqttException {
71-
return this.connect(serverUri, clientId, username, password, certPath, privateKeyPath, trustStorePath,
72-
trustStorePassword, null);
71+
return this.connect(serverUri, clientId, username, password, certPem, privateKeyPem, trustStorePem, null);
7372
}
7473

7574
protected synchronized CompletableFuture<IMqttClient> connect(String serverUri, String clientId, String username,
76-
String password, String certPath, String privateKeyPath, String trustStorePath, String trustStorePassword,
77-
MqttCallback callback) throws IllegalArgumentException, MqttException {
75+
String password, String certPem, String privateKeyPem, String trustStorePem, MqttCallback callback)
76+
throws IllegalArgumentException, MqttException {
7877
IMqttClient client = new MqttClient(serverUri, clientId);
7978
if (callback != null) {
8079
client.setCallback(callback);
@@ -89,9 +88,8 @@ protected synchronized CompletableFuture<IMqttClient> connect(String serverUri,
8988
options.setCleanStart(true);
9089
options.setConnectionTimeout(10);
9190

92-
if (certPath != null && privateKeyPath != null) {
93-
options.setSocketFactory(
94-
MqttUtils.createSslSocketFactory(certPath, privateKeyPath, trustStorePath, trustStorePassword));
91+
if (certPem != null && privateKeyPem != null) {
92+
options.setSocketFactory(MqttUtils.createSslSocketFactory(certPem, privateKeyPem, trustStorePem));
9593
}
9694

9795
this.connector = new MyConnector(client, options);
Lines changed: 85 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
package io.openems.edge.controller.api.mqtt;
22

3+
import org.bouncycastle.jce.provider.BouncyCastleProvider;
4+
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
5+
import org.bouncycastle.openssl.PEMKeyPair;
6+
import org.bouncycastle.openssl.PEMParser;
7+
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
8+
39
import javax.net.ssl.*;
4-
import java.io.FileInputStream;
510
import java.io.InputStream;
6-
import java.security.KeyFactory;
7-
import java.security.KeyStore;
8-
import java.security.PrivateKey;
11+
import java.io.InputStreamReader;
12+
import java.security.*;
13+
import java.security.cert.CertificateException;
914
import java.security.cert.CertificateFactory;
1015
import java.security.cert.X509Certificate;
11-
import java.security.spec.PKCS8EncodedKeySpec;
12-
import java.util.ArrayList;
13-
import java.util.List;
16+
import java.security.spec.InvalidKeySpecException;
17+
import java.io.ByteArrayInputStream;
18+
import java.io.IOException;
1419

1520
/**
1621
* This Utility class provides methods for handling MQTT-related operations.
@@ -25,105 +30,93 @@ public class MqttUtils {
2530
* server truststore information, allowing the creation of an SSLSocketFactory
2631
* with the configured security settings.
2732
*
28-
* @param certPath The file path to the client's certificate.
29-
* @param privateKeyPath The file path to the private key file.
30-
* @param trustStorePath The file path to the server's truststore.
31-
* @param trustStorePassword The password for accessing the server's truststore.
33+
* @param cert The client's certificate as String.
34+
* @param privateKey The private key as String.
35+
* @param trustStore The server's trust store as String.
3236
* @return An SSLSocketFactory configured with the specified security settings.
3337
* @throws RuntimeException If there is an error during the creation of the
3438
* SSLSocketFactory.
3539
*/
3640

37-
public static SSLSocketFactory createSslSocketFactory(String certPath, String privateKeyPath, String trustStorePath,
38-
String trustStorePassword) {
41+
public static SSLSocketFactory createSslSocketFactory(String cert, String privateKey, String trustStore) {
3942
try {
40-
X509Certificate clientCertificate = loadClientCertificate(certPath);
41-
42-
PrivateKey privateKey = loadPrivateKey(privateKeyPath);
43-
44-
KeyManagerFactory keyManagerFactory = KeyManagerFactory
45-
.getInstance(KeyManagerFactory.getDefaultAlgorithm());
46-
KeyStore clientKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
47-
clientKeyStore.load(null);
48-
clientKeyStore.setCertificateEntry("clientCert", clientCertificate);
49-
char pw[] = (trustStorePassword != null) ? trustStorePassword.toCharArray() : null;
50-
clientKeyStore.setKeyEntry("clientKey", privateKey, pw,
51-
new java.security.cert.Certificate[] { clientCertificate });
52-
keyManagerFactory.init(clientKeyStore, pw);
53-
54-
TrustManagerFactory trustManagerFactory = TrustManagerFactory
55-
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
56-
trustManagerFactory.init(loadTrustStore(trustStorePath));
43+
Security.addProvider(new BouncyCastleProvider());
5744

58-
SSLContext sslContext = SSLContext.getInstance("TLS");
59-
sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
60-
61-
return sslContext.getSocketFactory();
62-
} catch (Exception e) {
63-
throw new RuntimeException("Error creating SSLSocketFactory", e);
64-
}
65-
}
66-
67-
/**
68-
* Loads a client certificate from a given path.
69-
*
70-
* @param certPath The path to the certificate file.
71-
* @return The loaded client certificate.
72-
* @throws Exception If an error occurs while loading the certificate.
73-
*/
74-
private static X509Certificate loadClientCertificate(String certPath) throws Exception {
75-
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
76-
X509Certificate clientCertificate;
77-
try (InputStream certInputStream = new FileInputStream(certPath)) {
78-
clientCertificate = (X509Certificate) certificateFactory.generateCertificate(certInputStream);
79-
return clientCertificate;
80-
}
8145

82-
}
46+
// Load client certificate
47+
X509Certificate clientCert = loadCertificate(cert);
8348

84-
/**
85-
* Loads a trust store from a given path.
86-
*
87-
* @param trustStorePath The path to the trust store file.
88-
* @return The loaded trust store.
89-
* @throws Exception If an error occurs while loading the trust store.
90-
*/
91-
private static KeyStore loadTrustStore(String trustStorePath) throws Exception {
92-
CertificateFactory cf = CertificateFactory.getInstance("X.509");
49+
// Load client private key
50+
PrivateKey clientKey = loadPrivateKey(privateKey);
51+
52+
// Load CA certificate
53+
X509Certificate caCert = loadCertificate(trustStore);
9354

94-
List<X509Certificate> certificates = new ArrayList<>();
95-
try (FileInputStream fis = new FileInputStream(trustStorePath)) {
96-
while (fis.available() > 0) {
97-
X509Certificate certificate = (X509Certificate) cf.generateCertificate(fis);
98-
certificates.add(certificate);
99-
}
100-
}
55+
// Create a KeyStore and add the CA certificate, client certificate, and private key
56+
KeyStore keyStore = KeyStore.getInstance("PKCS12");
57+
keyStore.load(null, null);
58+
keyStore.setCertificateEntry("caCert", caCert);
59+
keyStore.setCertificateEntry("clientCert", clientCert);
60+
keyStore.setKeyEntry("clientKey", clientKey, new char[0], new X509Certificate[]{clientCert});
10161

102-
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
103-
trustStore.load(null);
62+
// Create a TrustManager that trusts the CA certificate
63+
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
64+
trustManagerFactory.init(keyStore);
10465

105-
int i = 0;
106-
for (X509Certificate certificate : certificates) {
107-
String alias = "cert" + i++;
108-
trustStore.setCertificateEntry(alias, certificate);
109-
}
66+
// Create a KeyManager that uses the client certificate and private key
67+
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
68+
keyManagerFactory.init(keyStore, new char[0]);
11069

111-
return trustStore;
112-
}
70+
// Create an SSLContext with the TrustManager and KeyManager
71+
SSLContext sslContext = SSLContext.getInstance("TLS");
72+
sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom());
11373

114-
/**
115-
* Loads a private key from a given path.
116-
*
117-
* @param privateKeyPath The path to the private key file.
118-
* @return The loaded private key.
119-
* @throws Exception If an error occurs while loading the private key.
120-
*/
121-
private static PrivateKey loadPrivateKey(String privateKeyPath) throws Exception {
122-
try (FileInputStream keyInputStream = new FileInputStream(privateKeyPath)) {
123-
byte[] keyBytes = keyInputStream.readAllBytes();
124-
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
125-
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
126-
return keyFactory.generatePrivate(keySpec);
74+
return sslContext.getSocketFactory();
75+
} catch (Exception e) {
76+
throw new RuntimeException("Error creating SSLSocketFactory", e);
12777
}
12878
}
79+
80+
/**
81+
* Loads an X.509 certificate from a PEM-encoded string.
82+
*
83+
* @param cert The PEM-encoded certificate string.
84+
* @return The X.509 certificate.
85+
* @throws IOException If an I/O error occurs.
86+
* @throws CertificateException If an error occurs while processing the certificate.
87+
*/
88+
private static X509Certificate loadCertificate(String cert) throws IOException, CertificateException {
89+
CertificateFactory cf = CertificateFactory.getInstance("X.509");
90+
try (InputStream is = new ByteArrayInputStream(cert.getBytes())) {
91+
return (X509Certificate) cf.generateCertificate(is);
92+
}
93+
}
94+
95+
/**
96+
* Loads a private key from a PEM-encoded string.
97+
*
98+
* @param privateKey The PEM-encoded private key string.
99+
* @return The private key.
100+
* @throws IOException If an I/O error occurs.
101+
* @throws NoSuchAlgorithmException If the specified algorithm is not available.
102+
* @throws InvalidKeySpecException If the private key cannot be generated.
103+
*/
104+
private static PrivateKey loadPrivateKey(String privateKey) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
105+
try (PEMParser pemParser = new PEMParser(new InputStreamReader(new ByteArrayInputStream(privateKey.getBytes())))) {
106+
Object obj = pemParser.readObject();
107+
if (obj instanceof PEMKeyPair) {
108+
// Handle RSA private key
109+
PEMKeyPair pemKeyPair = (PEMKeyPair) obj;
110+
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
111+
return converter.getPrivateKey(pemKeyPair.getPrivateKeyInfo());
112+
} else if (obj instanceof PrivateKeyInfo) {
113+
// Handle other private key formats
114+
PrivateKeyInfo privateKeyInfo = (PrivateKeyInfo) obj;
115+
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
116+
return converter.getPrivateKey(privateKeyInfo);
117+
} else {
118+
throw new InvalidKeySpecException("Invalid private key format");
119+
}
120+
}
121+
}
129122
}

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,10 @@ public void test() throws Exception {
2424
.addComponent(new DummySum()) //
2525
.activate(MyConfig.create() //
2626
.setId(CTRL_ID) //
27-
.setClientId("iotconsole-28fca1b5-0a3d-405b-9b5c-92c87fcbbbcd\n"
28-
+ "")//.setClientId("edge0") //
27+
.setClientId("edge0") //
2928
.setUsername("guest") //
3029
.setPassword("guest") //
31-
.setUri("a3emae25wqghr3-ats.iot.eu-central-1.amazonaws.com\n"
32-
+ "")//.setUri("ws://localhost:1883") //
30+
.setUri("ws://localhost:1883") //
3331
.setPersistencePriority(PersistencePriority.VERY_LOW) //
3432
.setDebugMode(true) //
3533
.build());

0 commit comments

Comments
 (0)