diff --git a/pom.xml b/pom.xml index be9de72033..e8b006ed3a 100644 --- a/pom.xml +++ b/pom.xml @@ -62,6 +62,7 @@ 4.12 3.1.5 2.23.4 + 3.11.1 3.0.1 2.5.3 @@ -731,6 +732,12 @@ ${mockito.version} test + + org.assertj + assertj-core + ${assert4j.version} + test + org.hamcrest hamcrest-library diff --git a/src/main/java/com/rabbitmq/client/impl/SocketFrameHandler.java b/src/main/java/com/rabbitmq/client/impl/SocketFrameHandler.java index bbe76e0684..02cc9a2d2d 100644 --- a/src/main/java/com/rabbitmq/client/impl/SocketFrameHandler.java +++ b/src/main/java/com/rabbitmq/client/impl/SocketFrameHandler.java @@ -16,7 +16,11 @@ package com.rabbitmq.client.impl; import com.rabbitmq.client.AMQP; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLSocket; import java.io.*; import java.net.InetAddress; import java.net.Socket; @@ -31,6 +35,9 @@ */ public class SocketFrameHandler implements FrameHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(SocketFrameHandler.class); + /** The underlying socket */ private final Socket _socket; @@ -122,7 +129,12 @@ public void sendHeader(int major, int minor) throws IOException { _outputStream.write(1); _outputStream.write(major); _outputStream.write(minor); - _outputStream.flush(); + try { + _outputStream.flush(); + } catch (SSLHandshakeException e) { + LOGGER.error("TLS connection failed: {}", e.getMessage()); + throw e; + } } } @@ -144,13 +156,21 @@ public void sendHeader(int major, int minor, int revision) throws IOException { _outputStream.write(major); _outputStream.write(minor); _outputStream.write(revision); - _outputStream.flush(); + try { + _outputStream.flush(); + } catch (SSLHandshakeException e) { + LOGGER.error("TLS connection failed: {}", e.getMessage()); + throw e; + } } } @Override public void sendHeader() throws IOException { sendHeader(AMQP.PROTOCOL.MAJOR, AMQP.PROTOCOL.MINOR, AMQP.PROTOCOL.REVISION); + if (this._socket instanceof SSLSocket) { + TlsUtils.logPeerCertificateInfo(((SSLSocket) this._socket).getSession()); + } } @Override diff --git a/src/main/java/com/rabbitmq/client/impl/TlsUtils.java b/src/main/java/com/rabbitmq/client/impl/TlsUtils.java new file mode 100644 index 0000000000..b517e51d6c --- /dev/null +++ b/src/main/java/com/rabbitmq/client/impl/TlsUtils.java @@ -0,0 +1,229 @@ +// Copyright (c) 2019 Pivotal Software, Inc. All rights reserved. +// +// This software, the RabbitMQ Java client library, is triple-licensed under the +// Mozilla Public License 1.1 ("MPL"), the GNU General Public License version 2 +// ("GPL") and the Apache License version 2 ("ASL"). For the MPL, please see +// LICENSE-MPL-RabbitMQ. For the GPL, please see LICENSE-GPL2. For the ASL, +// please see LICENSE-APACHE2. +// +// This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, +// either express or implied. See the LICENSE file for specific language governing +// rights and limitations of this software. +// +// If you have any questions regarding licensing, please contact us at +// info@rabbitmq.com. + +package com.rabbitmq.client.impl; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.SSLSession; +import java.security.cert.Certificate; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.*; +import java.util.function.BiFunction; +import java.util.stream.Collectors; + +/** + * Utility to extract information from X509 certificates. + * + * @since 5.7.0 + */ +public class TlsUtils { + + private static final Logger LOGGER = LoggerFactory.getLogger(TlsUtils.class); + private static final List KEY_USAGE = Collections.unmodifiableList(Arrays.asList( + "digitalSignature", "nonRepudiation", "keyEncipherment", + "dataEncipherment", "keyAgreement", "keyCertSign", + "cRLSign", "encipherOnly", "decipherOnly" + )); + private static final Map EXTENDED_KEY_USAGE = Collections.unmodifiableMap(new HashMap() {{ + put("1.3.6.1.5.5.7.3.1", "TLS Web server authentication"); + put("1.3.6.1.5.5.7.3.2", "TLS Web client authentication"); + put("1.3.6.1.5.5.7.3.3", "Signing of downloadable executable code"); + put("1.3.6.1.5.5.7.3.4", "E-mail protection"); + put("1.3.6.1.5.5.7.3.8", "Binding the hash of an object to a time from an agreed-upon time"); + }}); + private static String PARSING_ERROR = ""; + private static final Map> EXTENSIONS = Collections.unmodifiableMap( + new HashMap>() {{ + put("2.5.29.14", (v, c) -> "SubjectKeyIdentifier = " + octetStringHexDump(v)); + put("2.5.29.15", (v, c) -> "KeyUsage = " + keyUsageBitString(c.getKeyUsage(), v)); + put("2.5.29.16", (v, c) -> "PrivateKeyUsage = " + hexDump(0, v)); + put("2.5.29.17", (v, c) -> { + try { + return "SubjectAlternativeName = " + sans(c, "/"); + } catch (CertificateParsingException e) { + return "SubjectAlternativeName = " + PARSING_ERROR; + } + }); + put("2.5.29.18", (v, c) -> "IssuerAlternativeName = " + hexDump(0, v)); + put("2.5.29.19", (v, c) -> "BasicConstraints = " + basicConstraints(v)); + put("2.5.29.30", (v, c) -> "NameConstraints = " + hexDump(0, v)); + put("2.5.29.33", (v, c) -> "PolicyMappings = " + hexDump(0, v)); + put("2.5.29.35", (v, c) -> "AuthorityKeyIdentifier = " + authorityKeyIdentifier(v)); + put("2.5.29.36", (v, c) -> "PolicyConstraints = " + hexDump(0, v)); + put("2.5.29.37", (v, c) -> "ExtendedKeyUsage = " + extendedKeyUsage(v, c)); + }}); + + /** + * Log details on peer certificate and certification chain. + *

+ * The log level is debug. Common X509 extensions are displayed in a best-effort + * fashion, a hexadecimal dump is made for less commonly used extensions. + * + * @param session the {@link SSLSession} to extract the certificates from + */ + public static void logPeerCertificateInfo(SSLSession session) { + if (LOGGER.isDebugEnabled()) { + try { + Certificate[] peerCertificates = session.getPeerCertificates(); + if (peerCertificates != null && peerCertificates.length > 0) { + LOGGER.debug(peerCertificateInfo(peerCertificates[0], "Peer's leaf certificate")); + for (int i = 1; i < peerCertificates.length; i++) { + LOGGER.debug(peerCertificateInfo(peerCertificates[i], "Peer's certificate chain entry")); + } + } + } catch (Exception e) { + LOGGER.debug("Error while logging peer certificate info: {}", e.getMessage()); + } + } + } + + /** + * Get a string representation of certificate info. + * + * @param certificate the certificate to analyze + * @param prefix the line prefix + * @return information about the certificate + */ + public static String peerCertificateInfo(Certificate certificate, String prefix) { + X509Certificate c = (X509Certificate) certificate; + try { + return String.format("%s subject: %s, subject alternative names: %s, " + + "issuer: %s, not valid after: %s, X.509 usage extensions: %s", + prefix, c.getSubjectDN().getName(), sans(c, ","), c.getIssuerDN().getName(), + c.getNotAfter(), extensions(c)); + } catch (Exception e) { + return "Error while retrieving " + prefix + " certificate information"; + } + } + + private static String sans(X509Certificate c, String separator) throws CertificateParsingException { + return String.join(separator, Optional.ofNullable(c.getSubjectAlternativeNames()) + .orElse(new ArrayList<>()) + .stream() + .map(v -> v.toString()) + .collect(Collectors.toList())); + } + + /** + * Human-readable representation of an X509 certificate extension. + *

+ * Common extensions are supported in a best-effort fashion, less commonly + * used extensions are displayed as an hexadecimal dump. + *

+ * Extensions come encoded as a DER Octet String, which itself can contain + * other DER-encoded objects, making a comprehensive support in this utility + * impossible. + * + * @param oid extension OID + * @param derOctetString the extension value as a DER octet string + * @param certificate the certificate + * @return the OID and the value + * @see A Layman's Guide to a Subset of ASN.1, BER, and DER + * @see DER Encoding of ASN.1 Types + */ + public static String extensionPrettyPrint(String oid, byte[] derOctetString, X509Certificate certificate) { + try { + return EXTENSIONS.getOrDefault(oid, (v, c) -> oid + " = " + hexDump(0, derOctetString)) + .apply(derOctetString, certificate); + } catch (Exception e) { + return oid + " = " + PARSING_ERROR; + } + } + + private static String extensions(X509Certificate certificate) { + List extensions = new ArrayList<>(); + for (String oid : certificate.getCriticalExtensionOIDs()) { + extensions.add(extensionPrettyPrint(oid, certificate.getExtensionValue(oid), certificate) + " (critical)"); + } + for (String oid : certificate.getNonCriticalExtensionOIDs()) { + extensions.add(extensionPrettyPrint(oid, certificate.getExtensionValue(oid), certificate) + " (non-critical)"); + } + return String.join(", ", extensions); + } + + private static String octetStringHexDump(byte[] derOctetString) { + // this is an octet string in a octet string, [4 total_length 4 length ...] + if (derOctetString.length > 4 && derOctetString[0] == 4 && derOctetString[2] == 4) { + return hexDump(4, derOctetString); + } else { + return hexDump(0, derOctetString); + } + } + + private static String hexDump(int start, byte[] derOctetString) { + List hexs = new ArrayList<>(); + for (int i = start; i < derOctetString.length; i++) { + hexs.add(String.format("%02X", derOctetString[i])); + } + return String.join(":", hexs); + } + + private static String keyUsageBitString(boolean[] keyUsage, byte[] derOctetString) { + if (keyUsage != null) { + List usage = new ArrayList<>(); + for (int i = 0; i < keyUsage.length; i++) { + if (keyUsage[i]) { + usage.add(KEY_USAGE.get(i)); + } + } + return String.join("/", usage); + } else { + return hexDump(0, derOctetString); + } + } + + private static String basicConstraints(byte[] derOctetString) { + if (derOctetString.length == 4 && derOctetString[3] == 0) { + // e.g. 04:02:30:00 [octet_string length sequence size] + return "CA:FALSE"; + } else if (derOctetString.length >= 7 && derOctetString[2] == 48 && derOctetString[4] == 1) { + // e.g. 04:05:30:03:01:01:FF [octet_string length sequence boolean length boolean_value] + return "CA:" + (derOctetString[6] == 0 ? "FALSE" : "TRUE"); + } else { + return hexDump(0, derOctetString); + } + } + + private static String authorityKeyIdentifier(byte[] derOctetString) { + if (derOctetString.length == 26 && derOctetString[0] == 04) { + // e.g. 04:18:30:16:80:14:FB:D2:7C:63:DF:7F:D4:A4:8E:9A:20:43:F5:DC:75:6F:B6:D8:51:6F + // [octet_string length sequence ?? ?? key_length key] + return "keyid:" + hexDump(6, derOctetString); + } else { + return hexDump(0, derOctetString); + } + + } + + private static String extendedKeyUsage(byte[] derOctetString, X509Certificate certificate) { + List extendedKeyUsage = null; + try { + extendedKeyUsage = certificate.getExtendedKeyUsage(); + if (extendedKeyUsage == null) { + return hexDump(0, derOctetString); + } else { + return String.join("/", extendedKeyUsage.stream() + .map(oid -> EXTENDED_KEY_USAGE.getOrDefault(oid, oid)) + .collect(Collectors.toList())); + } + } catch (CertificateParsingException e) { + return PARSING_ERROR; + } + } + +} diff --git a/src/main/java/com/rabbitmq/client/impl/nio/SocketChannelFrameHandlerFactory.java b/src/main/java/com/rabbitmq/client/impl/nio/SocketChannelFrameHandlerFactory.java index c9e1ffb202..784a5f80cd 100644 --- a/src/main/java/com/rabbitmq/client/impl/nio/SocketChannelFrameHandlerFactory.java +++ b/src/main/java/com/rabbitmq/client/impl/nio/SocketChannelFrameHandlerFactory.java @@ -20,10 +20,14 @@ import com.rabbitmq.client.SslContextFactory; import com.rabbitmq.client.impl.AbstractFrameHandlerFactory; import com.rabbitmq.client.impl.FrameHandler; +import com.rabbitmq.client.impl.TlsUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLException; +import javax.net.ssl.SSLHandshakeException; import java.io.IOException; import java.net.InetSocketAddress; import java.net.SocketAddress; @@ -39,6 +43,8 @@ */ public class SocketChannelFrameHandlerFactory extends AbstractFrameHandlerFactory { + private static final Logger LOGGER = LoggerFactory.getLogger(SocketChannelFrameHandler.class); + final NioParams nioParams; private final SslContextFactory sslContextFactory; @@ -91,10 +97,17 @@ public FrameHandler create(Address addr, String connectionName) throws IOExcepti if (ssl) { sslEngine.beginHandshake(); - boolean handshake = SslEngineHelper.doHandshake(channel, sslEngine); - if (!handshake) { - throw new SSLException("TLS handshake failed"); + try { + boolean handshake = SslEngineHelper.doHandshake(channel, sslEngine); + if (!handshake) { + LOGGER.error("TLS connection failed"); + throw new SSLException("TLS handshake failed"); + } + } catch (SSLHandshakeException e) { + LOGGER.error("TLS connection failed: {}", e.getMessage()); + throw e; } + TlsUtils.logPeerCertificateInfo(sslEngine.getSession()); } channel.configureBlocking(false); diff --git a/src/test/java/com/rabbitmq/client/test/ClientTests.java b/src/test/java/com/rabbitmq/client/test/ClientTests.java index 799cfd9126..dfae29e976 100644 --- a/src/test/java/com/rabbitmq/client/test/ClientTests.java +++ b/src/test/java/com/rabbitmq/client/test/ClientTests.java @@ -66,7 +66,8 @@ NioDeadlockOnConnectionClosing.class, GeneratedClassesTest.class, RpcTopologyRecordingTest.class, - ConnectionTest.class + ConnectionTest.class, + TlsUtilsTest.class }) public class ClientTests { diff --git a/src/test/java/com/rabbitmq/client/test/TlsUtilsTest.java b/src/test/java/com/rabbitmq/client/test/TlsUtilsTest.java new file mode 100644 index 0000000000..e632e47282 --- /dev/null +++ b/src/test/java/com/rabbitmq/client/test/TlsUtilsTest.java @@ -0,0 +1,123 @@ +// Copyright (c) 2019 Pivotal Software, Inc. All rights reserved. +// +// This software, the RabbitMQ Java client library, is triple-licensed under the +// Mozilla Public License 1.1 ("MPL"), the GNU General Public License version 2 +// ("GPL") and the Apache License version 2 ("ASL"). For the MPL, please see +// LICENSE-MPL-RabbitMQ. For the GPL, please see LICENSE-GPL2. For the ASL, +// please see LICENSE-APACHE2. +// +// This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, +// either express or implied. See the LICENSE file for specific language governing +// rights and limitations of this software. +// +// If you have any questions regarding licensing, please contact us at +// info@rabbitmq.com. + +package com.rabbitmq.client.test; + +import org.junit.Test; +import org.mockito.Mockito; + +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.Arrays; + +import static com.rabbitmq.client.impl.TlsUtils.extensionPrettyPrint; +import static org.assertj.core.api.Assertions.assertThat; + +public class TlsUtilsTest { + + static final byte [] DOES_NOT_MATTER = new byte[0]; + + @Test + public void subjectKeyIdentifier() { + // https://www.alvestrand.no/objectid/2.5.29.14.html + byte[] derOctetString = new byte[]{ + 4, 22, 4, 20, -2, -87, -45, -120, 29, -126, -88, -17, 95, -39, -122, 23, 10, -62, -54, -82, 113, -121, -70, -121 + }; // 04:16:04:14:FE:A9:D3:88:1D:82:A8:EF:5F:D9:86:17:0A:C2:CA:AE:71:87:BA:87 + assertThat(extensionPrettyPrint("2.5.29.14", derOctetString, null)) + .isEqualTo("SubjectKeyIdentifier = FE:A9:D3:88:1D:82:A8:EF:5F:D9:86:17:0A:C2:CA:AE:71:87:BA:87"); + // change the 3rd byte to mimic it's not a octet string, the whole array should be then hex-dumped + derOctetString = new byte[]{ + 4, 22, 3, 20, -2, -87, -45, -120, 29, -126, -88, -17, 95, -39, -122, 23, 10, -62, -54, -82, 113, -121, -70, -121 + }; // 04:16:04:14:FE:A9:D3:88:1D:82:A8:EF:5F:D9:86:17:0A:C2:CA:AE:71:87:BA:87 + assertThat(extensionPrettyPrint("2.5.29.14", derOctetString, null)) + .isEqualTo("SubjectKeyIdentifier = 04:16:03:14:FE:A9:D3:88:1D:82:A8:EF:5F:D9:86:17:0A:C2:CA:AE:71:87:BA:87"); + } + + @Test public void keyUsage() { + // https://www.alvestrand.no/objectid/2.5.29.15.html + // http://javadoc.iaik.tugraz.at/iaik_jce/current/iaik/asn1/BIT_STRING.html + X509Certificate c = Mockito.mock(X509Certificate.class); + Mockito.when(c.getKeyUsage()) + .thenReturn(new boolean[] {true,false,true,false,false,false,false,false,false}) + .thenReturn(new boolean[] {false,false,false,false,false,true,true,false,false}) + .thenReturn(null); + assertThat(extensionPrettyPrint("2.5.29.15", DOES_NOT_MATTER, c)) + .isEqualTo("KeyUsage = digitalSignature/keyEncipherment"); + assertThat(extensionPrettyPrint("2.5.29.15", DOES_NOT_MATTER, c)) + .isEqualTo("KeyUsage = keyCertSign/cRLSign"); + // change the 3rd byte to mimic it's not a bit string, the whole array should be then hex-dumped + byte[] derOctetString = new byte[] { 4, 4, 3, 2, 1, 6}; // 04:04:03:02:01:06 => Certificate Sign, CRL Sign + assertThat(extensionPrettyPrint("2.5.29.15", derOctetString, c)) + .isEqualTo("KeyUsage = 04:04:03:02:01:06"); + } + + @Test public void basicConstraints() { + // https://www.alvestrand.no/objectid/2.5.29.19.html + byte [] derOctetString = new byte [] {0x04, 0x02, 0x30, 0x00}; + assertThat(extensionPrettyPrint("2.5.29.19", derOctetString, null)) + .isEqualTo("BasicConstraints = CA:FALSE"); + derOctetString = new byte [] {4, 5, 48, 3, 1, 1, -1}; // 04:05:30:03:01:01:FF + assertThat(extensionPrettyPrint("2.5.29.19", derOctetString, null)) + .isEqualTo("BasicConstraints = CA:TRUE"); + derOctetString = new byte [] {4, 5, 48, 3, 1, 1, 0}; // 04:05:30:03:01:01:00 + assertThat(extensionPrettyPrint("2.5.29.19", derOctetString, null)) + .isEqualTo("BasicConstraints = CA:FALSE"); + // change the 3rd to mimic it's not what the utils expects, the whole array should be hex-dump + derOctetString = new byte [] {4, 5, 4, 3, 1, 1, 0}; // 04:05:04:03:01:01:00 + assertThat(extensionPrettyPrint("2.5.29.19", derOctetString, null)) + .isEqualTo("BasicConstraints = 04:05:04:03:01:01:00"); + + } + + @Test public void authorityKeyIdentifier() { + // https://www.alvestrand.no/objectid/2.5.29.35.html + byte[] derOctetString = new byte[]{ + 4,24,48,22,-128,20,-5,-46,124,99,-33,127,-44,-92,-114,-102,32,67,-11,-36,117,111,-74,-40,81,111 + }; // 04:18:30:16:80:14:FB:D2:7C:63:DF:7F:D4:A4:8E:9A:20:43:F5:DC:75:6F:B6:D8:51:6F + assertThat(extensionPrettyPrint("2.5.29.35", derOctetString, null)) + .isEqualTo("AuthorityKeyIdentifier = keyid:FB:D2:7C:63:DF:7F:D4:A4:8E:9A:20:43:F5:DC:75:6F:B6:D8:51:6F"); + + // add a byte to mimic not-expected length, the whole array should be hex-dump + derOctetString = new byte[]{ + 4,24,48,22,-128,20,-5,-46,124,99,-33,127,-44,-92,-114,-102,32,67,-11,-36,117,111,-74,-40,81,111, -1 + }; // 04:18:30:16:80:14:FB:D2:7C:63:DF:7F:D4:A4:8E:9A:20:43:F5:DC:75:6F:B6:D8:51:6F + assertThat(extensionPrettyPrint("2.5.29.35", derOctetString, null)) + .isEqualTo("AuthorityKeyIdentifier = 04:18:30:16:80:14:FB:D2:7C:63:DF:7F:D4:A4:8E:9A:20:43:F5:DC:75:6F:B6:D8:51:6F:FF"); + } + + @Test public void extendedKeyUsage() throws CertificateParsingException { + // https://www.alvestrand.no/objectid/2.5.29.37.html + X509Certificate c = Mockito.mock(X509Certificate.class); + Mockito.when(c.getExtendedKeyUsage()) + .thenReturn(Arrays.asList("1.3.6.1.5.5.7.3.1")) + .thenReturn(Arrays.asList("1.3.6.1.5.5.7.3.1", "1.3.6.1.5.5.7.3.2")) + .thenReturn(Arrays.asList("1.3.6.1.5.5.7.3.unknown")) + .thenReturn(null) + .thenThrow(CertificateParsingException.class); + + assertThat(extensionPrettyPrint("2.5.29.37", DOES_NOT_MATTER, c)) + .isEqualTo("ExtendedKeyUsage = TLS Web server authentication"); + assertThat(extensionPrettyPrint("2.5.29.37", DOES_NOT_MATTER, c)) + .isEqualTo("ExtendedKeyUsage = TLS Web server authentication/TLS Web client authentication"); + assertThat(extensionPrettyPrint("2.5.29.37", DOES_NOT_MATTER, c)) + .isEqualTo("ExtendedKeyUsage = 1.3.6.1.5.5.7.3.unknown"); + byte [] derOctetString = new byte[] {0x04, 0x0C, 0x30, 0x0A, 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x01}; + assertThat(extensionPrettyPrint("2.5.29.37", derOctetString, c)) + .isEqualTo("ExtendedKeyUsage = 04:0C:30:0A:06:08:2B:06:01:05:05:07:03:01"); + assertThat(extensionPrettyPrint("2.5.29.37", DOES_NOT_MATTER, c)) + .isEqualTo("ExtendedKeyUsage = "); + } + +} diff --git a/src/test/java/com/rabbitmq/client/test/ssl/SSLTests.java b/src/test/java/com/rabbitmq/client/test/ssl/SSLTests.java index 679468a59f..88107b41fe 100644 --- a/src/test/java/com/rabbitmq/client/test/ssl/SSLTests.java +++ b/src/test/java/com/rabbitmq/client/test/ssl/SSLTests.java @@ -33,7 +33,8 @@ BadVerifiedConnection.class, ConnectionFactoryDefaultTlsVersion.class, NioTlsUnverifiedConnection.class, - HostnameVerification.class + HostnameVerification.class, + TlsConnectionLogging.class }) public class SSLTests { diff --git a/src/test/java/com/rabbitmq/client/test/ssl/TlsConnectionLogging.java b/src/test/java/com/rabbitmq/client/test/ssl/TlsConnectionLogging.java new file mode 100644 index 0000000000..12ec082a4d --- /dev/null +++ b/src/test/java/com/rabbitmq/client/test/ssl/TlsConnectionLogging.java @@ -0,0 +1,71 @@ +// Copyright (c) 2019 Pivotal Software, Inc. All rights reserved. +// +// This software, the RabbitMQ Java client library, is triple-licensed under the +// Mozilla Public License 1.1 ("MPL"), the GNU General Public License version 2 +// ("GPL") and the Apache License version 2 ("ASL"). For the MPL, please see +// LICENSE-MPL-RabbitMQ. For the GPL, please see LICENSE-GPL2. For the ASL, +// please see LICENSE-APACHE2. +// +// This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, +// either express or implied. See the LICENSE file for specific language governing +// rights and limitations of this software. +// +// If you have any questions regarding licensing, please contact us at +// info@rabbitmq.com. + +package com.rabbitmq.client.test.ssl; + +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.impl.TlsUtils; +import com.rabbitmq.client.test.TestUtils; +import org.assertj.core.api.Assertions; +import org.junit.Test; + +import javax.net.ssl.*; +import java.security.cert.X509Certificate; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.Assert.assertNotNull; + +public class TlsConnectionLogging { + + @Test + public void certificateInfoAreProperlyExtracted() throws Exception { + SSLContext sslContext = TestUtils.getSSLContext(); + sslContext.init(null, new TrustManager[]{new AlwaysTrustTrustManager()}, null); + ConnectionFactory connectionFactory = TestUtils.connectionFactory(); + connectionFactory.useSslProtocol(sslContext); + connectionFactory.useBlockingIo(); + AtomicReference socketCaptor = new AtomicReference<>(); + connectionFactory.setSocketConfigurator(socket -> socketCaptor.set((SSLSocket) socket)); + try (Connection ignored = connectionFactory.newConnection()) { + SSLSession session = socketCaptor.get().getSession(); + assertNotNull(session); + String info = TlsUtils.peerCertificateInfo(session.getPeerCertificates()[0], "some prefix"); + Assertions.assertThat(info).contains("some prefix") + .contains("CN=") + .contains("X.509 usage extensions") + .contains("KeyUsage"); + + } + } + + private static class AlwaysTrustTrustManager implements X509TrustManager { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) { + + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) { + + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + } + +}