From e82afbc4fed73dd9b5f65d94a420107b28fae2e9 Mon Sep 17 00:00:00 2001 From: Norman Maurer Date: Wed, 13 Dec 2023 11:58:24 +0100 Subject: [PATCH] Add support to explicit configure session tickets to be used (#634) Motivation: Sometimes you want to set the session tickets that should be used explicit to be able to use the same on on multiple servers. Modifications: Add required code to be able to configure the session tickets explicit. Result: Be able to manually set the session tickets --- .../netty/incubator/codec/quic/BoringSSL.java | 7 +- .../quic/BoringSSLSessionTicketCallback.java | 65 +++++++++ .../incubator/codec/quic/QuicSslContext.java | 3 + .../codec/quic/QuicSslSessionContext.java | 34 +++++ .../codec/quic/QuicheQuicSslContext.java | 24 +++- .../codec/quic/SslSessionTicketKey.java | 129 ++++++++++++++++++ .../src/main/c/netty_quic_boringssl.c | 129 +++++++++++++++++- .../codec/quic/QuicChannelConnectTest.java | 43 ++++-- 8 files changed, 413 insertions(+), 21 deletions(-) create mode 100644 codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/BoringSSLSessionTicketCallback.java create mode 100644 codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicSslSessionContext.java create mode 100644 codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/SslSessionTicketKey.java diff --git a/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/BoringSSL.java b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/BoringSSL.java index d6268fad4..df3b92347 100644 --- a/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/BoringSSL.java +++ b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/BoringSSL.java @@ -45,11 +45,12 @@ static long SSLContext_new(boolean server, String[] applicationProtocols, BoringSSLKeylogCallback keylogCallback, BoringSSLSessionCallback sessionCallback, BoringSSLPrivateKeyMethod privateKeyMethod, + BoringSSLSessionTicketCallback sessionTicketCallback, int verifyMode, byte[][] subjectNames) { return SSLContext_new0(server, toWireFormat(applicationProtocols), handshakeCompleteCallback, certificateCallback, verifyCallback, servernameCallback, - keylogCallback, sessionCallback, privateKeyMethod, verifyMode, subjectNames); + keylogCallback, sessionCallback, privateKeyMethod, sessionTicketCallback, verifyMode, subjectNames); } private static byte[] toWireFormat(String[] applicationProtocols) { @@ -74,10 +75,14 @@ private static native long SSLContext_new0(boolean server, Object servernameCallback, Object keylogCallback, Object sessionCallback, Object privateKeyMethod, + Object sessionTicketCallback, 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); + + static native void SSLContext_setSessionTicketKeys(long context, boolean enableCallback); + static native void SSLContext_free(long context); static long SSL_new(long context, boolean server, String hostname) { return SSL_new0(context, server, tlsExtHostName(hostname)); diff --git a/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/BoringSSLSessionTicketCallback.java b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/BoringSSLSessionTicketCallback.java new file mode 100644 index 000000000..0b082d9e7 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/BoringSSLSessionTicketCallback.java @@ -0,0 +1,65 @@ +/* + * Copyright 2023 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.PlatformDependent; + +final class BoringSSLSessionTicketCallback { + + // As we dont assume to have a lot of keys configured we will just use an array for now as a data store. + private volatile byte[][] sessionKeys; + + // Accessed via JNI. + byte[] findSessionTicket(byte[] keyname) { + byte[][] keys = this.sessionKeys; + if (keys == null || keys.length == 0) { + return null; + } + if (keyname == null) { + return keys[0]; + } + + for (int i = 0; i < keys.length; i++) { + byte[] key = keys[i]; + if (PlatformDependent.equals(keyname, 0, key, 1, keyname.length)) { + return key; + } + } + return null; + } + + void setSessionTicketKeys(SslSessionTicketKey[] keys) { + if (keys != null && keys.length != 0) { + byte[][] sessionKeys = new byte[keys.length][]; + for(int i = 0; i < keys.length; ++i) { + SslSessionTicketKey key = keys[i]; + byte[] binaryKey = new byte[49]; + // We mark the first key as preferred by using 1 as byte marker + binaryKey[0] = i == 0 ? (byte) 1 : (byte) 0; + int dstCurPos = 1; + System.arraycopy(key.name, 0, binaryKey, dstCurPos, 16); + dstCurPos += 16; + System.arraycopy(key.hmacKey, 0, binaryKey, dstCurPos, 16); + dstCurPos += 16; + System.arraycopy(key.aesKey, 0, binaryKey, dstCurPos, 16); + sessionKeys[i] = binaryKey; + } + this.sessionKeys = sessionKeys; + } else { + sessionKeys = null; + } + } +} diff --git a/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicSslContext.java b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicSslContext.java index 6d40aebd5..7a3436ae8 100644 --- a/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicSslContext.java +++ b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicSslContext.java @@ -33,6 +33,9 @@ public abstract class QuicSslContext extends SslContext { @Override public abstract QuicSslEngine newEngine(ByteBufAllocator alloc, String peerHost, int peerPort); + @Override + public abstract QuicSslSessionContext sessionContext(); + static X509Certificate[] toX509Certificates0(InputStream stream) throws CertificateException { return SslContext.toX509Certificates(stream); diff --git a/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicSslSessionContext.java b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicSslSessionContext.java new file mode 100644 index 000000000..93126ead0 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicSslSessionContext.java @@ -0,0 +1,34 @@ +/* + * Copyright 2023 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 javax.net.ssl.SSLSessionContext; + +/** + * {@link SSLSessionContext} which also supports advanced operations. + */ +public interface QuicSslSessionContext extends SSLSessionContext { + + /** + * Sets the {@link SslSessionTicketKey}s that should be used. The first key of the array is used for encryption + * and decryption while the rest of the array is only used for decryption. This allows you to better handling + * rotating of the keys. The rotating is the responsibility of the user. + * If {@code null} is used for {@code keys} a key will automatically generated by the library and also rotated. + * + * @param keys the tickets to use. + */ + void setTicketKeys(SslSessionTicketKey... keys); +} diff --git a/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicheQuicSslContext.java b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicheQuicSslContext.java index bfc2a414e..df5b2fecf 100644 --- a/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicheQuicSslContext.java +++ b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/QuicheQuicSslContext.java @@ -27,7 +27,6 @@ import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLSession; -import javax.net.ssl.SSLSessionContext; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509ExtendedKeyManager; @@ -53,6 +52,7 @@ import java.util.function.LongFunction; import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static java.util.Objects.requireNonNull; final class QuicheQuicSslContext extends QuicSslContext { final ClientAuth clientAuth; @@ -64,6 +64,9 @@ final class QuicheQuicSslContext extends QuicSslContext { private final QuicheQuicSslSessionContext sessionCtx; private final QuicheQuicSslEngineMap engineMap = new QuicheQuicSslEngineMap(); private final QuicClientSessionCache sessionCache; + + private final BoringSSLSessionTicketCallback sessionTicketCallback = new BoringSSLSessionTicketCallback(); + final NativeSslContext nativeSslContext; QuicheQuicSslContext(boolean server, long sessionTimeout, long sessionCacheSize, @@ -112,7 +115,8 @@ final class QuicheQuicSslContext extends QuicSslContext { new BoringSSLCertificateVerifyCallback(engineMap, trustManager), mapping == null ? null : new BoringSSLTlsextServernameCallback(engineMap, mapping), keylog == null ? null : new BoringSSLKeylogCallback(engineMap, keylog), - server ? null : new BoringSSLSessionCallback(engineMap, sessionCache), privateKeyMethod, verifyMode, + server ? null : new BoringSSLSessionCallback(engineMap, sessionCache), privateKeyMethod, + sessionTicketCallback, verifyMode, BoringSSL.subjectNames(trustManager.getAcceptedIssuers()))); apn = new QuicheQuicApplicationProtocolNegotiator(applicationProtocols); if (this.sessionCache != null) { @@ -275,7 +279,7 @@ public QuicSslEngine newEngine(ByteBufAllocator alloc, String peerHost, int peer } @Override - public SSLSessionContext sessionContext() { + public QuicSslSessionContext sessionContext() { return sessionCtx; } @@ -338,6 +342,12 @@ void setSessionCacheSize(int size) throws IllegalArgumentException { } } + void setSessionTicketKeys(SslSessionTicketKey[] ticketKeys) { + sessionTicketCallback.setSessionTicketKeys(ticketKeys); + BoringSSL.SSLContext_setSessionTicketKeys( + nativeSslContext.address(), ticketKeys != null && ticketKeys.length != 0); + } + @SuppressWarnings("deprecation") private static final class QuicheQuicApplicationProtocolNegotiator implements ApplicationProtocolNegotiator { private final List protocols; @@ -356,8 +366,7 @@ public List protocols() { } } - private static final class QuicheQuicSslSessionContext implements SSLSessionContext { - + private static final class QuicheQuicSslSessionContext implements QuicSslSessionContext { private final QuicheQuicSslContext context; QuicheQuicSslSessionContext(QuicheQuicSslContext context) { @@ -403,6 +412,11 @@ public void setSessionCacheSize(int size) throws IllegalArgumentException { public int getSessionCacheSize() { return (int) context.sessionCacheSize(); } + + @Override + public void setTicketKeys(SslSessionTicketKey... keys) { + context.setSessionTicketKeys(keys); + } } static final class NativeSslContext extends AbstractReferenceCounted { diff --git a/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/SslSessionTicketKey.java b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/SslSessionTicketKey.java new file mode 100644 index 000000000..c8e7ea5e7 --- /dev/null +++ b/codec-classes-quic/src/main/java/io/netty/incubator/codec/quic/SslSessionTicketKey.java @@ -0,0 +1,129 @@ +/* + * Copyright 2023 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: + * + * http://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 java.util.Arrays; + +/** + * Session Ticket Key + */ +public final class SslSessionTicketKey { + /** + * Size of session ticket key name + */ + public static final int NAME_SIZE = 16; + /** + * Size of session ticket key HMAC key + */ + public static final int HMAC_KEY_SIZE = 16; + /** + * Size of session ticket key AES key + */ + public static final int AES_KEY_SIZE = 16; + /** + * Size of session ticket key + */ + public static final int TICKET_KEY_SIZE = NAME_SIZE + HMAC_KEY_SIZE + AES_KEY_SIZE; + + // package private so we can access these in BoringSSLSessionTicketCallback without calling clone() on the byte[]. + final byte[] name; + final byte[] hmacKey; + final byte[] aesKey; + + /** + * Construct SessionTicketKey. + * @param name the name of the session ticket key + * @param hmacKey the HMAC key of the session ticket key + * @param aesKey the AES key of the session ticket key + */ + public SslSessionTicketKey(byte[] name, byte[] hmacKey, byte[] aesKey) { + if (name == null || name.length != NAME_SIZE) { + throw new IllegalArgumentException("Length of name must be " + NAME_SIZE); + } + if (hmacKey == null || hmacKey.length != HMAC_KEY_SIZE) { + throw new IllegalArgumentException("Length of hmacKey must be " + HMAC_KEY_SIZE); + } + if (aesKey == null || aesKey.length != AES_KEY_SIZE) { + throw new IllegalArgumentException("Length of aesKey must be " + AES_KEY_SIZE); + } + this.name = name.clone(); + this.hmacKey = hmacKey.clone(); + this.aesKey = aesKey.clone(); + } + + /** + * Get name. + * + * @return the name of the session ticket key + */ + public byte[] name() { + return name.clone(); + } + + /** + * Get HMAC key. + * @return the HMAC key of the session ticket key + */ + public byte[] hmacKey() { + return hmacKey.clone(); + } + + /** + * Get AES Key. + * @return the AES key of the session ticket key + */ + public byte[] aesKey() { + return aesKey.clone(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + SslSessionTicketKey that = (SslSessionTicketKey) o; + + if (!Arrays.equals(name, that.name)) { + return false; + } + if (!Arrays.equals(hmacKey, that.hmacKey)) { + return false; + } + return Arrays.equals(aesKey, that.aesKey); + } + + @Override + public int hashCode() { + int result = Arrays.hashCode(name); + result = 31 * result + Arrays.hashCode(hmacKey); + result = 31 * result + Arrays.hashCode(aesKey); + return result; + } + + @Override + public String toString() { + return "SessionTicketKey{" + + "name=" + Arrays.toString(name) + + ", hmacKey=" + Arrays.toString(hmacKey) + + ", aesKey=" + Arrays.toString(aesKey) + + '}'; + } +} diff --git a/codec-native-quic/src/main/c/netty_quic_boringssl.c b/codec-native-quic/src/main/c/netty_quic_boringssl.c index cae82385d..da82e36b2 100644 --- a/codec-native-quic/src/main/c/netty_quic_boringssl.c +++ b/codec-native-quic/src/main/c/netty_quic_boringssl.c @@ -19,6 +19,8 @@ #include #include #include +#include +#include #include "netty_jni_util.h" #include "netty_quic.h" @@ -34,6 +36,15 @@ #define ERR_LEN 256 +// For encoding of keys see BoringSSLSessionTicketCallback.setSessionTicketKeys(...) +#define SSL_SESSION_TICKET_KEY_NAME_OFFSET 1 +#define SSL_SESSION_TICKET_KEY_HMAC_OFFSET 17 +#define SSL_SESSION_TICKET_KEY_EVP_OFFSET 33 +#define SSL_SESSION_TICKET_KEY_NAME_LEN 16 +#define SSL_SESSION_TICKET_AES_KEY_LEN 16 +#define SSL_SESSION_TICKET_HMAC_KEY_LEN 16 +#define SSL_SESSION_TICKET_KEY_LEN 49 + static jweak sslTaskClassWeak = NULL; static jmethodID sslTaskDestroyMethod = NULL; static jfieldID sslTaskReturnValue = NULL; @@ -68,6 +79,9 @@ static jmethodID keylogCallbackMethod = NULL; static jweak sessionCallbackClassWeak = NULL; static jmethodID sessionCallbackMethod = NULL; +static jweak sessionTicketCallbackClassWeak = NULL; +static jmethodID sessionTicketCallbackMethod = NULL; + static jclass byteArrayClass = NULL; static jclass stringClass = NULL; @@ -79,6 +93,7 @@ static int keylogCallbackIdx = -1; static int sessionCallbackIdx = -1; static int sslPrivateKeyMethodIdx = -1; static int sslTaskIdx = -1; +static int sessionTicketCallbackIdx = -1; static int alpn_data_idx = -1; static int crypto_buffer_pool_idx = -1; @@ -962,7 +977,7 @@ int new_session_callback(SSL *ssl, SSL_SESSION *session) { return 0; } -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, jobject sessionCallback, jobject privateKeyMethod, jint verifyMode, jobjectArray subjectNames) { +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, jobject sessionCallback, jobject privateKeyMethod, jobject sessionTicketCallback, jint verifyMode, jobjectArray subjectNames) { jobject handshakeCompleteCallbackRef = NULL; jobject certificateCallbackRef = NULL; jobject verifyCallbackRef = NULL; @@ -970,6 +985,7 @@ static jlong netty_boringssl_SSLContext_new0(JNIEnv* env, jclass clazz, jboolean jobject keylogCallbackRef = NULL; jobject sessionCallbackRef = NULL; jobject privateKeyMethodRef = NULL; + jobject sessionTicketCallbackRef = NULL; if ((handshakeCompleteCallbackRef = (*env)->NewGlobalRef(env, handshakeCompleteCallback)) == NULL) { goto error; @@ -1006,6 +1022,9 @@ static jlong netty_boringssl_SSLContext_new0(JNIEnv* env, jclass clazz, jboolean goto error; } } + if ((sessionTicketCallbackRef = (*env)->NewGlobalRef(env, sessionTicketCallback)) == 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 @@ -1050,6 +1069,8 @@ static jlong netty_boringssl_SSLContext_new0(JNIEnv* env, jclass clazz, jboolean SSL_CTX_set_ex_data(ctx, sslPrivateKeyMethodIdx, privateKeyMethodRef); } + SSL_CTX_set_ex_data(ctx, sessionTicketCallbackIdx, sessionTicketCallbackRef); + // 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()); @@ -1095,10 +1116,12 @@ static jlong netty_boringssl_SSLContext_new0(JNIEnv* env, jclass clazz, jboolean if (sessionCallbackRef != NULL) { (*env)->DeleteGlobalRef(env, sessionCallbackRef); } - if (privateKeyMethodRef != NULL) { (*env)->DeleteGlobalRef(env, privateKeyMethodRef); } + if (sessionTicketCallbackRef != NULL) { + (*env)->DeleteGlobalRef(env, sessionTicketCallbackRef); + } return -1; } @@ -1141,10 +1164,15 @@ static void netty_boringssl_SSLContext_free(JNIEnv* env, jclass clazz, jlong ctx alpn_data* data = SSL_CTX_get_ex_data(ssl_ctx, alpn_data_idx); OPENSSL_free(data); + jobject sessionTicketCallbackRef = SSL_CTX_get_ex_data(ssl_ctx, sessionTicketCallbackIdx); + if (sessionCallbackRef != NULL) { + (*env)->DeleteGlobalRef(env, sessionTicketCallbackRef); + } + CRYPTO_BUFFER_POOL* pool = SSL_CTX_get_ex_data(ssl_ctx, crypto_buffer_pool_idx); SSL_CTX_free(ssl_ctx); - // The pool should be freed last in case that the SSL_CTX has a reference to things tha are stored in the + // The pool should be freed last in case that the SSL_CTX has a reference to things that are stored in the // pool itself. Otherwise we may see an assert error when trying to call CRYPTO_BUFFER_POOL_free. if (pool != NULL) { CRYPTO_BUFFER_POOL_free(pool); @@ -1282,6 +1310,87 @@ jstring netty_boringssl_ERR_last_error(JNIEnv* env, jclass clazz) { return (*env)->NewStringUTF(env, buf); } +static int netty_boringssl_tlsext_ticket_key_cb(SSL *s, unsigned char key_name[16], unsigned char *iv, EVP_CIPHER_CTX *ctx, HMAC_CTX *hctx, int enc) { + SSL_CTX *c = SSL_get_SSL_CTX(s); + if (c == NULL) { + return 0; + } + + jobject sessionTicketCallback = SSL_CTX_get_ex_data(c, sessionTicketCallbackIdx); + if (sessionTicketCallback == NULL) { + return 0; + } + JNIEnv* env = NULL; + if (quic_get_java_env(&env) != JNI_OK) { + return 0; + } + + if (enc) { /* create new session */ + jbyteArray key = (jbyteArray) (*env)->CallObjectMethod(env, sessionTicketCallback, sessionTicketCallbackMethod, NULL); + if (key != NULL) { + int keyLen = (*env)->GetArrayLength(env, key); + if (keyLen != SSL_SESSION_TICKET_KEY_LEN) { + return -1; + } + if (RAND_bytes(iv, EVP_MAX_IV_LENGTH) <= 0) { + return -1; /* insufficient random */ + } + + uint8_t* data = (uint8_t*) (*env)->GetByteArrayElements(env, key, 0); + + memcpy(key_name, data + SSL_SESSION_TICKET_KEY_NAME_OFFSET, SSL_SESSION_TICKET_KEY_NAME_LEN); + + HMAC_Init_ex(hctx, (void*) (data + SSL_SESSION_TICKET_KEY_HMAC_OFFSET), SSL_SESSION_TICKET_HMAC_KEY_LEN, EVP_sha256(), NULL); + + EVP_EncryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, (void*) (data + SSL_SESSION_TICKET_KEY_EVP_OFFSET), iv); + + (*env)->ReleaseByteArrayElements(env, key, (jbyte*) data, JNI_ABORT); + + return 1; + } + // No ticket configured + return 0; + } else { /* retrieve session */ + jbyteArray name = to_byte_array(env, (uint8_t*) key_name, 16); + jbyteArray key = (jbyteArray) (*env)->CallObjectMethod(env, sessionTicketCallback, sessionTicketCallbackMethod, name); + + if (key != NULL) { + int keyLen = (*env)->GetArrayLength(env, key); + if (keyLen != SSL_SESSION_TICKET_KEY_LEN) { + return -1; + } + + uint8_t* data = (uint8_t*) (*env)->GetByteArrayElements(env, key, 0); + // The first byte is used to encode if the key needs to be upgraded. + int is_current_key = *data != 0; + + HMAC_Init_ex(hctx, (void*) (data + SSL_SESSION_TICKET_KEY_HMAC_OFFSET), SSL_SESSION_TICKET_HMAC_KEY_LEN, EVP_sha256(), NULL); + + EVP_DecryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, (void*) (data + SSL_SESSION_TICKET_KEY_EVP_OFFSET), iv); + + (*env)->ReleaseByteArrayElements(env, key, (jbyte*) data, JNI_ABORT); + + if (!is_current_key) { + // The ticket matched a key in the list, and we want to upgrade it to the current + // key. + return 2; + } + // The ticket matched the current key. + return 1; + } + // No matching ticket. + return 0; + } +} + +void netty_boringssl_SSLContext_setSessionTicketKeys(JNIEnv* env, jclass clazz, jlong ctx, jboolean enableCallback) { + if (enableCallback == JNI_TRUE) { + SSL_CTX_set_tlsext_ticket_key_cb((SSL_CTX *) ctx, netty_boringssl_tlsext_ticket_key_cb); + } else { + SSL_CTX_set_tlsext_ticket_key_cb((SSL_CTX *) ctx, NULL); + } +} + // JNI Registered Methods End // JNI Method Registration Table Begin @@ -1311,11 +1420,12 @@ 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;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;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 }, { "SSLContext_set_early_data_enabled", "(JZ)V", (void *) netty_boringssl_SSLContext_set_early_data_enabled }, + { "SSLContext_setSessionTicketKeys", "(JZ)V", (void *) netty_boringssl_SSLContext_setSessionTicketKeys }, { "SSL_new0", "(JZLjava/lang/String;)J", (void *) netty_boringssl_SSL_new0 }, { "SSL_free", "(J)V", (void *) netty_boringssl_SSL_free }, { "SSL_getTask", "(J)Ljava/lang/Runnable;", (void *) netty_boringssl_SSL_getTask }, @@ -1344,6 +1454,7 @@ static void unload_all_classes(JNIEnv* env) { NETTY_JNI_UTIL_UNLOAD_CLASS_WEAK(env, servernameCallbackClassWeak); NETTY_JNI_UTIL_UNLOAD_CLASS_WEAK(env, keylogCallbackClassWeak); NETTY_JNI_UTIL_UNLOAD_CLASS_WEAK(env, sessionCallbackClassWeak); + NETTY_JNI_UTIL_UNLOAD_CLASS_WEAK(env, sessionTicketCallbackClassWeak); } // IMPORTANT: If you add any NETTY_JNI_UTIL_LOAD_CLASS or NETTY_JNI_UTIL_FIND_CLASS calls you also need to update @@ -1365,6 +1476,7 @@ jint netty_boringssl_JNI_OnLoad(JNIEnv* env, const char* packagePrefix) { jclass servernameCallbackClass = NULL; jclass keylogCallbackClass = NULL; jclass sessionCallbackClass = NULL; + jclass sessionTicketCallbackClass = NULL; // We must register the statically referenced methods first! if (netty_jni_util_register_natives(env, @@ -1465,6 +1577,11 @@ jint netty_boringssl_JNI_OnLoad(JNIEnv* env, const char* packagePrefix) { NETTY_JNI_UTIL_NEW_LOCAL_FROM_WEAK(env, sessionCallbackClass, sessionCallbackClassWeak, done); NETTY_JNI_UTIL_GET_METHOD(env, sessionCallbackClass, sessionCallbackMethod, "newSession", "(JJJ[BZ[B)V", done); + NETTY_JNI_UTIL_PREPEND(packagePrefix, "io/netty/incubator/codec/quic/BoringSSLSessionTicketCallback", name, done); + NETTY_JNI_UTIL_LOAD_CLASS_WEAK(env, sessionTicketCallbackClassWeak, name, done); + NETTY_JNI_UTIL_NEW_LOCAL_FROM_WEAK(env, sessionTicketCallbackClass, sessionTicketCallbackClassWeak, done); + NETTY_JNI_UTIL_GET_METHOD(env, sessionTicketCallbackClass, sessionTicketCallbackMethod, "findSessionTicket", "([B)[B", 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); @@ -1473,9 +1590,10 @@ jint netty_boringssl_JNI_OnLoad(JNIEnv* env, const char* packagePrefix) { sessionCallbackIdx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL); sslPrivateKeyMethodIdx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL); sslTaskIdx = SSL_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); + sessionTicketCallbackIdx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL); + ret = NETTY_JNI_UTIL_JNI_VERSION; done: if (ret == JNI_ERR) { @@ -1499,6 +1617,7 @@ jint netty_boringssl_JNI_OnLoad(JNIEnv* env, const char* packagePrefix) { NETTY_JNI_UTIL_DELETE_LOCAL(env, servernameCallbackClass); NETTY_JNI_UTIL_DELETE_LOCAL(env, keylogCallbackClass); NETTY_JNI_UTIL_DELETE_LOCAL(env, sessionCallbackClass); + NETTY_JNI_UTIL_DELETE_LOCAL(env, sessionTicketCallbackClass); return ret; } diff --git a/codec-native-quic/src/test/java/io/netty/incubator/codec/quic/QuicChannelConnectTest.java b/codec-native-quic/src/test/java/io/netty/incubator/codec/quic/QuicChannelConnectTest.java index 1c70030d6..62272ff3a 100644 --- a/codec-native-quic/src/test/java/io/netty/incubator/codec/quic/QuicChannelConnectTest.java +++ b/codec-native-quic/src/test/java/io/netty/incubator/codec/quic/QuicChannelConnectTest.java @@ -1277,13 +1277,38 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { } } + @ParameterizedTest + @MethodSource("newSslTaskExecutors") + public void testSessionTickets(Executor executor) throws Throwable { + testSessionReuse(executor, true); + } @ParameterizedTest @MethodSource("newSslTaskExecutors") @Timeout(5) public void testSessionReusedOnClientSide(Executor executor) throws Exception { + testSessionReuse(executor, false); + } + + private static void testSessionReuse(Executor executor, boolean ticketKey) throws Exception { + QuicSslContext sslServerCtx = QuicSslContextBuilder.forServer( + QuicTestUtils.SELF_SIGNED_CERTIFICATE.key(), null, + QuicTestUtils.SELF_SIGNED_CERTIFICATE.cert()) + .applicationProtocols(QuicTestUtils.PROTOS) + .build(); + QuicSslContext sslClientCtx = QuicSslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE).applicationProtocols(QuicTestUtils.PROTOS).build(); + + if (ticketKey) { + + SslSessionTicketKey key = new SslSessionTicketKey(new byte[SslSessionTicketKey.NAME_SIZE], + new byte[SslSessionTicketKey.HMAC_KEY_SIZE], new byte[SslSessionTicketKey.AES_KEY_SIZE]); + sslClientCtx.sessionContext().setTicketKeys(key); + sslServerCtx.sessionContext().setTicketKeys(key); + } CountDownLatch serverSslCompletionEventLatch = new CountDownLatch(2); - Channel server = QuicTestUtils.newServer(executor, + Channel server = QuicTestUtils.newServer(QuicTestUtils.newQuicServerBuilder(executor, sslServerCtx), + InsecureQuicTokenHandler.INSTANCE, new ChannelInboundHandlerAdapter() { @Override public boolean isSharable() { @@ -1294,12 +1319,12 @@ public boolean isSharable() { public void channelActive(ChannelHandlerContext ctx) { ((QuicChannel) ctx.channel()).createStream(QuicStreamType.BIDIRECTIONAL, new ChannelInboundHandlerAdapter() { - @Override - public void channelActive(ChannelHandlerContext ctx) { - ctx.writeAndFlush(ctx.alloc().directBuffer(10).writeZero(10)) - .addListener(f -> ctx.close()); - } - }); + @Override + public void channelActive(ChannelHandlerContext ctx) { + ctx.writeAndFlush(ctx.alloc().directBuffer(10).writeZero(10)) + .addListener(f -> ctx.close()); + } + }); ctx.fireChannelActive(); } @@ -1312,11 +1337,9 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc }, new ChannelInboundHandlerAdapter()); InetSocketAddress address = (InetSocketAddress) server.localAddress(); - QuicSslContext clientSslContext = QuicSslContextBuilder.forClient() - .trustManager(InsecureTrustManagerFactory.INSTANCE).applicationProtocols(QuicTestUtils.PROTOS).build(); Channel channel = QuicTestUtils.newClient(QuicTestUtils.newQuicClientBuilder(executor).sslEngineProvider(c -> - clientSslContext.newEngine(c.alloc(), "localhost", 9999))); + sslClientCtx.newEngine(c.alloc(), "localhost", 9999))); try { CountDownLatch clientSslCompletionEventLatch = new CountDownLatch(2);