diff --git a/src/main/c/netty_quic_boringssl.c b/src/main/c/netty_quic_boringssl.c
index 37592ce48..3cbfe44e8 100644
--- a/src/main/c/netty_quic_boringssl.c
+++ b/src/main/c/netty_quic_boringssl.c
@@ -46,6 +46,9 @@ 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;
@@ -53,6 +56,7 @@ 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;
@@ -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;
@@ -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.
@@ -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());
@@ -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);
@@ -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 },
@@ -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);
@@ -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;
}
@@ -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);
diff --git a/src/main/java/io/netty/incubator/codec/quic/BoringSSL.java b/src/main/java/io/netty/incubator/codec/quic/BoringSSL.java
index 3eba81681..c7fd1d255 100644
--- a/src/main/java/io/netty/incubator/codec/quic/BoringSSL.java
+++ b/src/main/java/io/netty/incubator/codec/quic/BoringSSL.java
@@ -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) {
@@ -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);
diff --git a/src/main/java/io/netty/incubator/codec/quic/BoringSSLKeylogCallback.java b/src/main/java/io/netty/incubator/codec/quic/BoringSSLKeylogCallback.java
new file mode 100644
index 000000000..df4ccc3f1
--- /dev/null
+++ b/src/main/java/io/netty/incubator/codec/quic/BoringSSLKeylogCallback.java
@@ -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);
+ }
+}
diff --git a/src/main/java/io/netty/incubator/codec/quic/QuicSslContextBuilder.java b/src/main/java/io/netty/incubator/codec/quic/QuicSslContextBuilder.java
index 56bd6e7ef..2aad470ee 100644
--- a/src/main/java/io/netty/incubator/codec/quic/QuicSslContextBuilder.java
+++ b/src/main/java/io/netty/incubator/codec/quic/QuicSslContextBuilder.java
@@ -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) {
@@ -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
+ *
+ * NSS Key Log Format. 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.
@@ -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);
}
}
}
diff --git a/src/main/java/io/netty/incubator/codec/quic/QuicheQuicSslContext.java b/src/main/java/io/netty/incubator/codec/quic/QuicheQuicSslContext.java
index c1d8322ce..9ddd37427 100644
--- a/src/main/java/io/netty/incubator/codec/quic/QuicheQuicSslContext.java
+++ b/src/main/java/io/netty/incubator/codec/quic/QuicheQuicSslContext.java
@@ -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;
@@ -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);
diff --git a/src/test/java/io/netty/incubator/codec/quic/QuicChannelConnectTest.java b/src/test/java/io/netty/incubator/codec/quic/QuicChannelConnectTest.java
index bec5bb909..7ecf4168b 100644
--- a/src/test/java/io/netty/incubator/codec/quic/QuicChannelConnectTest.java
+++ b/src/test/java/io/netty/incubator/codec/quic/QuicChannelConnectTest.java
@@ -138,6 +138,47 @@ private void testQLog(Path path, Consumer 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.
diff --git a/src/test/java/io/netty/incubator/codec/quic/TestLogBackAppender.java b/src/test/java/io/netty/incubator/codec/quic/TestLogBackAppender.java
new file mode 100644
index 000000000..ec24f9643
--- /dev/null
+++ b/src/test/java/io/netty/incubator/codec/quic/TestLogBackAppender.java
@@ -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 {
+ private static final List logs = new CopyOnWriteArrayList<>();
+
+ @Override
+ protected void append(ILoggingEvent iLoggingEvent) {
+ logs.add(iLoggingEvent.getFormattedMessage());
+ }
+
+ public static List getLogs() {
+ return logs;
+ }
+
+ public static void clearLogs() {
+ logs.clear();
+ }
+}
diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml
index 6f49af1e9..854fc8162 100644
--- a/src/test/resources/logback-test.xml
+++ b/src/test/resources/logback-test.xml
@@ -21,6 +21,12 @@
+
+
+
+
+
+