>() {{
+ 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];
+ }
+ }
+
+}