Skip to content

Commit 9244e75

Browse files
committed
PR feedback
1 parent 86d1ccd commit 9244e75

2 files changed

Lines changed: 142 additions & 26 deletions

File tree

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

Lines changed: 81 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@
2121
import javax.crypto.SecretKeyFactory;
2222
import javax.crypto.spec.PBEKeySpec;
2323
import java.io.ByteArrayInputStream;
24-
import java.io.File;
2524
import java.io.IOException;
26-
import java.nio.file.Files;
27-
import java.security.*;
25+
import java.security.GeneralSecurityException;
26+
import java.security.KeyFactory;
27+
import java.security.KeyStore;
28+
import java.security.KeyStoreException;
29+
import java.security.PrivateKey;
2830
import java.security.cert.Certificate;
2931
import java.security.cert.CertificateException;
3032
import java.security.cert.CertificateFactory;
@@ -57,26 +59,47 @@ public final class PemReader {
5759

5860
private static final Pattern CERT_PATTERN = Pattern.compile(
5961
"-+BEGIN\\s+.*CERTIFICATE[^-]*-+\\s*" // Header
60-
+ "([a-z0-9+/=\\r\\n]+)" // Base64 text
62+
+ "([a-z0-9+/=\\s]+)" // Base64 text
6163
+ "-+END\\s+.*CERTIFICATE[^-]*-+", // Footer
6264
CASE_INSENSITIVE);
6365

6466
private static final Pattern PRIVATE_KEY_PATTERN = Pattern.compile(
6567
"-+BEGIN\\s+.*PRIVATE\\s+KEY[^-]*-+\\s*" // Header
66-
+ "([a-z0-9+/=\\r\\n]+)" // Base64 text
68+
+ "([a-z0-9+/=\\s]+)" // Base64 text
6769
+ "-+END\\s+.*PRIVATE\\s+KEY[^-]*-+", // Footer
6870
CASE_INSENSITIVE);
6971

7072
private PemReader() {
7173
}
7274

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);
75+
/**
76+
* Loads a KeyStore from PEM file contents.
77+
* <p>
78+
* This method reads a private key and certificate chain from PEM-formatted content
79+
* and stores them in a JKS KeyStore. The certificate file must contain at least one
80+
* certificate.
81+
*
82+
* @param certificateChainContents the PEM-formatted content containing the certificate chain
83+
* @param privateKeyContents the PEM-formatted content containing the private key
84+
* @param keyPassword optional password for the private key; if the key is encrypted, this password
85+
* will be used to decrypt it
86+
* @return a KeyStore containing the private key and certificate chain
87+
* @throws IOException if an I/O error occurs while reading the PEM content
88+
* @throws GeneralSecurityException if a security-related error occurs, such as:
89+
* <ul>
90+
* <li>the private key cannot be loaded</li>
91+
* <li>the certificate chain is empty</li>
92+
* <li>the KeyStore cannot be initialized</li>
93+
* </ul>
94+
* @throws CertificateException if the certificate file does not contain any certificates
95+
*/
96+
public static KeyStore loadKeyStore(String certificateChainContents, String privateKeyContents, Optional<String> keyPassword) throws IOException, GeneralSecurityException {
97+
PrivateKey key = loadPrivateKey(privateKeyContents, keyPassword);
98+
99+
List<X509Certificate> certificateChain = readCertificateChain(certificateChainContents);
77100
if (certificateChain.isEmpty()) {
78101
throw new CertificateException("Certificate file does not contain any certificates: "
79-
+ certificateChainFile);
102+
+ certificateChainContents);
80103
}
81104

82105
KeyStore keyStore = KeyStore.getInstance("JKS");
@@ -88,8 +111,20 @@ public static KeyStore loadKeyStore(String certificateChainFile, String privateK
88111
return keyStore;
89112
}
90113

91-
public static List<X509Certificate> readCertificateChain(String certificateChain) throws CertificateException {
92-
Matcher matcher = CERT_PATTERN.matcher(certificateChain);
114+
/**
115+
* Reads a chain of X.509 certificates from PEM-formatted content.
116+
* <p>
117+
* This method extracts all certificates found in the provided PEM content. Certificates
118+
* are identified by BEGIN CERTIFICATE and END CERTIFICATE markers. The certificates are
119+
* returned in the order they appear in the input.
120+
*
121+
* @param certificateChainContents the PEM-formatted content containing one or more certificates
122+
* @return a list of X.509 certificates extracted from the PEM content; may be empty if no
123+
* certificates are found
124+
* @throws CertificateException if any certificate cannot be parsed or generated
125+
*/
126+
public static List<X509Certificate> readCertificateChain(String certificateChainContents) throws CertificateException {
127+
Matcher matcher = CERT_PATTERN.matcher(certificateChainContents);
93128
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
94129
List<X509Certificate> certificates = new ArrayList<>();
95130

@@ -103,6 +138,28 @@ public static List<X509Certificate> readCertificateChain(String certificateChain
103138
return certificates;
104139
}
105140

141+
/**
142+
* Loads a private key from PEM-formatted content.
143+
* <p>
144+
* This method supports both encrypted and unencrypted private keys. The key must be in
145+
* PKCS#8 format. To convert a key to PKCS#8 format, use:
146+
* {@code openssl pkcs8 -topk8 ...}
147+
* <p>
148+
* The method attempts to load the key using RSA, EC, and DSA algorithms in that order.
149+
*
150+
* @param privateKey the PEM-formatted content containing the private key
151+
* @param keyPassword optional password for decrypting an encrypted private key; if empty,
152+
* the key is assumed to be unencrypted
153+
* @return the loaded private key
154+
* @throws IOException if an I/O error occurs while reading the key
155+
* @throws GeneralSecurityException if a security-related error occurs, such as:
156+
* <ul>
157+
* <li>no private key is found in the content</li>
158+
* <li>the key format is invalid</li>
159+
* <li>the key cannot be decrypted with the provided password</li>
160+
* <li>the key algorithm is not supported (RSA, EC, or DSA)</li>
161+
* </ul>
162+
*/
106163
public static PrivateKey loadPrivateKey(String privateKey, Optional<String> keyPassword) throws IOException, GeneralSecurityException {
107164
Matcher matcher = PRIVATE_KEY_PATTERN.matcher(privateKey);
108165
if (!matcher.find()) {
@@ -126,20 +183,28 @@ public static PrivateKey loadPrivateKey(String privateKey, Optional<String> keyP
126183

127184
// this code requires a key in PKCS8 format which is not the default openssl format
128185
// to convert to the PKCS8 format you use : openssl pkcs8 -topk8 ...
186+
List<String> attemptedAlgorithms = new ArrayList<>();
129187
try {
130188
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
131189
return keyFactory.generatePrivate(encodedKeySpec);
132-
} catch (InvalidKeySpecException ignore) {
190+
} catch (InvalidKeySpecException e) {
191+
attemptedAlgorithms.add("RSA: " + e.getMessage());
133192
}
134193

135194
try {
136195
KeyFactory keyFactory = KeyFactory.getInstance("EC");
137196
return keyFactory.generatePrivate(encodedKeySpec);
138-
} catch (InvalidKeySpecException ignore) {
197+
} catch (InvalidKeySpecException e) {
198+
attemptedAlgorithms.add("RSA: " + e.getMessage());
139199
}
140200

141-
KeyFactory keyFactory = KeyFactory.getInstance("DSA");
142-
return keyFactory.generatePrivate(encodedKeySpec);
201+
try {
202+
return KeyFactory.getInstance("DSA").generatePrivate(encodedKeySpec);
203+
} catch (InvalidKeySpecException e) {
204+
attemptedAlgorithms.add("DSA: " + e.getMessage());
205+
throw new KeyStoreException(
206+
"Failed to load private key with any supported algorithm. Attempts: " + attemptedAlgorithms, e);
207+
}
143208
}
144209

145210
private static byte[] base64Decode(String base64) {

src/test/java/com/rabbitmq/client/test/ssl/TlsTestUtils.java

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,28 +17,35 @@
1717
package com.rabbitmq.client.test.ssl;
1818

1919
import com.rabbitmq.client.ConnectionFactory;
20+
import com.rabbitmq.client.PemReader;
2021
import com.rabbitmq.client.test.TestUtils;
2122
import com.rabbitmq.tools.Host;
2223
import io.netty.handler.ssl.ClientAuth;
2324
import io.netty.handler.ssl.IdentityCipherSuiteFilter;
2425
import io.netty.handler.ssl.JdkSslContext;
2526
import io.netty.handler.ssl.SslContext;
2627
import io.netty.handler.ssl.SslContextBuilder;
27-
import java.io.FileInputStream;
28+
29+
import javax.net.ssl.SSLContext;
30+
import javax.net.ssl.TrustManager;
31+
import javax.net.ssl.TrustManagerFactory;
32+
import javax.net.ssl.X509TrustManager;
2833
import java.net.InetAddress;
2934
import java.net.UnknownHostException;
35+
import java.nio.file.Files;
36+
import java.nio.file.Paths;
3037
import java.security.KeyStore;
3138
import java.security.NoSuchAlgorithmException;
3239
import java.security.cert.Certificate;
33-
import java.security.cert.CertificateFactory;
40+
import java.security.cert.CertificateException;
3441
import java.security.cert.X509Certificate;
3542
import java.util.Arrays;
3643
import java.util.Collection;
44+
import java.util.List;
45+
import java.util.Optional;
3746
import java.util.stream.Collectors;
38-
import javax.net.ssl.SSLContext;
39-
import javax.net.ssl.TrustManager;
40-
import javax.net.ssl.TrustManagerFactory;
41-
import javax.net.ssl.X509TrustManager;
47+
48+
import static java.nio.charset.StandardCharsets.US_ASCII;
4249

4350
class TlsTestUtils {
4451

@@ -147,11 +154,55 @@ static String clientCertificateFile() {
147154
"./rabbitmq-configuration/tls/client_" + hostname() + "_certificate.pem"));
148155
}
149156

150-
static X509Certificate loadCertificate(String file) throws Exception {
151-
try (FileInputStream inputStream = new FileInputStream(file)) {
152-
CertificateFactory fact = CertificateFactory.getInstance("X.509");
153-
return (X509Certificate) fact.generateCertificate(inputStream);
157+
static X509Certificate loadCertificate(String file) throws Exception
158+
{
159+
List<X509Certificate> certs = loadCertificateChain(file);
160+
if (certs.isEmpty()) {
161+
throw new CertificateException("No certificates found in file: " + file);
162+
}
163+
return certs.get(0);
154164
}
165+
166+
/**
167+
* Load certificate chain from PEM file.
168+
* Supports files with multiple concatenated certificates and mixed content (cert + key).
169+
*
170+
* @param file Path to PEM file
171+
* @return List of certificates found in the file
172+
*/
173+
static List<X509Certificate> loadCertificateChain(String file) throws Exception
174+
{
175+
String pemContent = new String(Files.readAllBytes(Paths.get(file)), US_ASCII);
176+
return PemReader.readCertificateChain(pemContent);
177+
}
178+
179+
/**
180+
* Load KeyStore from combined PEM file containing both certificate(s) and private key.
181+
*
182+
* @param file Path to PEM file containing certificate chain and private key
183+
* @param keyPassword Password for encrypted private key (null if unencrypted)
184+
* @return KeyStore containing the certificate chain and private key
185+
*/
186+
static KeyStore loadKeyStoreFromPem(String file, String keyPassword) throws Exception
187+
{
188+
String pemContent = new String(Files.readAllBytes(Paths.get(file)), US_ASCII);
189+
return PemReader.loadKeyStore(pemContent, pemContent, Optional.ofNullable(keyPassword));
190+
}
191+
192+
/**
193+
* Load KeyStore from separate certificate and private key PEM files.
194+
*
195+
* @param certFile Path to PEM file containing certificate chain
196+
* @param keyFile Path to PEM file containing private key
197+
* @param keyPassword Password for encrypted private key (null if unencrypted)
198+
* @return KeyStore containing the certificate chain and private key
199+
*/
200+
static KeyStore loadKeyStoreFromPem(String certFile, String keyFile, String keyPassword)
201+
throws Exception
202+
{
203+
String certContent = new String(Files.readAllBytes(Paths.get(certFile)), US_ASCII);
204+
String keyContent = new String(Files.readAllBytes(Paths.get(keyFile)), US_ASCII);
205+
return PemReader.loadKeyStore(certContent, keyContent, Optional.ofNullable(keyPassword));
155206
}
156207

157208
private static String tlsArtefactPath(String in) {

0 commit comments

Comments
 (0)