This is an automated email from the ASF dual-hosted git repository.
pkarwasz pushed a commit to branch 2.25.x
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
The following commit(s) were added to refs/heads/2.25.x by this push:
new 3b1e54c178 Align `SslConfiguration` factory method usage with Log4j
2.12+ API (#4075)
3b1e54c178 is described below
commit 3b1e54c1784e8e456233caf2eafead9e40848f3e
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Wed Mar 25 07:03:49 2026 +0100
Align `SslConfiguration` factory method usage with Log4j 2.12+ API (#4075)
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 | 346 +++++++++++++++++++++
.../log4j/core/appender/X509Certificates.java | 193 ++++++++++++
.../log4j/core/net/ssl/SslConfigurationTest.java | 21 ++
.../resources/TlsSocketAppenderTest/log4j2.xml | 42 +++
.../log4j/core/net/ssl/SslConfiguration.java | 10 +-
src/changelog/.2.x.x/4061_ssl-connection.xml | 14 +
8 files changed, 649 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..171f1849ab
--- /dev/null
+++
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/TlsSocketAppenderTest.java
@@ -0,0 +1,346 @@
+/*
+ * 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.InetAddress;
+import java.net.Socket;
+import java.net.URL;
+import java.net.UnknownHostException;
+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.Assumptions;
+import org.junit.jupiter.api.BeforeAll;
+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;
+
+ @BeforeAll
+ static void setup() {
+ Assumptions.assumeTrue(() -> {
+ // RFC 6761 recommends that *.localhost resolve to the loopback
interface, but DNS behavior varies
+ // across platforms and test environments. If two distinct
hostnames do not resolve to the local
+ // machine, tests that require different hostnames cannot be
executed reliably.
+ try {
+ InetAddress.getByName(TARGET_HOSTNAME);
+ InetAddress.getByName(ATTACKER_HOSTNAME);
+ return true;
+ } catch (UnknownHostException e) {
+ return false;
+ }
+ });
+ }
+
+ 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(sanitizePath(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(sanitizePath(alias) + "-keystore.p12");
+ try (OutputStream out = Files.newOutputStream(file)) {
+ keyStore.store(out, KEYSTORE_PWD);
+ }
+ return file.toAbsolutePath().toString();
+ }
+
+ private static String sanitizePath(String alias) {
+ return alias.replace(':', '_');
+ }
+
+ 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..9b9caf2613
--- /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()).abs();
+
+ 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..1d4065f9af 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
@@ -180,13 +180,10 @@ public class SslConfiguration {
* @return a new SslConfiguration
*/
@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 +198,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/4061_ssl-connection.xml
b/src/changelog/.2.x.x/4061_ssl-connection.xml
new file mode 100644
index 0000000000..4c8831c1d3
--- /dev/null
+++ b/src/changelog/.2.x.x/4061_ssl-connection.xml
@@ -0,0 +1,14 @@
+<?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">
+ <issue id="4061"
link="https://github.com/apache/logging-log4j2/issues/4061"/>
+ <issue id="4075"
link="https://github.com/apache/logging-log4j2/pull/4075"/>
+ <description format="asciidoc">
+ Align `SslConfiguration` factory method usage with Log4j 2.12+ API.
+ The `verifyHostname` attribute is not correctly recognized.
+ </description>
+</entry>