Skip to content

Basic TLS Encrypted ClientHello (ECH) support (updated) #1340

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c9968c0
wire up ECH functions from boringssl
eighthave Sep 17, 2021
3ae5bd7
implement ECH Retry Config handling
eighthave Nov 10, 2021
e39aca1
use Android DnsPacket to implement DNS using JNDI/DnsResolver
eighthave Oct 21, 2021
dcbddbb
hack in Exception to handle "ECH_REJECTED" and Retry Configs
eighthave Nov 17, 2021
b1a6773
gradlew: add distributionSha256Sum to verify download
eighthave May 11, 2021
50f54ed
Convenient debug print for ECH Config Lists
eighthave Nov 10, 2021
cb3fb9c
EchInteropTest
eighthave Nov 10, 2021
7ed0dd3
add .gitlab-ci.yml
eighthave Oct 15, 2019
37198ff
additional tests that trigger AssertionError with checkErrorQueue
eighthave Nov 12, 2021
c3a1454
WIP implement full server ECH API
eighthave Nov 10, 2021
0f2c303
changes to resolve compile issues
mnbogner May 12, 2025
e4bf0ab
Merge branch 'master' of https://github.com/google/conscrypt into mas…
mnbogner May 12, 2025
679e4ec
Merge branch 'master' of https://github.com/eighthave/conscrypt into …
mnbogner May 13, 2025
e9de453
fixing several compile errors
mnbogner May 14, 2025
3235c53
restore missing test code
mnbogner May 15, 2025
4cb4ebd
revised test cases to handle exceptions better and print louder log m…
mnbogner May 23, 2025
5bb8514
revert changes required for local build issues
mnbogner May 26, 2025
a7de26c
revised tests to use live dns results instead of parsed files
mnbogner May 31, 2025
1481f4b
replace parameters with parameter object
mnbogner Jun 3, 2025
23ba5bb
fleshed out Conscrypt method descriptions based on feedback
mnbogner Jun 11, 2025
d24922d
Merge branch 'master' of https://github.com/google/conscrypt into bas…
mnbogner Jun 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -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
271 changes: 270 additions & 1 deletion common/src/jni/main/cpp/conscrypt/native_crypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<SSL_ECH_KEYS*>(static_cast<uintptr_t>(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 <typename T>
static T* fromContextObject(JNIEnv* env, jobject contextObject) {
if (contextObject == nullptr) {
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -11610,6 +11619,250 @@ static jlong NativeCrypto_SSL_get1_session(JNIEnv* env, jclass, jlong ssl_addres
return reinterpret_cast<uintptr_t>(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<const uint8_t*>(configBytes.get());
int ret = SSL_set1_ech_config_list(ssl, reinterpret_cast<const uint8_t*>(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<jsize>(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<jsize>(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<const uint8_t*>(keyBytes.get());
size_t keySize = keyBytes.size();
bssl::UniquePtr<SSL_ECH_KEYS> 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<SSL_ECH_KEYS> 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<SSL_ECH_KEYS> 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<uint8_t> free_ech_config_list(ech_config_list); // is this OPENSSL_free(ech_config_list)?
jbyteArray result = env->NewByteArray(static_cast<jsize>(ech_config_list_len));
if (result != nullptr) {
env->SetByteArrayRegion(result, 0, static_cast<jsize>(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<const uint8_t*>(keyBytes.get());
size_t ech_key_size = keyBytes.size();
const uint8_t* ech_config = reinterpret_cast<const uint8_t*>(configBytes.get());
size_t ech_config_size = configBytes.size();
bssl::UniquePtr<SSL_ECH_KEYS> 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) \
Expand Down Expand Up @@ -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"),
Expand Down
10 changes: 10 additions & 0 deletions common/src/main/java/org/conscrypt/AbstractConscryptEngine.java
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading