Skip to content

Commit 7fc355a

Browse files
fzakarianormanmaurer
authored andcommitted
Introduce SslMasterKeyHandler (netty#8653)
Motivation Debugging SSL/TLS connections through wireshark is a pain -- if the cipher used involves Diffie-Hellman then it is essentially impossible unless you can have the client dump out the master key [1] This is a work-in-progress change (tests & comments to come!) that introduces a new handler you can set on the SslContext to receive the master key & session id. I'm hoping to get feedback if a change in this vein would be welcomed. An implementation that conforms to Wireshark's NSS key log[2] file is also included. Depending on feedback on the PR going forward I am planning to "clean it up" by adding documentation, example server & tests. Implementation will need to be finished as well for retrieving the master key from the OpenSSL context. [1] https://jimshaver.net/2015/02/11/decrypting-tls-browser-traffic-with-wireshark-the-easy-way/ [2] https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/Key_Log_Format Modification - Added SslMasterKeyHandler - An implementation of the handler that conforms to Wireshark's key log format is included. Result: Be able to debug SSL / TLS connections more easily. Signed-off-by: Farid Zakaria <[email protected]>
1 parent c0f9364 commit 7fc355a

File tree

2 files changed

+279
-0
lines changed

2 files changed

+279
-0
lines changed
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/*
2+
* Copyright 2019 The Netty Project
3+
*
4+
* The Netty Project licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
package io.netty.handler.ssl;
17+
18+
import io.netty.buffer.ByteBufUtil;
19+
import io.netty.channel.ChannelHandlerContext;
20+
import io.netty.channel.ChannelInboundHandlerAdapter;
21+
import io.netty.internal.tcnative.SSL;
22+
import io.netty.util.internal.ReflectionUtil;
23+
import io.netty.util.internal.SystemPropertyUtil;
24+
import io.netty.util.internal.logging.InternalLogger;
25+
import io.netty.util.internal.logging.InternalLoggerFactory;
26+
27+
import javax.crypto.SecretKey;
28+
import javax.crypto.spec.SecretKeySpec;
29+
import javax.net.ssl.SSLEngine;
30+
import javax.net.ssl.SSLSession;
31+
import java.lang.reflect.Field;
32+
33+
/**
34+
* The {@link SslMasterKeyHandler} is a channel-handler you can include in your pipeline to consume the master key
35+
* & session identifier for a TLS session.
36+
* This can be very useful, for instance the {@link WiresharkSslMasterKeyHandler} implementation will
37+
* log the secret & identifier in a format that is consumable by Wireshark -- allowing easy decryption of pcap/tcpdumps.
38+
*/
39+
public abstract class SslMasterKeyHandler extends ChannelInboundHandlerAdapter {
40+
41+
private static final InternalLogger logger = InternalLoggerFactory.getInstance(SslMasterKeyHandler.class);
42+
43+
/**
44+
* The JRE SSLSessionImpl cannot be imported
45+
*/
46+
private static final Class<?> SSL_SESSIONIMPL_CLASS;
47+
48+
/**
49+
* The master key field in the SSLSessionImpl
50+
*/
51+
private static final Field SSL_SESSIONIMPL_MASTER_SECRET_FIELD;
52+
53+
/**
54+
* A system property that can be used to turn on/off the {@link SslMasterKeyHandler} dynamically without having
55+
* to edit your pipeline.
56+
* <code>-Dio.netty.ssl.masterKeyHandler=true</code>
57+
*/
58+
public static final String SYSTEM_PROP_KEY = "io.netty.ssl.masterKeyHandler";
59+
60+
/**
61+
* The unavailability cause of whether the private Sun implementation of SSLSessionImpl is available.
62+
*/
63+
private static final Throwable UNAVAILABILITY_CAUSE;
64+
65+
static {
66+
Throwable cause = null;
67+
Class<?> clazz = null;
68+
Field field = null;
69+
try {
70+
clazz = Class.forName("sun.security.ssl.SSLSessionImpl");
71+
field = clazz.getDeclaredField("masterSecret");
72+
cause = ReflectionUtil.trySetAccessible(field, true);
73+
} catch (Throwable e) {
74+
cause = e;
75+
logger.debug("sun.security.ssl.SSLSessionImpl is unavailable.", e);
76+
}
77+
UNAVAILABILITY_CAUSE = cause;
78+
SSL_SESSIONIMPL_CLASS = clazz;
79+
SSL_SESSIONIMPL_MASTER_SECRET_FIELD = field;
80+
}
81+
82+
/**
83+
* Constructor.
84+
*/
85+
protected SslMasterKeyHandler() {
86+
}
87+
88+
/**
89+
* Ensure that SSLSessionImpl is available.
90+
* @throws UnsatisfiedLinkError if unavailable
91+
*/
92+
public static void ensureSunSslEngineAvailability() {
93+
if (UNAVAILABILITY_CAUSE != null) {
94+
throw new IllegalStateException(
95+
"Failed to find SSLSessionImpl on classpath", UNAVAILABILITY_CAUSE);
96+
}
97+
}
98+
99+
/**
100+
* Returns the cause of unavailability.
101+
*
102+
* @return the cause if unavailable. {@code null} if available.
103+
*/
104+
public static Throwable sunSslEngineUnavailabilityCause() {
105+
return UNAVAILABILITY_CAUSE;
106+
}
107+
108+
/* Returns {@code true} if and only if sun.security.ssl.SSLSessionImpl exists in the runtime.
109+
*/
110+
public static boolean isSunSslEngineAvailable() {
111+
return UNAVAILABILITY_CAUSE == null;
112+
}
113+
114+
/**
115+
* Consume the master key for the session and the sessionId
116+
* @param masterKey A 48-byte secret shared between the client and server.
117+
* @param session The current TLS session
118+
*/
119+
protected abstract void accept(SecretKey masterKey, SSLSession session);
120+
121+
@Override
122+
public final void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
123+
//only try to log the session info if the ssl handshake has successfully completed.
124+
if (evt == SslHandshakeCompletionEvent.SUCCESS) {
125+
boolean shouldHandle = SystemPropertyUtil.getBoolean(SYSTEM_PROP_KEY, false);
126+
127+
if (shouldHandle) {
128+
final SslHandler handler = ctx.pipeline().get(SslHandler.class);
129+
final SSLEngine engine = handler.engine();
130+
final SSLSession sslSession = engine.getSession();
131+
132+
//the OpenJDK does not expose a way to get the master secret, so try to use reflection to get it.
133+
if (isSunSslEngineAvailable() && sslSession.getClass().equals(SSL_SESSIONIMPL_CLASS)) {
134+
final SecretKey secretKey;
135+
try {
136+
secretKey = (SecretKey) SSL_SESSIONIMPL_MASTER_SECRET_FIELD.get(sslSession);
137+
} catch (IllegalAccessException e) {
138+
throw new IllegalArgumentException("Failed to access the field 'masterSecret' " +
139+
"via reflection.", e);
140+
}
141+
accept(secretKey, sslSession);
142+
} else if (OpenSsl.isAvailable() && engine instanceof ReferenceCountedOpenSslEngine) {
143+
SecretKeySpec secretKey = new SecretKeySpec(
144+
SSL.getMasterKey(((ReferenceCountedOpenSslEngine) engine).sslPointer()), "AES");
145+
accept(secretKey, sslSession);
146+
}
147+
}
148+
}
149+
150+
ctx.fireUserEventTriggered(evt);
151+
}
152+
153+
/**
154+
* Create a {@link WiresharkSslMasterKeyHandler} instance.
155+
* This TLS master key handler logs the master key and session-id in a format
156+
* understood by Wireshark -- this can be especially useful if you need to ever
157+
* decrypt a TLS session and are using perfect forward secrecy (i.e. Diffie-Hellman)
158+
* The key and session identifier are forwarded to the log named 'io.netty.wireshark'.
159+
*/
160+
public static SslMasterKeyHandler newWireSharkSslMasterKeyHandler() {
161+
return new WiresharkSslMasterKeyHandler();
162+
}
163+
164+
/**
165+
* Record the session identifier and master key to the {@link InternalLogger} named <code>io.netty.wireshark</code>.
166+
* ex. <code>RSA Session-ID:XXX Master-Key:YYY</code>
167+
* This format is understood by Wireshark 1.6.0.
168+
* https://code.wireshark.org/review/gitweb?p=wireshark.git;a=commit;h=686d4cabb41185591c361f9ec6b709034317144b
169+
* The key and session identifier are forwarded to the log named 'io.netty.wireshark'.
170+
*/
171+
private static final class WiresharkSslMasterKeyHandler extends SslMasterKeyHandler {
172+
173+
private static final InternalLogger wireshark_logger =
174+
InternalLoggerFactory.getInstance("io.netty.wireshark");
175+
176+
private static final char[] hexCode = "0123456789ABCDEF".toCharArray();
177+
178+
@Override
179+
protected void accept(SecretKey masterKey, SSLSession session) {
180+
if (masterKey.getEncoded().length != 48) {
181+
throw new IllegalArgumentException("An invalid length master key was provided.");
182+
}
183+
final byte[] sessionId = session.getId();
184+
wireshark_logger.warn("RSA Session-ID:{} Master-Key:{}",
185+
ByteBufUtil.hexDump(sessionId).toLowerCase(),
186+
ByteBufUtil.hexDump(masterKey.getEncoded()).toLowerCase());
187+
}
188+
}
189+
190+
}

handler/src/test/java/io/netty/handler/ssl/SSLEngineTest.java

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646
import io.netty.util.internal.EmptyArrays;
4747
import io.netty.util.internal.PlatformDependent;
4848
import io.netty.util.internal.StringUtil;
49+
import io.netty.util.internal.SystemPropertyUtil;
50+
import org.conscrypt.OpenSSLProvider;
4951
import org.junit.After;
5052
import org.junit.Assume;
5153
import org.junit.Before;
@@ -55,10 +57,12 @@
5557
import org.mockito.MockitoAnnotations;
5658

5759
import java.io.ByteArrayInputStream;
60+
import java.io.Closeable;
5861
import java.io.File;
5962
import java.io.FileInputStream;
6063
import java.io.IOException;
6164
import java.io.InputStream;
65+
import java.io.OutputStream;
6266
import java.net.InetSocketAddress;
6367
import java.net.Socket;
6468
import java.nio.ByteBuffer;
@@ -84,12 +88,14 @@
8488
import java.util.concurrent.Executors;
8589
import java.util.concurrent.TimeUnit;
8690

91+
import javax.crypto.SecretKey;
8792
import javax.net.ssl.ExtendedSSLSession;
8893
import javax.net.ssl.KeyManager;
8994
import javax.net.ssl.KeyManagerFactory;
9095
import javax.net.ssl.KeyManagerFactorySpi;
9196
import javax.net.ssl.ManagerFactoryParameters;
9297
import javax.net.ssl.SNIHostName;
98+
import javax.net.ssl.SSLContext;
9399
import javax.net.ssl.SSLEngine;
94100
import javax.net.ssl.SSLEngineResult;
95101
import javax.net.ssl.SSLEngineResult.Status;
@@ -3245,6 +3251,89 @@ protected TrustManager[] engineGetTrustManagers() {
32453251
}
32463252
}
32473253

3254+
@Test
3255+
public void testMasterKeyLogging() throws Exception {
3256+
3257+
/*
3258+
* At the moment master key logging is not supported for conscrypt
3259+
*/
3260+
Assume.assumeFalse(serverSslContextProvider() instanceof OpenSSLProvider);
3261+
3262+
/*
3263+
* The JDK SSL engine master key retrieval relies on being able to set field access to true.
3264+
* That is not available in JDK9+
3265+
*/
3266+
Assume.assumeFalse(sslServerProvider() == SslProvider.JDK && PlatformDependent.javaVersion() > 8);
3267+
3268+
String originalSystemPropertyValue = SystemPropertyUtil.get(SslMasterKeyHandler.SYSTEM_PROP_KEY);
3269+
System.setProperty(SslMasterKeyHandler.SYSTEM_PROP_KEY, Boolean.TRUE.toString());
3270+
3271+
SelfSignedCertificate ssc = new SelfSignedCertificate();
3272+
serverSslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
3273+
.sslProvider(sslServerProvider())
3274+
.sslContextProvider(serverSslContextProvider())
3275+
.build();
3276+
Socket socket = null;
3277+
3278+
try {
3279+
sb = new ServerBootstrap();
3280+
sb.group(new NioEventLoopGroup(), new NioEventLoopGroup());
3281+
sb.channel(NioServerSocketChannel.class);
3282+
3283+
final Promise<SecretKey> promise = sb.config().group().next().newPromise();
3284+
serverChannel = sb.childHandler(new ChannelInitializer<Channel>() {
3285+
@Override
3286+
protected void initChannel(Channel ch) throws Exception {
3287+
ch.config().setAllocator(new TestByteBufAllocator(ch.config().getAllocator(), type));
3288+
3289+
SslHandler sslHandler = delegatingExecutor == null ?
3290+
serverSslCtx.newHandler(ch.alloc()) :
3291+
serverSslCtx.newHandler(ch.alloc(), delegatingExecutor);
3292+
3293+
ch.pipeline().addLast(sslHandler);
3294+
ch.pipeline().addLast(new SslMasterKeyHandler() {
3295+
@Override
3296+
protected void accept(SecretKey masterKey, SSLSession session) {
3297+
promise.setSuccess(masterKey);
3298+
}
3299+
});
3300+
serverConnectedChannel = ch;
3301+
}
3302+
}).bind(new InetSocketAddress(0)).sync().channel();
3303+
3304+
int port = ((InetSocketAddress) serverChannel.localAddress()).getPort();
3305+
3306+
SSLContext sslContext = SSLContext.getInstance("TLS");
3307+
sslContext.init(null, InsecureTrustManagerFactory.INSTANCE.getTrustManagers(), null);
3308+
socket = sslContext.getSocketFactory().createSocket(NetUtil.LOCALHOST, port);
3309+
OutputStream out = socket.getOutputStream();
3310+
out.write(1);
3311+
out.flush();
3312+
3313+
assertTrue(promise.await(10, TimeUnit.SECONDS));
3314+
SecretKey key = promise.get();
3315+
assertEquals("AES secret key must be 48 bytes", 48, key.getEncoded().length);
3316+
} finally {
3317+
closeQuietly(socket);
3318+
if (originalSystemPropertyValue != null) {
3319+
System.setProperty(SslMasterKeyHandler.SYSTEM_PROP_KEY, originalSystemPropertyValue);
3320+
} else {
3321+
System.clearProperty(SslMasterKeyHandler.SYSTEM_PROP_KEY);
3322+
}
3323+
ssc.delete();
3324+
}
3325+
}
3326+
3327+
private static void closeQuietly(Closeable c) {
3328+
if (c != null) {
3329+
try {
3330+
c.close();
3331+
} catch (IOException ignore) {
3332+
// ignore
3333+
}
3334+
}
3335+
}
3336+
32483337
private KeyManagerFactory newKeyManagerFactory(SelfSignedCertificate ssc)
32493338
throws UnrecoverableKeyException, KeyStoreException, NoSuchAlgorithmException,
32503339
CertificateException, IOException {

0 commit comments

Comments
 (0)