Skip to content

Commit 86d1ccd

Browse files
committed
Add support for PEM files
1 parent 0a2293e commit 86d1ccd

4 files changed

Lines changed: 317 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: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
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+
* <li>updated the regular expressions to avoid exponential backtracking.</li>
54+
* </ul>
55+
*/
56+
public final class PemReader {
57+
58+
private static final Pattern CERT_PATTERN = Pattern.compile(
59+
"-+BEGIN\\s+.*CERTIFICATE[^-]*-+\\s*" // Header
60+
+ "([a-z0-9+/=\\r\\n]+)" // Base64 text
61+
+ "-+END\\s+.*CERTIFICATE[^-]*-+", // Footer
62+
CASE_INSENSITIVE);
63+
64+
private static final Pattern PRIVATE_KEY_PATTERN = Pattern.compile(
65+
"-+BEGIN\\s+.*PRIVATE\\s+KEY[^-]*-+\\s*" // Header
66+
+ "([a-z0-9+/=\\r\\n]+)" // Base64 text
67+
+ "-+END\\s+.*PRIVATE\\s+KEY[^-]*-+", // Footer
68+
CASE_INSENSITIVE);
69+
70+
private PemReader() {
71+
}
72+
73+
public static KeyStore loadKeyStore(String certificateChainFile, String privateKeyFile, Optional<String> keyPassword) throws IOException, GeneralSecurityException {
74+
PrivateKey key = loadPrivateKey(privateKeyFile, keyPassword);
75+
76+
List<X509Certificate> certificateChain = readCertificateChain(certificateChainFile);
77+
if (certificateChain.isEmpty()) {
78+
throw new CertificateException("Certificate file does not contain any certificates: "
79+
+ certificateChainFile);
80+
}
81+
82+
KeyStore keyStore = KeyStore.getInstance("JKS");
83+
keyStore.load(null, null);
84+
keyStore.setKeyEntry("key",
85+
key,
86+
keyPassword.orElse("").toCharArray(),
87+
certificateChain.toArray(new Certificate[0]));
88+
return keyStore;
89+
}
90+
91+
public static List<X509Certificate> readCertificateChain(String certificateChain) throws CertificateException {
92+
Matcher matcher = CERT_PATTERN.matcher(certificateChain);
93+
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
94+
List<X509Certificate> certificates = new ArrayList<>();
95+
96+
int start = 0;
97+
while (matcher.find(start)) {
98+
byte[] buffer = base64Decode(matcher.group(1));
99+
certificates.add((X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(buffer)));
100+
start = matcher.end();
101+
}
102+
103+
return certificates;
104+
}
105+
106+
public static PrivateKey loadPrivateKey(String privateKey, Optional<String> keyPassword) throws IOException, GeneralSecurityException {
107+
Matcher matcher = PRIVATE_KEY_PATTERN.matcher(privateKey);
108+
if (!matcher.find()) {
109+
throw new KeyStoreException("did not find a private key");
110+
}
111+
byte[] encodedKey = base64Decode(matcher.group(1));
112+
113+
PKCS8EncodedKeySpec encodedKeySpec;
114+
if (keyPassword.isPresent()) {
115+
EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = new EncryptedPrivateKeyInfo(encodedKey);
116+
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(encryptedPrivateKeyInfo.getAlgName());
117+
SecretKey secretKey = keyFactory.generateSecret(new PBEKeySpec(keyPassword.get().toCharArray()));
118+
119+
Cipher cipher = Cipher.getInstance(encryptedPrivateKeyInfo.getAlgName());
120+
cipher.init(DECRYPT_MODE, secretKey, encryptedPrivateKeyInfo.getAlgParameters());
121+
122+
encodedKeySpec = encryptedPrivateKeyInfo.getKeySpec(cipher);
123+
} else {
124+
encodedKeySpec = new PKCS8EncodedKeySpec(encodedKey);
125+
}
126+
127+
// this code requires a key in PKCS8 format which is not the default openssl format
128+
// to convert to the PKCS8 format you use : openssl pkcs8 -topk8 ...
129+
try {
130+
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
131+
return keyFactory.generatePrivate(encodedKeySpec);
132+
} catch (InvalidKeySpecException ignore) {
133+
}
134+
135+
try {
136+
KeyFactory keyFactory = KeyFactory.getInstance("EC");
137+
return keyFactory.generatePrivate(encodedKeySpec);
138+
} catch (InvalidKeySpecException ignore) {
139+
}
140+
141+
KeyFactory keyFactory = KeyFactory.getInstance("DSA");
142+
return keyFactory.generatePrivate(encodedKeySpec);
143+
}
144+
145+
private static byte[] base64Decode(String base64) {
146+
return getMimeDecoder().decode(base64.getBytes(US_ASCII));
147+
}
148+
149+
}

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)