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>
