diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 000000000..a3254f727 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,79 @@ + +variables: + wget: "wget --quiet --tries=0" + JAVA_HOME: /usr/lib/jvm/java-8-openjdk-amd64 + TERM: dumb # to stop verbose build output + +.apt-template: &apt-template +- export LC_ALL=C.UTF-8 +- export DEBIAN_FRONTEND=noninteractive +- echo Etc/UTC > /etc/timezone +- echo 'quiet "1";' + 'APT::Install-Recommends "0";' + 'APT::Install-Suggests "0";' + 'APT::Acquire::Retries "20";' + 'APT::Get::Assume-Yes "true";' + 'Dpkg::Use-Pty "0";' + > /etc/apt/apt.conf.d/99gitlab +- echo "deb http://deb.debian.org/debian stretch main" >> /etc/apt/sources.list +- apt-get update +- apt-get dist-upgrade +- apt-get install + build-essential + ca-certificates + cmake + git + ninja-build + openjdk-8-jdk-headless + +.artifacts-template: &artifacts-template + artifacts: + name: "${CI_PROJECT_PATH}_${CI_JOB_STAGE}_${CI_COMMIT_REF_NAME}_${CI_COMMIT_SHA}" + paths: + - build/ + - packages/*.* + - "*/build/reports/" + when: + always + expire_in: 1 week + +# based on https://github.com/google/conscrypt/blob/master/.travis.yml +build and test: + image: debian:buster-backports + <<: *artifacts-template + script: + - *apt-template + + - export EXITVALUE=0 + - function set_error() { + export EXITVALUE=1; + printf "\x1b[31mERROR `history|tail -2|head -1|cut -b 6-500`\x1b[0m\n"; + } + + - apt-get install python3-argcomplete python3-requests + - export ANDROID_SDK_HOME=/opt/android-sdk + - export ANDROID_HOME=/opt/android-sdk + - git clone --depth=1 https://gitlab.com/eighthave/sdkmanager.git + - ndkVersion=$(sed -En 's,.*[nN]dkVersion\s*=?\s*.([1-9][0-9]\.[0-9]\.[0-9]{7}).*,\1,p' android/build.gradle) + - ./sdkmanager/sdkmanager.py "tools;26.1.1" "ndk;$ndkVersion" + - export ANDROID_NDK_HOME=$ANDROID_SDK_HOME/ndks/$ndkVersion + + - apt-get install -t buster-backports golang-go # needs >=1.13 + - export BORINGSSL_HOME=$PWD/boringssl + - mkdir $BORINGSSL_HOME + - git clone --depth 1 https://boringssl.googlesource.com/boringssl $BORINGSSL_HOME + - mkdir $BORINGSSL_HOME/build64 && pushd $BORINGSSL_HOME/build64 + - cmake -DCMAKE_POSITION_INDEPENDENT_CODE=TRUE -DCMAKE_BUILD_TYPE=Release -DCMAKE_ASM_FLAGS=-Wa,--noexecstack + -GNinja .. + - ninja + - popd + + - yes | ./sdkmanager/sdkmanager.py --licenses || true + - ./sdkmanager/sdkmanager.py tools || set_error + + - ./gradlew build -PcheckErrorQueue || set_error + - ./gradlew check -PcheckErrorQueue || set_error + + - ./gradlew :conscrypt-android:build || set_error + - ./gradlew :conscrypt-android-platform:build || set_error + - exit $EXITVALUE diff --git a/common/src/jni/main/cpp/conscrypt/native_crypto.cc b/common/src/jni/main/cpp/conscrypt/native_crypto.cc index bdf6990e7..41856251e 100644 --- a/common/src/jni/main/cpp/conscrypt/native_crypto.cc +++ b/common/src/jni/main/cpp/conscrypt/native_crypto.cc @@ -127,6 +127,15 @@ static SSL_CIPHER* to_SSL_CIPHER(JNIEnv* env, jlong ssl_cipher_address, bool thr return ssl_cipher; } +static SSL_ECH_KEYS* to_SSL_ECH_KEYS(JNIEnv* env, jlong ssl_ech_keys_address, bool throwIfNull) { + SSL_ECH_KEYS* ssl_ech_keys = reinterpret_cast(static_cast(ssl_ech_keys_address)); + if ((ssl_ech_keys == nullptr) && throwIfNull) { + JNI_TRACE("ssl_ech_keys == null"); + conscrypt::jniutil::throwNullPointerException(env, "ssl_ech_keys == null"); + } + return ssl_ech_keys; +} + template static T* fromContextObject(JNIEnv* env, jobject contextObject) { if (contextObject == nullptr) { @@ -10390,7 +10399,7 @@ static void NativeCrypto_SSL_SESSION_up_ref(JNIEnv* env, jclass, jlong ssl_sessi SSL_SESSION* ssl_session = to_SSL_SESSION(env, ssl_session_address, true); JNI_TRACE("ssl_session=%p NativeCrypto_SSL_SESSION_up_ref", ssl_session); if (ssl_session == nullptr) { - return; + return; // TODO throw NPE } SSL_SESSION_up_ref(ssl_session); } @@ -11610,6 +11619,250 @@ static jlong NativeCrypto_SSL_get1_session(JNIEnv* env, jclass, jlong ssl_addres return reinterpret_cast(SSL_get1_session(ssl)); } +static void NativeCrypto_SSL_set_enable_ech_grease(JNIEnv* env, jclass, jlong ssl_address, + CONSCRYPT_UNUSED jobject ssl_holder, + jboolean enable) { + CHECK_ERROR_QUEUE_ON_RETURN; + SSL* ssl = to_SSL(env, ssl_address, true); + JNI_TRACE("ssl=%p NativeCrypto_SSL_set_enable_ech_grease(%d)", ssl, enable); + if (ssl == nullptr) { + return; + } + SSL_set_enable_ech_grease(ssl, enable ? 1 : 0); + JNI_TRACE("ssl=%p NativeCrypto_SSL_set_enable_ech_grease(%d) => success", ssl, enable); +} + +static jboolean NativeCrypto_SSL_set1_ech_config_list(JNIEnv* env, jclass, jlong ssl_address, + CONSCRYPT_UNUSED jobject ssl_holder, + jbyteArray configJavaBytes) { + CHECK_ERROR_QUEUE_ON_RETURN; + SSL* ssl = to_SSL(env, ssl_address, true); + JNI_TRACE("ssl=%p NativeCrypto_SSL_set1_ech_config_list(%p)", ssl, configJavaBytes); + if (ssl == nullptr) { + return JNI_FALSE; + } + ScopedByteArrayRO configBytes(env, configJavaBytes); + if (configBytes.get() == nullptr) { + JNI_TRACE("NativeCrypto_SSL_set1_ech_config_list => threw exception:" + " could not read config bytes"); + return JNI_FALSE; + } + // UNUSED? + // const uint8_t* bs = reinterpret_cast(configBytes.get()); + int ret = SSL_set1_ech_config_list(ssl, reinterpret_cast(configBytes.get()), + configBytes.size()); + JNI_TRACE("ssl=%p NativeCrypto_SSL_set1_ech_config_list(%p) => %d", ssl, configJavaBytes, ret); + return !!ret; +} + +static jstring NativeCrypto_SSL_get0_ech_name_override(JNIEnv* env, jclass, jlong ssl_address, + CONSCRYPT_UNUSED jobject ssl_holder) { + CHECK_ERROR_QUEUE_ON_RETURN; + SSL* ssl = to_SSL(env, ssl_address, true); + JNI_TRACE("ssl=%p NativeCrypto_SSL_get0_ech_name_override()", ssl); + if (ssl == nullptr) { + JNI_TRACE("ssl=%p NativeCrypto_SSL_get0_ech_name_override() => nullptr", ssl); + return nullptr; + } + const char* ech_name_override; + size_t ech_name_override_len; + SSL_get0_ech_name_override(ssl, &ech_name_override, &ech_name_override_len); + if (ech_name_override_len > 0) { + jstring name = env->NewStringUTF(ech_name_override); + return name; + } + return nullptr; +} + +static jbyteArray NativeCrypto_SSL_get0_ech_retry_configs(JNIEnv* env, jclass, jlong ssl_address, + CONSCRYPT_UNUSED jobject ssl_holder) { + CHECK_ERROR_QUEUE_ON_RETURN; + SSL* ssl = to_SSL(env, ssl_address, true); + JNI_TRACE("ssl=%p NativeCrypto_SSL_get0_ech_retry_configs()", ssl); + if (ssl == nullptr) { + return nullptr; + } + + const uint8_t *retry_configs; + size_t retry_configs_len; + SSL_get0_ech_retry_configs(ssl, &retry_configs, &retry_configs_len); + + jbyteArray result = env->NewByteArray(static_cast(retry_configs_len)); + if (result == nullptr) { + JNI_TRACE("ssl=%p NativeCrypto_SSL_get0_ech_retry_configs() => creating byte array failed", + ssl); + return nullptr; + } + env->SetByteArrayRegion(result, 0, static_cast(retry_configs_len), + (const jbyte*) retry_configs); + JNI_TRACE("ssl=%p NativeCrypto_SSL_get0_ech_retry_configs(%p) => %p", ssl, ssl, result); + return result; +} + +static jbyteArray NativeCrypto_SSL_marshal_ech_config(JNIEnv* env, jclass, short configId, + jbyteArray keyJavaBytes, jstring publicName) { + /* + CHECK_ERROR_QUEUE_ON_RETURN; + if (configId < 0) { + // TODO throw IllegalArgumentException + return nullptr; + } + JNI_TRACE("NativeCrypto_SSL_marshal_ech_config(keyJavaBytes=%p)", keyJavaBytes); + ScopedByteArrayRO keyBytes(env, keyJavaBytes); + if (keyBytes.get() == nullptr) { + JNI_TRACE("NativeCrypto_SSL_marshal_ech_config => threw exception:" + " could not read key bytes"); + return nullptr; + } + const uint8_t* key = reinterpret_cast(keyBytes.get()); + size_t keySize = keyBytes.size(); + bssl::UniquePtr keys(SSL_ECH_KEYS_new()); + SSL_ECH_KEYS_add(keys.get(), [SLASH,ASTERIX]is_retry_config=*1, ech_config, ech_config_len, key.get()); + bssl::ScopedEVP_HPKE_KEY hpke_key; + JNI_TRACE("NativeCrypto_SSL_marshal_ech_config(keyJavaBytes=%p) =>", keyJavaBytes); + */ + return nullptr; // TODO return something real +} + +/* + * public static native int SSL_ECH_KEYS_new(); + */ +static jlong NativeCrypto_SSL_ECH_KEYS_new(JNIEnv* env, jclass) { + CHECK_ERROR_QUEUE_ON_RETURN; + bssl::UniquePtr sslEchKeys(SSL_ECH_KEYS_new()); + if (sslEchKeys.get() == nullptr) { + conscrypt::jniutil::throwExceptionFromBoringSSLError(env, "SSL_ECH_KEYS_new"); + return 0; + } + + JNI_TRACE("NativeCrypto_SSL_ECH_KEYS_new => %p", sslEchKeys.get()); + return (jlong)sslEchKeys.release(); +} + +/** + * public static native void SSL_ECH_KEYS_up_ref(long sslEchKeys) + */ +static void NativeCrypto_SSL_ECH_KEYS_up_ref(JNIEnv* env, jclass, jlong ssl_ech_keys_address) { + CHECK_ERROR_QUEUE_ON_RETURN; + SSL_ECH_KEYS* ssl_ech_keys = to_SSL_ECH_KEYS(env, ssl_ech_keys_address, true); + JNI_TRACE("ssl_ech_keys=%p NativeCrypto_SSL_ECH_KEYS_up_ref", ssl_ech_keys); + if (ssl_ech_keys == nullptr) { + return; + } + SSL_ECH_KEYS_up_ref(ssl_ech_keys); +} + +/** + * public static native void SSL_ECH_KEYS_free(long sslEchKeys) + */ +static void NativeCrypto_SSL_ECH_KEYS_free(JNIEnv* env, jclass, jlong ssl_ech_keys_address) { + CHECK_ERROR_QUEUE_ON_RETURN; + SSL_ECH_KEYS* ssl_ech_keys = to_SSL_ECH_KEYS(env, ssl_ech_keys_address, true); + JNI_TRACE("ssl_ech_keys=%p NativeCrypto_SSL_ECH_KEYS_free", ssl_ech_keys); + if (ssl_ech_keys == nullptr) { + return; + } + SSL_ECH_KEYS_free(ssl_ech_keys); +} + +/** + * public static native void SSL_ECH_KEYS_marshal_retry_configs(long sslEchKeys) + */ +// TODO receive retry configs as byte[] and convert to SSH_ECH_KEYS +static jbyteArray NativeCrypto_SSL_ECH_KEYS_marshal_retry_configs(JNIEnv* env, jclass, + jbyteArray keysJavaBytes) { + /* + CHECK_ERROR_QUEUE_ON_RETURN; + JNI_TRACE("keys=%p NativeCrypto_SSL_ECH_KEYS_marshal_retry_configs", keysJavaBytes); + if (keysJavaBytes == nullptr) { + return nullptr; + } + bssl::UniquePtr keys(SSL_ECH_KEYS_new()); + if (!keys || + !EVP_HPKE_KEY_init(hpke_key.get(), EVP_hpke_x25519_hkdf_sha256(), key, keySize) + ) { + JNI_TRACE("keys=%p NativeCrypto_SSL_ECH_KEYS_marshal_retry_configs => null", keysJavaBytes); + return nullptr; + } + if (!keys || + !EVP_HPKE_KEY_init(hpke_key.get(), EVP_hpke_x25519_hkdf_sha256(), key, keySize) || + !SSL_ECH_KEYS_add(keys.get(), [SLASH,ASTERIX]is_retry_config=*1, config, configSize, hpke_key.get()) || + JNI_TRACE("NativeCrypto_SSL_ECH_KEYS_marshal_retry_configs: Error setting private key\n"); + return nullptr; + } + + uint8_t *ech_config_list; + size_t ech_config_list_len; + if (!SSL_ECH_KEYS_marshal_retry_configs(keys, &ech_config_list, &ech_config_list_len)) { + JNI_TRACE("keys=%p NativeCrypto_SSL_ECH_KEYS_marshal_retry_configs => null", keys); + return nullptr; + } + bssl::UniquePtr free_ech_config_list(ech_config_list); // is this OPENSSL_free(ech_config_list)? + jbyteArray result = env->NewByteArray(static_cast(ech_config_list_len)); + if (result != nullptr) { + env->SetByteArrayRegion(result, 0, static_cast(ech_config_list_len), + (const jbyte*)ech_config_list); + } + return result; +*/ + return nullptr; +} + +/** + * public static native long SSL_ech_accepted(long ssl); + */ +static jboolean NativeCrypto_SSL_ech_accepted(JNIEnv* env, jclass, jlong ssl_address, + CONSCRYPT_UNUSED jobject ssl_holder) { + JNI_TRACE("NativeCrypto_SSL_ech_accepted"); + CHECK_ERROR_QUEUE_ON_RETURN; + SSL* ssl = to_SSL(env, ssl_address, true); + JNI_TRACE("ssl=%p NativeCrypto_SSL_ech_accepted", ssl); + if (ssl == nullptr) { + return 0; + } + jboolean accepted = SSL_ech_accepted(ssl); + JNI_TRACE("ssl=%p NativeCrypto_SSL_ech_accepted => %d", ssl, accepted); + return accepted; +} + +static jboolean NativeCrypto_SSL_CTX_ech_enable_server(JNIEnv* env, jclass, jlong ssl_ctx_address, + CONSCRYPT_UNUSED jobject holder, + jbyteArray keyJavaBytes, + jbyteArray configJavaBytes) { + CHECK_ERROR_QUEUE_ON_RETURN; + SSL_CTX* ssl_ctx = to_SSL_CTX(env, ssl_ctx_address, true); + JNI_TRACE("NativeCrypto_SSL_CTX_ech_enable_server(keyJavaBytes=%p, configJavaBytes=%p)", + keyJavaBytes, configJavaBytes); + ScopedByteArrayRO keyBytes(env, keyJavaBytes); + if (keyBytes.get() == nullptr) { + JNI_TRACE("NativeCrypto_SSL_CTX_ech_enable_server => threw exception: " + "could not read key bytes"); + return JNI_FALSE; + } + ScopedByteArrayRO configBytes(env, configJavaBytes); + if (configBytes.get() == nullptr) { + JNI_TRACE("NativeCrypto_SSL_CTX_ech_enable_server => threw exception: " + "could not read config bytes"); + return JNI_FALSE; + } + const uint8_t* ech_key = reinterpret_cast(keyBytes.get()); + size_t ech_key_size = keyBytes.size(); + const uint8_t* ech_config = reinterpret_cast(configBytes.get()); + size_t ech_config_size = configBytes.size(); + bssl::UniquePtr keys(SSL_ECH_KEYS_new()); + bssl::ScopedEVP_HPKE_KEY key; + if (!keys || + !EVP_HPKE_KEY_init(key.get(), EVP_hpke_x25519_hkdf_sha256(), ech_key, ech_key_size) || + !SSL_ECH_KEYS_add(keys.get(), /*is_retry_config=*/1, + ech_config, ech_config_size, key.get()) || + !SSL_CTX_set1_ech_keys(ssl_ctx, keys.get())) { + JNI_TRACE("NativeCrypto_SSL_CTX_ech_enable_server: " + "Error setting server's ECHConfig and private key\n"); + return JNI_FALSE; + } + return JNI_TRUE; +} + // TESTING METHODS END #define CONSCRYPT_NATIVE_METHOD(functionName, signature) \ @@ -11960,6 +12213,22 @@ static JNINativeMethod sNativeCryptoMethods[] = { CONSCRYPT_NATIVE_METHOD(Scrypt_generate_key, "([B[BIIII)[B"), CONSCRYPT_NATIVE_METHOD(SSL_CTX_set_spake_credential, "([B[B[B[BZIJ" REF_SSL_CTX ")V"), + // For ECH + CONSCRYPT_NATIVE_METHOD(SSL_set_enable_ech_grease, "(J" REF_SSL "Z)V"), + CONSCRYPT_NATIVE_METHOD(SSL_set1_ech_config_list, "(J" REF_SSL "[B)Z"), + CONSCRYPT_NATIVE_METHOD(SSL_get0_ech_name_override, "(J" REF_SSL ")Ljava/lang/String;"), + CONSCRYPT_NATIVE_METHOD(SSL_get0_ech_retry_configs, "(J" REF_SSL ")[B"), + CONSCRYPT_NATIVE_METHOD(SSL_marshal_ech_config, "(S[BLjava/lang/String;)[B"), + CONSCRYPT_NATIVE_METHOD(SSL_ECH_KEYS_new, "()J"), + CONSCRYPT_NATIVE_METHOD(SSL_ECH_KEYS_up_ref, "(J)V"), + CONSCRYPT_NATIVE_METHOD(SSL_ECH_KEYS_free, "(J)V"), + // CONSCRYPT_NATIVE_METHOD(SSL_ECH_KEYS_add(SSL_ECH_KEYS *keys, int is_retry_config, const uint8_t *ech_config, + // CONSCRYPT_NATIVE_METHOD(SSL_ECH_KEYS_has_duplicate_config_id(const SSL_ECH_KEYS *keys), + CONSCRYPT_NATIVE_METHOD(SSL_ECH_KEYS_marshal_retry_configs, "([B)[B"), + //CONSCRYPT_NATIVE_METHOD(SSL_CTX_set1_ech_keys, "(J" REF_SSL_CTX "[B)Z"), + CONSCRYPT_NATIVE_METHOD(SSL_ech_accepted, "(J" REF_SSL ")Z"), + CONSCRYPT_NATIVE_METHOD(SSL_CTX_ech_enable_server, "(J" REF_SSL_CTX "[B[B)Z"), + // Used for testing only. CONSCRYPT_NATIVE_METHOD(BIO_read, "(J[B)I"), CONSCRYPT_NATIVE_METHOD(BIO_write, "(J[BII)V"), diff --git a/common/src/main/java/org/conscrypt/AbstractConscryptEngine.java b/common/src/main/java/org/conscrypt/AbstractConscryptEngine.java index 0f1354a93..241947b18 100644 --- a/common/src/main/java/org/conscrypt/AbstractConscryptEngine.java +++ b/common/src/main/java/org/conscrypt/AbstractConscryptEngine.java @@ -92,6 +92,16 @@ abstract class AbstractConscryptEngine extends SSLEngine { @Override public abstract int getPeerPort(); + public abstract void setEchParameters(EchParameters parameters); + + public abstract EchParameters getEchParameters(); + + public abstract String getEchNameOverride(); + + public abstract byte[] getEchRetryConfigList(); + + public abstract boolean echAccepted(); + /* @Override */ @SuppressWarnings("MissingOverride") // For compilation with Java 6. public final SSLSession getHandshakeSession() { diff --git a/common/src/main/java/org/conscrypt/AbstractConscryptSocket.java b/common/src/main/java/org/conscrypt/AbstractConscryptSocket.java index b177e9d61..76cc04e8c 100644 --- a/common/src/main/java/org/conscrypt/AbstractConscryptSocket.java +++ b/common/src/main/java/org/conscrypt/AbstractConscryptSocket.java @@ -738,4 +738,14 @@ private boolean isDelegating() { */ abstract byte[] exportKeyingMaterial(String label, byte[] context, int length) throws SSLException; + + public abstract void setEchParameters(EchParameters parameters); + + public abstract EchParameters getEchParameters(); + + public abstract String getEchNameOverride(); + + public abstract byte[] getEchRetryConfigList(); + + public abstract boolean echAccepted(); } diff --git a/common/src/main/java/org/conscrypt/Conscrypt.java b/common/src/main/java/org/conscrypt/Conscrypt.java index c88292637..b86718300 100644 --- a/common/src/main/java/org/conscrypt/Conscrypt.java +++ b/common/src/main/java/org/conscrypt/Conscrypt.java @@ -15,6 +15,7 @@ */ package org.conscrypt; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; @@ -37,6 +38,8 @@ import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; + +import org.conscrypt.com.android.net.module.util.DnsPacket; import org.conscrypt.io.IoUtils; /** @@ -509,6 +512,57 @@ public static byte[] exportKeyingMaterial(SSLSocket socket, String label, byte[] return toConscrypt(socket).exportKeyingMaterial(label, context, length); } + /** + * Casts a socket to a Conscrypt socket if possible, and sets the parameters + * required to configure ECH for that socket. + * Throws an IllegalArgumentException if the socket is not a ConscryptSocket + * @param socket the socket (instance of ConscryptSocket) + * @param parameters parameters required to configure ECH + */ + public static void setEchParameters(SSLSocket socket, EchParameters parameters) { + toConscrypt(socket).setEchParameters(parameters); + } + + /** + * Casts a socket to a Conscrypt socket if possible, and returns the parameters + * used to configure ECH for that socket. + * Throws an IllegalArgumentException if the socket is not a ConscryptSocket + * @param socket the socket (instance of ConscryptSocket) + */ + public static EchParameters getEchParameters(SSLSocket socket) { + return toConscrypt(socket).getEchParameters(); + } + + /** + * Casts a socket to a Conscrypt socket if possible, and returns the string used + * to replace the hostname as the public name. + * Throws an IllegalArgumentException if the socket is not a ConscryptSocket + * @param socket the socket (instance of ConscryptSocket) + */ + public static String getEchNameOverride(SSLSocket socket) { + return toConscrypt(socket).getEchNameOverride(); + } + + /** + * Casts a socket to a Conscrypt socket if possible, and returns the ECH config + * cached after a failed handshake. + * Throws an IllegalArgumentException if the socket is not a ConscryptSocket + * @param socket the socket (instance of ConscryptSocket) + */ + public static byte[] getEchRetryConfigList(SSLSocket socket) { + return toConscrypt(socket).getEchRetryConfigList(); + } + + /** + * Casts a socket to a Conscrypt socket if possible, and returns whether the native + * SSL/crypto implementation detects that the connection supports ECH. + * Throws an IllegalArgumentException if the socket is not a ConscryptSocket + * @param socket the socket (instance of ConscryptSocket) + */ + public static boolean echAccepted(SSLSocket socket) { + return toConscrypt(socket).echAccepted(); + } + /** * Indicates whether the given {@link SSLEngine} was created by this distribution of Conscrypt. */ @@ -753,6 +807,152 @@ public static byte[] exportKeyingMaterial(SSLEngine engine, String label, byte[] return toConscrypt(engine).exportKeyingMaterial(label, context, length); } + /** + * This method enables or disables Encrypted Client Hello (ECH) GREASE. + * + * @param engine the engine + * @param enabled Whether to enable TLSv1.3 ECH GREASE + * + * @see TLS Encrypted Client Hello 6.2. GREASE ECH + */ + + public static void setEchParameters(SSLEngine engine, EchParameters parameters) { + toConscrypt(engine).setEchParameters(parameters); + } + + public static EchParameters getEchParameters(SSLEngine engine) { + return toConscrypt(engine).getEchParameters(); + } + + public static String getEchNameOverride(SSLEngine engine) { + return toConscrypt(engine).getEchNameOverride(); + } + + public static byte[] getEchRetryConfigList(SSLEngine engine) { + return toConscrypt(engine).getEchRetryConfigList(); + } + + public static boolean echAccepted(SSLEngine engine) { + return toConscrypt(engine).echAccepted(); + } + + + /** + * < Max RR value size, as given to API + */ + private static int ECH_MAX_RRVALUE_LEN = 2000; + /** + * < Max for an ECHConfig extension + */ + private static int ECH_MAX_ECHCONFIGEXT_LEN = 100; + /** + * < just for a sanity check + */ + private static int ECH_MIN_ECHCONFIG_LEN = 32; + /** + * < for a sanity check + */ + private static int ECH_MAX_ECHCONFIG_LEN = ECH_MAX_RRVALUE_LEN; + + /** + * One or more catenated binary ECHConfigs + */ + public static int ECH_FMT_BIN = 1; + /** + * < presentation form of HTTPSSVC + */ + public static int ECH_FMT_HTTPSSVC = 4; + /** + * the wire-format code for ECH within an SVCB or HTTPS RData + */ + private static int ECH_PCODE_ECH = 0x0005; + + /** + * Decode SVCB/HTTPS RR value provided as binary or ascii-hex. + *

+ * The rrval may be the catenation of multiple encoded ECHConfigs. + * We internally try decode and handle those and (later) + * use whichever is relevant/best. + *

+ * Note that we "succeed" even if there is no ECHConfigs in the input - some + * callers might download the RR from DNS and pass it here without looking + * inside, and there are valid uses of such RRs. The caller can check though + * using the num_echs output. + * + * @param rrval is the binary encoded RData + * @return is 1 for success, error otherwise + */ + public static byte[] getEchConfigListFromDnsRR(byte[] rrval) { + int rv = 0; + int binlen = 0; /* the RData */ + byte[] binbuf = null; + int pos = 0; + int remaining = rrval.length;; + String dnsname = null; + int plen = 0; + boolean done = false; + + /* + * skip 2 octet priority and TargetName as those are the + * application's responsibility, not the library's + */ + if (remaining <= 2) { + return null; + } + pos += 2; + remaining -= 2; + pos++; + int clen = DnsPacket.byteToUnsignedInt(rrval[pos]); + ByteArrayOutputStream thename = new ByteArrayOutputStream(); + if (clen == 0) { + // special case - return "." as name + thename.write('.'); + rv = 1; + } + while (clen != 0) { + if (clen > remaining) { + rv = 1; + break; + } + for (int i =pos; i < clen; i++) { + thename.write(DnsPacket.byteToUnsignedInt(rrval[pos + i])); + } + thename.write('.'); + pos += clen; + remaining -= clen + 1; + clen = DnsPacket.byteToUnsignedInt(rrval[pos]); + } + if (rv != 1) { + return null; + } + + int echStart = 0; + while (!done && remaining >= 4) { + int pcode = (rrval[pos] << 8) + rrval[pos + 1]; + pos += 2; + plen = (rrval[pos] << 8) + rrval[pos + 1]; + pos += 2; + remaining -= 4; + if (pcode == ECH_PCODE_ECH) { + echStart = pos; + done = true; + } + if (plen != 0 && plen <= remaining) { + pos += plen; + remaining -= plen; + } + } + if (!done) { + return null; + } + if (plen <=0) { + return null; + } + byte[] ret = new byte[plen]; + System.arraycopy(rrval, echStart, ret, 0, plen); + return ret; + } + /** * Indicates whether the given {@link TrustManager} was created by this distribution of * Conscrypt. diff --git a/common/src/main/java/org/conscrypt/ConscryptEngine.java b/common/src/main/java/org/conscrypt/ConscryptEngine.java index b48b219a0..20d338f04 100644 --- a/common/src/main/java/org/conscrypt/ConscryptEngine.java +++ b/common/src/main/java/org/conscrypt/ConscryptEngine.java @@ -132,6 +132,8 @@ final class ConscryptEngine extends AbstractConscryptEngine implements NativeCry private int state = STATE_NEW; private boolean handshakeFinished; + private byte[] echRetryConfigList; + /** * Wrapper around the underlying SSL object. */ @@ -395,6 +397,33 @@ public int getPeerPort() { return peerInfoProvider.getPort(); } + public void setEchParameters(EchParameters parameters) { + sslParameters.setEchParameters(parameters); + } + + public EchParameters getEchParameters() { + return sslParameters.getEchParameters(); + } + + @Override + public String getEchNameOverride() { + return ssl.getEchNameOverride(); + } + + @Override + public byte[] getEchRetryConfigList() { + return echRetryConfigList; + } + + private void cacheEchRetryConfigList() { + this.echRetryConfigList = ssl.getEchRetryConfigList(); + } + + @Override + public boolean echAccepted() { + return ssl.echAccepted(); + } + @Override public void beginHandshake() throws SSLException { synchronized (ssl) { @@ -911,6 +940,10 @@ SSLEngineResult unwrap(final ByteBuffer[] srcs, int srcsOffset, final int srcsLe } catch (IOException e) { // Shut down the SSL and rethrow the exception. Users will need to drain any alerts // from the SSL before closing. + if (!handshakeFinished && e.getMessage().contains(":ECH_REJECTED ")) { + // TODO this should probably be implemented in boringssl + cacheEchRetryConfigList(); + } closeAll(); throw convertException(e); } diff --git a/common/src/main/java/org/conscrypt/ConscryptEngineSocket.java b/common/src/main/java/org/conscrypt/ConscryptEngineSocket.java index c2db73e59..c791eeb99 100644 --- a/common/src/main/java/org/conscrypt/ConscryptEngineSocket.java +++ b/common/src/main/java/org/conscrypt/ConscryptEngineSocket.java @@ -473,6 +473,31 @@ byte[] exportKeyingMaterial(String label, byte[] context, int length) throws SSL return engine.exportKeyingMaterial(label, context, length); } + @Override + public void setEchParameters(EchParameters parameters) { + engine.setEchParameters(parameters); + } + + @Override + public EchParameters getEchParameters() { + return engine.getEchParameters(); + } + + @Override + public String getEchNameOverride() { + return engine.getEchNameOverride(); + } + + @Override + public byte[] getEchRetryConfigList() { + return engine.getEchRetryConfigList(); + } + + @Override + public boolean echAccepted() { + return engine.echAccepted(); + } + @Override public final boolean getUseClientMode() { return engine.getUseClientMode(); diff --git a/common/src/main/java/org/conscrypt/ConscryptFileDescriptorSocket.java b/common/src/main/java/org/conscrypt/ConscryptFileDescriptorSocket.java index 1f7940ecf..e2b200d15 100644 --- a/common/src/main/java/org/conscrypt/ConscryptFileDescriptorSocket.java +++ b/common/src/main/java/org/conscrypt/ConscryptFileDescriptorSocket.java @@ -895,6 +895,31 @@ byte[] exportKeyingMaterial(String label, byte[] context, int length) throws SSL return ssl.exportKeyingMaterial(label, context, length); } + @Override + public void setEchParameters(EchParameters parameters) { + sslParameters.setEchParameters(parameters); + } + + @Override + public EchParameters getEchParameters() { + return sslParameters.getEchParameters(); + } + + @Override + public String getEchNameOverride() { + return ssl.getEchNameOverride(); + } + + @Override + public byte[] getEchRetryConfigList() { + return ssl.getEchRetryConfigList(); + } + + @Override + public boolean echAccepted() { + return ssl.echAccepted(); + } + @Override public final boolean getUseClientMode() { return sslParameters.getUseClientMode(); diff --git a/common/src/main/java/org/conscrypt/EchParameters.java b/common/src/main/java/org/conscrypt/EchParameters.java new file mode 100644 index 000000000..9c581569d --- /dev/null +++ b/common/src/main/java/org/conscrypt/EchParameters.java @@ -0,0 +1,29 @@ +package org.conscrypt; + +public class EchParameters { + + public boolean useEchGrease; + + public byte[] configList; + + public EchParameters() { + this.useEchGrease = false; + this.configList = null; + } + + public EchParameters(boolean useEchGrease) { + this.useEchGrease = useEchGrease; + this.configList = null; + } + + public EchParameters(byte[] configList) { + this.useEchGrease = false; + this.configList = configList; + } + + public EchParameters(boolean useEchGrease, byte[] configList) { + this.useEchGrease = useEchGrease; + this.configList = configList; + } + +} diff --git a/common/src/main/java/org/conscrypt/EchRejectedException.java b/common/src/main/java/org/conscrypt/EchRejectedException.java new file mode 100644 index 000000000..15b19431f --- /dev/null +++ b/common/src/main/java/org/conscrypt/EchRejectedException.java @@ -0,0 +1,18 @@ +package org.conscrypt; + +import javax.net.ssl.SSLHandshakeException; + +/** + * The server rejected the ECH Config List, and might have supplied an ECH + * Retry Config. + * + * @see NativeCrypto#SSL_get0_ech_retry_configs(long, NativeSsl) + */ +public class EchRejectedException extends SSLHandshakeException { + private static final long serialVersionUID = 98723498273473923L; + + EchRejectedException(String message) { + super(message); + } +} + diff --git a/common/src/main/java/org/conscrypt/Java8EngineWrapper.java b/common/src/main/java/org/conscrypt/Java8EngineWrapper.java index 5cf135d4f..b36d7d5dc 100644 --- a/common/src/main/java/org/conscrypt/Java8EngineWrapper.java +++ b/common/src/main/java/org/conscrypt/Java8EngineWrapper.java @@ -116,6 +116,29 @@ public int getPeerPort() { return delegate.getPeerPort(); } + public void setEchParameters(EchParameters parameters) { + delegate.setEchParameters(parameters); + } + + public EchParameters getEchParameters() { + return delegate.getEchParameters(); + } + + @Override + public String getEchNameOverride() { + return delegate.getEchNameOverride(); + } + + @Override + public byte[] getEchRetryConfigList() { + return delegate.getEchRetryConfigList(); + } + + @Override + public boolean echAccepted() { + return delegate.echAccepted(); + } + @Override public void beginHandshake() throws SSLException { delegate.beginHandshake(); diff --git a/common/src/main/java/org/conscrypt/NativeCrypto.java b/common/src/main/java/org/conscrypt/NativeCrypto.java index cec03cf95..013bc2b8a 100644 --- a/common/src/main/java/org/conscrypt/NativeCrypto.java +++ b/common/src/main/java/org/conscrypt/NativeCrypto.java @@ -1688,6 +1688,38 @@ static native byte[] Scrypt_generate_key( /** Return {@code true} if BoringSSL has been built in FIPS mode. */ @CriticalNative static native boolean usesBoringSsl_FIPS_mode(); +<<<<<<< HEAD + /* Encrypted Client Hello */ + + static native void SSL_set_enable_ech_grease(long ssl, NativeSsl ssl_holder, boolean enable); + + static native boolean SSL_set1_ech_config_list(long ssl, NativeSsl ssl_holder, byte[] echConfig); + + static native String SSL_get0_ech_name_override(long ssl, NativeSsl ssl_holder); + + static native byte[] SSL_get0_ech_retry_configs(long ssl, NativeSsl ssl_holder); + + static native byte[] SSL_marshal_ech_config(short configId, byte[] key, String publicName); + + static native long SSL_ECH_KEYS_new(); + + static native void SSL_ECH_KEYS_up_ref(long sslEchKeys); + + static native void SSL_ECH_KEYS_free(long sslEchKeys); + + //static native int SSL_ECH_KEYS_add(sslEchKeys, int is_retry_config, const uint8_t *ech_config, + + //static native int SSL_ECH_KEYS_has_duplicate_config_id(long sslEchKeys); + + static native byte[] SSL_ECH_KEYS_marshal_retry_configs(byte[] key); + + //static native int SSL_CTX_set1_ech_keys(long sslCtx, AbstractSessionContext holder, long sslEchKeys); + + static native boolean SSL_ech_accepted(long ssl, NativeSsl ssl_holder); + + static native boolean SSL_CTX_ech_enable_server(long sslCtx, AbstractSessionContext holder, + byte[] key, byte[] config); + /** Used for testing only. */ @FastNative static native int BIO_read(long bioRef, byte[] buffer) throws IOException; diff --git a/common/src/main/java/org/conscrypt/NativeSsl.java b/common/src/main/java/org/conscrypt/NativeSsl.java index 406c68f2c..7ca959246 100644 --- a/common/src/main/java/org/conscrypt/NativeSsl.java +++ b/common/src/main/java/org/conscrypt/NativeSsl.java @@ -274,6 +274,18 @@ byte[] getTlsChannelId() throws SSLException { return NativeCrypto.SSL_get_tls_channel_id(ssl, this); } + String getEchNameOverride() { + return NativeCrypto.SSL_get0_ech_name_override(ssl, this); + } + + byte[] getEchRetryConfigList() { + return NativeCrypto.SSL_get0_ech_retry_configs(ssl, this); + } + + boolean echAccepted() { + return NativeCrypto.SSL_ech_accepted(ssl, this); + } + void initialize(String hostname, OpenSSLKey channelIdPrivateKey) throws IOException { boolean enableSessionCreation = parameters.getEnableSessionCreation(); if (!enableSessionCreation) { @@ -293,6 +305,11 @@ void initialize(String hostname, OpenSSLKey channelIdPrivateKey) throws IOExcept if (parameters.isCTVerificationEnabled(hostname)) { NativeCrypto.SSL_enable_signed_cert_timestamps(ssl, this); } + NativeCrypto.SSL_set_enable_ech_grease(ssl, this, parameters.getEchParameters().useEchGrease); + if (parameters.getEchParameters().configList != null + && !NativeCrypto.SSL_set1_ech_config_list(ssl, this, parameters.getEchParameters().configList)) { + throw new SSLHandshakeException("Error setting ECHConfigList"); + } } else { NativeCrypto.SSL_set_accept_state(ssl, this); diff --git a/common/src/main/java/org/conscrypt/SSLParametersImpl.java b/common/src/main/java/org/conscrypt/SSLParametersImpl.java index f2056f2bd..f51e6b174 100644 --- a/common/src/main/java/org/conscrypt/SSLParametersImpl.java +++ b/common/src/main/java/org/conscrypt/SSLParametersImpl.java @@ -109,6 +109,8 @@ final class SSLParametersImpl implements Cloneable { boolean useSessionTickets; private Boolean useSni; + private EchParameters echParameters; + /** * Whether the TLS Channel ID extension is enabled. This field is * server-side only. @@ -235,6 +237,7 @@ private SSLParametersImpl(ClientSessionContext clientSessionContext, this.useSessionTickets = sslParams.useSessionTickets; this.useSni = sslParams.useSni; this.channelIdEnabled = sslParams.channelIdEnabled; + this.echParameters = sslParams.echParameters; } /** @@ -474,6 +477,17 @@ boolean getUseSni() { return useSni != null ? useSni : isSniEnabledByDefault(); } + /* + * Includes parameters for supporting ECH with this SSL connection + */ + void setEchParameters(EchParameters parameters) { + this.echParameters = parameters; + } + + EchParameters getEchParameters() { + return echParameters; + } + /* * For testing only. */ diff --git a/common/src/main/java/org/conscrypt/SSLUtils.java b/common/src/main/java/org/conscrypt/SSLUtils.java index 39eb05a42..96109e594 100644 --- a/common/src/main/java/org/conscrypt/SSLUtils.java +++ b/common/src/main/java/org/conscrypt/SSLUtils.java @@ -351,6 +351,10 @@ static SSLHandshakeException toSSLHandshakeException(Throwable e) { if (e instanceof SSLHandshakeException) { return (SSLHandshakeException) e; } + if (e.getMessage().contains(":ECH_REJECTED ")) { + // TODO this should probably be implemented in boringssl + return (SSLHandshakeException) new EchRejectedException(e.getMessage()).initCause(e); + } return (SSLHandshakeException) new SSLHandshakeException(e.getMessage()).initCause(e); } diff --git a/common/src/main/java/org/conscrypt/com/android/net/module/util/DnsPacket.java b/common/src/main/java/org/conscrypt/com/android/net/module/util/DnsPacket.java new file mode 100644 index 000000000..1dc37c5c9 --- /dev/null +++ b/common/src/main/java/org/conscrypt/com/android/net/module/util/DnsPacket.java @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed 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 org.conscrypt.com.android.net.module.util; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.text.DecimalFormat; +import java.text.FieldPosition; +import java.util.ArrayList; +import java.util.List; + +/** + * @see original source + */ +public abstract class DnsPacket { + /** + * Thrown when parsing packet failed. + */ + public static class ParseException extends RuntimeException { + public String reason; + public ParseException(String reason) { + super(reason); + this.reason = reason; + } + + public ParseException(String reason, Throwable cause) { + super(reason, cause); + this.reason = reason; + } + } + + /** + * DNS header for DNS protocol based on RFC 1035. + */ + public class DnsHeader { + private static final String TAG = "DnsHeader"; + public final int id; + public final int flags; + public final int rcode; + private final int[] mRecordCount; + + /** + * Create a new DnsHeader from a positioned ByteBuffer. + * + * The ByteBuffer must be in network byte order (which is the default). + * Reads the passed ByteBuffer from its current position and decodes a DNS header. + * When this constructor returns, the reading position of the ByteBuffer has been + * advanced to the end of the DNS header record. + * This is meant to chain with other methods reading a DNS response in sequence. + */ + DnsHeader(ByteBuffer buf) throws BufferUnderflowException { + id = shortToUnsignedInt(buf.getShort()); + flags = shortToUnsignedInt(buf.getShort()); + rcode = flags & 0xF; + mRecordCount = new int[NUM_SECTIONS]; + for (int i = 0; i < NUM_SECTIONS; ++i) { + mRecordCount[i] = shortToUnsignedInt(buf.getShort()); + } + } + + /** + * Get record count by type. + */ + public int getRecordCount(int type) { + return mRecordCount[type]; + } + } + + /** + * Superclass for DNS questions and DNS resource records. + * + * DNS questions (No TTL/RDATA) + * DNS resource records (With TTL/RDATA) + */ + public class DnsRecord { + private static final int MAXNAMESIZE = 255; + private static final int MAXLABELSIZE = 63; + private static final int MAXLABELCOUNT = 128; + public static final int NAME_NORMAL = 0; + public static final int NAME_COMPRESSION = 0xC0; + private final DecimalFormat mByteFormat = new DecimalFormat(); + private final FieldPosition mPos = new FieldPosition(0); + + private static final String TAG = "DnsRecord"; + + public final String dName; + public final int nsType; + public final int nsClass; + public final long ttl; + private final byte[] mRdata; + + /** + * Create a new DnsRecord from a positioned ByteBuffer. + * + * Reads the passed ByteBuffer from its current position and decodes a DNS record. + * When this constructor returns, the reading position of the ByteBuffer has been + * advanced to the end of the DNS header record. + * This is meant to chain with other methods reading a DNS response in sequence. + * + * @param ByteBuffer input of record, must be in network byte order + * (which is the default). + */ + DnsRecord(int recordType, ByteBuffer buf) + throws BufferUnderflowException, ParseException { + dName = parseName(buf, 0 /* Parse depth */); + if (dName.length() > MAXNAMESIZE) { + throw new ParseException( + "Parse name fail, name size is too long: " + dName.length()); + } + nsType = shortToUnsignedInt(buf.getShort()); + nsClass = shortToUnsignedInt(buf.getShort()); + + if (recordType != QDSECTION) { + ttl = DnsPacket.integerToUnsignedLong(buf.getInt()); + final int length = shortToUnsignedInt(buf.getShort()); + mRdata = new byte[length]; + buf.get(mRdata); + } else { + ttl = 0; + mRdata = null; + } + } + + /** + * Get a copy of rdata. + */ + public byte[] getRR() { + return (mRdata == null) ? null : mRdata.clone(); + } + + /** + * Convert label from {@code byte[]} to {@code String} + * + * Follows the same conversion rules of the native code (ns_name.c in libc) + */ + private String labelToString(byte[] label) { + final StringBuffer sb = new StringBuffer(); + for (int i = 0; i < label.length; ++i) { + int b = byteToUnsignedInt(label[i]); + // Control characters and non-ASCII characters. + if (b <= 0x20 || b >= 0x7f) { + // Append the byte as an escaped decimal number, e.g., "\19" for 0x13. + sb.append('\\'); + mByteFormat.format(b, sb, mPos); + } else if (b == '"' || b == '.' || b == ';' || b == '\\' + || b == '(' || b == ')' || b == '@' || b == '$') { + // Append the byte as an escaped character, e.g., "\:" for 0x3a. + sb.append('\\'); + sb.append((char) b); + } else { + // Append the byte as a character, e.g., "a" for 0x61. + sb.append((char) b); + } + } + return sb.toString(); + } + + private String parseName(ByteBuffer buf, int depth) throws + BufferUnderflowException, ParseException { + if (depth > MAXLABELCOUNT) { + throw new ParseException("Failed to parse name, too many labels"); + } + final int len = byteToUnsignedInt(buf.get()); + final int mask = len & NAME_COMPRESSION; + if (0 == len) { + return ""; + } else if (mask != NAME_NORMAL && mask != NAME_COMPRESSION) { + throw new ParseException("Parse name fail, bad label type"); + } else if (mask == NAME_COMPRESSION) { + // Name compression based on RFC 1035 - 4.1.4 Message compression + final int offset = ((len & ~NAME_COMPRESSION) << 8) + byteToUnsignedInt(buf.get()); + final int oldPos = buf.position(); + if (offset >= oldPos - 2) { + throw new ParseException("Parse compression name fail, invalid compression"); + } + buf.position(offset); + final String pointed = parseName(buf, depth + 1); + buf.position(oldPos); + return pointed; + } else { + final byte[] label = new byte[len]; + buf.get(label); + final String head = labelToString(label); + if (head.length() > MAXLABELSIZE) { + throw new ParseException("Parse name fail, invalid label length"); + } + final String tail = parseName(buf, depth + 1); + return tail.isEmpty() ? head : head + "." + tail; + } + } + } + + /** {@link Byte#toUnsignedInt(byte)} was added to Android in API 26. */ + public static int byteToUnsignedInt(byte b) { + return b & 255; + } + + /** {@link Short#toUnsignedInt(short)} was added to Android in API 26. */ + public static int shortToUnsignedInt(short s) { + return s & '\uffff'; + } + + /** {@link Integer#toUnsignedLong(int)} was added to Android in API 26. */ + public static long integerToUnsignedLong(int i) { + return (long) i & 4294967295L; + } + + public static final int QDSECTION = 0; + public static final int ANSECTION = 1; + public static final int NSSECTION = 2; + public static final int ARSECTION = 3; + private static final int NUM_SECTIONS = ARSECTION + 1; + + private static final String TAG = DnsPacket.class.getSimpleName(); + + protected final DnsHeader mHeader; + protected final List[] mRecords; + + protected DnsPacket(byte[] data) throws ParseException { + if (null == data) throw new ParseException("Parse header failed, null input data"); + final ByteBuffer buffer; + try { + buffer = ByteBuffer.wrap(data); + mHeader = new DnsHeader(buffer); + } catch (BufferUnderflowException e) { + throw new ParseException("Parse Header fail, bad input data", e); + } + + mRecords = new ArrayList[NUM_SECTIONS]; + + for (int i = 0; i < NUM_SECTIONS; ++i) { + final int count = mHeader.getRecordCount(i); + if (count > 0) { + mRecords[i] = new ArrayList(count); + } + for (int j = 0; j < count; ++j) { + try { + mRecords[i].add(new DnsRecord(i, buffer)); + } catch (BufferUnderflowException e) { + throw new ParseException("Parse record fail", e); + } + } + } + } +} diff --git a/get-defo-ech-config-lists.sh b/get-defo-ech-config-lists.sh new file mode 100755 index 000000000..af0288910 --- /dev/null +++ b/get-defo-ech-config-lists.sh @@ -0,0 +1,14 @@ +#!/bin/bash -ex + +defohost=draft-13.esni.defo.ie +for defoport in 8413 8414 9413 10413 11413 12413 12414; do + ECH=`dig +short -t TYPE65 "_$defoport._https.$defohost" | \ + tail -1 | cut -f 3- -d' ' | sed -e 's/ //g' | sed -e 'N;s/\n//'` + if [[ "$ECH" == "" ]] + then + echo "Can't read ECHConfigList for $defohost:$defoport" + exit 2 + fi + ah_ech=${ECH:14} + echo $ah_ech | xxd -p -r > openjdk/src/test/resources/${defohost}_${defoport}-ech-config-list.bin +done diff --git a/get-https-dns-answer.sh b/get-https-dns-answer.sh new file mode 100755 index 000000000..f3e3a323f --- /dev/null +++ b/get-https-dns-answer.sh @@ -0,0 +1,25 @@ +#!/bin/bash -ex + +for host in check-tls.akamaized.net \ + cloudflare-esni.com \ + cloudflareresearch.com \ + crypto.cloudflare.com \ + deb.debian.org \ + duckduckgo.com \ + en.wikipedia.org \ + enabled.tls13.com \ + mirrors.kernel.org \ + openstreetmap.org \ + tls13.1d.pw \ + web.wechat.com \ + www.google.com \ + www.yandex.ru \ + ; do + ECH=`dig +short -t TYPE65 $host | \ + tail -1 | cut -f 3- -d' ' | sed -e 's/ //g' | sed -e 'N;s/\n//'` + if [[ "$ECH" == "" ]]; then + echo "Can't read HTTPS/TYPE65 for $host" + else + echo $ECH | xxd -p -r > openjdk/src/test/resources/${host}.bin + fi +done diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8049c684f..34ca0eaa8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +#distributionSha256Sum=c9910513d0eed63cd8f5c7fec4cb4a05731144770104a0871234a4edc3ba3cef zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/openjdk/src/test/java/org/conscrypt/ConscryptOpenJdkSuite.java b/openjdk/src/test/java/org/conscrypt/ConscryptOpenJdkSuite.java index 4c0e8b21e..2e148bd88 100644 --- a/openjdk/src/test/java/org/conscrypt/ConscryptOpenJdkSuite.java +++ b/openjdk/src/test/java/org/conscrypt/ConscryptOpenJdkSuite.java @@ -179,6 +179,8 @@ TrustManagerFactoryTest.class, VeryBasicHttpServerTest.class, X509KeyManagerTest.class, + // ech tests + EchInteropTest.class, }) public class ConscryptOpenJdkSuite { @BeforeClass diff --git a/openjdk/src/test/java/org/conscrypt/EchInteropTest.java b/openjdk/src/test/java/org/conscrypt/EchInteropTest.java new file mode 100644 index 000000000..a39dd366c --- /dev/null +++ b/openjdk/src/test/java/org/conscrypt/EchInteropTest.java @@ -0,0 +1,590 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed 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 org.conscrypt; + +import org.conscrypt.com.android.net.module.util.DnsPacket; +import org.junit.*; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.IOException; +import java.net.*; +import java.security.NoSuchAlgorithmException; +import java.security.Security; +import java.util.Arrays; +import java.util.Hashtable; +import java.util.List; + +import javax.naming.Context; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.DirContext; +import javax.naming.directory.InitialDirContext; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +@RunWith(JUnit4.class) +public class EchInteropTest { + + private static final int TIMEOUT_MILLISECONDS = 30000; + + private static String[] hostsNonEch = { + "www.yandex.ru", + "en.wikipedia.org", + // TEMP - causes prefetch exception "web.wechat.com", + "mirrors.kernel.org", + "www.google.com", + "check-tls.akamaized.net", // uses SNI + "duckduckgo.com", // TLS 1.3 + "deb.debian.org", // TLS 1.3 Fastly + "tls13.1d.pw", // TLS 1.3 only, no ECH + "cloudflareresearch.com", // no ECH + + "enabled.tls13.com", // no longer supports ECH + "crypto.cloudflare.com", // no longer supports ECH + }; + private static String[] hostsEch = { + "openstreetmap.org", // now supports ECH + "cloudflare-esni.com", // now supports ECH + + // TEMP - commented out to avoid issues with unique formatting + //"draft-13.esni.defo.ie:8413", // OpenSSL s_server + //"draft-13.esni.defo.ie:8414", // OpenSSL s_server, likely forces HRR as it only likes P-384 for TLS =09 + // TEMP - causes prefetch exception "draft-13.esni.defo.ie:9413", + //"draft-13.esni.defo.ie:10413", // nginx + //"draft-13.esni.defo.ie:11413", // apache + //"draft-13.esni.defo.ie:12413", // haproxy shared mode (haproxy terminates TLS) + //"draft-13.esni.defo.ie:12414", // haproxy split mode (haproxy only decrypts ECH) + }; + + private static String[] hosts = new String[hostsNonEch.length + hostsEch.length]; + + @BeforeClass + public static void setUp() throws NoSuchAlgorithmException { + System.out.println("========== SETUP BEGIN ==============================================================="); + Security.insertProviderAt(Conscrypt.newProvider(), 1); + assertTrue(Conscrypt.isAvailable()); + assertTrue(Conscrypt.isConscrypt(SSLContext.getInstance("TLSv1.3"))); + System.arraycopy(hostsNonEch, 0, hosts, 0, hostsNonEch.length); + System.arraycopy(hostsEch, 0, hosts, hostsNonEch.length, hostsEch.length); + prefetchDns(hosts); + System.out.println("========== SETUP END ================================================================="); + } + + @AfterClass + public static void tearDown() throws NoSuchAlgorithmException { + System.out.println("========== TEARDOWN BEGIN ============================================================"); + Security.removeProvider("Conscrypt"); + assertFalse(Conscrypt.isConscrypt(SSLContext.getInstance("TLSv1"))); + System.out.println("========== TEARDOWN END =============================================================="); + } + + @Test + public void testConnectSocket() throws IOException { + boolean hostFailed = false; + for (String h : hosts) { + System.out.println(" = TEST CONNECT SOCKET FOR " + h); + String[] hostPort = h.split(":"); + String host = hostPort[0]; + int port = 443; + if (hostPort.length == 2) { + port = Integer.parseInt(hostPort[1]); + } + + SSLSocketFactory sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); + assertTrue(Conscrypt.isConscrypt(sslSocketFactory)); + SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(host, port); + assertTrue(Conscrypt.isConscrypt(sslSocket)); + boolean setUpEch = false; + try { + byte[] echConfigList = getEchConfigListFromDns(h); + if (echConfigList != null) { + Conscrypt.setEchParameters(sslSocket, new EchParameters(true, echConfigList)); + System.out.println("ENABLED ECH GREASE AND CONFIG LIST"); + setUpEch = true; + } else { + Conscrypt.setEchParameters(sslSocket, new EchParameters(true)); + System.out.println("ENABLED ECH GREASE"); + } + } catch (NamingException e) { + System.out.println("GET CONFIG LIST THREW EXCEPTION FOR " + host); + System.out.println(e.getMessage()); + hostFailed = true; + continue; + } + sslSocket.setSoTimeout(TIMEOUT_MILLISECONDS); + try { + sslSocket.startHandshake(); + System.out.println("HANDSHAKE OK FOR " + host); + } catch (Exception e) { + System.out.println("HANDSHAKE THREW EXCEPTION FOR " + host); + System.out.println(e.getMessage()); + } + assertTrue(sslSocket.isConnected()); + AbstractConscryptSocket abstractConscryptSocket = (AbstractConscryptSocket) sslSocket; + if (setUpEch) { + assertTrue(abstractConscryptSocket.echAccepted()); + } else { + assertFalse(abstractConscryptSocket.echAccepted()); + } + sslSocket.close(); + } + System.out.println("TEST FAILED FOR ONE OR MORE HOSTS: " + hostFailed); + assertFalse(hostFailed); + } + + @Test + public void testEchRetryConfigWithConnectSocket() throws IOException, NamingException { + for (String h : hostsEch) { + System.out.println(" = TEST ECH RETRY CONFIG WITH CONNECT SOCKET FOR " + h); + String[] hostPort = h.split(":"); + String host = hostPort[0]; + int port = 443; + if (hostPort.length == 2) { + port = Integer.parseInt(hostPort[1]); + } + + SSLSocketFactory sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); + assertTrue(Conscrypt.isConscrypt(sslSocketFactory)); + SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(host, port); + assertTrue(h + " should use Conscrypt", Conscrypt.isConscrypt(sslSocket)); + + byte[] echConfigList = null; + try { + echConfigList = getEchConfigListFromDns(h); + System.out.println("ECH CONFIG LIST OK FOR " + h); + } catch (Exception e) { + System.out.println("ECH CONFIG LIST THREW EXCEPTION FOR " + h); + System.out.println(e.getMessage()); + } + + if (echConfigList == null) { + System.out.println("NO ECH CONFIG LIST FOUND IN DNS FOR " + h); + continue; + } + assertEquals("length should match inline declaration", + echConfigList[1] + 2, // leading 0x00 and length bytes + echConfigList.length + ); + // corrupt the key while leaving the SNI intact + echConfigList[20] = (byte) 0xff; + echConfigList[21] = (byte) 0xff; + echConfigList[22] = (byte) 0xff; + echConfigList[23] = (byte) 0xff; + echPbuf("CORRUPT ECH CONFIG LIST FOR " + h, echConfigList); + Conscrypt.setEchParameters(sslSocket, new EchParameters(echConfigList)); + + try { + sslSocket.setSoTimeout(TIMEOUT_MILLISECONDS); + sslSocket.startHandshake(); + sslSocket.close(); + fail("Used corrupt ECH Config List, should not connect to " + h); + } catch (EchRejectedException e) { + byte[] echRetryConfig = Conscrypt.getEchRetryConfigList(sslSocket); + assertNotNull(echRetryConfig); + sslSocket.close(); + echPbuf("ECH RETRY CONFIG", echRetryConfig); + SSLSocket sslSocket2 = (SSLSocket) sslSocketFactory.createSocket(host, port); + Conscrypt.setEchParameters(sslSocket2, new EchParameters(echRetryConfig)); + sslSocket2.setSoTimeout(TIMEOUT_MILLISECONDS); + sslSocket2.startHandshake(); + assertTrue(h + " should connect with ECH Retry Config", sslSocket2.isConnected()); + AbstractConscryptSocket abstractConscryptSocket = (AbstractConscryptSocket) sslSocket2; + assertTrue(h + " should use ECH with Retry Config", abstractConscryptSocket.echAccepted()); + sslSocket2.close(); + + } catch (SSLHandshakeException e) { + // prints boolean? + System.out.println(e.getMessage().contains(":ECH_REJECTED ") + " | " + e.getMessage()); + e.printStackTrace(); + fail(e.getMessage()); + } + } + } + + @Rule + public ExpectedException echRejectedExceptionRule = ExpectedException.none(); + + @Test + public void testEchConfigOnNonEchHosts() throws IOException { + for (String h : hostsNonEch) { + System.out.println(" = TEST ECH CONFIG ON NON ECH HOSTS FOR " + h); + String[] hostPort = h.split(":"); + String host = hostPort[0]; + int port = 443; + if (hostPort.length == 2) { + port = Integer.parseInt(hostPort[1]); + } + + SSLSocketFactory sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); + assertTrue(Conscrypt.isConscrypt(sslSocketFactory)); + SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(host, port); + assertTrue(Conscrypt.isConscrypt(sslSocket)); + + // load saved ech config with the expecation that the key mismatch will cause rejection + byte[] echConfigList = TestUtils.readTestFile("draft-13.esni.defo.ie_12414-ech-config-list.bin"); + Conscrypt.setEchParameters(sslSocket, new EchParameters(echConfigList)); + + echRejectedExceptionRule.expect(SSLHandshakeException.class); + echRejectedExceptionRule.expectMessage("ECH_REJECTED"); + sslSocket.setSoTimeout(TIMEOUT_MILLISECONDS); + sslSocket.startHandshake(); + assertTrue(sslSocket.isConnected()); + AbstractConscryptSocket abstractConscryptSocket = (AbstractConscryptSocket) sslSocket; + assertTrue(abstractConscryptSocket.echAccepted()); + sslSocket.close(); + } + } + + @Test + public void testConnectHttpsURLConnection() throws IOException { + boolean hostFailed = false; + for (String host : hosts) { + URL url = new URL("https://" + host); + System.out.println(" = TEST CONNECT HTTPS URL CONNECTION FOR " + url); + HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); + SSLSocketFactory delegateSocketFactory = connection.getSSLSocketFactory(); + assertTrue(Conscrypt.isConscrypt(delegateSocketFactory)); + try { + byte[] echConfigList = getEchConfigListFromDns(host); + if (echConfigList != null) { + connection.setSSLSocketFactory(new EchSSLSocketFactory(delegateSocketFactory, echConfigList)); + System.out.println("CREATED SOCKET FACTORY WITH ECH GREASE AND CONFIG LIST"); + } else { + connection.setSSLSocketFactory(new EchSSLSocketFactory(delegateSocketFactory, true)); + System.out.println("CREATED SOCKET FACTORY WITH ECH GREASE"); + } + } catch (NamingException e) { + System.out.println("GET CONFIG LIST THREW EXCEPTION FOR " + host); + System.out.println(e.getMessage()); + hostFailed = true; + continue; + } + // Cloudflare will return 403 Forbidden (error code 1010) unless a User Agent is set :-| + connection.setRequestProperty("User-Agent", "Conscrypt EchInteropTest"); + connection.setConnectTimeout(0); // blocking connect with TCP timeout + connection.setReadTimeout(0); + + int responseCode = -1; + String contentType = "error"; + String cipherSuite = "error"; + try { + responseCode = connection.getResponseCode(); + contentType = connection.getContentType().split(";")[0]; + cipherSuite = connection.getCipherSuite(); + System.out.println("GET CONNECTION INFO OK FOR " + url + " -> " + responseCode + " | " + contentType + " | " + cipherSuite); + } catch (Exception e) { + System.out.println("GET CONNECTION INFO THREW EXCEPTION FOR " + url); + System.out.println(e.getMessage()); + } + connection.getContent(); + assertEquals(200, responseCode); + String[] options = {"text/html", "text/plain"}; + List contentTypes = Arrays.asList(options); + // some defo urls have different content types, is this an error? + assertTrue(contentTypes.contains(contentType)); + assertTrue(cipherSuite.startsWith("TLS")); + connection.disconnect(); + } + System.out.println("TEST FAILED FOR ONE OR MORE HOSTS: " + hostFailed); + assertFalse(hostFailed); + } + + @Test + public void testParseDnsAndConnect() throws IOException, NamingException { + for (String h : hosts) { + System.out.println(" = TEST PARSE DNS AND CONNECT FOR " + h); + String[] hostPort = h.split(":"); + String host = hostPort[0]; + int port = 443; + if (hostPort.length > 1) { + port = Integer.parseInt(hostPort[1]); + } + + byte[] echConfigList = null; + try { + echConfigList = getEchConfigListFromDns(h); + System.out.println("ECH CONFIG LIST OK FOR " + h); + } catch (Exception e) { + System.out.println("ECH CONFIG LIST THREW EXCEPTION FOR " + h); + System.out.println(e.getMessage()); + } + + if (echConfigList != null) { + assertEquals("length should match inline declaration", + echConfigList[1] + 2, // leading 0x00 and length bytes + echConfigList.length + ); + } else { + System.out.println("NO ECH CONFIG LIST FOUND IN DNS FOR " + h); + } + + SSLSocketFactory sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); + assertTrue(Conscrypt.isConscrypt(sslSocketFactory)); + SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(host, port); + assertTrue(Conscrypt.isConscrypt(sslSocket)); + if (echConfigList != null) { + Conscrypt.setEchParameters(sslSocket, new EchParameters(true, echConfigList)); + System.out.println("ENABLED ECH GREASE AND CONFIG LIST"); + } else { + Conscrypt.setEchParameters(sslSocket, new EchParameters(true)); + System.out.println("ENABLED ECH GREASE"); + } + sslSocket.setSoTimeout(TIMEOUT_MILLISECONDS); + sslSocket.startHandshake(); + assertTrue(sslSocket.isConnected()); + AbstractConscryptSocket abstractConscryptSocket = (AbstractConscryptSocket) sslSocket; + System.out.println("ECHACCEPTED SET TO " + abstractConscryptSocket.echAccepted() + " FOR " + host); + if (echConfigList != null) { + assertTrue(abstractConscryptSocket.echAccepted()); + } else { + assertFalse(abstractConscryptSocket.echAccepted()); + } + sslSocket.close(); + } + } + + @Test + public void testParseDnsFromFiles() { + for (String hostString : hosts) { + System.out.println(" = TEST PARSE DNS FROM FILES FOR " + hostString); + String[] h = hostString.split(":"); + String host = h[0]; + if (h.length > 1) { + if (!"443".equals(h[1])) { + host = "_" + h[1] + "._https." + h[0]; // query for non-standard port + } + } + try { + byte[] dnsAnswer = TestUtils.readTestFile(host + ".bin"); + echPbuf("DNS ANSWER", dnsAnswer); + try { + DnsEchAnswer dnsEchAnswer = new DnsEchAnswer(dnsAnswer); + if (dnsEchAnswer.getEchConfigList() == null) { + System.out.println("ECH CONFIG LIST NULL FOR " + host); + } else { + echPbuf("ECH CONFIG LIST", dnsEchAnswer.getEchConfigList()); + } + } catch (DnsPacket.ParseException e) { + e.printStackTrace(); + } + } catch (IOException e) { + e.printStackTrace(); + } + + } + } + + static byte[] getEchConfigListFromDns(String hostPort) throws NamingException { + String[] h = hostPort.split(":"); + String dnshost = h[0]; + if (h.length > 1 && !"443".equals(h[1])) { + dnshost = "_" + h[1] + "._https." + h[0]; // query for non-standard port + } + + byte[] echConfigList = null; + Hashtable envProps = + new Hashtable(); + envProps.put(Context.INITIAL_CONTEXT_FACTORY, + "com.sun.jndi.dns.DnsContextFactory"); + DirContext dnsContext = new InitialDirContext(envProps); + Attributes dnsEntries = dnsContext.getAttributes(dnshost, new String[]{"65"}); + NamingEnumeration ae = dnsEntries.getAll(); + while (ae.hasMore()) { + Attribute attr = (Attribute) ae.next(); + // only parse HTTPS/65 (previous included SVCB/64, but why?) + for (int i = 0; i < attr.size(); i++) { + Object rr = attr.get(i); + if (!(rr instanceof byte[])) { + continue; + } else { + echConfigList = Conscrypt.getEchConfigListFromDnsRR((byte[]) rr); + } + } + } + ae.close(); + return echConfigList; + } + + class DnsEchAnswer extends DnsPacket { + private static final String TAG = "DnsResolver.DnsAddressAnswer"; + private static final boolean DBG = true; + + /** + * Service Binding [draft-ietf-dnsop-svcb-https-00] + */ + public static final int TYPE_SVCB = 64; + + /** + * HTTPS Binding [draft-ietf-dnsop-svcb-https-00] + */ + public static final int TYPE_HTTPS = 65; + + private final int mQueryType; + + protected DnsEchAnswer(byte[] data) throws ParseException { + super(data); + if ((mHeader.flags & (1 << 15)) == 0) { + throw new IllegalArgumentException("Not an answer packet"); + } + if (mHeader.getRecordCount(QDSECTION) == 0) { + throw new IllegalArgumentException("No question found"); + } + // Expect only one question in question section. + mQueryType = mRecords[QDSECTION].get(0).nsType; + } + + public byte[] getEchConfigList() { + byte[] results = new byte[0]; + if (mHeader.getRecordCount(ANSECTION) == 0) return results; + + for (final DnsRecord ansSec : mRecords[ANSECTION]) { + // Only support SVCB and HTTPS since only they can have ECH Config Lists + int nsType = ansSec.nsType; + if (nsType != mQueryType || (nsType != TYPE_SVCB && nsType != TYPE_HTTPS)) { + continue; + } + echPbuf("RR", ansSec.getRR()); + results = Conscrypt.getEchConfigListFromDnsRR(ansSec.getRR()); + } + return results; + } + } + + private static class EchSSLSocketFactory extends SSLSocketFactory { + private final SSLSocketFactory delegate; + private final boolean enableEchGrease; + + private byte[] echConfigList; + + public EchSSLSocketFactory(SSLSocketFactory delegate, boolean enableEchGrease) { + this.delegate = delegate; + this.enableEchGrease = enableEchGrease; + } + + public EchSSLSocketFactory(SSLSocketFactory delegate, byte[] echConfigList) { + this.delegate = delegate; + this.enableEchGrease = true; + this.echConfigList = echConfigList; + } + + @Override + public String[] getDefaultCipherSuites() { + return delegate.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return delegate.getSupportedCipherSuites(); + } + + @Override + public Socket createSocket(Socket socket, String host, int port, boolean autoClose) + throws IOException { + return setEchSettings(delegate.createSocket(socket, host, port, autoClose)); + } + + @Override + public Socket createSocket(String host, int port) + throws IOException, UnknownHostException { + return setEchSettings(delegate.createSocket(host, port)); + } + + @Override + public Socket createSocket(String host, int port, InetAddress localAddress, int localPort) + throws IOException, UnknownHostException { + return setEchSettings(delegate.createSocket(host, port, localAddress, localPort)); + } + + @Override + public Socket createSocket(InetAddress host, int port) + throws IOException { + return setEchSettings(delegate.createSocket(host, port)); + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) + throws IOException { + return setEchSettings(delegate.createSocket(address, port, localAddress, localPort)); + } + + private Socket setEchSettings(Socket socket) { + SSLSocket sslSocket = (SSLSocket) socket; + Conscrypt.setEchParameters(sslSocket, new EchParameters(enableEchGrease, echConfigList)); + return sslSocket; + } + + } + + public static void echPbuf(String msg, byte[] buf) { + if (buf == null) { + System.out.println(msg + " ():\n null"); + return; + } + int blen = buf.length; + System.out.print(msg + " (" + blen + "):\n "); + for (int i = 0; i < blen; i++) { + if ((i != 0) && (i % 16 == 0)) + System.out.print("\n "); + System.out.print(String.format("%02x:", Byte.toUnsignedInt(buf[i]))); + } + System.out.print("\n"); + } + + /** + * Prime the DNS cache with the hosts that are used in these tests. + */ + private static void prefetchDns(String[] hosts) { + System.out.println("========== PREFETCH BEGIN ============================================================"); + for (final String host : hosts) { + new Thread() { + @Override + public void run() { + String actualHost = host; + if (actualHost.contains(":")) { + // the reformatted host strings with ports for defo don't return ips + actualHost = actualHost.split(":")[0]; + } + try { + InetAddress.getByName(actualHost); + getEchConfigListFromDns(host); + System.out.println("PREFETCH OK FOR " + actualHost); + } catch (NamingException e) { + System.out.println("PREFETCH FAILED FOR " + actualHost + ", GET ECH LIST THREW EXCEPTION"); + System.out.println(e.getMessage()); + } catch (UnknownHostException e) { + System.out.println("PREFETCH FAILED FOR " + actualHost + ", IP LOOKUP THREW EXCEPTION"); + System.out.println(e.getMessage()); + } + } + }.start(); + } + System.out.println("========== PREFETCH END =============================================================="); + } +} diff --git a/openjdk/src/test/java/org/conscrypt/NativeCryptoTest.java b/openjdk/src/test/java/org/conscrypt/NativeCryptoTest.java index 98f4c675c..1376c0137 100644 --- a/openjdk/src/test/java/org/conscrypt/NativeCryptoTest.java +++ b/openjdk/src/test/java/org/conscrypt/NativeCryptoTest.java @@ -25,6 +25,7 @@ import static org.conscrypt.NativeConstants.SSL_VERIFY_PEER; import static org.conscrypt.NativeConstants.TLS1_1_VERSION; import static org.conscrypt.NativeConstants.TLS1_2_VERSION; +import static org.conscrypt.NativeConstants.TLS1_3_VERSION; import static org.conscrypt.NativeConstants.TLS1_VERSION; import static org.conscrypt.TestUtils.decodeHex; import static org.conscrypt.TestUtils.isWindows; @@ -96,6 +97,9 @@ import javax.net.ssl.SSLProtocolException; import javax.security.auth.x500.X500Principal; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + @RunWith(JUnit4.class) public class NativeCryptoTest { private static final long NULL = 0; @@ -120,6 +124,50 @@ public class NativeCryptoTest { private static RSAPrivateCrtKey TEST_RSA_KEY; + + @BeforeClass + public static void getPlatformMethods() throws Exception { + Class c_Platform = TestUtils.conscryptClass("Platform"); + m_Platform_getFileDescriptor = + c_Platform.getDeclaredMethod("getFileDescriptor", Socket.class); + m_Platform_getFileDescriptor.setAccessible(true); + } + + private static OpenSSLKey getServerPrivateKey() throws Exception { + initStatics(); + return SERVER_PRIVATE_KEY; + } + + private static long[] getServerCertificateRefs() throws Exception { + initStatics(); + return SERVER_CERTIFICATE_REFS; + } + + private static byte[][] getEncodedServerCertificates() throws Exception { + initStatics(); + return ENCODED_SERVER_CERTIFICATES; + } + + private static OpenSSLKey getClientPrivateKey() throws Exception { + initStatics(); + return CLIENT_PRIVATE_KEY; + } + + private static long[] getClientCertificateRefs() throws Exception { + initStatics(); + return CLIENT_CERTIFICATE_REFS; + } + + private static byte[][] getEncodedClientCertificates() throws Exception { + initStatics(); + return ENCODED_CLIENT_CERTIFICATES; + } + + private static byte[][] getCaPrincipals() throws Exception { + initStatics(); + return CA_PRINCIPALS; + } + @BeforeClass @SuppressWarnings("JdkObsolete") // Public API KeyStore.aliases() uses Enumeration public static void initStatics() throws Exception { @@ -473,6 +521,478 @@ public void test_SSL_set_mode_and_clear_mode() throws Exception { NativeCrypto.SSL_CTX_free(c, null); } + @Test + public void test_SSL_do_handshake_ech_grease_only() throws Exception { + System.out.println("test_SSL_ech_accepted_exchange"); + final ServerSocket listener = newServerSocket(); + + final byte[] key = readTestFile("boringssl-ech-private-key.bin"); + final byte[] serverConfig = readTestFile("boringssl-server-ech-config.bin"); + Hooks cHooks = new ClientHooks() { + @Override + public long beforeHandshake(long c) throws SSLException { + long ssl = super.beforeHandshake(c); + assertEquals(1, NativeCrypto.SSL_set_protocol_versions(ssl, null, TLS1_VERSION, TLS1_3_VERSION)); + NativeCrypto.SSL_set_enable_ech_grease(ssl, null, true); + return ssl; + } + + @Override + public void afterHandshake(long session, long ssl, long context, Socket socket, + FileDescriptor fd, SSLHandshakeCallbacks callback) throws Exception { + assertFalse(NativeCrypto.SSL_ech_accepted(ssl, null)); + assertNull(NativeCrypto.SSL_get0_ech_name_override(ssl, null)); + byte[] retryConfigs = NativeCrypto.SSL_get0_ech_retry_configs(ssl, null); + assertEquals(5, retryConfigs.length); // should be the invalid ECH Config List + super.afterHandshake(session, ssl, context, socket, fd, callback); + } + }; + Hooks sHooks = new ServerHooks(getServerPrivateKey(), getEncodedServerCertificates()) { + @Override + public long beforeHandshake(long c) throws SSLException { + long ssl = super.beforeHandshake(c); + assertEquals(1, NativeCrypto.SSL_set_protocol_versions(ssl, null, TLS1_VERSION, TLS1_3_VERSION)); + assertTrue(NativeCrypto.SSL_CTX_ech_enable_server(c, null, key, serverConfig)); + return ssl; + } + }; + Future client = handshake(listener, 0, true, cHooks, null, null); + Future server = + handshake(listener, 0, false, sHooks, null, null); + TestSSLHandshakeCallbacks clientCallback = + client.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + TestSSLHandshakeCallbacks serverCallback = + server.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + assertTrue(clientCallback.verifyCertificateChainCalled); + assertEqualCertificateChains( + getServerCertificateRefs(), clientCallback.certificateChainRefs); + assertFalse(serverCallback.verifyCertificateChainCalled); + assertFalse(clientCallback.clientCertificateRequestedCalled); + assertFalse(serverCallback.clientCertificateRequestedCalled); + assertFalse(clientCallback.clientPSKKeyRequestedInvoked); + assertFalse(serverCallback.clientPSKKeyRequestedInvoked); + assertFalse(clientCallback.serverPSKKeyRequestedInvoked); + assertFalse(serverCallback.serverPSKKeyRequestedInvoked); + assertTrue(clientCallback.handshakeCompletedCalled); + assertTrue(serverCallback.handshakeCompletedCalled); + assertFalse(clientCallback.serverCertificateRequestedInvoked); + assertTrue(serverCallback.serverCertificateRequestedInvoked); + } + + /** Convenient debug print for ECH Config Lists */ + private void printEchConfigList(String msg, byte[] buf) { + int blen = buf.length; + System.out.print(msg + " (" + blen + "):\n "); + for (int i = 0; i < blen; i++) { + if ((i != 0) && (i % 16 == 0)) + System.out.print("\n "); + System.out.print(String.format("%02x:", Byte.toUnsignedInt(buf[i]))); + } + System.out.print("\n"); + } + + @Test + public void test_SSL_do_handshake_ech_client_server() throws Exception { + System.out.println("test_SSL_do_handshake_ech_client_server"); + final ServerSocket listener = newServerSocket(); + + final byte[] key = readTestFile("boringssl-ech-private-key.bin"); + final byte[] serverConfig = readTestFile("boringssl-server-ech-config.bin"); + final byte[] clientConfigList = readTestFile("boringssl-ech-config-list.bin"); + Hooks cHooks = new ClientHooks() { + @Override + public long beforeHandshake(long c) throws SSLException { + long ssl = super.beforeHandshake(c); + assertEquals(1, NativeCrypto.SSL_set_protocol_versions(ssl, null, TLS1_VERSION, TLS1_3_VERSION)); + assertTrue(NativeCrypto.SSL_set1_ech_config_list(ssl, null, clientConfigList)); + return ssl; + } + + @Override + public void afterHandshake(long session, long ssl, long context, Socket socket, + FileDescriptor fd, SSLHandshakeCallbacks callback) throws Exception { + assertTrue(NativeCrypto.SSL_ech_accepted(ssl, null)); + super.afterHandshake(session, ssl, context, socket, fd, callback); + } + }; + Hooks sHooks = new ServerHooks(getServerPrivateKey(), getEncodedServerCertificates()) { + @Override + public long beforeHandshake(long c) throws SSLException { + long ssl = super.beforeHandshake(c); + assertEquals(1, NativeCrypto.SSL_set_protocol_versions(ssl, null, TLS1_VERSION, TLS1_3_VERSION)); + assertTrue(NativeCrypto.SSL_CTX_ech_enable_server(c, null, key, serverConfig)); + return ssl; + } + + @Override + public void afterHandshake(long session, long ssl, long context, Socket socket, + FileDescriptor fd, SSLHandshakeCallbacks callback) throws Exception { + assertTrue(NativeCrypto.SSL_ech_accepted(ssl, null)); + super.afterHandshake(session, ssl, context, socket, fd, callback); + } + }; + Future client = handshake(listener, 0, true, cHooks, null, null); + Future server = + handshake(listener, 0, false, sHooks, null, null); + TestSSLHandshakeCallbacks clientCallback = + client.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + TestSSLHandshakeCallbacks serverCallback = + server.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + assertTrue(clientCallback.verifyCertificateChainCalled); + assertEqualCertificateChains( + getServerCertificateRefs(), clientCallback.certificateChainRefs); + assertFalse(serverCallback.verifyCertificateChainCalled); + assertFalse(clientCallback.clientCertificateRequestedCalled); + assertFalse(serverCallback.clientCertificateRequestedCalled); + assertFalse(clientCallback.clientPSKKeyRequestedInvoked); + assertFalse(serverCallback.clientPSKKeyRequestedInvoked); + assertFalse(clientCallback.serverPSKKeyRequestedInvoked); + assertFalse(serverCallback.serverPSKKeyRequestedInvoked); + assertTrue(clientCallback.handshakeCompletedCalled); + assertTrue(serverCallback.handshakeCompletedCalled); + assertFalse(clientCallback.serverCertificateRequestedInvoked); + assertTrue(serverCallback.serverCertificateRequestedInvoked); + } + + @Test + public void test_SSL_do_handshake_ech_retry_configs() throws Exception { + final ServerSocket listener = newServerSocket(); + + final byte[] key = readTestFile("boringssl-ech-private-key.bin"); + final byte[] serverConfig = readTestFile("boringssl-server-ech-config.bin"); + final byte[] originalClientConfigList = readTestFile("boringssl-ech-config-list.bin"); + final byte[] clientConfigList = originalClientConfigList.clone(); + clientConfigList[20] = (byte) (clientConfigList[20] % 255 + 1); // corrupt it + + Hooks cHooks = new ClientHooks() { + @Override + public long beforeHandshake(long c) throws SSLException { + long ssl = super.beforeHandshake(c); + assertEquals(1, NativeCrypto.SSL_set_protocol_versions(ssl, null, TLS1_VERSION, TLS1_3_VERSION)); + assertTrue(NativeCrypto.SSL_set1_ech_config_list(ssl, null, clientConfigList)); + return ssl; + } + + @Override + public void afterHandshake(long session, long ssl, long context, Socket socket, + FileDescriptor fd, SSLHandshakeCallbacks callback) { + fail(); + } + }; + Hooks sHooks = new ServerHooks(getServerPrivateKey(), getEncodedServerCertificates()) { + @Override + public long beforeHandshake(long c) throws SSLException { + long ssl = super.beforeHandshake(c); + assertEquals(1, NativeCrypto.SSL_set_protocol_versions(ssl, null, TLS1_VERSION, TLS1_3_VERSION)); + assertTrue(NativeCrypto.SSL_CTX_ech_enable_server(c, null, key, serverConfig)); + return ssl; + } + + @Override + public void afterHandshake(long session, long ssl, long context, Socket socket, + FileDescriptor fd, SSLHandshakeCallbacks callback) throws Exception { + assertTrue(NativeCrypto.SSL_ech_accepted(ssl, null)); + super.afterHandshake(session, ssl, context, socket, fd, callback); + } + }; + Future client = handshake(listener, 0, true, cHooks, null, null, true); + Future server = handshake(listener, 0, false, sHooks, null, null, true); + TestSSLHandshakeCallbacks clientCallback = null; + TestSSLHandshakeCallbacks serverCallback = null; + try { + clientCallback = client.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + serverCallback = server.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (ExecutionException e) { + // caused by SSLProtocolException + } + assertNull(clientCallback); + assertNull(serverCallback); + assertArrayEquals(originalClientConfigList, cHooks.echRetryConfigs); + assertEquals("example.com", cHooks.echNameOverride); + assertNotNull(cHooks.echRetryConfigs); + assertNull(sHooks.echNameOverride); + assertNull(sHooks.echRetryConfigs); + + final byte[] echRetryConfigsFromPrevious = cHooks.echRetryConfigs; + cHooks = new ClientHooks() { + @Override + public long beforeHandshake(long c) throws SSLException { + long ssl = super.beforeHandshake(c); + assertEquals(1, NativeCrypto.SSL_set_protocol_versions(ssl, null, TLS1_VERSION, TLS1_3_VERSION)); + assertTrue(NativeCrypto.SSL_set1_ech_config_list(ssl, null, echRetryConfigsFromPrevious)); + return ssl; + } + + @Override + public void afterHandshake(long session, long ssl, long context, Socket socket, + FileDescriptor fd, SSLHandshakeCallbacks callback) throws Exception { + assertTrue(NativeCrypto.SSL_ech_accepted(ssl, null)); + super.afterHandshake(session, ssl, context, socket, fd, callback); + } + }; + + client = handshake(listener, 0, true, cHooks, null, null); + server = handshake(listener, 0, false, sHooks, null, null); + clientCallback = client.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + serverCallback = server.get(TIMEOUT_SECONDS, TimeUnit.SECONDS); + assertTrue(clientCallback.verifyCertificateChainCalled); + assertEqualCertificateChains( + getServerCertificateRefs(), clientCallback.certificateChainRefs); + assertFalse(serverCallback.verifyCertificateChainCalled); + assertFalse(clientCallback.clientCertificateRequestedCalled); + assertFalse(serverCallback.clientCertificateRequestedCalled); + assertFalse(clientCallback.clientPSKKeyRequestedInvoked); + assertFalse(serverCallback.clientPSKKeyRequestedInvoked); + assertFalse(clientCallback.serverPSKKeyRequestedInvoked); + assertFalse(serverCallback.serverPSKKeyRequestedInvoked); + assertTrue(clientCallback.handshakeCompletedCalled); + assertTrue(serverCallback.handshakeCompletedCalled); + assertFalse(clientCallback.serverCertificateRequestedInvoked); + assertTrue(serverCallback.serverCertificateRequestedInvoked); + } + + @Test + public void test_SSL_set_enable_ech_grease() throws Exception { + long c = NativeCrypto.SSL_CTX_new(); + long s = NativeCrypto.SSL_new(c, null); + + NativeCrypto.SSL_set_enable_ech_grease(s, null, true); + NativeCrypto.SSL_set_enable_ech_grease(s, null, false); + + NativeCrypto.SSL_free(s, null); + NativeCrypto.SSL_CTX_free(c, null); + } + + @Test + public void test_SSL_set1_ech_config_list() throws Exception { + long c = NativeCrypto.SSL_CTX_new(); + long s = NativeCrypto.SSL_new(c, null); + + final byte[] configList = readTestFile("boringssl-ech-config-list.bin"); + assertTrue(NativeCrypto.SSL_set1_ech_config_list(s, null, configList)); + byte[] badConfigList = { + 0x00, 0x05, (byte) 0xfe, 0x0d, (byte) 0xff, (byte) 0xff, (byte) 0xff + }; + boolean set = false; + try { + set = NativeCrypto.SSL_set1_ech_config_list(s, null, badConfigList); + NativeCrypto.SSL_free(s, null); + NativeCrypto.SSL_CTX_free(c, null); + } catch(AssertionError e) { + // ignored when running with checkErrorQueue + } + assertFalse(set); + } + + @Test(expected = NullPointerException.class) + public void test_SSL_set1_ech_config_list_withNull() throws Exception { + long c = NativeCrypto.SSL_CTX_new(); + long s = NativeCrypto.SSL_new(c, null); + NativeCrypto.SSL_set1_ech_config_list(s, null, null); + } + + @Test + public void test_SSL_ECH_KEYS_new() throws Exception { + long k = NativeCrypto.SSL_ECH_KEYS_new(); + NativeCrypto.SSL_ECH_KEYS_up_ref(k); + assertTrue(k != NULL); + long k2 = NativeCrypto.SSL_ECH_KEYS_new(); + NativeCrypto.SSL_ECH_KEYS_up_ref(k2); + assertTrue(k != k2); + NativeCrypto.SSL_ECH_KEYS_free(k); + NativeCrypto.SSL_ECH_KEYS_free(k2); + } + + /* + @Test(expected = NullPointerException.class) + public void SSL_ECH_KEYS_has_duplicate_config_id_withNullShouldThrow() throws Exception { + // TODO what should throw NPEs? + NativeCrypto.SSL_ECH_KEYS_has_duplicate_config_id(null); + } + */ + + @Test + public void test_SSL_marshal_ech_config() throws Exception { + int[] kPrivateKey = { + 0xbc, 0xb5, 0x51, 0x29, 0x31, 0x10, 0x30, 0xc9, 0xed, 0x26, 0xde, + 0xd4, 0xb3, 0xdf, 0x3a, 0xce, 0x06, 0x8a, 0xee, 0x17, 0xab, 0xce, + 0xd7, 0xdb, 0xf3, 0x11, 0xe5, 0xa8, 0xf3, 0xb1, 0x8e, 0x24 + }; + int[] kECHConfig = { + // version + 0xfe, 0x0d, + // length + 0x00, 0x41, + // contents.config_id + 0x01, + // contents.kem_id + 0x00, 0x20, + // contents.public_key + 0x00, 0x20, 0xa6, 0x9a, 0x41, 0x48, 0x5d, 0x32, 0x96, 0xa4, 0xe0, 0xc3, + 0x6a, 0xee, 0xf6, 0x63, 0x0f, 0x59, 0x32, 0x6f, 0xdc, 0xff, 0x81, 0x29, + 0x59, 0xa5, 0x85, 0xd3, 0x9b, 0x3b, 0xde, 0x98, 0x55, 0x5c, + // contents.cipher_suites + 0x00, 0x08, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x03, + // contents.maximum_name_length + 0x10, + // contents.public_name + 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x2e, 0x65, 0x78, 0x61, 0x6d, + 0x70, 0x6c, 0x65, + // contents.extensions + 0x00, 0x00 + }; + //byte[] echConfig = NativeCrypto.SSL_marshal_ech_config(1, kPrivateKey, "public.example"); + //assertEqual(kECHConfig, echConfig); + /* + ASSERT_TRUE(SSL_marshal_ech_config(&ech_config, &ech_config_len, + /*config_id=*1, key.get(), + "public.example", 16)); + EXPECT_EQ(Bytes(kECHConfig), Bytes(ech_config, ech_config_len)); + + // Generate a second ECHConfig. + ASSERT_TRUE(EVP_HPKE_KEY_generate(key2.get(), EVP_hpke_x25519_hkdf_sha256())); + ASSERT_TRUE(SSL_marshal_ech_config(&ech_config2, &ech_config2_len, + /*config_id=*2, key2.get(), + "public.example", 16)); + + // Install both ECHConfigs in an |SSL_ECH_KEYS|. + ASSERT_TRUE(keys); + ASSERT_TRUE(SSL_ECH_KEYS_add(keys.get(), /*is_retry_config=*1, ech_config, + ech_config_len, key.get())); + ASSERT_TRUE(SSL_ECH_KEYS_add(keys.get(), /*is_retry_config=*1, ech_config2, + ech_config2_len, key2.get())); + + */ + + // The ECHConfigList should be correctly serialized. + //NativeCrypto.SSL_ECH_KEYS_marshal_retry_configs(kPrivateKey); + //ASSERT_TRUE(SSL_ECH_KEYS_marshal_retry_configs(keys.get(), &ech_config_list, &ech_config_list_len)); + + /* + // ECHConfigList is just the concatenation with a length prefix. + size_t len = ech_config_len + ech_config2_len; + std::vector expected = {uint8_t(len >> 8), uint8_t(len)}; + expected.insert(expected.end(), ech_config, ech_config + ech_config_len); + expected.insert(expected.end(), ech_config2, ech_config2 + ech_config2_len); + EXPECT_EQ(Bytes(expected), Bytes(ech_config_list, ech_config_list_len)); + */ + } + + @Test + public void test_SSL_ech_accepted() throws Exception { + long c = NativeCrypto.SSL_CTX_new(); + long s = NativeCrypto.SSL_new(c, null); + + assertFalse(NativeCrypto.SSL_ech_accepted(s, null)); + + NativeCrypto.SSL_free(s, null); + NativeCrypto.SSL_CTX_free(c, null); + } + + @Test + public void test_SSL_CTX_ech_enable_server() throws Exception { + long c = NativeCrypto.SSL_CTX_new(); + + final byte[] key = readTestFile("boringssl-ech-private-key.bin"); + final byte[] serverConfig = readTestFile("boringssl-server-ech-config.bin"); + assertTrue(NativeCrypto.SSL_CTX_ech_enable_server(c, null, key, serverConfig)); + + NativeCrypto.SSL_CTX_free(c, null); + } + + @Test(expected = NullPointerException.class) + public void test_SSL_get0_ech_retry_configs_withNullShouldThrow() throws Exception { + NativeCrypto.SSL_get0_ech_retry_configs(NULL, null); + } + + @Test(expected = NullPointerException.class) + public void test_SSL_CTX_ech_enable_server_NULL_SSL_CTX() throws Exception { + NativeCrypto.SSL_CTX_ech_enable_server(NULL, null, null, null); + } + + @Test + public void test_SSL_CTX_ech_enable_server_ssl_withNullsShouldThrow() { + long c = NativeCrypto.SSL_CTX_new(); + try { + NativeCrypto.SSL_CTX_ech_enable_server(c, null, null, null); + } catch (NullPointerException | AssertionError e){ + // AssertionError when running with checkErrorQueue + return; + } + fail(); + } + + @Test + public void test_SSL_CTX_ech_enable_server_ssl_withNullConfigShouldThrow() throws Exception { + long c = NativeCrypto.SSL_CTX_new(); + // TODO running this with checkErrorQueue after test_SSL_CTX_ech_enable_server_ssl_with_bad_config fails here + final byte[] serverConfig = readTestFile("boringssl-server-ech-config.bin"); + try { + NativeCrypto.SSL_CTX_ech_enable_server(c, null, null, serverConfig); + } catch (NullPointerException | AssertionError e) { + // AssertionError when running with checkErrorQueue + return; + } + fail(); + } + + @Test + public void test_SSL_CTX_ech_enable_server_ssl_withNullKeyShouldThrow() throws Exception { + long c = NativeCrypto.SSL_CTX_new(); + final byte[] key = readTestFile("boringssl-ech-private-key.bin"); + try { + NativeCrypto.SSL_CTX_ech_enable_server(c, null, key, null); + } catch (NullPointerException | AssertionError e) { + // AssertionError when running with checkErrorQueue + return; + } + fail(); + } + @Test + public void test_SSL_CTX_ech_enable_server_ssl_with_bad_key() throws Exception { + long c = NativeCrypto.SSL_CTX_new(); + final byte[] badKey = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05}; + final byte[] serverConfig = readTestFile("boringssl-server-ech-config.bin"); + boolean enabled = false; + try { + enabled = NativeCrypto.SSL_CTX_ech_enable_server(c, null, badKey, serverConfig); + NativeCrypto.SSL_CTX_free(c, null); + } catch (AssertionError e) { + // ignored when running with checkErrorQueue + } + assertFalse(enabled); + } + + @Test + public void test_SSL_CTX_ech_enable_server_ssl_with_bad_config() throws Exception { + long c = NativeCrypto.SSL_CTX_new(); + final byte[] key = readTestFile("boringssl-ech-private-key.bin"); + byte[] badConfig = {(byte) 0xfe, (byte) 0x0d, (byte) 0xff, (byte) 0xff, (byte) 0xff}; + boolean enabled = false; + try { + enabled = NativeCrypto.SSL_CTX_ech_enable_server(c, null, key, badConfig); + NativeCrypto.SSL_CTX_free(c, null); + } catch(AssertionError e) { + // ignored when running with checkErrorQueue + } + assertFalse(enabled); + } + + @Test + public void test_SSL_CTX_ech_enable_server_ssl_with_bad_key_config() throws Exception { + long c = NativeCrypto.SSL_CTX_new(); + final byte[] badKey = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05}; + byte[] badConfig = {(byte) 0xfe, (byte) 0x0d, (byte) 0xff, (byte) 0xff, (byte) 0xff}; + boolean enabled = false; + try { + enabled = NativeCrypto.SSL_CTX_ech_enable_server(c, null, badKey, badConfig); + NativeCrypto.SSL_CTX_free(c, null); + } catch(AssertionError e) { + // ignored when running with checkErrorQueue + } + assertFalse(enabled); + } + @Test public void SSL_get_options_withNullShouldThrow() throws Exception { assertThrows(NullPointerException.class, () -> NativeCrypto.SSL_get_options(NULL, null)); @@ -534,6 +1054,7 @@ public void SSL_set_protocol_versions() throws Exception { long s = NativeCrypto.SSL_new(c, null); assertEquals(1, NativeCrypto.SSL_set_protocol_versions(s, null, TLS1_VERSION, TLS1_1_VERSION)); assertEquals(1, NativeCrypto.SSL_set_protocol_versions(s, null, TLS1_2_VERSION, TLS1_2_VERSION)); + assertEquals(1, NativeCrypto.SSL_set_protocol_versions(s, null, TLS1_3_VERSION, TLS1_3_VERSION)); assertEquals(0, NativeCrypto.SSL_set_protocol_versions(s, null, TLS1_2_VERSION + 413, TLS1_1_VERSION)); assertEquals(0, NativeCrypto.SSL_set_protocol_versions(s, null, TLS1_1_VERSION, TLS1_2_VERSION + 413)); NativeCrypto.SSL_free(s, null); @@ -650,6 +1171,8 @@ public static class Hooks { boolean pskEnabled; byte[] pskKey; List enabledCipherSuites; + byte[] echRetryConfigs; + String echNameOverride; /** * @throws SSLException if an error occurs creating the context. @@ -964,11 +1487,21 @@ public void clientCertificateRequested(long s) { } } + // wrapper method added for ECH testing + public static Future handshake(final ServerSocket listener, + final int timeout, final boolean client, final Hooks hooks, final byte[] alpnProtocols, + final ApplicationProtocolSelectorAdapter alpnSelector) { + return handshake(listener, timeout, client, hooks, alpnProtocols, alpnSelector, false); + } + public static Future handshake(final ServerSocket listener, final int timeout, final boolean client, final Hooks hooks, final byte[] alpnProtocols, - final ApplicationProtocolSelectorAdapter alpnSelector) { + final ApplicationProtocolSelectorAdapter alpnSelector, final boolean useEchRetryConfig) { + + // new in current google master branch // TODO(prb) rewrite for engine socket. FD socket calls infeasible to test on Java 17+ assumeFalse(TestUtils.isJavaVersion(17)); + ExecutorService executor = Executors.newSingleThreadExecutor(); Future future = executor.submit(new Callable() { @@ -976,8 +1509,8 @@ public static Future handshake(final ServerSocket lis public TestSSLHandshakeCallbacks call() throws Exception { // Socket needs to remain open after the handshake Socket socket = (client ? new Socket(listener.getInetAddress(), - listener.getLocalPort()) - : listener.accept()); + listener.getLocalPort()) + : listener.accept()); if (timeout == -1) { return new TestSSLHandshakeCallbacks(socket, 0, null, null); } @@ -1008,7 +1541,22 @@ public TestSSLHandshakeCallbacks call() throws Exception { if (!client && alpnSelector != null) { NativeCrypto.setHasApplicationProtocolSelector(s, null, true); } - NativeCrypto.SSL_do_handshake(s, null, fd, callback, timeout); + + // "if" added for ECH testing + if (useEchRetryConfig) { + try { + NativeCrypto.SSL_do_handshake(s, null, fd, callback, timeout); + } catch (SSLProtocolException e) { + hooks.echRetryConfigs = + NativeCrypto.SSL_get0_ech_retry_configs(s, null); + hooks.echNameOverride = + NativeCrypto.SSL_get0_ech_name_override(s, null); + throw e; + } + } else { + NativeCrypto.SSL_do_handshake(s, null, fd, callback, timeout); + } + session = NativeCrypto.SSL_get1_session(s, null); if (DEBUG) { System.out.println("ssl=0x" + Long.toString(s, 16) diff --git a/openjdk/src/test/resources/_10413._https.draft-13.esni.defo.ie.bin b/openjdk/src/test/resources/_10413._https.draft-13.esni.defo.ie.bin new file mode 100644 index 000000000..f8bb2d13a Binary files /dev/null and b/openjdk/src/test/resources/_10413._https.draft-13.esni.defo.ie.bin differ diff --git a/openjdk/src/test/resources/_11413._https.draft-13.esni.defo.ie.bin b/openjdk/src/test/resources/_11413._https.draft-13.esni.defo.ie.bin new file mode 100644 index 000000000..c8ea77d79 Binary files /dev/null and b/openjdk/src/test/resources/_11413._https.draft-13.esni.defo.ie.bin differ diff --git a/openjdk/src/test/resources/_12413._https.draft-13.esni.defo.ie.bin b/openjdk/src/test/resources/_12413._https.draft-13.esni.defo.ie.bin new file mode 100644 index 000000000..1215e1566 Binary files /dev/null and b/openjdk/src/test/resources/_12413._https.draft-13.esni.defo.ie.bin differ diff --git a/openjdk/src/test/resources/_12414._https.draft-13.esni.defo.ie.bin b/openjdk/src/test/resources/_12414._https.draft-13.esni.defo.ie.bin new file mode 100644 index 000000000..a6513f04d Binary files /dev/null and b/openjdk/src/test/resources/_12414._https.draft-13.esni.defo.ie.bin differ diff --git a/openjdk/src/test/resources/_8413._https.draft-13.esni.defo.ie.bin b/openjdk/src/test/resources/_8413._https.draft-13.esni.defo.ie.bin new file mode 100644 index 000000000..bbe810de7 Binary files /dev/null and b/openjdk/src/test/resources/_8413._https.draft-13.esni.defo.ie.bin differ diff --git a/openjdk/src/test/resources/_8414._https.draft-13.esni.defo.ie.bin b/openjdk/src/test/resources/_8414._https.draft-13.esni.defo.ie.bin new file mode 100644 index 000000000..2815e4b3a Binary files /dev/null and b/openjdk/src/test/resources/_8414._https.draft-13.esni.defo.ie.bin differ diff --git a/openjdk/src/test/resources/_9413._https.draft-13.esni.defo.ie.bin b/openjdk/src/test/resources/_9413._https.draft-13.esni.defo.ie.bin new file mode 100644 index 000000000..33032f759 Binary files /dev/null and b/openjdk/src/test/resources/_9413._https.draft-13.esni.defo.ie.bin differ diff --git a/openjdk/src/test/resources/boringssl-ech-config-list.bin b/openjdk/src/test/resources/boringssl-ech-config-list.bin new file mode 100644 index 000000000..b2d4c4baf Binary files /dev/null and b/openjdk/src/test/resources/boringssl-ech-config-list.bin differ diff --git a/openjdk/src/test/resources/boringssl-ech-private-key.bin b/openjdk/src/test/resources/boringssl-ech-private-key.bin new file mode 100644 index 000000000..f391fb419 --- /dev/null +++ b/openjdk/src/test/resources/boringssl-ech-private-key.bin @@ -0,0 +1 @@ + EE��W�~�|�@Ũ��G8d����+�|� \ No newline at end of file diff --git a/openjdk/src/test/resources/boringssl-server-ech-config.bin b/openjdk/src/test/resources/boringssl-server-ech-config.bin new file mode 100644 index 000000000..ddf593ba5 Binary files /dev/null and b/openjdk/src/test/resources/boringssl-server-ech-config.bin differ diff --git a/openjdk/src/test/resources/check-tls.akamaized.net.bin b/openjdk/src/test/resources/check-tls.akamaized.net.bin new file mode 100644 index 000000000..1ed80b328 Binary files /dev/null and b/openjdk/src/test/resources/check-tls.akamaized.net.bin differ diff --git a/openjdk/src/test/resources/cloudflare-esni.com.bin b/openjdk/src/test/resources/cloudflare-esni.com.bin new file mode 100644 index 000000000..d794be2bd Binary files /dev/null and b/openjdk/src/test/resources/cloudflare-esni.com.bin differ diff --git a/openjdk/src/test/resources/cloudflareresearch.com.bin b/openjdk/src/test/resources/cloudflareresearch.com.bin new file mode 100644 index 000000000..522390706 Binary files /dev/null and b/openjdk/src/test/resources/cloudflareresearch.com.bin differ diff --git a/openjdk/src/test/resources/crypto.cloudflare.com.bin b/openjdk/src/test/resources/crypto.cloudflare.com.bin new file mode 100644 index 000000000..081f672af Binary files /dev/null and b/openjdk/src/test/resources/crypto.cloudflare.com.bin differ diff --git a/openjdk/src/test/resources/deb.debian.org.bin b/openjdk/src/test/resources/deb.debian.org.bin new file mode 100644 index 000000000..05fb08270 Binary files /dev/null and b/openjdk/src/test/resources/deb.debian.org.bin differ diff --git a/openjdk/src/test/resources/draft-13.esni.defo.ie_10413-ech-config-list.bin b/openjdk/src/test/resources/draft-13.esni.defo.ie_10413-ech-config-list.bin new file mode 100644 index 000000000..b94202bf3 Binary files /dev/null and b/openjdk/src/test/resources/draft-13.esni.defo.ie_10413-ech-config-list.bin differ diff --git a/openjdk/src/test/resources/draft-13.esni.defo.ie_11413-ech-config-list.bin b/openjdk/src/test/resources/draft-13.esni.defo.ie_11413-ech-config-list.bin new file mode 100644 index 000000000..b94202bf3 Binary files /dev/null and b/openjdk/src/test/resources/draft-13.esni.defo.ie_11413-ech-config-list.bin differ diff --git a/openjdk/src/test/resources/draft-13.esni.defo.ie_12413-ech-config-list.bin b/openjdk/src/test/resources/draft-13.esni.defo.ie_12413-ech-config-list.bin new file mode 100644 index 000000000..b94202bf3 Binary files /dev/null and b/openjdk/src/test/resources/draft-13.esni.defo.ie_12413-ech-config-list.bin differ diff --git a/openjdk/src/test/resources/draft-13.esni.defo.ie_12414-ech-config-list.bin b/openjdk/src/test/resources/draft-13.esni.defo.ie_12414-ech-config-list.bin new file mode 100644 index 000000000..b94202bf3 Binary files /dev/null and b/openjdk/src/test/resources/draft-13.esni.defo.ie_12414-ech-config-list.bin differ diff --git a/openjdk/src/test/resources/draft-13.esni.defo.ie_8413-ech-config-list.bin b/openjdk/src/test/resources/draft-13.esni.defo.ie_8413-ech-config-list.bin new file mode 100644 index 000000000..b94202bf3 Binary files /dev/null and b/openjdk/src/test/resources/draft-13.esni.defo.ie_8413-ech-config-list.bin differ diff --git a/openjdk/src/test/resources/draft-13.esni.defo.ie_8414-ech-config-list.bin b/openjdk/src/test/resources/draft-13.esni.defo.ie_8414-ech-config-list.bin new file mode 100644 index 000000000..c41868c4d Binary files /dev/null and b/openjdk/src/test/resources/draft-13.esni.defo.ie_8414-ech-config-list.bin differ diff --git a/openjdk/src/test/resources/draft-13.esni.defo.ie_9413-ech-config-list.bin b/openjdk/src/test/resources/draft-13.esni.defo.ie_9413-ech-config-list.bin new file mode 100644 index 000000000..b94202bf3 Binary files /dev/null and b/openjdk/src/test/resources/draft-13.esni.defo.ie_9413-ech-config-list.bin differ diff --git a/openjdk/src/test/resources/duckduckgo.com.bin b/openjdk/src/test/resources/duckduckgo.com.bin new file mode 100644 index 000000000..05089961e Binary files /dev/null and b/openjdk/src/test/resources/duckduckgo.com.bin differ diff --git a/openjdk/src/test/resources/en.wikipedia.org.bin b/openjdk/src/test/resources/en.wikipedia.org.bin new file mode 100644 index 000000000..625edeebc Binary files /dev/null and b/openjdk/src/test/resources/en.wikipedia.org.bin differ diff --git a/openjdk/src/test/resources/enabled.tls13.com.bin b/openjdk/src/test/resources/enabled.tls13.com.bin new file mode 100644 index 000000000..912bc1694 Binary files /dev/null and b/openjdk/src/test/resources/enabled.tls13.com.bin differ diff --git a/openjdk/src/test/resources/mirrors.kernel.org.bin b/openjdk/src/test/resources/mirrors.kernel.org.bin new file mode 100644 index 000000000..21a140170 Binary files /dev/null and b/openjdk/src/test/resources/mirrors.kernel.org.bin differ diff --git a/openjdk/src/test/resources/openstreetmap.org.bin b/openjdk/src/test/resources/openstreetmap.org.bin new file mode 100644 index 000000000..1239566fc Binary files /dev/null and b/openjdk/src/test/resources/openstreetmap.org.bin differ diff --git a/openjdk/src/test/resources/tls13.1d.pw.bin b/openjdk/src/test/resources/tls13.1d.pw.bin new file mode 100644 index 000000000..96ff98d0c Binary files /dev/null and b/openjdk/src/test/resources/tls13.1d.pw.bin differ diff --git a/openjdk/src/test/resources/web.wechat.com.bin b/openjdk/src/test/resources/web.wechat.com.bin new file mode 100644 index 000000000..e69de29bb diff --git a/openjdk/src/test/resources/www.google.com.bin b/openjdk/src/test/resources/www.google.com.bin new file mode 100644 index 000000000..25ef07d78 Binary files /dev/null and b/openjdk/src/test/resources/www.google.com.bin differ diff --git a/openjdk/src/test/resources/www.yandex.ru.bin b/openjdk/src/test/resources/www.yandex.ru.bin new file mode 100644 index 000000000..a1260e08d Binary files /dev/null and b/openjdk/src/test/resources/www.yandex.ru.bin differ