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 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 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 @@ + + + + + +