This is an automated email from the ASF dual-hosted git repository.

rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 3e176e4d2923a2e2413c1cb0112ab912fd50ca2e
Author: opencode <[email protected]>
AuthorDate: Thu Oct 15 15:07:26 2026 +0200

    Fix the PEM output of PEMFile.toPEM()
    
    The encoded text was written with the final line of the Base64 data joined
    onto the end boundary and without a line separator after the end boundary.
    The result was not valid PEM: OpenSSL was unable to decode it
    ("Could not find or decode certificate") and Tomcat's own PEM parser did not
    see any certificate in it, since the end boundary never started a line.
    
    Write each boundary on a line of its own, terminate the encoded text with a
    line separator and encode with the line length of 64 characters that RFC
    7468 requires. The Base64 line separator is now the same as the one used for
    the boundaries, so the output no longer mixes line separators when the JVM
    uses a line feed. The output of the method now has the same format as the 
one
    produced by keytool -exportcert -rfc for the same certificate, apart from 
the
    line separator, which on a JVM using a line feed will be that separator
    rather than the carriage return line feed used by keytool.
    
    These values are used for the mod_rewrite compatible SSL_SERVER_CERT,
    SSL_CLIENT_CERT and CERT_CHAIN variables, which are expected to contain a
    full PEM encoded certificate as Apache HTTP Server's mod_ssl provides them.
    
    Tests: testToPem encodes the certificate in the new certificate.pem test
    file and checks the resulting structure as well as that the result can be
    read back as PEM. It previously failed because the end boundary was not on a
    line of its own.
---
 java/org/apache/tomcat/util/net/jsse/PEMFile.java  | 13 ++++--
 .../apache/tomcat/util/net/jsse/TestPEMFile.java   | 49 ++++++++++++++++++++++
 .../apache/tomcat/util/net/jsse/certificate.pem    | 32 ++++++++++++++
 3 files changed, 91 insertions(+), 3 deletions(-)

diff --git a/java/org/apache/tomcat/util/net/jsse/PEMFile.java 
b/java/org/apache/tomcat/util/net/jsse/PEMFile.java
index ec45a7b34c..f9023bd885 100644
--- a/java/org/apache/tomcat/util/net/jsse/PEMFile.java
+++ b/java/org/apache/tomcat/util/net/jsse/PEMFile.java
@@ -120,9 +120,16 @@ public class PEMFile {
      * @throws CertificateEncodingException If the certificate cannot be 
encoded
      */
     public static String toPEM(X509Certificate certificate) throws 
CertificateEncodingException {
-        return Part.BEGIN_BOUNDARY + Part.CERTIFICATE + Part.FINISH_BOUNDARY + 
System.lineSeparator() +
-                
Base64.getMimeEncoder().encodeToString(certificate.getEncoded()) + 
Part.END_BOUNDARY +
-                Part.CERTIFICATE + Part.FINISH_BOUNDARY;
+        // RFC 7468 requires encoded lines of 64 characters, a line separator
+        // after each line and a line separator after the end boundary. Use the
+        // same line separator here as for the boundaries.
+        String lineSeparator = System.lineSeparator();
+        Base64.Encoder encoder =
+                Base64.getMimeEncoder(64, 
lineSeparator.getBytes(StandardCharsets.US_ASCII));
+
+        return Part.BEGIN_BOUNDARY + Part.CERTIFICATE + Part.FINISH_BOUNDARY + 
lineSeparator +
+                encoder.encodeToString(certificate.getEncoded()) + 
lineSeparator + Part.END_BOUNDARY +
+                Part.CERTIFICATE + Part.FINISH_BOUNDARY + lineSeparator;
     }
 
     private final List<X509Certificate> certificates = new ArrayList<>();
diff --git a/test/org/apache/tomcat/util/net/jsse/TestPEMFile.java 
b/test/org/apache/tomcat/util/net/jsse/TestPEMFile.java
index d5f9b29b35..f7b0665abc 100644
--- a/test/org/apache/tomcat/util/net/jsse/TestPEMFile.java
+++ b/test/org/apache/tomcat/util/net/jsse/TestPEMFile.java
@@ -16,9 +16,12 @@
  */
 package org.apache.tomcat.util.net.jsse;
 
+import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
@@ -44,6 +47,15 @@ public class TestPEMFile {
     private static final String KEY_ENCRYPTED_PKCS8_HMACSHA256_AES_256_CBC = 
"key-encrypted-pkcs8-hmacsha256-aes-256-cbc.pem";
     private static final String KEY_ENCRYPTED_PKCS8_HMACSHA256_DES_EDE3_CBC = 
"key-encrypted-pkcs8-hmacsha256-des-ede3-cbc.pem";
 
+    /*
+     * A clean single certificate PEM. Generated with:
+     *   keytool -exportcert -rfc -keystore test/localhost-rsa.jks
+     *           -storepass changeit -alias tomcat
+     * with the line endings converted from CRLF to LF. The certificate itself
+     * expires on 2028-08-11.
+     */
+    private static final String CERTIFICATE = "certificate.pem";
+
     @Parameterized.Parameters
     public static Collection<Object[]> parameters() {
         List<Object[]> parameterSets = new ArrayList<>();
@@ -117,6 +129,43 @@ public class TestPEMFile {
     }
 
 
+    @Test
+    public void testToPem() throws Exception {
+        PEMFile pemFile = new PEMFile(getPath(CERTIFICATE));
+        Assert.assertEquals(1, pemFile.getCertificates().size());
+        X509Certificate certificate = pemFile.getCertificates().get(0);
+
+        String pem = PEMFile.toPEM(certificate);
+
+        // RFC 7468: each boundary is on a line of its own and the encoded text
+        // ends with a line separator
+        String[] lines = pem.split(System.lineSeparator());
+        Assert.assertEquals("-----BEGIN CERTIFICATE-----", lines[0]);
+        Assert.assertEquals("-----END CERTIFICATE-----", lines[lines.length - 
1]);
+        Assert.assertTrue("Missing line separator at end of: " + pem, 
pem.endsWith(System.lineSeparator()));
+
+        // The same line separator is used throughout the encoded text and no
+        // line is longer than the 64 characters RFC 7468 requires
+        String separatorsRemoved = pem.replace(System.lineSeparator(), "");
+        Assert.assertFalse("Unexpected line separator in: " + pem, 
separatorsRemoved.contains("\r"));
+        Assert.assertFalse("Unexpected line separator in: " + pem, 
separatorsRemoved.contains("\n"));
+        for (String line : lines) {
+            Assert.assertTrue("Long line: " + line, line.length() <= 64 || 
line.startsWith("-----"));
+        }
+        // RFC 7468 requires all the lines of encoded text but the last one to
+        // contain exactly 64 characters
+        for (int i = 1; i < lines.length - 2; i++) {
+            Assert.assertEquals("Short line: " + lines[i], 64, 
lines[i].length());
+        }
+
+        // The encoded text must be readable as PEM
+        PEMFile roundTrip =
+                new PEMFile("toPEM", new 
ByteArrayInputStream(pem.getBytes(StandardCharsets.US_ASCII)), null, null);
+        Assert.assertEquals(1, roundTrip.getCertificates().size());
+        Assert.assertEquals(certificate, roundTrip.getCertificates().get(0));
+    }
+
+
     private void testKeyEncrypted(String file) throws Exception {
         testKey(file, password, passwordFile);
     }
diff --git a/test/org/apache/tomcat/util/net/jsse/certificate.pem 
b/test/org/apache/tomcat/util/net/jsse/certificate.pem
new file mode 100644
index 0000000000..69a5c5241b
--- /dev/null
+++ b/test/org/apache/tomcat/util/net/jsse/certificate.pem
@@ -0,0 +1,32 @@
+-----BEGIN CERTIFICATE-----
+MIIFiTCCA3GgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZQxCzAJBgNVBAYTAlVT
+MQswCQYDVQQIEwJERTETMBEGA1UEBxMKV2lsbWluZ3RvbjEnMCUGA1UEChMeVGhl
+IEFwYWNoZSBTb2Z0d2FyZSBGb3VuZGF0aW9uMRowGAYDVQQLExFBcGFjaGUgVG9t
+Y2F0IFBNQzEeMBwGA1UEAxMVQXBhY2hlIFRvbWNhdCBUZXN0IENBMB4XDTI2MDgx
+MjEzMTkxOFoXDTI4MDgxMTEzMTkxOFowgYgxCzAJBgNVBAYTAlVTMQswCQYDVQQI
+EwJERTETMBEGA1UEBxMKV2lsbWluZ3RvbjEnMCUGA1UEChMeVGhlIEFwYWNoZSBT
+b2Z0d2FyZSBGb3VuZGF0aW9uMRowGAYDVQQLExFBcGFjaGUgVG9tY2F0IFBNQzES
+MBAGA1UEAxMJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+AQEAiGhGjxWRZ+1wz9O/FXKUJE9htc2EFul6UopXbz2k5Is2IO/QVO6XbWJRpqEY
+fAIfnccWSoNwhV5bOMPJBZgG2skNWF43aG3PI3t+Ppklblp6VsJJeWc5Z14TY+j4
+95W8gUSTDO8c9Fd4ylJQHAjVEbwZnQuf4fKoGgBYkxO2XYy/4Udkc6TTpDduPAHJ
+BsgXNenAFO1Doa1Uj1SMGcKcuYJHEFDrQPT57hC+ibxBYNWeKkGZ5xrvppMqZ3lE
+vtGPhJVK+zqDFssyiyoypgCxYLpOO5a7Llc21dxWeYK4tCl0OFz9B7FEv4T6pVLv
+2L7gKV00PuOUR2DMrfjBTNWK0wIDAQABo4HuMIHrMAkGA1UdEwQCMAAwLAYJYIZI
+AYb4QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0GA1UdDgQW
+BBTk9ApBBF0sQtd97iqARo4OsmLmSjAfBgNVHSMEGDAWgBTbdmYmmMu/aTGZL5Vs
+nsJZPF1NjDBUBggrBgEFBQcBAQRIMEYwIQYIKwYBBQUHMAGGFWh0dHA6Ly8xMjcu
+MC4wLjE6ODg4ODAhBggrBgEFBQcwAYYVaHR0cDovLzEyNy4wLjAuMTo4ODg5MBoG
+A1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATANBgkqhkiG9w0BAQsFAAOCAgEAL8sm
+Gza6GgAv06lxYQN6545uT19XyZS12urav5wQI+PLlEjAT/hfbbGOtheJshoUmeLK
+FqDipW+tRLDtdlYCdi4hpljtr3L5xXB1qnfmxTOfjMTzxkLiYtL+fkwY2rL/gTNU
+Eem+1xJeIf78WmbzTk0cI6EoIPi8RzT0/w78sJH3s4j1HQnnfA6A5hMaJ4lIaqo/
+fRsiI18i9XVnE6R+s0hzzVaxQVrTJMxBwvWhJRC77hDECrwtOg2Zq2VjAsVGWcEW
+ayHvvPA8Q3ZmNCsD6p2+9VnDpLhKd/UNfl726Y8t573BfWa+TX+wHp0VkNEama0P
+Pr4WbsitG92sZJQP9Y5OO9I2WH6re00gvXCicdXFH+Za7XfzXODCzHxAYMU0+nwB
+c1zXa0AbLzq2aI5vyh/eT2nLQSEYQhDV+DrYRGImPERPATzRrSgTGzny6aBE62v5
+9hdokliY50z1nSqG+dXCkZSPmdeDmPOUPdglZUBZKTh35qD4kE8lRqoQEmNaYX76
+jBvWD18UFI8imYmCrNTXz0YDy50iGxrdESANmLpi+vFd+3yqUVr163ab3iaTHWF4
+dfRuPYDCJrccZG9x1aKJ+5Fqkq+SJHut5jVFPl3PGdwgowrr/TJw/8J2I3huLS0G
+Uc+/fKyTIr2fPvGdmXr9RA6CM3yfsMcPdwtqPeY=
+-----END CERTIFICATE-----


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to