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

pkarwasz pushed a commit to branch fix/2.25.x/ssl-connection
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git

commit 9ab3a4cfe47fa5b7bd00e35df3e8a23438196728
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Wed Jan 21 14:12:20 2026 +0100

    Align `SslConfiguration` factory method usage with Log4j 2.12+ API
    
    This change updates the usage of `SslConfiguration#createSSLConfiguration` 
to the 4-parameter factory method introduced in Log4j 2.12.0.
    
    Using the newer factory method keeps the code aligned with the current API 
and ensures that all configuration parameters supported by recent Log4j 
versions are correctly propagated during SSL configuration creation.
    
    Fixes #4061
---
 log4j-core-test/pom.xml                            |  17 ++
 .../log4j/core/appender/LineReadingTcpServer.java  |  18 +-
 .../log4j/core/appender/TlsSocketAppenderTest.java | 322 +++++++++++++++++++++
 .../log4j/core/appender/X509Certificates.java      | 193 ++++++++++++
 .../log4j/core/net/ssl/SslConfigurationTest.java   |  21 ++
 .../resources/TlsSocketAppenderTest/log4j2.xml     |  42 +++
 .../log4j/core/net/ssl/SslConfiguration.java       |  12 +-
 src/changelog/.2.x.x/ssl-connection.xml            |  11 +
 8 files changed, 624 insertions(+), 12 deletions(-)

diff --git a/log4j-core-test/pom.xml b/log4j-core-test/pom.xml
index 8c8cbc19bb..34ccfc0453 100644
--- a/log4j-core-test/pom.xml
+++ b/log4j-core-test/pom.xml
@@ -64,8 +64,19 @@
     <!-- Additional version of LMAX Disruptor to test -->
     <disruptor4.version>4.0.0</disruptor4.version>
     <json-unit.version>2.40.1</json-unit.version>
+    <bouncycastle.version>1.83</bouncycastle.version>
   </properties>
 
+  <dependencyManagement>
+    <dependencies>
+      <dependency>
+        <groupId>org.bouncycastle</groupId>
+        <artifactId>bcpkix-jdk18on</artifactId>
+        <version>${bouncycastle.version}</version>
+      </dependency>
+    </dependencies>
+  </dependencyManagement>
+
   <dependencies>
 
     <dependency>
@@ -149,6 +160,12 @@
       <scope>test</scope>
     </dependency>
 
+    <dependency>
+      <groupId>org.bouncycastle</groupId>
+      <artifactId>bcpkix-jdk18on</artifactId>
+      <scope>test</scope>
+    </dependency>
+
     <!-- Other -->
     <dependency>
       <groupId>commons-codec</groupId>
diff --git 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/LineReadingTcpServer.java
 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/LineReadingTcpServer.java
index 9f4028423a..df923a457c 100644
--- 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/LineReadingTcpServer.java
+++ 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/LineReadingTcpServer.java
@@ -58,6 +58,8 @@ final class LineReadingTcpServer implements AutoCloseable {
 
     private volatile boolean running;
 
+    private InetAddress bindAddress = InetAddress.getLoopbackAddress();
+
     private ServerSocket serverSocket;
 
     private Socket clientSocket;
@@ -74,6 +76,11 @@ final class LineReadingTcpServer implements AutoCloseable {
         this.serverSocketFactory = serverSocketFactory;
     }
 
+    // For testing purposes
+    void setBindAddress(final InetAddress bindAddress) {
+        this.bindAddress = bindAddress;
+    }
+
     synchronized void start(final String name, final int port) throws 
IOException {
         if (!running) {
             running = true;
@@ -83,8 +90,7 @@ final class LineReadingTcpServer implements AutoCloseable {
     }
 
     private ServerSocket createServerSocket(final int port) throws IOException 
{
-        final ServerSocket serverSocket =
-                serverSocketFactory.createServerSocket(port, 1, 
InetAddress.getLoopbackAddress());
+        final ServerSocket serverSocket = 
serverSocketFactory.createServerSocket(port, 1, bindAddress);
         serverSocket.setReuseAddress(true);
         serverSocket.setSoTimeout(0); // Zero indicates `accept()` will block 
indefinitely
         await("server socket binding")
@@ -104,12 +110,12 @@ final class LineReadingTcpServer implements AutoCloseable 
{
     }
 
     private void acceptClients() {
-        try {
-            while (running) {
+        while (running) {
+            try {
                 acceptClient();
+            } catch (final Exception error) {
+                LOGGER.error("failed accepting client connections", error);
             }
-        } catch (final Exception error) {
-            LOGGER.error("failed accepting client connections", error);
         }
     }
 
diff --git 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/TlsSocketAppenderTest.java
 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/TlsSocketAppenderTest.java
new file mode 100644
index 0000000000..0904b6c848
--- /dev/null
+++ 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/TlsSocketAppenderTest.java
@@ -0,0 +1,322 @@
+/*
+ * 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.
+ */
+package org.apache.logging.log4j.core.appender;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.OutputStream;
+import java.net.Socket;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.KeyPair;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.stream.Stream;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLServerSocket;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.net.SslSocketManager;
+import org.apache.logging.log4j.test.TestProperties;
+import org.apache.logging.log4j.test.junit.UsingTestProperties;
+import org.jspecify.annotations.Nullable;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+@UsingTestProperties
+class TlsSocketAppenderTest {
+
+    // Test DNS names and IP addresses
+    private static final String TARGET_HOSTNAME = "log4j.localhost";
+    private static final String TARGET_IP = "::1";
+    private static final String ATTACKER_HOSTNAME = "not-log4j.localhost";
+    private static final String ATTACKER_IP = "127.0.0.1";
+
+    // Test PKI material
+    private static final KeyPair CA_KEY_PAIR = 
X509Certificates.generateKeyPair();
+    private static final KeyPair SERVER_KEY_PAIR = 
X509Certificates.generateKeyPair();
+    private static final KeyPair CLIENT_KEY_PAIR = 
X509Certificates.generateKeyPair();
+
+    private static final X509Certificate CA_CERT;
+
+    private static final X509Certificate TARGET_CERT1;
+    private static final X509Certificate TARGET_CERT2;
+    private static final X509Certificate TARGET_CERT3;
+
+    private static final X509Certificate ATTACKER_CERT1;
+    private static final X509Certificate ATTACKER_CERT2;
+    private static final X509Certificate ATTACKER_CERT3;
+
+    /** Client certificate used for mutual TLS (mTLS) scenarios. */
+    private static final X509Certificate CLIENT_CERT;
+
+    static {
+        try {
+            CA_CERT = X509Certificates.generateCACertificate(CA_KEY_PAIR);
+            PrivateKey caPrivateKey = CA_KEY_PAIR.getPrivate();
+
+            // Certificates with CN only
+            TARGET_CERT1 = X509Certificates.generateServerCertificate(
+                    SERVER_KEY_PAIR, caPrivateKey, "CN=" + TARGET_HOSTNAME, 
null, null);
+            ATTACKER_CERT1 = X509Certificates.generateServerCertificate(
+                    SERVER_KEY_PAIR, caPrivateKey, "CN=" + ATTACKER_HOSTNAME, 
null, null);
+
+            // Certificates with SAN (DNS)
+            TARGET_CERT2 = X509Certificates.generateServerCertificate(
+                    SERVER_KEY_PAIR, caPrivateKey, "CN=Test Server", 
TARGET_HOSTNAME, null);
+            ATTACKER_CERT2 = X509Certificates.generateServerCertificate(
+                    SERVER_KEY_PAIR, caPrivateKey, "CN=Test Attacker Server", 
ATTACKER_HOSTNAME, null);
+
+            // Certificates with SAN (IP)
+            TARGET_CERT3 = X509Certificates.generateServerCertificate(
+                    SERVER_KEY_PAIR, caPrivateKey, "CN=Test Server", null, 
TARGET_IP);
+            ATTACKER_CERT3 = X509Certificates.generateServerCertificate(
+                    SERVER_KEY_PAIR, caPrivateKey, "CN=Test Attacker Server", 
null, ATTACKER_IP);
+
+            CLIENT_CERT = 
X509Certificates.generateClientCertificate(CLIENT_KEY_PAIR, caPrivateKey, 
"CN=Test Client");
+
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    // Store parameters
+
+    private static final String KEYSTORE_TYPE = "PKCS12";
+    private static final char[] KEYSTORE_PWD = "aKeyStoreSecret".toCharArray();
+    private static final String TRUSTSTORE_TYPE = "PKCS12";
+    private static final char[] TRUSTSTORE_PWD = 
"aTrustStoreSecret".toCharArray();
+
+    @TempDir
+    private static Path certPath;
+
+    static Stream<Arguments> 
connectionAlwaysSucceedsWithoutHostnameVerification() {
+        return Stream.of(
+                Arguments.of(TARGET_HOSTNAME, ATTACKER_CERT1),
+                Arguments.of(TARGET_HOSTNAME, ATTACKER_CERT2),
+                Arguments.of(TARGET_IP, ATTACKER_CERT3));
+    }
+
+    static Stream<Arguments> connectionSucceedsOnHostNameMatch() {
+        return Stream.of(
+                // No client certificate
+                Arguments.of(TARGET_HOSTNAME, TARGET_CERT1, null),
+                Arguments.of(TARGET_HOSTNAME, TARGET_CERT2, null),
+                Arguments.of(TARGET_IP, TARGET_CERT3, null),
+
+                // These tests ensure that connections to the attacher fail 
because of hostname mismatch,
+                // not because of other TLS issues.
+                Arguments.of(ATTACKER_HOSTNAME, ATTACKER_CERT1, null),
+                Arguments.of(ATTACKER_HOSTNAME, ATTACKER_CERT2, null),
+                Arguments.of(ATTACKER_IP, ATTACKER_CERT3, null),
+
+                // Mutual TLS
+                Arguments.of(TARGET_HOSTNAME, TARGET_CERT1, CLIENT_CERT),
+                Arguments.of(TARGET_HOSTNAME, TARGET_CERT2, CLIENT_CERT),
+                Arguments.of(TARGET_IP, TARGET_CERT3, CLIENT_CERT));
+    }
+
+    static Stream<Arguments> connectionFailsOnHostNameMismatch() {
+        return Stream.of(
+                Arguments.of(TARGET_HOSTNAME, ATTACKER_CERT1),
+                Arguments.of(TARGET_HOSTNAME, ATTACKER_CERT2),
+                Arguments.of(TARGET_IP, ATTACKER_CERT3));
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void connectionAlwaysSucceedsWithoutHostnameVerification(
+            String hostName, X509Certificate serverCertificate, TestProperties 
props) throws Exception {
+
+        TestTlsMaterial tls = createTlsMaterial(hostName, serverCertificate, 
null);
+        applyClientTlsProperties(props, tls);
+        props.setProperty("ssl.verifyHostname", "false");
+
+        try (LineReadingTcpServer server = createTlsServer(hostName, 
tls.serverSslContext, false)) {
+            props.setProperty("server.host", hostName);
+            props.setProperty("server.port", 
server.getServerSocket().getLocalPort());
+
+            try (LoggerContext ctx = createLoggerContext()) {
+                Logger logger = ctx.getLogger(TlsSocketAppenderTest.class);
+
+                String expected = "Test message for host " + hostName;
+                logger.info(expected);
+
+                assertThat(server.pollLines(1)).containsExactly(expected);
+            }
+        }
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void connectionSucceedsOnHostNameMatch(
+            String hostName,
+            X509Certificate serverCertificate,
+            @Nullable X509Certificate clientCertificate,
+            TestProperties props)
+            throws Exception {
+
+        TestTlsMaterial tls = createTlsMaterial(hostName, serverCertificate, 
clientCertificate);
+        applyClientTlsProperties(props, tls);
+
+        try (LineReadingTcpServer server = createTlsServer(hostName, 
tls.serverSslContext, clientCertificate != null)) {
+            props.setProperty("server.host", hostName);
+            props.setProperty("server.port", 
server.getServerSocket().getLocalPort());
+
+            try (LoggerContext ctx = createLoggerContext()) {
+                Logger logger = ctx.getLogger(TlsSocketAppenderTest.class);
+
+                String expected = "Test message for host " + hostName;
+                logger.info(expected);
+
+                assertThat(server.pollLines(1)).containsExactly(expected);
+            }
+        }
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void connectionFailsOnHostNameMismatch(String hostName, X509Certificate 
serverCertificate, TestProperties props)
+            throws Exception {
+
+        // No mTLS needed; we only care about hostname verification failure.
+        TestTlsMaterial tls = createTlsMaterial(hostName, serverCertificate, 
null);
+        applyClientTlsProperties(props, tls);
+
+        try (LineReadingTcpServer server = createTlsServer(hostName, 
tls.serverSslContext, false)) {
+            props.setProperty("server.host", hostName);
+            props.setProperty("server.port", 
server.getServerSocket().getLocalPort());
+
+            try (LoggerContext ctx = createLoggerContext()) {
+                assertSocketAppenderNotConnected(ctx, hostName);
+            }
+        }
+    }
+
+    private static TestTlsMaterial createTlsMaterial(
+            String hostName, X509Certificate serverCertificate, @Nullable 
X509Certificate clientCertificate)
+            throws Exception {
+
+        // Client keystore: only populated when we test mutual TLS.
+        String clientKeystore = generateKeystore(
+                hostName + "-client", clientCertificate, clientCertificate != 
null ? CLIENT_KEY_PAIR : null);
+
+        String serverKeystore = generateKeystore(hostName + "-server", 
serverCertificate, SERVER_KEY_PAIR);
+
+        String truststore = generateTruststore(hostName);
+
+        SSLContext serverSslContext = SslContexts.createSslContext(
+                KEYSTORE_TYPE, serverKeystore, KEYSTORE_PWD, TRUSTSTORE_TYPE, 
truststore, TRUSTSTORE_PWD);
+
+        return new TestTlsMaterial(clientKeystore, truststore, 
serverSslContext);
+    }
+
+    private static void applyClientTlsProperties(TestProperties props, 
TestTlsMaterial tls) {
+        props.setProperty("keystore.location", tls.clientKeystoreLocation);
+        props.setProperty("keystore.password", new String(KEYSTORE_PWD));
+        props.setProperty("keystore.type", KEYSTORE_TYPE);
+
+        props.setProperty("truststore.location", tls.truststoreLocation);
+        props.setProperty("truststore.password", new String(TRUSTSTORE_PWD));
+        props.setProperty("truststore.type", TRUSTSTORE_TYPE);
+    }
+
+    private static LineReadingTcpServer createTlsServer(String hostName, 
SSLContext sslContext, boolean needClientAuth)
+            throws Exception {
+
+        LineReadingTcpServer server = new 
LineReadingTcpServer(sslContext.getServerSocketFactory());
+
+        // Bind to all interfaces to allow testing with different host names.
+        server.setBindAddress(null);
+
+        server.start("TlsSocketAppenderTest-" + hostName, 0);
+
+        SSLServerSocket socket = (SSLServerSocket) server.getServerSocket();
+        socket.setNeedClientAuth(needClientAuth);
+
+        return server;
+    }
+
+    private static LoggerContext createLoggerContext() throws Exception {
+        URL configLocation = 
TlsSocketAppenderTest.class.getResource("/TlsSocketAppenderTest/log4j2.xml");
+        assertThat(configLocation).isNotNull();
+
+        LoggerContext ctx = new LoggerContext("TlsSocketAppenderTest", null, 
configLocation.toURI());
+        ctx.start();
+        return ctx;
+    }
+
+    private static void assertSocketAppenderNotConnected(LoggerContext ctx, 
String hostName) {
+        SocketAppender appender = ctx.getConfiguration().getAppender("SOCKET-" 
+ hostName);
+        assertThat(appender).isNotNull();
+        assertThat(appender.getManager()).isInstanceOf(SslSocketManager.class);
+
+        SslSocketManager manager = (SslSocketManager) appender.getManager();
+        Socket socket = manager.getSocket();
+
+        if (socket != null) {
+            assertThat(socket.isConnected()).isFalse();
+        }
+    }
+
+    private static String generateTruststore(String alias) throws Exception {
+        KeyStore trustStore = KeyStore.getInstance(TRUSTSTORE_TYPE);
+        trustStore.load(null, null);
+        trustStore.setCertificateEntry(alias, CA_CERT);
+
+        Path file = certPath.resolve(alias + "-truststore.p12");
+        try (OutputStream out = Files.newOutputStream(file)) {
+            trustStore.store(out, TRUSTSTORE_PWD);
+        }
+        return file.toAbsolutePath().toString();
+    }
+
+    private static String generateKeystore(
+            String alias, @Nullable X509Certificate certificate, @Nullable 
KeyPair keyPair) throws Exception {
+
+        KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE);
+        keyStore.load(null, null);
+
+        if (certificate != null && keyPair != null) {
+            keyStore.setKeyEntry(
+                    alias, keyPair.getPrivate(), KEYSTORE_PWD, new 
X509Certificate[] {certificate, CA_CERT});
+        }
+
+        Path file = certPath.resolve(alias + "-keystore.p12");
+        try (OutputStream out = Files.newOutputStream(file)) {
+            keyStore.store(out, KEYSTORE_PWD);
+        }
+        return file.toAbsolutePath().toString();
+    }
+
+    private static class TestTlsMaterial {
+
+        private final @Nullable String clientKeystoreLocation;
+        private final String truststoreLocation;
+        private final SSLContext serverSslContext;
+
+        private TestTlsMaterial(String clientKeystoreLocation, String 
truststoreLocation, SSLContext serverSslContext) {
+            this.clientKeystoreLocation = clientKeystoreLocation;
+            this.truststoreLocation = truststoreLocation;
+            this.serverSslContext = serverSslContext;
+        }
+    }
+}
diff --git 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/X509Certificates.java
 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/X509Certificates.java
new file mode 100644
index 0000000000..a269eeee75
--- /dev/null
+++ 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/X509Certificates.java
@@ -0,0 +1,193 @@
+/*
+ * 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.
+ */
+package org.apache.logging.log4j.core.appender;
+
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Random;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x509.BasicConstraints;
+import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.GeneralName;
+import org.bouncycastle.asn1.x509.GeneralNames;
+import org.bouncycastle.asn1.x509.KeyPurposeId;
+import org.bouncycastle.asn1.x509.KeyUsage;
+import org.bouncycastle.cert.CertIOException;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Utility class to generate X.509 certificates for testing purposes.
+ */
+final class X509Certificates {
+
+    private static final String CA_DN = "CN=Test CA";
+    private static final long MINUTE_IN_MILLIS = 60_000L;
+    private static final long YEAR_IN_MILLIS = 365L * 24 * 60 * 
MINUTE_IN_MILLIS;
+
+    private static final KeyPairGenerator RSA_GENERATOR;
+    private static final Random RANDOM = new Random();
+
+    static {
+        try {
+            RSA_GENERATOR = KeyPairGenerator.getInstance("RSA");
+            RSA_GENERATOR.initialize(2048);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    static KeyPair generateKeyPair() {
+        return RSA_GENERATOR.generateKeyPair();
+    }
+
+    static X509Certificate generateCACertificate(KeyPair keyPair) throws 
Exception {
+        JcaX509v3CertificateBuilder builder = 
getCertificateBuilder(keyPair.getPublic(), CA_DN, true);
+        addKeyUsageExtension(builder, KeyUsage.keyCertSign);
+        return buildCertificate(builder, keyPair.getPrivate());
+    }
+
+    /**
+     * Create and sign a server X.509 certificate for tests.
+     *
+     * <p>The produced certificate complies with {@code 
sun.security.validator.EndEntityChecker}.</p>
+     *
+     * @param keyPair the subject key pair
+     * @param caKey the private key of the issuing CA used to sign the 
certificate
+     * @param subjectDn the subject distinguished name for the certificate 
(for example {@code CN=example.com})
+     * @param dnsAltSubject optional DNS Subject Alternative Name; pass {@code 
null} to omit
+     * @param ipAltSubject optional IP Subject Alternative Name; pass {@code 
null} to omit
+     * @return a signed X.509 server certificate
+     * @throws Exception if certificate creation or signing fails
+     */
+    static X509Certificate generateServerCertificate(
+            KeyPair keyPair,
+            PrivateKey caKey,
+            String subjectDn,
+            @Nullable String dnsAltSubject,
+            @Nullable String ipAltSubject)
+            throws Exception {
+        JcaX509v3CertificateBuilder builder = 
getCertificateBuilder(keyPair.getPublic(), subjectDn, false);
+        // The required key usage for the server certificate depends on the 
key exchange algorithm:
+        // - keyEncipherment for RSA key exchange (deprecated)
+        // - digitalSignature for ephemeral Diffie-Hellman key exchange (DHE 
or ECDHE)
+        // - keyAgreement for static Diffie-Hellman key exchange (DH or ECDH)
+        addKeyUsageExtension(builder, KeyUsage.digitalSignature | 
KeyUsage.keyAgreement);
+        addExtendedKeyUsageExtension(builder, KeyPurposeId.id_kp_serverAuth);
+        addSubjectAlternativeName(builder, dnsAltSubject, ipAltSubject);
+        return buildCertificate(builder, caKey);
+    }
+
+    /**
+     * Create and sign a client X.509 certificate for tests.
+     *
+     * <p>The produced certificate complies with {@code 
sun.security.validator.EndEntityChecker}.</p>
+     *
+     * @param keyPair the subject key pair
+     * @param caKey the private key of the issuing CA used to sign the 
certificate
+     * @param subjectDn the subject distinguished name for the certificate 
(for example {@code CN=example.com})
+     * @return a signed X.509 server certificate
+     * @throws Exception if certificate creation or signing fails
+     */
+    static X509Certificate generateClientCertificate(KeyPair keyPair, 
PrivateKey caKey, String subjectDn)
+            throws Exception {
+        JcaX509v3CertificateBuilder builder = 
getCertificateBuilder(keyPair.getPublic(), subjectDn, false);
+        // The required key usage for the client certificate
+        addKeyUsageExtension(builder, KeyUsage.digitalSignature);
+        addExtendedKeyUsageExtension(builder, KeyPurposeId.id_kp_clientAuth);
+        return buildCertificate(builder, caKey);
+    }
+
+    private static JcaX509v3CertificateBuilder getCertificateBuilder(
+            PublicKey subjectPub, String subjectDn, boolean isCa) throws 
CertIOException {
+        long now = System.currentTimeMillis();
+        Date notBefore = new Date(now - MINUTE_IN_MILLIS);
+        Date notAfter = new Date(now + YEAR_IN_MILLIS);
+        BigInteger serial = BigInteger.valueOf(RANDOM.nextLong());
+
+        X500Name issuer = new X500Name(CA_DN);
+        X500Name subject = new X500Name(subjectDn);
+
+        JcaX509v3CertificateBuilder builder =
+                new JcaX509v3CertificateBuilder(issuer, serial, notBefore, 
notAfter, subject, subjectPub);
+
+        // Basic Constraints
+        builder.addExtension(Extension.basicConstraints, true, new 
BasicConstraints(isCa));
+
+        return builder;
+    }
+
+    private static void addKeyUsageExtension(JcaX509v3CertificateBuilder 
builder, int keyUsage) throws CertIOException {
+        builder.addExtension(Extension.keyUsage, true, new KeyUsage(keyUsage));
+    }
+
+    private static void 
addExtendedKeyUsageExtension(JcaX509v3CertificateBuilder builder, KeyPurposeId 
kp)
+            throws CertIOException {
+        builder.addExtension(Extension.extendedKeyUsage, false, new 
ExtendedKeyUsage(kp));
+    }
+
+    private static GeneralName getIpAddressGeneralName(String ipAltSubject) {
+        return new GeneralName(GeneralName.iPAddress, ipAltSubject);
+    }
+
+    private static GeneralName getDnsGeneralName(String dnsAltSubject) {
+        return new GeneralName(GeneralName.dNSName, dnsAltSubject);
+    }
+
+    private static void addSubjectAlternativeName(
+            JcaX509v3CertificateBuilder builder, @Nullable String 
dnsAltSubject, @Nullable String ipAltSubject)
+            throws CertIOException {
+        if (ipAltSubject != null || dnsAltSubject != null) {
+            List<GeneralName> names = new ArrayList<>();
+            if (dnsAltSubject != null) {
+                names.add(getDnsGeneralName(dnsAltSubject));
+            }
+            if (ipAltSubject != null) {
+                names.add(getIpAddressGeneralName(ipAltSubject));
+            }
+            GeneralName[] gna = names.toArray(new GeneralName[0]);
+            builder.addExtension(Extension.subjectAlternativeName, false, new 
GeneralNames(gna));
+        }
+    }
+
+    private static X509Certificate 
buildCertificate(JcaX509v3CertificateBuilder builder, PrivateKey signerKey)
+            throws OperatorCreationException, CertificateException {
+        ContentSigner signer = new 
JcaContentSignerBuilder("SHA256withRSA").build(signerKey);
+
+        X509CertificateHolder holder = builder.build(signer);
+
+        return new JcaX509CertificateConverter().getCertificate(holder);
+    }
+
+    private X509Certificates() {
+        // private constructor to prevent instantiation
+    }
+}
diff --git 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/net/ssl/SslConfigurationTest.java
 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/net/ssl/SslConfigurationTest.java
index 93d02712d5..b916f4410e 100644
--- 
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/net/ssl/SslConfigurationTest.java
+++ 
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/net/ssl/SslConfigurationTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.logging.log4j.core.net.ssl;
 
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -25,6 +26,11 @@ import java.io.OutputStream;
 import java.net.UnknownHostException;
 import javax.net.ssl.SSLSocket;
 import javax.net.ssl.SSLSocketFactory;
+import org.apache.logging.log4j.core.config.DefaultConfiguration;
+import org.apache.logging.log4j.core.config.Node;
+import org.apache.logging.log4j.core.config.plugins.util.PluginBuilder;
+import org.apache.logging.log4j.core.config.plugins.util.PluginManager;
+import org.apache.logging.log4j.core.config.plugins.util.PluginType;
 import org.apache.logging.log4j.test.junit.UsingStatusListener;
 import org.junit.jupiter.api.Test;
 
@@ -138,4 +144,19 @@ class SslConfigurationTest {
         final SSLSocketFactory factory = 
sslConf.getSslContext().getSocketFactory();
         assertNotNull(factory);
     }
+
+    @Test
+    void verifyHostNameFromXml() {
+        PluginManager pluginManager = new PluginManager(Node.CATEGORY);
+        pluginManager.collectPlugins();
+        PluginType<?> pluginType = pluginManager.getPluginType("Ssl");
+        assertThat(pluginType).isNotNull();
+        Node ssl = new Node(null, pluginType.getElementName(), pluginType);
+        ssl.getAttributes().put("verifyHostName", "true");
+        PluginBuilder builder = new PluginBuilder(pluginType);
+        SslConfiguration sslConfiguration = (SslConfiguration) 
builder.withConfigurationNode(ssl)
+                .withConfiguration(new DefaultConfiguration())
+                .build();
+        assertThat(sslConfiguration.isVerifyHostName()).isTrue();
+    }
 }
diff --git 
a/log4j-core-test/src/test/resources/TlsSocketAppenderTest/log4j2.xml 
b/log4j-core-test/src/test/resources/TlsSocketAppenderTest/log4j2.xml
new file mode 100644
index 0000000000..b5f8e25a74
--- /dev/null
+++ b/log4j-core-test/src/test/resources/TlsSocketAppenderTest/log4j2.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+<Configuration xmlns="https://logging.apache.org/xml/ns";
+               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+               xsi:schemaLocation="https://logging.apache.org/xml/ns 
https://logging.apache.org/xml/ns/log4j-config-2.xsd";>
+    <Appenders>
+        <Socket name="SOCKET-${test:server.host}"
+                host="${test:server.host}"
+                port="${test:server.port}"
+                protocol="SSL">
+            <Ssl verifyHostName="${test:ssl.verifyHostname:-true}">
+                <KeyStore location="${test:keystore.location}"
+                          password="${test:keystore.password}"
+                          type="${test:keystore.type}"/>
+                <TrustStore location="${test:truststore.location}"
+                            password="${test:truststore.password}"
+                            type="${test:truststore.type}"/>
+            </Ssl>
+            <PatternLayout pattern="%m%n"/>
+        </Socket>
+    </Appenders>
+    <Loggers>
+        <Root level="INFO">
+            <AppenderRef ref="SOCKET-${test:server.host}"/>
+        </Root>
+    </Loggers>
+</Configuration>
diff --git 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/net/ssl/SslConfiguration.java
 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/net/ssl/SslConfiguration.java
index a27febcabb..544d465f7e 100644
--- 
a/log4j-core/src/main/java/org/apache/logging/log4j/core/net/ssl/SslConfiguration.java
+++ 
b/log4j-core/src/main/java/org/apache/logging/log4j/core/net/ssl/SslConfiguration.java
@@ -178,15 +178,14 @@ public class SslConfiguration {
      * @param keyStoreConfig   The KeyStoreConfiguration.
      * @param trustStoreConfig The TrustStoreConfiguration.
      * @return a new SslConfiguration
+     * @deprecated Since 2.26.0, use {@link #createSSLConfiguration(String, 
KeyStoreConfiguration, TrustStoreConfiguration, boolean)} instead.
      */
+    @Deprecated
     @NullUnmarked
-    @PluginFactory
     public static SslConfiguration createSSLConfiguration(
-            // @formatter:off
-            @PluginAttribute("protocol") final String protocol,
-            @PluginElement("KeyStore") final KeyStoreConfiguration 
keyStoreConfig,
-            @PluginElement("TrustStore") final TrustStoreConfiguration 
trustStoreConfig) {
-        // @formatter:on
+            final String protocol,
+            final KeyStoreConfiguration keyStoreConfig,
+            final TrustStoreConfiguration trustStoreConfig) {
         return new SslConfiguration(protocol, false, keyStoreConfig, 
trustStoreConfig);
     }
 
@@ -201,6 +200,7 @@ public class SslConfiguration {
      * @since 2.12
      */
     @NullUnmarked
+    @PluginFactory
     public static SslConfiguration createSSLConfiguration(
             // @formatter:off
             @PluginAttribute("protocol") final String protocol,
diff --git a/src/changelog/.2.x.x/ssl-connection.xml 
b/src/changelog/.2.x.x/ssl-connection.xml
new file mode 100644
index 0000000000..e5fbd539da
--- /dev/null
+++ b/src/changelog/.2.x.x/ssl-connection.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns="https://logging.apache.org/xml/ns";
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+       xsi:schemaLocation="
+           https://logging.apache.org/xml/ns
+           https://logging.apache.org/xml/ns/log4j-changelog-0.xsd";
+       type="fixed">
+  <description format="asciidoc">
+    Align `SslConfiguration` factory method usage with Log4j 2.12+ API.
+  </description>
+</entry>


Reply via email to