Skip to content

Commit 93087a0

Browse files
fix: resolve Base64 decoding error in certificate parsing (#2615) (#2… (#2707)
fix: resolve Base64 decoding error in certificate parsing (#2615) (#2693) - Fix IllegalArgumentException: Illegal base64 character 20 in S3StreamKafkaMetricsManager - Replace single newline removal with comprehensive whitespace cleanup using replaceAll("\s", "") - Add graceful error handling for both Base64 and certificate parsing failures - Add comprehensive unit tests covering various whitespace scenarios and edge cases - Improve logging with specific error messages for failed certificate parsing Fixes #2615 (cherry picked from commit 75bdea0) Co-authored-by: Vivek Chavan <[email protected]>
1 parent ca0e9bf commit 93087a0

File tree

2 files changed

+106
-7
lines changed

2 files changed

+106
-7
lines changed

server-common/src/main/java/org/apache/kafka/server/metrics/s3stream/S3StreamKafkaMetricsManager.java

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -407,14 +407,32 @@ private static void registerCertMetrics(Meter meter, X509Certificate cert, Strin
407407
private static X509Certificate[] parseCertificates(String pemContent) throws CertificateException {
408408
String[] pemArray = pemContent.split("-----END CERTIFICATE-----");
409409
CertificateFactory factory = CertificateFactory.getInstance("X.509");
410-
X509Certificate[] certs = new X509Certificate[pemArray.length];
411-
412-
for (int i = 0; i < pemArray.length; i++) {
413-
String pemPart = pemArray[i];
414-
byte[] certBytes = Base64.getDecoder().decode(pemPart.replace("-----BEGIN CERTIFICATE-----", "").replaceAll("\n", ""));
415-
certs[i] = (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(certBytes));
410+
List<X509Certificate> certList = new ArrayList<>();
411+
412+
for (String pemPart : pemArray) {
413+
// Clean the PEM part by removing headers and all whitespace characters
414+
String cleanedPemPart = pemPart.replace("-----BEGIN CERTIFICATE-----", "")
415+
.replaceAll("\\s", ""); // Remove all whitespace characters (spaces, tabs, newlines, etc.)
416+
417+
// Skip empty parts that might result from splitting
418+
if (cleanedPemPart.isEmpty()) {
419+
continue;
420+
}
421+
422+
try {
423+
byte[] certBytes = Base64.getDecoder().decode(cleanedPemPart);
424+
X509Certificate cert = (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(certBytes));
425+
certList.add(cert);
426+
} catch (IllegalArgumentException e) {
427+
LOGGER.warn("Failed to decode certificate part due to invalid Base64, skipping: {}", e.getMessage());
428+
// Continue processing other certificates instead of failing completely
429+
} catch (CertificateException e) {
430+
LOGGER.warn("Failed to parse certificate, skipping: {}", e.getMessage());
431+
// Continue processing other certificates instead of failing completely
432+
}
416433
}
417-
return certs;
434+
435+
return certList.toArray(new X509Certificate[0]);
418436
}
419437

420438
public static void setIsActiveSupplier(Supplier<Boolean> isActiveSupplier) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright 2025, AutoMQ HK Limited.
3+
*
4+
* Licensed to the Apache Software Foundation (ASF) under one or more
5+
* contributor license agreements. See the NOTICE file distributed with
6+
* this work for additional information regarding copyright ownership.
7+
* The ASF licenses this file to You under the Apache License, Version 2.0
8+
* (the "License"); you may not use this file except in compliance with
9+
* the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
package org.apache.kafka.server.metrics.s3stream;
21+
22+
import org.junit.jupiter.api.Test;
23+
24+
import java.lang.reflect.Method;
25+
import java.security.cert.X509Certificate;
26+
27+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
28+
import static org.junit.jupiter.api.Assertions.assertEquals;
29+
import static org.junit.jupiter.api.Assertions.assertNotNull;
30+
31+
public class S3StreamKafkaMetricsManagerTest {
32+
33+
@Test
34+
public void testParseCertificatesWithEmptyString() throws Exception {
35+
X509Certificate[] certificates = callParseCertificates("");
36+
37+
assertNotNull(certificates);
38+
assertEquals(0, certificates.length);
39+
}
40+
41+
@Test
42+
public void testParseCertificatesWithWhitespaceInBase64() throws Exception {
43+
// Test certificate with whitespace in Base64 content that would cause "Illegal base64 character 20" error
44+
String certWithSpaces = "-----BEGIN CERTIFICATE-----\n" +
45+
"TUlJQmtUQ0IrUFNKQnFaUUhpUWxDd0ZBTUJReEVqQVFCZ05W" + // base64 line with spaces
46+
" QkFNTUNXeHZZMkZzYUc5emREQWVGdzB5TlRFd01qbHhNREF3TUZG\n" + // Leading space
47+
"QUFNVUNXeHZZMkZzYUc5emREQWVGdzB5TlRFd01qbHhNREF3\t" + // Trailing tab
48+
"TUZGUUFNVUNXeHZZMG\r\n" + // Carriage return + newline
49+
"-----END CERTIFICATE-----";
50+
51+
// This should not throw IllegalArgumentException due to the fix
52+
assertDoesNotThrow(() -> {
53+
X509Certificate[] certificates = callParseCertificates(certWithSpaces);
54+
assertNotNull(certificates);
55+
// The certificate might not be valid (just test data), but at least it shouldn't crash with Base64 error
56+
});
57+
}
58+
59+
@Test
60+
public void testParseCertificatesWithInvalidBase64() throws Exception {
61+
String invalidCert = "-----BEGIN CERTIFICATE-----\n" +
62+
"InvalidBase64Content!!!\n" +
63+
"-----END CERTIFICATE-----";
64+
65+
// Should not throw exception but return empty array due to graceful error handling
66+
assertDoesNotThrow(() -> {
67+
X509Certificate[] certificates = callParseCertificates(invalidCert);
68+
assertNotNull(certificates);
69+
assertEquals(0, certificates.length); // Invalid cert should be skipped
70+
});
71+
}
72+
73+
/**
74+
* Helper method to call the private parseCertificates method using reflection
75+
*/
76+
private X509Certificate[] callParseCertificates(String pemContent) throws Exception {
77+
Method method = S3StreamKafkaMetricsManager.class.getDeclaredMethod("parseCertificates", String.class);
78+
method.setAccessible(true);
79+
return (X509Certificate[]) method.invoke(null, pemContent);
80+
}
81+
}

0 commit comments

Comments
 (0)