From bd7e619659be242f58323870fb6617c1e58bb897 Mon Sep 17 00:00:00 2001 From: Arturo Bernal Date: Fri, 20 Sep 2024 13:48:25 +0200 Subject: [PATCH] HTTPCLIENT-2337: Add sanitizeX500Principal method to escape control characters in X500Principal. Escapes ISO control characters in X500Principal using hexadecimal representation. --- .../http/ssl/SSLConnectionSocketFactory.java | 40 +++++++++- .../ssl/SSLConnectionSocketFactoryTest.java | 76 +++++++++++++++++++ 2 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 httpclient5/src/test/java/org/apache/hc/client5/http/ssl/SSLConnectionSocketFactoryTest.java diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/SSLConnectionSocketFactory.java b/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/SSLConnectionSocketFactory.java index 3ffe908e3..4b29d08a4 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/SSLConnectionSocketFactory.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/SSLConnectionSocketFactory.java @@ -57,6 +57,7 @@ import org.apache.hc.client5.http.config.TlsConfig; import org.apache.hc.core5.annotation.Contract; +import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.annotation.ThreadingBehavior; import org.apache.hc.core5.http.HttpHost; import org.apache.hc.core5.http.protocol.HttpContext; @@ -403,9 +404,15 @@ void verifySession( final Certificate cert = certs[0]; if (cert instanceof X509Certificate) { final X509Certificate x509 = (X509Certificate) cert; + // Sanitize and log peer principal final X500Principal peer = x509.getSubjectX500Principal(); + LOG.debug("Escaped peer principal: {}", toEscapedString(peer)); + + // Sanitize and log issuer principal + final X500Principal issuer = x509.getIssuerX500Principal(); + LOG.debug("Escaped issuer principal: {}", toEscapedString(issuer)); + - LOG.debug(" peer principal: {}", peer); final Collection> altNames1 = x509.getSubjectAlternativeNames(); if (altNames1 != null) { final List altNames = new ArrayList<>(); @@ -417,8 +424,6 @@ void verifySession( LOG.debug(" peer alternative names: {}", altNames); } - final X500Principal issuer = x509.getIssuerX500Principal(); - LOG.debug(" issuer principal: {}", issuer); final Collection> altNames2 = x509.getIssuerAlternativeNames(); if (altNames2 != null) { final List altNames = new ArrayList<>(); @@ -456,4 +461,33 @@ void verifySession( } } + /** + * Converts an X500Principal to a cleaned string by escaping control characters. + *

+ * This method processes the RFC2253 format of the X500Principal and escapes + * any ISO control characters to avoid issues in logging or other outputs. + * Control characters are replaced with their escaped hexadecimal representation. + *

+ * + *

Note: For testing purposes, this method is package-private + * to allow access within the same package. This allows tests to verify the correct + * behavior of the escaping process.

+ * + * @param principal the X500Principal to escape + * @return the escaped string representation of the X500Principal + */ + @Internal + String toEscapedString(final X500Principal principal) { + final String principalValue = principal.getName(X500Principal.RFC2253); + final StringBuilder sanitizedPrincipal = new StringBuilder(principalValue.length()); + for (final char c : principalValue.toCharArray()) { + if (Character.isISOControl(c)) { + sanitizedPrincipal.append("\\x").append(String.format("%02x", (int) c)); + } else { + sanitizedPrincipal.append(c); + } + } + return sanitizedPrincipal.toString(); + } + } diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/ssl/SSLConnectionSocketFactoryTest.java b/httpclient5/src/test/java/org/apache/hc/client5/http/ssl/SSLConnectionSocketFactoryTest.java new file mode 100644 index 000000000..f0800e1c3 --- /dev/null +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/ssl/SSLConnectionSocketFactoryTest.java @@ -0,0 +1,76 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.client5.http.ssl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.security.cert.X509Certificate; + +import javax.net.ssl.SSLSession; +import javax.security.auth.x500.X500Principal; + +import org.apache.hc.core5.ssl.SSLContexts; +import org.junit.jupiter.api.Test; + +public class SSLConnectionSocketFactoryTest { + + @Test + public void testToEscapedString_withControlCharacters() { + // Create a X500Principal with control characters + final X500Principal principal = new X500Principal("CN=Test\b\bName\n,O=TestOrg"); + + // Call the toEscapedString method + final SSLConnectionSocketFactory factory = new SSLConnectionSocketFactory(SSLContexts.createDefault()); + final String escaped = factory.toEscapedString(principal); + + // Assert that control characters are properly escaped + assertEquals("CN=Test\\x08\\x08Name\\x0a,O=TestOrg", escaped); + } + + @Test + public void testVerifySession_escapedPeerAndIssuer() throws Exception { + // Mock SSLSession and X509Certificate + final SSLSession mockSession = mock(SSLSession.class); + final X509Certificate mockCert = mock(X509Certificate.class); + + // Create a mock X500Principal with control characters + final X500Principal peerPrincipal = new X500Principal("CN=Peer\bName,O=PeerOrg"); + final X500Principal issuerPrincipal = new X500Principal("CN=Issuer\bName,O=IssuerOrg"); + + when(mockSession.getPeerCertificates()).thenReturn(new X509Certificate[]{mockCert}); + when(mockCert.getSubjectX500Principal()).thenReturn(peerPrincipal); + when(mockCert.getIssuerX500Principal()).thenReturn(issuerPrincipal); + + // Test the verifySession method + final SSLConnectionSocketFactory factory = new SSLConnectionSocketFactory(SSLContexts.createDefault()); + factory.verifySession("localhost", mockSession, null); + + } +}