Skip to content

Commit 787113d

Browse files
Merge pull request #442 from rabbitmq/rabbitmq-java-client-441-tls-connection-info
Improve logging for TLS connections
2 parents 7612b7c + 7cc8400 commit 787113d

File tree

8 files changed

+472
-7
lines changed

8 files changed

+472
-7
lines changed

pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
<junit.version>4.12</junit.version>
6363
<awaitility.version>3.1.5</awaitility.version>
6464
<mockito.version>2.23.4</mockito.version>
65+
<assert4j.version>3.11.1</assert4j.version>
6566

6667
<maven.javadoc.plugin.version>3.0.1</maven.javadoc.plugin.version>
6768
<maven.release.plugin.version>2.5.3</maven.release.plugin.version>
@@ -731,6 +732,12 @@
731732
<version>${mockito.version}</version>
732733
<scope>test</scope>
733734
</dependency>
735+
<dependency>
736+
<groupId>org.assertj</groupId>
737+
<artifactId>assertj-core</artifactId>
738+
<version>${assert4j.version}</version>
739+
<scope>test</scope>
740+
</dependency>
734741
<dependency>
735742
<groupId>org.hamcrest</groupId>
736743
<artifactId>hamcrest-library</artifactId>

src/main/java/com/rabbitmq/client/impl/SocketFrameHandler.java

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616
package com.rabbitmq.client.impl;
1717

1818
import com.rabbitmq.client.AMQP;
19+
import org.slf4j.Logger;
20+
import org.slf4j.LoggerFactory;
1921

22+
import javax.net.ssl.SSLHandshakeException;
23+
import javax.net.ssl.SSLSocket;
2024
import java.io.*;
2125
import java.net.InetAddress;
2226
import java.net.Socket;
@@ -31,6 +35,9 @@
3135
*/
3236

3337
public class SocketFrameHandler implements FrameHandler {
38+
39+
private static final Logger LOGGER = LoggerFactory.getLogger(SocketFrameHandler.class);
40+
3441
/** The underlying socket */
3542
private final Socket _socket;
3643

@@ -122,7 +129,12 @@ public void sendHeader(int major, int minor) throws IOException {
122129
_outputStream.write(1);
123130
_outputStream.write(major);
124131
_outputStream.write(minor);
125-
_outputStream.flush();
132+
try {
133+
_outputStream.flush();
134+
} catch (SSLHandshakeException e) {
135+
LOGGER.error("TLS connection failed: {}", e.getMessage());
136+
throw e;
137+
}
126138
}
127139
}
128140

@@ -144,13 +156,21 @@ public void sendHeader(int major, int minor, int revision) throws IOException {
144156
_outputStream.write(major);
145157
_outputStream.write(minor);
146158
_outputStream.write(revision);
147-
_outputStream.flush();
159+
try {
160+
_outputStream.flush();
161+
} catch (SSLHandshakeException e) {
162+
LOGGER.error("TLS connection failed: {}", e.getMessage());
163+
throw e;
164+
}
148165
}
149166
}
150167

151168
@Override
152169
public void sendHeader() throws IOException {
153170
sendHeader(AMQP.PROTOCOL.MAJOR, AMQP.PROTOCOL.MINOR, AMQP.PROTOCOL.REVISION);
171+
if (this._socket instanceof SSLSocket) {
172+
TlsUtils.logPeerCertificateInfo(((SSLSocket) this._socket).getSession());
173+
}
154174
}
155175

156176
@Override
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
// Copyright (c) 2019 Pivotal Software, Inc. All rights reserved.
2+
//
3+
// This software, the RabbitMQ Java client library, is triple-licensed under the
4+
// Mozilla Public License 1.1 ("MPL"), the GNU General Public License version 2
5+
// ("GPL") and the Apache License version 2 ("ASL"). For the MPL, please see
6+
// LICENSE-MPL-RabbitMQ. For the GPL, please see LICENSE-GPL2. For the ASL,
7+
// please see LICENSE-APACHE2.
8+
//
9+
// This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND,
10+
// either express or implied. See the LICENSE file for specific language governing
11+
// rights and limitations of this software.
12+
//
13+
// If you have any questions regarding licensing, please contact us at
14+
15+
16+
package com.rabbitmq.client.impl;
17+
18+
import org.slf4j.Logger;
19+
import org.slf4j.LoggerFactory;
20+
21+
import javax.net.ssl.SSLSession;
22+
import java.security.cert.Certificate;
23+
import java.security.cert.CertificateParsingException;
24+
import java.security.cert.X509Certificate;
25+
import java.util.*;
26+
import java.util.function.BiFunction;
27+
import java.util.stream.Collectors;
28+
29+
/**
30+
* Utility to extract information from X509 certificates.
31+
*
32+
* @since 5.7.0
33+
*/
34+
public class TlsUtils {
35+
36+
private static final Logger LOGGER = LoggerFactory.getLogger(TlsUtils.class);
37+
private static final List<String> KEY_USAGE = Collections.unmodifiableList(Arrays.asList(
38+
"digitalSignature", "nonRepudiation", "keyEncipherment",
39+
"dataEncipherment", "keyAgreement", "keyCertSign",
40+
"cRLSign", "encipherOnly", "decipherOnly"
41+
));
42+
private static final Map<String, String> EXTENDED_KEY_USAGE = Collections.unmodifiableMap(new HashMap<String, String>() {{
43+
put("1.3.6.1.5.5.7.3.1", "TLS Web server authentication");
44+
put("1.3.6.1.5.5.7.3.2", "TLS Web client authentication");
45+
put("1.3.6.1.5.5.7.3.3", "Signing of downloadable executable code");
46+
put("1.3.6.1.5.5.7.3.4", "E-mail protection");
47+
put("1.3.6.1.5.5.7.3.8", "Binding the hash of an object to a time from an agreed-upon time");
48+
}});
49+
private static String PARSING_ERROR = "<parsing-error>";
50+
private static final Map<String, BiFunction<byte[], X509Certificate, String>> EXTENSIONS = Collections.unmodifiableMap(
51+
new HashMap<String, BiFunction<byte[], X509Certificate, String>>() {{
52+
put("2.5.29.14", (v, c) -> "SubjectKeyIdentifier = " + octetStringHexDump(v));
53+
put("2.5.29.15", (v, c) -> "KeyUsage = " + keyUsageBitString(c.getKeyUsage(), v));
54+
put("2.5.29.16", (v, c) -> "PrivateKeyUsage = " + hexDump(0, v));
55+
put("2.5.29.17", (v, c) -> {
56+
try {
57+
return "SubjectAlternativeName = " + sans(c, "/");
58+
} catch (CertificateParsingException e) {
59+
return "SubjectAlternativeName = " + PARSING_ERROR;
60+
}
61+
});
62+
put("2.5.29.18", (v, c) -> "IssuerAlternativeName = " + hexDump(0, v));
63+
put("2.5.29.19", (v, c) -> "BasicConstraints = " + basicConstraints(v));
64+
put("2.5.29.30", (v, c) -> "NameConstraints = " + hexDump(0, v));
65+
put("2.5.29.33", (v, c) -> "PolicyMappings = " + hexDump(0, v));
66+
put("2.5.29.35", (v, c) -> "AuthorityKeyIdentifier = " + authorityKeyIdentifier(v));
67+
put("2.5.29.36", (v, c) -> "PolicyConstraints = " + hexDump(0, v));
68+
put("2.5.29.37", (v, c) -> "ExtendedKeyUsage = " + extendedKeyUsage(v, c));
69+
}});
70+
71+
/**
72+
* Log details on peer certificate and certification chain.
73+
* <p>
74+
* The log level is debug. Common X509 extensions are displayed in a best-effort
75+
* fashion, a hexadecimal dump is made for less commonly used extensions.
76+
*
77+
* @param session the {@link SSLSession} to extract the certificates from
78+
*/
79+
public static void logPeerCertificateInfo(SSLSession session) {
80+
if (LOGGER.isDebugEnabled()) {
81+
try {
82+
Certificate[] peerCertificates = session.getPeerCertificates();
83+
if (peerCertificates != null && peerCertificates.length > 0) {
84+
LOGGER.debug(peerCertificateInfo(peerCertificates[0], "Peer's leaf certificate"));
85+
for (int i = 1; i < peerCertificates.length; i++) {
86+
LOGGER.debug(peerCertificateInfo(peerCertificates[i], "Peer's certificate chain entry"));
87+
}
88+
}
89+
} catch (Exception e) {
90+
LOGGER.debug("Error while logging peer certificate info: {}", e.getMessage());
91+
}
92+
}
93+
}
94+
95+
/**
96+
* Get a string representation of certificate info.
97+
*
98+
* @param certificate the certificate to analyze
99+
* @param prefix the line prefix
100+
* @return information about the certificate
101+
*/
102+
public static String peerCertificateInfo(Certificate certificate, String prefix) {
103+
X509Certificate c = (X509Certificate) certificate;
104+
try {
105+
return String.format("%s subject: %s, subject alternative names: %s, " +
106+
"issuer: %s, not valid after: %s, X.509 usage extensions: %s",
107+
prefix, c.getSubjectDN().getName(), sans(c, ","), c.getIssuerDN().getName(),
108+
c.getNotAfter(), extensions(c));
109+
} catch (Exception e) {
110+
return "Error while retrieving " + prefix + " certificate information";
111+
}
112+
}
113+
114+
private static String sans(X509Certificate c, String separator) throws CertificateParsingException {
115+
return String.join(separator, Optional.ofNullable(c.getSubjectAlternativeNames())
116+
.orElse(new ArrayList<>())
117+
.stream()
118+
.map(v -> v.toString())
119+
.collect(Collectors.toList()));
120+
}
121+
122+
/**
123+
* Human-readable representation of an X509 certificate extension.
124+
* <p>
125+
* Common extensions are supported in a best-effort fashion, less commonly
126+
* used extensions are displayed as an hexadecimal dump.
127+
* <p>
128+
* Extensions come encoded as a DER Octet String, which itself can contain
129+
* other DER-encoded objects, making a comprehensive support in this utility
130+
* impossible.
131+
*
132+
* @param oid extension OID
133+
* @param derOctetString the extension value as a DER octet string
134+
* @param certificate the certificate
135+
* @return the OID and the value
136+
* @see <a href="http://luca.ntop.org/Teaching/Appunti/asn1.html">A Layman's Guide to a Subset of ASN.1, BER, and DER</a>
137+
* @see <a href="https://docs.microsoft.com/en-us/windows/desktop/seccertenroll/about-der-encoding-of-asn-1-types">DER Encoding of ASN.1 Types</a>
138+
*/
139+
public static String extensionPrettyPrint(String oid, byte[] derOctetString, X509Certificate certificate) {
140+
try {
141+
return EXTENSIONS.getOrDefault(oid, (v, c) -> oid + " = " + hexDump(0, derOctetString))
142+
.apply(derOctetString, certificate);
143+
} catch (Exception e) {
144+
return oid + " = " + PARSING_ERROR;
145+
}
146+
}
147+
148+
private static String extensions(X509Certificate certificate) {
149+
List<String> extensions = new ArrayList<>();
150+
for (String oid : certificate.getCriticalExtensionOIDs()) {
151+
extensions.add(extensionPrettyPrint(oid, certificate.getExtensionValue(oid), certificate) + " (critical)");
152+
}
153+
for (String oid : certificate.getNonCriticalExtensionOIDs()) {
154+
extensions.add(extensionPrettyPrint(oid, certificate.getExtensionValue(oid), certificate) + " (non-critical)");
155+
}
156+
return String.join(", ", extensions);
157+
}
158+
159+
private static String octetStringHexDump(byte[] derOctetString) {
160+
// this is an octet string in a octet string, [4 total_length 4 length ...]
161+
if (derOctetString.length > 4 && derOctetString[0] == 4 && derOctetString[2] == 4) {
162+
return hexDump(4, derOctetString);
163+
} else {
164+
return hexDump(0, derOctetString);
165+
}
166+
}
167+
168+
private static String hexDump(int start, byte[] derOctetString) {
169+
List<String> hexs = new ArrayList<>();
170+
for (int i = start; i < derOctetString.length; i++) {
171+
hexs.add(String.format("%02X", derOctetString[i]));
172+
}
173+
return String.join(":", hexs);
174+
}
175+
176+
private static String keyUsageBitString(boolean[] keyUsage, byte[] derOctetString) {
177+
if (keyUsage != null) {
178+
List<String> usage = new ArrayList<>();
179+
for (int i = 0; i < keyUsage.length; i++) {
180+
if (keyUsage[i]) {
181+
usage.add(KEY_USAGE.get(i));
182+
}
183+
}
184+
return String.join("/", usage);
185+
} else {
186+
return hexDump(0, derOctetString);
187+
}
188+
}
189+
190+
private static String basicConstraints(byte[] derOctetString) {
191+
if (derOctetString.length == 4 && derOctetString[3] == 0) {
192+
// e.g. 04:02:30:00 [octet_string length sequence size]
193+
return "CA:FALSE";
194+
} else if (derOctetString.length >= 7 && derOctetString[2] == 48 && derOctetString[4] == 1) {
195+
// e.g. 04:05:30:03:01:01:FF [octet_string length sequence boolean length boolean_value]
196+
return "CA:" + (derOctetString[6] == 0 ? "FALSE" : "TRUE");
197+
} else {
198+
return hexDump(0, derOctetString);
199+
}
200+
}
201+
202+
private static String authorityKeyIdentifier(byte[] derOctetString) {
203+
if (derOctetString.length == 26 && derOctetString[0] == 04) {
204+
// 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
205+
// [octet_string length sequence ?? ?? key_length key]
206+
return "keyid:" + hexDump(6, derOctetString);
207+
} else {
208+
return hexDump(0, derOctetString);
209+
}
210+
211+
}
212+
213+
private static String extendedKeyUsage(byte[] derOctetString, X509Certificate certificate) {
214+
List<String> extendedKeyUsage = null;
215+
try {
216+
extendedKeyUsage = certificate.getExtendedKeyUsage();
217+
if (extendedKeyUsage == null) {
218+
return hexDump(0, derOctetString);
219+
} else {
220+
return String.join("/", extendedKeyUsage.stream()
221+
.map(oid -> EXTENDED_KEY_USAGE.getOrDefault(oid, oid))
222+
.collect(Collectors.toList()));
223+
}
224+
} catch (CertificateParsingException e) {
225+
return PARSING_ERROR;
226+
}
227+
}
228+
229+
}

src/main/java/com/rabbitmq/client/impl/nio/SocketChannelFrameHandlerFactory.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,14 @@
2020
import com.rabbitmq.client.SslContextFactory;
2121
import com.rabbitmq.client.impl.AbstractFrameHandlerFactory;
2222
import com.rabbitmq.client.impl.FrameHandler;
23+
import com.rabbitmq.client.impl.TlsUtils;
24+
import org.slf4j.Logger;
25+
import org.slf4j.LoggerFactory;
2326

2427
import javax.net.ssl.SSLContext;
2528
import javax.net.ssl.SSLEngine;
2629
import javax.net.ssl.SSLException;
30+
import javax.net.ssl.SSLHandshakeException;
2731
import java.io.IOException;
2832
import java.net.InetSocketAddress;
2933
import java.net.SocketAddress;
@@ -39,6 +43,8 @@
3943
*/
4044
public class SocketChannelFrameHandlerFactory extends AbstractFrameHandlerFactory {
4145

46+
private static final Logger LOGGER = LoggerFactory.getLogger(SocketChannelFrameHandler.class);
47+
4248
final NioParams nioParams;
4349

4450
private final SslContextFactory sslContextFactory;
@@ -91,10 +97,17 @@ public FrameHandler create(Address addr, String connectionName) throws IOExcepti
9197

9298
if (ssl) {
9399
sslEngine.beginHandshake();
94-
boolean handshake = SslEngineHelper.doHandshake(channel, sslEngine);
95-
if (!handshake) {
96-
throw new SSLException("TLS handshake failed");
100+
try {
101+
boolean handshake = SslEngineHelper.doHandshake(channel, sslEngine);
102+
if (!handshake) {
103+
LOGGER.error("TLS connection failed");
104+
throw new SSLException("TLS handshake failed");
105+
}
106+
} catch (SSLHandshakeException e) {
107+
LOGGER.error("TLS connection failed: {}", e.getMessage());
108+
throw e;
97109
}
110+
TlsUtils.logPeerCertificateInfo(sslEngine.getSession());
98111
}
99112

100113
channel.configureBlocking(false);

src/test/java/com/rabbitmq/client/test/ClientTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@
6666
NioDeadlockOnConnectionClosing.class,
6767
GeneratedClassesTest.class,
6868
RpcTopologyRecordingTest.class,
69-
ConnectionTest.class
69+
ConnectionTest.class,
70+
TlsUtilsTest.class
7071
})
7172
public class ClientTests {
7273

0 commit comments

Comments
 (0)