Skip to content

Commit 15ede0f

Browse files
committed
Add support for PEM files
1 parent 4e5aa8b commit 15ede0f

4 files changed

Lines changed: 316 additions & 19 deletions

File tree

src/main/java/com/rabbitmq/client/ConnectionFactoryConfigurator.java

Lines changed: 65 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,35 @@
1717

1818
import com.rabbitmq.client.impl.AMQConnection;
1919

20-
import javax.net.ssl.*;
20+
import javax.net.ssl.KeyManager;
21+
import javax.net.ssl.KeyManagerFactory;
22+
import javax.net.ssl.SSLContext;
23+
import javax.net.ssl.TrustManager;
24+
import javax.net.ssl.TrustManagerFactory;
25+
import java.io.BufferedReader;
2126
import java.io.FileInputStream;
2227
import java.io.FileNotFoundException;
2328
import java.io.IOException;
2429
import java.io.InputStream;
30+
import java.io.InputStreamReader;
2531
import java.net.URISyntaxException;
26-
import java.security.*;
32+
import java.security.GeneralSecurityException;
33+
import java.security.KeyManagementException;
34+
import java.security.KeyStore;
35+
import java.security.KeyStoreException;
36+
import java.security.NoSuchAlgorithmException;
2737
import java.security.cert.CertificateException;
28-
import java.util.*;
38+
import java.util.Arrays;
39+
import java.util.Collections;
40+
import java.util.HashMap;
41+
import java.util.List;
42+
import java.util.Map;
43+
import java.util.Optional;
44+
import java.util.Properties;
2945
import java.util.concurrent.ConcurrentHashMap;
3046

47+
import static java.nio.charset.StandardCharsets.US_ASCII;
48+
3149
/**
3250
* Helper class to load {@link ConnectionFactory} settings from a property file.
3351
* <p>
@@ -72,6 +90,7 @@ public class ConnectionFactoryConfigurator {
7290
public static final String SSL_ENABLED = "ssl.enabled";
7391
public static final String SSL_KEY_STORE = "ssl.key.store";
7492
public static final String SSL_KEY_STORE_PASSWORD = "ssl.key.store.password";
93+
public static final String SSL_KEY_PASSWORD = "ssl.key.password";
7594
public static final String SSL_KEY_STORE_TYPE = "ssl.key.store.type";
7695
public static final String SSL_KEY_STORE_ALGORITHM = "ssl.key.store.algorithm";
7796
public static final String SSL_TRUST_STORE = "ssl.trust.store";
@@ -80,11 +99,13 @@ public class ConnectionFactoryConfigurator {
8099
public static final String SSL_TRUST_STORE_ALGORITHM = "ssl.trust.store.algorithm";
81100
public static final String SSL_VALIDATE_SERVER_CERTIFICATE = "ssl.validate.server.certificate";
82101
public static final String SSL_VERIFY_HOSTNAME = "ssl.verify.hostname";
102+
public static final String PEM_TYPE = "PEM";
83103

84104
// aliases allow to be compatible with keys from Spring Boot and still be consistent with
85105
// the initial naming of the keys
86106
private static final Map<String, List<String>> ALIASES = new ConcurrentHashMap<String, List<String>>() {{
87107
put(SSL_KEY_STORE, Arrays.asList("ssl.key-store"));
108+
put(SSL_KEY_PASSWORD, Arrays.asList("ssl.key-password"));
88109
put(SSL_KEY_STORE_PASSWORD, Arrays.asList("ssl.key-store-password"));
89110
put(SSL_KEY_STORE_TYPE, Arrays.asList("ssl.key-store-type"));
90111
put(SSL_KEY_STORE_ALGORITHM, Arrays.asList("ssl.key-store-algorithm"));
@@ -228,6 +249,7 @@ private static void setUpSsl(ConnectionFactory cf, Map<String, String> propertie
228249
String algorithm = lookUp(SSL_ALGORITHM, properties, prefix);
229250
String keyStoreLocation = lookUp(SSL_KEY_STORE, properties, prefix);
230251
String keyStorePassword = lookUp(SSL_KEY_STORE_PASSWORD, properties, prefix);
252+
String keyPassword = lookUp(SSL_KEY_PASSWORD, properties, prefix);
231253
String keyStoreType = lookUp(SSL_KEY_STORE_TYPE, properties, prefix, "PKCS12");
232254
String keyStoreAlgorithm = lookUp(SSL_KEY_STORE_ALGORITHM, properties, prefix, "SunX509");
233255
String trustStoreLocation = lookUp(SSL_TRUST_STORE, properties, prefix);
@@ -250,7 +272,7 @@ private static void setUpSsl(ConnectionFactory cf, Map<String, String> propertie
250272
algorithm
251273
);
252274
} else {
253-
KeyManager[] keyManagers = configureKeyManagers(keyStoreLocation, keyStorePassword, keyStoreType, keyStoreAlgorithm);
275+
KeyManager[] keyManagers = configureKeyManagers(keyStoreLocation, keyStorePassword, keyStoreType, keyStoreAlgorithm, keyPassword);
254276
TrustManager[] trustManagers = configureTrustManagers(trustStoreLocation, trustStorePassword, trustStoreType, trustStoreAlgorithm);
255277

256278
// create ssl context
@@ -263,31 +285,57 @@ private static void setUpSsl(ConnectionFactory cf, Map<String, String> propertie
263285
cf.enableHostnameVerification();
264286
}
265287
}
266-
} catch (NoSuchAlgorithmException | IOException | CertificateException |
267-
UnrecoverableKeyException | KeyStoreException | KeyManagementException e) {
288+
} catch (IOException | GeneralSecurityException e) {
268289
throw new IllegalStateException("Error while configuring TLS", e);
269290
}
270291
}
271292

272-
private static KeyManager[] configureKeyManagers(String keystore, String keystorePassword, String keystoreType, String keystoreAlgorithm) throws KeyStoreException, IOException, NoSuchAlgorithmException,
273-
CertificateException, UnrecoverableKeyException {
274-
char[] keyPassphrase = null;
275-
if (keystorePassword != null) {
276-
keyPassphrase = keystorePassword.toCharArray();
277-
}
293+
private static KeyManager[] configureKeyManagers(String keyStoreLocation, String keystorePassword, String keystoreType, String keystoreAlgorithm, String keyPassword) throws IOException, GeneralSecurityException {
278294
KeyManager[] keyManagers = null;
279-
if (keystore != null) {
280-
KeyStore ks = KeyStore.getInstance(keystoreType);
281-
try (InputStream in = loadResource(keystore)) {
282-
ks.load(in, keyPassphrase);
295+
if (keyStoreLocation != null) {
296+
KeyStore ks = configureKeyStore(keyStoreLocation, keystorePassword, keystoreType, keyPassword);
297+
298+
char[] password = "".toCharArray();
299+
if (PEM_TYPE.equalsIgnoreCase(keystoreType) && keyPassword != null) {
300+
password = keyPassword.toCharArray();
301+
} else if (keystorePassword != null) {
302+
password = keystorePassword.toCharArray();
283303
}
304+
284305
KeyManagerFactory kmf = KeyManagerFactory.getInstance(keystoreAlgorithm);
285-
kmf.init(ks, keyPassphrase);
306+
kmf.init(ks, password);
286307
keyManagers = kmf.getKeyManagers();
287308
}
288309
return keyManagers;
289310
}
290311

312+
private static KeyStore configureKeyStore(String keyStoreLocation, String keyStorePassword, String keystoreType, String keyPassword) throws GeneralSecurityException, IOException {
313+
try (InputStream in = loadResource(keyStoreLocation)) {
314+
if (PEM_TYPE.equalsIgnoreCase(keystoreType)) {
315+
if (keyStorePassword != null && !keyStorePassword.isEmpty())
316+
throw new CertificateException("KeyStore password cannot be specified with PEM format, only key password may be specified");
317+
else {
318+
StringBuilder readerBuilder = new StringBuilder();
319+
try (BufferedReader br = new BufferedReader(new InputStreamReader(in, US_ASCII))) {
320+
String inputLine;
321+
while ((inputLine = br.readLine()) != null) {
322+
readerBuilder.append(inputLine).append('\n');
323+
}
324+
}
325+
326+
String pemFileContents = readerBuilder.toString();
327+
return PemReader.loadKeyStore(pemFileContents, pemFileContents, Optional.ofNullable(keyPassword));
328+
}
329+
} else {
330+
char[] keyPassphrase = keyStorePassword == null ? null : keyStorePassword.toCharArray();
331+
332+
KeyStore ks = KeyStore.getInstance(keystoreType);
333+
ks.load(in, keyPassphrase);
334+
return ks;
335+
}
336+
}
337+
}
338+
291339
private static TrustManager[] configureTrustManagers(String truststore, String truststorePassword, String truststoreType, String truststoreAlgorithm)
292340
throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {
293341
char[] trustPassphrase = null;
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
// Copyright (c) 2017-2025 Broadcom. All Rights Reserved. The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
2+
//
3+
// This software, the RabbitMQ Java client library, is triple-licensed under the
4+
// Mozilla Public License 2.0 ("MPL"), the GNU General Public License version 2
5+
// ("GPL") and the Apache License version 2 ("ASL"). For the MPL, please see
6+
// LICENSE-MPL-RabbitMQ. For the GPL, please see LICENSE-GPL2. For the ASL,
7+
// please see LICENSE-APACHE2.
8+
//
9+
// This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND,
10+
// either express or implied. See the LICENSE file for specific language governing
11+
// rights and limitations of this software.
12+
//
13+
// If you have any questions regarding licensing, please contact us at
14+
// info@rabbitmq.com.
15+
16+
package com.rabbitmq.client;
17+
18+
import javax.crypto.Cipher;
19+
import javax.crypto.EncryptedPrivateKeyInfo;
20+
import javax.crypto.SecretKey;
21+
import javax.crypto.SecretKeyFactory;
22+
import javax.crypto.spec.PBEKeySpec;
23+
import java.io.ByteArrayInputStream;
24+
import java.io.File;
25+
import java.io.IOException;
26+
import java.nio.file.Files;
27+
import java.security.*;
28+
import java.security.cert.Certificate;
29+
import java.security.cert.CertificateException;
30+
import java.security.cert.CertificateFactory;
31+
import java.security.cert.X509Certificate;
32+
import java.security.spec.InvalidKeySpecException;
33+
import java.security.spec.PKCS8EncodedKeySpec;
34+
import java.util.ArrayList;
35+
import java.util.List;
36+
import java.util.Optional;
37+
import java.util.regex.Matcher;
38+
import java.util.regex.Pattern;
39+
40+
import static java.nio.charset.StandardCharsets.US_ASCII;
41+
import static java.util.Base64.getMimeDecoder;
42+
import static java.util.regex.Pattern.CASE_INSENSITIVE;
43+
import static javax.crypto.Cipher.DECRYPT_MODE;
44+
45+
/**
46+
* Note: this class is copied from org.apache.zookeeper.util.PemReader (see
47+
* https://github.com/apache/zookeeper/blob/master/zookeeper-server/src/main/java/org/apache/zookeeper/util/PemReader.java)
48+
* under the same Apache License 2.0 (ASL).
49+
*
50+
* The following modifications have been made to the original source code:
51+
* <ul>
52+
* <li>removed methods around loading trustStores.</li>
53+
* </ul>
54+
*/
55+
public final class PemReader {
56+
57+
private static final Pattern CERT_PATTERN = Pattern.compile(
58+
"-+BEGIN\\s+.*CERTIFICATE[^-]*-+(?:\\s|\\r|\\n)+" // Header
59+
+ "([a-z0-9+/=\\r\\n]+)" // Base64 text
60+
+ "-+END\\s+.*CERTIFICATE[^-]*-+", // Footer
61+
CASE_INSENSITIVE);
62+
63+
private static final Pattern PRIVATE_KEY_PATTERN = Pattern.compile(
64+
"-+BEGIN\\s+.*PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+" // Header
65+
+ "([a-z0-9+/=\\r\\n]+)" // Base64 text
66+
+ "-+END\\s+.*PRIVATE\\s+KEY[^-]*-+", // Footer
67+
CASE_INSENSITIVE);
68+
69+
private PemReader() {
70+
}
71+
72+
public static KeyStore loadKeyStore(String certificateChainFile, String privateKeyFile, Optional<String> keyPassword) throws IOException, GeneralSecurityException {
73+
PrivateKey key = loadPrivateKey(privateKeyFile, keyPassword);
74+
75+
List<X509Certificate> certificateChain = readCertificateChain(certificateChainFile);
76+
if (certificateChain.isEmpty()) {
77+
throw new CertificateException("Certificate file does not contain any certificates: "
78+
+ certificateChainFile);
79+
}
80+
81+
KeyStore keyStore = KeyStore.getInstance("JKS");
82+
keyStore.load(null, null);
83+
keyStore.setKeyEntry("key",
84+
key,
85+
keyPassword.orElse("").toCharArray(),
86+
certificateChain.toArray(new Certificate[0]));
87+
return keyStore;
88+
}
89+
90+
public static List<X509Certificate> readCertificateChain(String certificateChain) throws CertificateException {
91+
Matcher matcher = CERT_PATTERN.matcher(certificateChain);
92+
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
93+
List<X509Certificate> certificates = new ArrayList<>();
94+
95+
int start = 0;
96+
while (matcher.find(start)) {
97+
byte[] buffer = base64Decode(matcher.group(1));
98+
certificates.add((X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(buffer)));
99+
start = matcher.end();
100+
}
101+
102+
return certificates;
103+
}
104+
105+
public static PrivateKey loadPrivateKey(String privateKey, Optional<String> keyPassword) throws IOException, GeneralSecurityException {
106+
Matcher matcher = PRIVATE_KEY_PATTERN.matcher(privateKey);
107+
if (!matcher.find()) {
108+
throw new KeyStoreException("did not find a private key");
109+
}
110+
byte[] encodedKey = base64Decode(matcher.group(1));
111+
112+
PKCS8EncodedKeySpec encodedKeySpec;
113+
if (keyPassword.isPresent()) {
114+
EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = new EncryptedPrivateKeyInfo(encodedKey);
115+
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(encryptedPrivateKeyInfo.getAlgName());
116+
SecretKey secretKey = keyFactory.generateSecret(new PBEKeySpec(keyPassword.get().toCharArray()));
117+
118+
Cipher cipher = Cipher.getInstance(encryptedPrivateKeyInfo.getAlgName());
119+
cipher.init(DECRYPT_MODE, secretKey, encryptedPrivateKeyInfo.getAlgParameters());
120+
121+
encodedKeySpec = encryptedPrivateKeyInfo.getKeySpec(cipher);
122+
} else {
123+
encodedKeySpec = new PKCS8EncodedKeySpec(encodedKey);
124+
}
125+
126+
// this code requires a key in PKCS8 format which is not the default openssl format
127+
// to convert to the PKCS8 format you use : openssl pkcs8 -topk8 ...
128+
try {
129+
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
130+
return keyFactory.generatePrivate(encodedKeySpec);
131+
} catch (InvalidKeySpecException ignore) {
132+
}
133+
134+
try {
135+
KeyFactory keyFactory = KeyFactory.getInstance("EC");
136+
return keyFactory.generatePrivate(encodedKeySpec);
137+
} catch (InvalidKeySpecException ignore) {
138+
}
139+
140+
KeyFactory keyFactory = KeyFactory.getInstance("DSA");
141+
return keyFactory.generatePrivate(encodedKeySpec);
142+
}
143+
144+
private static byte[] base64Decode(String base64) {
145+
return getMimeDecoder().decode(base64.getBytes(US_ASCII));
146+
}
147+
148+
}

src/test/java/com/rabbitmq/client/test/PropertyFileInitialisationTest.java

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,31 @@
1717

1818
import com.rabbitmq.client.ConnectionFactory;
1919
import com.rabbitmq.client.ConnectionFactoryConfigurator;
20+
import org.junit.jupiter.api.DisplayName;
2021
import org.junit.jupiter.api.Test;
2122

2223
import javax.net.ssl.SSLContext;
2324
import java.io.FileReader;
2425
import java.io.IOException;
2526
import java.io.Reader;
26-
import java.util.*;
27+
import java.security.cert.CertificateException;
28+
import java.util.Arrays;
29+
import java.util.Collections;
30+
import java.util.HashMap;
31+
import java.util.Map;
32+
import java.util.Properties;
2733
import java.util.concurrent.atomic.AtomicBoolean;
2834
import java.util.stream.Stream;
2935

3036
import static com.rabbitmq.client.impl.AMQConnection.defaultClientProperties;
3137
import static org.assertj.core.api.Assertions.assertThat;
38+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
3239
import static org.mockito.ArgumentMatchers.any;
33-
import static org.mockito.Mockito.*;
40+
import static org.mockito.Mockito.anyString;
41+
import static org.mockito.Mockito.mock;
42+
import static org.mockito.Mockito.never;
43+
import static org.mockito.Mockito.times;
44+
import static org.mockito.Mockito.verify;
3445

3546
/**
3647
*
@@ -173,6 +184,48 @@ public void tlsInitialisationWithKeyManagerAndTrustManagerShouldSucceed() {
173184
});
174185
}
175186

187+
@Test
188+
@DisplayName("TLS initialisation with PEM keystore and Key Store password should fail")
189+
void tlsInitialisationWithPemAndKeyStorePasswordShouldFail() {
190+
Map<String, String> configuration = new HashMap<>();
191+
configuration.put(ConnectionFactoryConfigurator.SSL_ENABLED, "true");
192+
configuration.put(ConnectionFactoryConfigurator.SSL_KEY_STORE, "./src/test/resources/property-file-initialisation/tls/certificates.pem");
193+
configuration.put(ConnectionFactoryConfigurator.SSL_KEY_STORE_PASSWORD, "bunnies");
194+
configuration.put(ConnectionFactoryConfigurator.SSL_KEY_STORE_TYPE, "PEM");
195+
configuration.put(ConnectionFactoryConfigurator.SSL_KEY_STORE_ALGORITHM, "S unX509");
196+
197+
ConnectionFactory connectionFactory = mock(ConnectionFactory.class);
198+
assertThatThrownBy(() -> ConnectionFactoryConfigurator.load(connectionFactory, configuration, ""))
199+
.isInstanceOf(IllegalStateException.class)
200+
.hasRootCauseInstanceOf(CertificateException.class);
201+
}
202+
203+
@Test
204+
@DisplayName("TLS initialisation with PEM certificates should succeed")
205+
void tlsInitialisationWithPemCertificatesShouldSucceed() {
206+
Stream.of("./src/test/resources/property-file-initialisation/tls/",
207+
"classpath:/property-file-initialisation/tls/").forEach(baseDirectory -> {
208+
Map<String, String> configuration = new HashMap<>();
209+
configuration.put(ConnectionFactoryConfigurator.SSL_ENABLED, "true");
210+
configuration.put(ConnectionFactoryConfigurator.SSL_KEY_STORE, baseDirectory + "certificates.pem");
211+
configuration.put(ConnectionFactoryConfigurator.SSL_KEY_STORE_TYPE, "PEM");
212+
configuration.put(ConnectionFactoryConfigurator.SSL_KEY_STORE_ALGORITHM, "SunX509");
213+
214+
configuration.put(ConnectionFactoryConfigurator.SSL_TRUST_STORE, baseDirectory + "truststore.jks");
215+
configuration.put(ConnectionFactoryConfigurator.SSL_TRUST_STORE_PASSWORD, "bunnies");
216+
configuration.put(ConnectionFactoryConfigurator.SSL_TRUST_STORE_TYPE, "JKS");
217+
configuration.put(ConnectionFactoryConfigurator.SSL_TRUST_STORE_ALGORITHM, "SunX509");
218+
219+
configuration.put(ConnectionFactoryConfigurator.SSL_VERIFY_HOSTNAME, "true");
220+
221+
ConnectionFactory connectionFactory = mock(ConnectionFactory.class);
222+
ConnectionFactoryConfigurator.load(connectionFactory, configuration, "");
223+
224+
verify(connectionFactory, times(1)).useSslProtocol(any(SSLContext.class));
225+
verify(connectionFactory, times(1)).enableHostnameVerification();
226+
});
227+
}
228+
176229
@Test
177230
public void tlsNotEnabledIfNotConfigured() {
178231
ConnectionFactory connectionFactory = mock(ConnectionFactory.class);

0 commit comments

Comments
 (0)