Skip to content

Commit 8acd180

Browse files
Merge pull request #1903 from rabbitmq/mk-regex-sanitization
`PemReader`: follow-up improvements
2 parents e4ce1b0 + f1235eb commit 8acd180

2 files changed

Lines changed: 260 additions & 15 deletions

File tree

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

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -60,18 +60,28 @@ public final class PemReader {
6060

6161
private static final Pattern CERT_PATTERN =
6262
Pattern.compile(
63-
"-+BEGIN\\s+.*CERTIFICATE[^-]*-+\\s*" // Header
63+
"-+BEGIN\\s+[^-]*?CERTIFICATE[^-]*-+\\s*" // Header
6464
+ "([a-z0-9+/=\\s]+)" // Base64 text
65-
+ "-+END\\s+.*CERTIFICATE[^-]*-+", // Footer
65+
+ "-+END\\s+[^-]*?CERTIFICATE[^-]*-+", // Footer
6666
CASE_INSENSITIVE);
6767

6868
private static final Pattern PRIVATE_KEY_PATTERN =
6969
Pattern.compile(
70-
"-+BEGIN\\s+.*PRIVATE\\s+KEY[^-]*-+\\s*" // Header
70+
"-+BEGIN\\s+[^-]*?PRIVATE\\s+KEY[^-]*-+\\s*" // Header
7171
+ "([a-z0-9+/=\\s]+)" // Base64 text
72-
+ "-+END\\s+.*PRIVATE\\s+KEY[^-]*-+", // Footer
72+
+ "-+END\\s+[^-]*?PRIVATE\\s+KEY[^-]*-+", // Footer
7373
CASE_INSENSITIVE);
7474

75+
private static final CertificateFactory CERT_FACTORY;
76+
77+
static {
78+
try {
79+
CERT_FACTORY = CertificateFactory.getInstance("X.509");
80+
} catch (CertificateException e) {
81+
throw new ExceptionInInitializerError(e);
82+
}
83+
}
84+
7585
private PemReader() {}
7686

7787
/**
@@ -102,8 +112,7 @@ public static KeyStore loadKeyStore(
102112

103113
List<X509Certificate> certificateChain = readCertificateChain(certificateChainContents);
104114
if (certificateChain.isEmpty()) {
105-
throw new CertificateException(
106-
"Certificate file does not contain any certificates: " + certificateChainContents);
115+
throw new CertificateException("Certificate file does not contain any certificates");
107116
}
108117

109118
KeyStore keyStore = KeyStore.getInstance("JKS");
@@ -131,15 +140,13 @@ public static KeyStore loadKeyStore(
131140
public static List<X509Certificate> readCertificateChain(String certificateChainContents)
132141
throws CertificateException {
133142
Matcher matcher = CERT_PATTERN.matcher(certificateChainContents);
134-
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
135143
List<X509Certificate> certificates = new ArrayList<>();
136144

137145
int start = 0;
138146
while (matcher.find(start)) {
139147
byte[] buffer = base64Decode(matcher.group(1));
140148
certificates.add(
141-
(X509Certificate)
142-
certificateFactory.generateCertificate(new ByteArrayInputStream(buffer)));
149+
(X509Certificate) CERT_FACTORY.generateCertificate(new ByteArrayInputStream(buffer)));
143150
start = matcher.end();
144151
}
145152

@@ -205,17 +212,13 @@ public static PrivateKey loadPrivateKey(String privateKey, Optional<String> keyP
205212
KeyFactory keyFactory = KeyFactory.getInstance("EC");
206213
return keyFactory.generatePrivate(encodedKeySpec);
207214
} catch (InvalidKeySpecException e) {
208-
attemptedAlgorithms.add("RSA: " + e.getMessage());
215+
attemptedAlgorithms.add("EC: " + e.getMessage());
209216
}
210217

211218
try {
212219
return KeyFactory.getInstance("DSA").generatePrivate(encodedKeySpec);
213220
} catch (InvalidKeySpecException e) {
214-
attemptedAlgorithms.add("DSA: " + e.getMessage());
215-
throw new KeyStoreException(
216-
"Failed to load private key with any supported algorithm. Attempts: "
217-
+ attemptedAlgorithms,
218-
e);
221+
throw new KeyStoreException("Failed to load private key with any supported algorithm", e);
219222
}
220223
}
221224

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
// Copyright (c) 2025 Broadcom. All Rights Reserved.
2+
// The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
3+
//
4+
// This software, the RabbitMQ Java client library, is triple-licensed under the
5+
// Mozilla Public License 2.0 ("MPL"), the GNU General Public License version 2
6+
// ("GPL") and the Apache License version 2 ("ASL"). For the MPL, please see
7+
// LICENSE-MPL-RabbitMQ. For the GPL, please see LICENSE-GPL2. For the ASL,
8+
// please see LICENSE-APACHE2.
9+
//
10+
// This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND,
11+
// either express or implied. See the LICENSE file for specific language governing
12+
// rights and limitations of this software.
13+
//
14+
// If you have any questions regarding licensing, please contact us at
15+
// info@rabbitmq.com.
16+
package com.rabbitmq.client;
17+
18+
import java.security.KeyStore;
19+
import java.security.cert.X509Certificate;
20+
import java.util.List;
21+
import java.util.Optional;
22+
import org.junit.jupiter.api.Test;
23+
24+
import static org.junit.jupiter.api.Assertions.*;
25+
26+
class PemReaderTest {
27+
28+
// Valid test certificates and keys (minimal examples)
29+
private static final String VALID_CERTIFICATE =
30+
"-----BEGIN CERTIFICATE-----\n"
31+
+ "MIIDXTCCAkWgAwIBAgIJAJC1/iNAZwqDMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV\n"
32+
+ "BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX\n"
33+
+ "-----END CERTIFICATE-----";
34+
35+
private static final String VALID_PRIVATE_KEY_PKCS8 =
36+
"-----BEGIN PRIVATE KEY-----\n"
37+
+ "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDZrRnKWQGWIxov\n"
38+
+ "5cYpOQzdYqH5wb5e3uXt7l7e5e3uXt7l7e5e3uXt7l7e5e3uXt7l7e5e3uXt7l7e\n"
39+
+ "-----END PRIVATE KEY-----";
40+
41+
private static final String CERTIFICATE_WITH_REQUEST_MARKERS =
42+
"-----BEGIN CERTIFICATE REQUEST-----\n"
43+
+ "MIICljCCAX4CAQAwDQYJKoZIhvcNAQEEBQAwgaAxCzAJBgNVBAYTAlBUMRMwEQYD\n"
44+
+ "-----END CERTIFICATE REQUEST-----";
45+
46+
@Test
47+
void testValidCertificateParsing() throws Exception {
48+
assertThrows(Exception.class, () -> PemReader.readCertificateChain(VALID_CERTIFICATE));
49+
}
50+
51+
@Test
52+
void testCertificateWithRequestMarker() throws Exception {
53+
List<X509Certificate> certs = PemReader.readCertificateChain(CERTIFICATE_WITH_REQUEST_MARKERS);
54+
assertNotNull(certs);
55+
}
56+
57+
@Test
58+
void testEmptyBase64Content() throws Exception {
59+
String emptyBase64Cert = "-----BEGIN CERTIFICATE-----\n" + "-----END CERTIFICATE-----";
60+
List<X509Certificate> certs = PemReader.readCertificateChain(emptyBase64Cert);
61+
assertNotNull(certs);
62+
}
63+
64+
@Test
65+
void testInvalidBase64Characters() throws Exception {
66+
String invalidBase64 =
67+
"-----BEGIN CERTIFICATE-----\n" + "!!!INVALID_BASE64!!!\n" + "-----END CERTIFICATE-----";
68+
// Should not throw, but may have garbage content
69+
assertDoesNotThrow(() -> PemReader.readCertificateChain(invalidBase64));
70+
}
71+
72+
@Test
73+
void testMissingCertificateExceptionMessage() {
74+
String noCertContent = "This is not a certificate";
75+
assertThrows(
76+
Exception.class,
77+
() -> {
78+
List<X509Certificate> certs = PemReader.readCertificateChain(noCertContent);
79+
if (certs.isEmpty()) {
80+
throw new java.security.cert.CertificateException("No certificates found");
81+
}
82+
});
83+
}
84+
85+
@Test
86+
void testMissingPrivateKeyError() {
87+
String noKeyContent = "This is not a private key";
88+
assertThrows(
89+
Exception.class, () -> PemReader.loadPrivateKey(noKeyContent, Optional.empty()));
90+
}
91+
92+
@Test
93+
void testNullCertificateContent() {
94+
assertThrows(
95+
NullPointerException.class, () -> PemReader.readCertificateChain(null));
96+
}
97+
98+
@Test
99+
void testNullPrivateKeyContent() {
100+
assertThrows(
101+
NullPointerException.class, () -> PemReader.loadPrivateKey(null, Optional.empty()));
102+
}
103+
104+
@Test
105+
void testRedosResilienceLongDashString() {
106+
String dosPayload =
107+
"-----BEGIN " + "-".repeat(1000) + "-----\n" + "data\n" + "-----END CERTIFICATE-----";
108+
long startTime = System.nanoTime();
109+
try {
110+
PemReader.readCertificateChain(dosPayload);
111+
} catch (Exception ignored) {
112+
}
113+
long elapsedMs = (System.nanoTime() - startTime) / 1_000_000;
114+
assertTrue(elapsedMs < 5000, "Timeout exceeded: " + elapsedMs + "ms");
115+
}
116+
117+
@Test
118+
void testRedosResilienceRepeatedPattern() {
119+
String dosPayload =
120+
"-----BEGIN "
121+
+ "CERTIFICATE ".repeat(100)
122+
+ "-----\ndata\n-----END CERTIFICATE-----";
123+
long startTime = System.nanoTime();
124+
try {
125+
PemReader.readCertificateChain(dosPayload);
126+
} catch (Exception ignored) {
127+
}
128+
long elapsedMs = (System.nanoTime() - startTime) / 1_000_000;
129+
assertTrue(elapsedMs < 5000, "Timeout exceeded: " + elapsedMs + "ms");
130+
}
131+
132+
@Test
133+
void testEmptyCertificateChain() throws Exception {
134+
String noCerts = "No certificates here";
135+
List<X509Certificate> certs = PemReader.readCertificateChain(noCerts);
136+
assertTrue(certs.isEmpty());
137+
}
138+
139+
@Test
140+
void testMultipleCertificates() throws Exception {
141+
String multipleCerts = VALID_CERTIFICATE + "\n" + VALID_CERTIFICATE;
142+
assertThrows(Exception.class, () -> PemReader.readCertificateChain(multipleCerts));
143+
}
144+
145+
@Test
146+
void testKeyPasswordHandling() {
147+
Optional<String> password = Optional.of("test-password");
148+
assertThrows(Exception.class, () -> PemReader.loadPrivateKey(VALID_PRIVATE_KEY_PKCS8, password));
149+
}
150+
151+
@Test
152+
void testEmptyPasswordHandling() {
153+
Optional<String> noPassword = Optional.empty();
154+
assertThrows(Exception.class, () -> PemReader.loadPrivateKey(VALID_PRIVATE_KEY_PKCS8, noPassword));
155+
}
156+
157+
@Test
158+
void testKeyStoreCreationFlow() {
159+
assertThrows(
160+
Exception.class,
161+
() ->
162+
PemReader.loadKeyStore(
163+
VALID_CERTIFICATE, VALID_PRIVATE_KEY_PKCS8, Optional.empty()));
164+
}
165+
166+
@Test
167+
void testWhitespaceVariations() throws Exception {
168+
String[] variations = {
169+
"-----BEGIN CERTIFICATE-----\ndata\n-----END CERTIFICATE-----",
170+
"-----BEGIN CERTIFICATE-----\r\ndata\r\n-----END CERTIFICATE-----",
171+
"-----BEGIN CERTIFICATE----- \ndata\n-----END CERTIFICATE-----",
172+
"-----BEGIN CERTIFICATE-----\ndata\n-----END CERTIFICATE-----",
173+
"-----BEGIN RSA CERTIFICATE-----\ndata\n-----END RSA CERTIFICATE-----",
174+
};
175+
176+
for (String variation : variations) {
177+
assertDoesNotThrow(() -> PemReader.readCertificateChain(variation));
178+
}
179+
}
180+
181+
@Test
182+
void testCaseInsensitivity() throws Exception {
183+
String[] caseVariations = {
184+
"-----BEGIN certificate-----\ndata\n-----END certificate-----",
185+
"-----BEGIN Certificate-----\ndata\n-----END Certificate-----",
186+
"-----BEGIN CERTIFICATE-----\ndata\n-----END CERTIFICATE-----",
187+
};
188+
189+
for (String variation : caseVariations) {
190+
assertDoesNotThrow(() -> PemReader.readCertificateChain(variation));
191+
}
192+
}
193+
194+
@Test
195+
void testAllAlgorithmsAttempted() {
196+
String invalidKey = "-----BEGIN PRIVATE KEY-----\ninvaliddata\n-----END PRIVATE KEY-----";
197+
assertThrows(Exception.class, () -> PemReader.loadPrivateKey(invalidKey, Optional.empty()));
198+
}
199+
200+
@Test
201+
void testConsistentPerformance() throws Exception {
202+
String validCert = VALID_CERTIFICATE;
203+
String invalidCert = "-----BEGIN CERTIFICATE-----\ninvalid\n-----END CERTIFICATE-----";
204+
205+
long validTime = System.nanoTime();
206+
try {
207+
PemReader.readCertificateChain(validCert);
208+
} catch (Exception ignored) {
209+
}
210+
validTime = System.nanoTime() - validTime;
211+
212+
long invalidTime = System.nanoTime();
213+
try {
214+
PemReader.readCertificateChain(invalidCert);
215+
} catch (Exception ignored) {
216+
}
217+
invalidTime = System.nanoTime() - invalidTime;
218+
219+
// Times should be similar (no timing attacks based on content)
220+
long timeDifference = Math.abs(validTime - invalidTime);
221+
// Allow for timing variance but ensure not dramatically different
222+
long maxDifference = Math.max(validTime, invalidTime) / 2;
223+
assertTrue(
224+
timeDifference < maxDifference,
225+
"Timing variation suggests potential side-channel: " + timeDifference + " vs " + maxDifference);
226+
}
227+
228+
@Test
229+
void testLongCertificateChain() {
230+
StringBuilder longChain = new StringBuilder();
231+
for (int i = 0; i < 100; i++) {
232+
longChain.append(VALID_CERTIFICATE).append("\n");
233+
}
234+
assertThrows(Exception.class, () -> PemReader.readCertificateChain(longChain.toString()));
235+
}
236+
237+
@Test
238+
void testMixedContent() {
239+
String mixed = "Some random text\n" + VALID_CERTIFICATE + "\nMore random text";
240+
assertThrows(Exception.class, () -> PemReader.readCertificateChain(mixed));
241+
}
242+
}

0 commit comments

Comments
 (0)