Skip to content

Commit

Permalink
add keylog support using SSL_CTX_set_keylog_callback (java-native-acc…
Browse files Browse the repository at this point in the history
…ess#343)

Motivation:

- In order to debug QUIC with tools like Wireshark, we should have access to connection secrets. 
- BoringSSL/OpenSSL provides callback method to log keys. https://www.openssl.org/docs/man1.1.1/man3/SSL_CTX_set_keylog_callback.html

Modifications:

- JNI functions and class BoringSSLKeylogCallback to support keylog callback
- New SSL configuration keylog

Result:

- Now it's possible to log SSL connection secrets with configuration keylog
  • Loading branch information
lostk1ng authored Oct 13, 2021
1 parent d7b6e36 commit 65742d5
Show file tree
Hide file tree
Showing 8 changed files with 197 additions and 11 deletions.
61 changes: 59 additions & 2 deletions src/main/c/netty_quic_boringssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,17 @@ static jmethodID handshakeCompleteCallbackMethod = NULL;
static jclass servernameCallbackClass = NULL;
static jmethodID servernameCallbackMethod = NULL;

static jclass keylogCallbackClass = NULL;
static jmethodID keylogCallbackMethod = NULL;

static jclass byteArrayClass = NULL;
static jclass stringClass = NULL;

static int handshakeCompleteCallbackIdx = -1;
static int verifyCallbackIdx = -1;
static int certificateCallbackIdx = -1;
static int servernameCallbackIdx = -1;
static int keylogCallbackIdx = -1;
static int alpn_data_idx = -1;
static int crypto_buffer_pool_idx = -1;

Expand Down Expand Up @@ -564,11 +568,41 @@ int quic_tlsext_servername_callback(SSL *ssl, int *out_alert, void *arg) {
return resultValue;
}

static jlong netty_boringssl_SSLContext_new0(JNIEnv* env, jclass clazz, jboolean server, jbyteArray alpn_protos, jobject handshakeCompleteCallback, jobject certificateCallback, jobject verifyCallback, jobject servernameCallback, jint verifyMode, jobjectArray subjectNames) {
// see https://www.openssl.org/docs/man1.1.1/man3/SSL_CTX_set_keylog_callback.html
void keylog_callback(const SSL* ssl, const char* line) {
SSL_CTX* ctx = SSL_get_SSL_CTX(ssl);
if (ctx == NULL) {
return;
}

JNIEnv* e = NULL;
if (quic_get_java_env(&e) != JNI_OK) {
return;
}

jobject keylogCallback = SSL_CTX_get_ex_data(ctx, keylogCallbackIdx);
if (keylogCallback == NULL) {
return;
}

jstring keyString = NULL;
if (line != NULL) {
keyString = (*e)->NewStringUTF(e, line);
if (keyString == NULL) {
return;
}
}

// Execute the java callback
(*e)->CallVoidMethod(e, keylogCallback, keylogCallbackMethod, (jlong) ssl, keyString);
}

static jlong netty_boringssl_SSLContext_new0(JNIEnv* env, jclass clazz, jboolean server, jbyteArray alpn_protos, jobject handshakeCompleteCallback, jobject certificateCallback, jobject verifyCallback, jobject servernameCallback, jobject keylogCallback, jint verifyMode, jobjectArray subjectNames) {
jobject handshakeCompleteCallbackRef = NULL;
jobject certificateCallbackRef = NULL;
jobject verifyCallbackRef = NULL;
jobject servernameCallbackRef = NULL;
jobject keylogCallbackRef = NULL;

if ((handshakeCompleteCallbackRef = (*env)->NewGlobalRef(env, handshakeCompleteCallback)) == NULL) {
goto error;
Expand All @@ -588,6 +622,12 @@ static jlong netty_boringssl_SSLContext_new0(JNIEnv* env, jclass clazz, jboolean
}
}

if (keylogCallback != NULL) {
if ((keylogCallbackRef = (*env)->NewGlobalRef(env, keylogCallback)) == NULL) {
goto error;
}
}

SSL_CTX *ctx = SSL_CTX_new(TLS_with_buffers_method());
// When using BoringSSL we want to use CRYPTO_BUFFER to reduce memory usage and minimize overhead as we do not need
// X509* at all and just need the raw bytes of the certificates to construct our Java X509Certificate.
Expand All @@ -614,6 +654,11 @@ static jlong netty_boringssl_SSLContext_new0(JNIEnv* env, jclass clazz, jboolean
SSL_CTX_set_ex_data(ctx, servernameCallbackIdx, servernameCallbackRef);
SSL_CTX_set_tlsext_servername_callback(ctx, quic_tlsext_servername_callback);
}

if (keylogCallbackRef != NULL) {
SSL_CTX_set_ex_data(ctx, keylogCallbackIdx, keylogCallbackRef);
SSL_CTX_set_keylog_callback(ctx, keylog_callback);
}
// Use a pool for our certificates so we can share these across connections.
SSL_CTX_set_ex_data(ctx, crypto_buffer_pool_idx, CRYPTO_BUFFER_POOL_new());

Expand Down Expand Up @@ -674,6 +719,11 @@ static void netty_boringssl_SSLContext_free(JNIEnv* env, jclass clazz, jlong ctx
(*env)->DeleteGlobalRef(env, servernameCallbackRef);
}

jobject keylogCallbackRef = SSL_CTX_get_ex_data(ssl_ctx, keylogCallbackIdx);
if (keylogCallbackRef != NULL) {
(*env)->DeleteGlobalRef(env, keylogCallbackRef);
}

alpn_data* data = SSL_CTX_get_ex_data(ssl_ctx, alpn_data_idx);
OPENSSL_free(data);

Expand Down Expand Up @@ -814,7 +864,7 @@ static const JNINativeMethod statically_referenced_fixed_method_table[] = {

static const jint statically_referenced_fixed_method_table_size = sizeof(statically_referenced_fixed_method_table) / sizeof(statically_referenced_fixed_method_table[0]);
static const JNINativeMethod fixed_method_table[] = {
{ "SSLContext_new0", "(Z[BLjava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;I[[B)J", (void *) netty_boringssl_SSLContext_new0 },
{ "SSLContext_new0", "(Z[BLjava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;I[[B)J", (void *) netty_boringssl_SSLContext_new0 },
{ "SSLContext_free", "(J)V", (void *) netty_boringssl_SSLContext_free },
{ "SSLContext_setSessionCacheTimeout", "(JJ)J", (void *) netty_boringssl_SSLContext_setSessionCacheTimeout },
{ "SSLContext_setSessionCacheSize", "(JJ)J", (void *) netty_boringssl_SSLContext_setSessionCacheSize },
Expand Down Expand Up @@ -880,10 +930,15 @@ jint netty_boringssl_JNI_OnLoad(JNIEnv* env, const char* packagePrefix) {
NETTY_JNI_UTIL_LOAD_CLASS(env, servernameCallbackClass, name, done);
NETTY_JNI_UTIL_GET_METHOD(env, servernameCallbackClass, servernameCallbackMethod, "selectCtx", "(JLjava/lang/String;)J", done);

NETTY_JNI_UTIL_PREPEND(packagePrefix, "io/netty/incubator/codec/quic/BoringSSLKeylogCallback", name, done);
NETTY_JNI_UTIL_LOAD_CLASS(env, keylogCallbackClass, name, done);
NETTY_JNI_UTIL_GET_METHOD(env, keylogCallbackClass, keylogCallbackMethod, "logKey", "(JLjava/lang/String;)V", done);

verifyCallbackIdx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL);
certificateCallbackIdx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL);
handshakeCompleteCallbackIdx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL);
servernameCallbackIdx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL);
keylogCallbackIdx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL);

alpn_data_idx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL);
crypto_buffer_pool_idx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL);
Expand All @@ -903,6 +958,7 @@ jint netty_boringssl_JNI_OnLoad(JNIEnv* env, const char* packagePrefix) {
NETTY_JNI_UTIL_UNLOAD_CLASS(env, verifyCallbackClass);
NETTY_JNI_UTIL_UNLOAD_CLASS(env, handshakeCompleteCallbackClass);
NETTY_JNI_UTIL_UNLOAD_CLASS(env, servernameCallbackClass);
NETTY_JNI_UTIL_UNLOAD_CLASS(env, keylogCallbackClass);
}
return ret;
}
Expand All @@ -913,6 +969,7 @@ void netty_boringssl_JNI_OnUnload(JNIEnv* env, const char* packagePrefix) {
NETTY_JNI_UTIL_UNLOAD_CLASS(env, verifyCallbackClass);
NETTY_JNI_UTIL_UNLOAD_CLASS(env, handshakeCompleteCallbackClass);
NETTY_JNI_UTIL_UNLOAD_CLASS(env, servernameCallbackClass);
NETTY_JNI_UTIL_UNLOAD_CLASS(env, keylogCallbackClass);

netty_jni_util_unregister_natives(env, packagePrefix, STATICALLY_CLASSNAME);
netty_jni_util_unregister_natives(env, packagePrefix, CLASSNAME);
Expand Down
7 changes: 4 additions & 3 deletions src/main/java/io/netty/incubator/codec/quic/BoringSSL.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,12 @@ static long SSLContext_new(boolean server, String[] applicationProtocols,
BoringSSLCertificateCallback certificateCallback,
BoringSSLCertificateVerifyCallback verifyCallback,
BoringSSLTlsextServernameCallback servernameCallback,
BoringSSLKeylogCallback keylogCallback,
int verifyMode,
byte[][] subjectNames) {
return SSLContext_new0(server, toWireFormat(applicationProtocols),
handshakeCompleteCallback,
certificateCallback, verifyCallback, servernameCallback, verifyMode, subjectNames);
certificateCallback, verifyCallback, servernameCallback, keylogCallback, verifyMode, subjectNames);
}

private static byte[] toWireFormat(String[] applicationProtocols) {
Expand All @@ -68,8 +69,8 @@ private static byte[] toWireFormat(String[] applicationProtocols) {
private static native long SSLContext_new0(boolean server,
byte[] applicationProtocols, Object handshakeCompleteCallback,
Object certificateCallback, Object verifyCallback,
Object servernameCallback, int verifyDepth,
byte[][] subjectNames);
Object servernameCallback, Object keylogCallback,
int verifyDepth, byte[][] subjectNames);
static native void SSLContext_set_early_data_enabled(long context, boolean enabled);
static native long SSLContext_setSessionCacheSize(long context, long size);
static native long SSLContext_setSessionCacheTimeout(long context, long size);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright 2021 The Netty Project
*
* The Netty Project 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:
*
* https://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 io.netty.incubator.codec.quic;

import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;

final class BoringSSLKeylogCallback {
private static final InternalLogger logger = InternalLoggerFactory.getInstance(BoringSSLKeylogCallback.class);

@SuppressWarnings("unused")
void logKey(long ssl, String keylog) {
logger.debug(keylog);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ public static QuicSslContext buildForServerWithSni(Mapping<? super String, ? ext
private ClientAuth clientAuth = ClientAuth.NONE;
private String[] applicationProtocols;
private Boolean earlyData;
private boolean keylog;
private Mapping<? super String, ? extends QuicSslContext> mapping;

private QuicSslContextBuilder(boolean forServer) {
Expand All @@ -178,6 +179,18 @@ public QuicSslContextBuilder earlyData(boolean enabled) {
return this;
}

/**
* Enable / disable keylog. When enabled, TLS keys are logged to an internal logger named
* "io.netty.incubator.codec.quic.BoringSSLKeylogCallback" with DEBUG level, see
* {@link io.netty.incubator.codec.quic.BoringSSLKeylogCallback} for detail, logging keys are following
* <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/Key_Log_Format">
* NSS Key Log Format</a>. This is intended for debugging use with tools like Wireshark.
*/
public QuicSslContextBuilder keylog(boolean enabled) {
this.keylog = enabled;
return this;
}

/**
* Trusted certificates for verifying the remote endpoint's certificate. The file should
* contain an X.509 certificate collection in PEM format. {@code null} uses the system default.
Expand Down Expand Up @@ -334,11 +347,11 @@ public QuicSslContextBuilder clientAuth(ClientAuth clientAuth) {
*/
public QuicSslContext build() {
if (forServer) {
return new QuicheQuicSslContext(true, sessionCacheSize, sessionTimeout, clientAuth,
trustManagerFactory, keyManagerFactory, keyPassword, mapping, earlyData, applicationProtocols);
return new QuicheQuicSslContext(true, sessionCacheSize, sessionTimeout, clientAuth, trustManagerFactory,
keyManagerFactory, keyPassword, mapping, earlyData, keylog, applicationProtocols);
} else {
return new QuicheQuicSslContext(false, sessionCacheSize, sessionTimeout, clientAuth,
trustManagerFactory, keyManagerFactory, keyPassword, mapping, earlyData, applicationProtocols);
return new QuicheQuicSslContext(false, sessionCacheSize, sessionTimeout, clientAuth, trustManagerFactory,
keyManagerFactory, keyPassword, mapping, earlyData, keylog, applicationProtocols);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ final class QuicheQuicSslContext extends QuicSslContext {
ClientAuth clientAuth, TrustManagerFactory trustManagerFactory,
KeyManagerFactory keyManagerFactory, String password,
Mapping<? super String, ? extends QuicSslContext> mapping,
Boolean earlyData,
Boolean earlyData, boolean keylog,
String... applicationProtocols) {
Quic.ensureAvailability();
this.server = server;
Expand Down Expand Up @@ -101,7 +101,8 @@ final class QuicheQuicSslContext extends QuicSslContext {
new BoringSSLCertificateCallback(engineMap, keyManager, password),
new BoringSSLCertificateVerifyCallback(engineMap, trustManager),
mapping == null ? null : new BoringSSLTlsextServernameCallback(engineMap, mapping),
verifyMode, BoringSSL.subjectNames(trustManager.getAcceptedIssuers())));
keylog ? new BoringSSLKeylogCallback() : null, verifyMode,
BoringSSL.subjectNames(trustManager.getAcceptedIssuers())));
apn = new QuicheQuicApplicationProtocolNegotiator(applicationProtocols);
this.sessionCacheSize = BoringSSL.SSLContext_setSessionCacheSize(nativeSslContext.address(), sessionCacheSize);
this.sessionTimeout = BoringSSL.SSLContext_setSessionCacheTimeout(nativeSslContext.address(), sessionTimeout);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,47 @@ private void testQLog(Path path, Consumer<Path> consumer) throws Throwable {
}
}

@Test
public void testKeylogEnabled() throws Throwable {
testKeylog(true);
}

@Test
public void testKeylogDisabled() throws Throwable {
testKeylog(false);
}

private static void testKeylog(boolean enable) throws Throwable {
TestLogBackAppender.clearLogs();
QuicChannelValidationHandler serverValidationHandler = new QuicChannelValidationHandler();
QuicChannelValidationHandler clientValidationHandler = new QuicChannelValidationHandler();
Channel server = QuicTestUtils.newServer(serverValidationHandler,
new ChannelInboundHandlerAdapter());
InetSocketAddress address = (InetSocketAddress) server.localAddress();
Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(
QuicSslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE)
.applicationProtocols(QuicTestUtils.PROTOS).keylog(enable).build()));

try {
QuicChannel quicChannel = QuicChannel.newBootstrap(channel)
.handler(clientValidationHandler)
.streamHandler(new ChannelInboundHandlerAdapter())
.remoteAddress(address)
.connect()
.get();

quicChannel.close().sync();
quicChannel.closeFuture().sync();
assertTrue(enable ? TestLogBackAppender.getLogs().size() > 0 : TestLogBackAppender.getLogs().size() == 0);
serverValidationHandler.assertState();
clientValidationHandler.assertState();
} finally {
server.close().sync();
// Close the parent Datagram channel as well.
channel.close().sync();
}
}

@Test
public void testAddressValidation() throws Throwable {
// Bind to something so we can use the port to connect too and so can ensure we really timeout.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2021 The Netty Project
*
* The Netty Project 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:
*
* https://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 io.netty.incubator.codec.quic;

import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.AppenderBase;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public final class TestLogBackAppender extends AppenderBase<ILoggingEvent> {
private static final List<String> logs = new CopyOnWriteArrayList<>();

@Override
protected void append(ILoggingEvent iLoggingEvent) {
logs.add(iLoggingEvent.getFormattedMessage());
}

public static List<String> getLogs() {
return logs;
}

public static void clearLogs() {
logs.clear();
}
}
6 changes: 6 additions & 0 deletions src/test/resources/logback-test.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@
</encoder>
</appender>

<appender name="TEST" class="io.netty.incubator.codec.quic.TestLogBackAppender"/>

<logger name="io.netty.incubator.codec.quic.BoringSSLKeylogCallback" level="debug" additivity="false">
<appender-ref ref="TEST" />
</logger>

<root level="${logLevel:-info}">
<appender-ref ref="STDOUT" />
</root>
Expand Down

0 comments on commit 65742d5

Please sign in to comment.