Skip to content
Merged
6 changes: 6 additions & 0 deletions android/lint.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,10 @@
<issue id="Assert">
<ignore path="**/common/src/main/java/org/conscrypt/OpenSSLCipherChaCha20.java" />
</issue>

<!-- Workaround for "Unexpected failure during lint analysis". -->
<issue id="LintError">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

side note: do you know why this is necessary for your setup? I don't see any issues locally, but it might be due to some version difference?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had forgotten about this change. I assume it's an issue with my local configuration but the error wasn't specific. I can pull it out when I'm done.

<ignore regexp=".*module-info\.class.*"/>
</issue>

</lint>
236 changes: 232 additions & 4 deletions common/src/jni/main/cpp/conscrypt/native_crypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,16 @@ 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 @@ -1157,19 +1167,30 @@ static jint NativeCrypto_EVP_PKEY_cmp(JNIEnv* env, jclass, jobject pkey1Ref, job
JNI_TRACE("EVP_PKEY_cmp(%p, %p)", pkey1Ref, pkey2Ref);
EVP_PKEY* pkey1 = fromContextObject<EVP_PKEY>(env, pkey1Ref);
if (pkey1 == nullptr) {
conscrypt::jniutil::throwNullPointerException(env, "Null pointer, key 1");
ERR_clear_error();
JNI_TRACE("EVP_PKEY_cmp => pkey1 == null");
return 0;
}
EVP_PKEY* pkey2 = fromContextObject<EVP_PKEY>(env, pkey2Ref);
if (pkey2 == nullptr) {
conscrypt::jniutil::throwNullPointerException(env, "Null pointer, key 2");
ERR_clear_error();
JNI_TRACE("EVP_PKEY_cmp => pkey2 == null");
return 0;
}
JNI_TRACE("EVP_PKEY_cmp(%p, %p) <- ptr", pkey1, pkey2);

int result = EVP_PKEY_cmp(pkey1, pkey2);
JNI_TRACE("EVP_PKEY_cmp(%p, %p) => %d", pkey1, pkey2, result);
return result;
if (result < 0) {
conscrypt::jniutil::throwInvalidKeyException(env, "Decoding error");
ERR_clear_error();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drive-by comment: If you're going to throw a checked exception, you should reflect that in the Java method signature and add a regression test for it. Same for the parsing exception below.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, but more generally let's not change the non-ECH part of the code in this pull request. If you wanted to, please follow up with a new PR.

Could you please revert the changes here that are not related to ECH? Thanks

Copy link
Contributor Author

@mnbogner mnbogner Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ended up making these changes because one of the failing tests was the one that tested this method:

org.conscrypt.ConscryptOpenJdkSuite > org.conscrypt.NativeCryptoTest.EVP_PKEY_cmp_BothNullParameters FAILED
    java.lang.AssertionError: unexpected exception type thrown; expected:<java.lang.NullPointerException> but was:<java.lang.AssertionError>
        at org.junit.Assert.assertThrows(Assert.java:1020)
        at org.junit.Assert.assertThrows(Assert.java:981)
        at org.conscrypt.NativeCryptoTest.EVP_PKEY_cmp_BothNullParameters(NativeCryptoTest.java:244)
        Caused by:
        java.lang.AssertionError: Error queue should have been empty but was (/home/mnbogner/Software/Repository/boringssl/ssl/encrypted_client_hello.cc:1039) error:10000089:SSL routines:OPENSSL_internal:DECODE_ERROR
            at org.conscrypt.NativeCrypto.EVP_PKEY_cmp(Native Method)
            at org.conscrypt.NativeCryptoTest.lambda$EVP_PKEY_cmp_BothNullParameters$0(NativeCryptoTest.java:244)
            at org.junit.Assert.assertThrows(Assert.java:1001)
            ... 2 more

Could this just be failing locally for some reason? I suppose I can revert it and see what happens with the CI tests.

(UPDATE) Commented this part out for now and pushed it. If there's no CI failures I'll pull out all the extra stuff and we can wrap this up.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, but more generally let's not change the non-ECH part of the code in this pull request. If you wanted to, please follow up with a new PR.

Could you please revert the changes here that are not related to ECH? Thanks

Unfortunately it looks like EVP_PKEY_cmp_BothNullParameters still fails (and test_get_RSA_public_params, which doesn't fail locally). I assume this isn't failing elsewhere, but I'm not sure how my changes could have caused this. Could the failure be caused by errors placed in the queue by a prior test? I notice that the failing test is right after one of the new ECH tests.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I think your hunch is correct. If you look at the error in the CI, the complete error is:

2025-09-23T20:09:49.2214351Z org.conscrypt.ConscryptOpenJdkSuite > org.conscrypt.NativeCryptoTest.EVP_PKEY_cmp_BothNullParameters FAILED
2025-09-23T20:09:49.2215827Z     java.lang.AssertionError: unexpected exception type thrown; expected:<java.lang.NullPointerException> but was:<java.lang.AssertionError>
2025-09-23T20:09:49.2229754Z         at org.junit.Assert.assertThrows(Assert.java:1020)
2025-09-23T20:09:49.2230440Z         at org.junit.Assert.assertThrows(Assert.java:981)
2025-09-23T20:09:49.2231640Z         at org.conscrypt.NativeCryptoTest.EVP_PKEY_cmp_BothNullParameters(NativeCryptoTest.java:248)
2025-09-23T20:09:49.2232278Z 
2025-09-23T20:09:49.2232411Z         Caused by:
2025-09-23T20:09:49.2233793Z         java.lang.AssertionError: Error queue should have been empty but was (/home/runner/work/_temp/boringssl/ssl/encrypted_client_hello.cc:1039) error:10000089:SSL routines:OPENSSL_internal:DECODE_ERROR
2025-09-23T20:09:49.2235956Z             at org.conscrypt.NativeCrypto.EVP_PKEY_cmp(Native Method)
2025-09-23T20:09:49.2236915Z             at org.conscrypt.NativeCryptoTest.lambda$EVP_PKEY_cmp_BothNullParameters$0(NativeCryptoTest.java:248)
2025-09-23T20:09:49.2237827Z             at org.junit.Assert.assertThrows(Assert.java:1001)
2025-09-23T20:09:49.2238310Z             ... 2 more

Note how the error queue is populated in encrypted_client_hello.cc:1039. Looking at NativeCrypto_SSL_CTX_ech_enable_server, I think you would need to clear the queue on line 1193, if SSL_ECH_KEYS_add failed.

JNI_TRACE("VP_PKEY_cmp(%p, %p) => threw exception", pkey1, pkey2);
return result;
} else {
JNI_TRACE("EVP_PKEY_cmp(%p, %p) => %d", pkey1, pkey2, result);
return result;
}
}

/*
Expand Down Expand Up @@ -7786,7 +7807,12 @@ static int sslSelect(JNIEnv* env, int type, jobject fdObject, AppData* appData,
if (fds[1].revents & POLLIN) {
char token;
do {
(void)read(appData->fdsEmergency[0], &token, 1);
// TEMP - fixes build error
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you get a compiler warning here otherwise?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one was more specific:

error: ignoring return value of ‘ssize_t read(int, void*, size_t)’ declared with attribute ‘warn_unused_result’ [-Werror=unused-result]
 7798 |                 (void)read(appData->fdsEmergency[0], &token, 1);
      |                       ~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

First I tried assigning the result to a variable, but then it complained that the variable was unused and that's how I ended up with the current fix. There's about 4 places where this happens but this was the only one in a file I had to commit.

int foo = 0;
foo = read(appData->fdsEmergency[0], &token, 1);
if (foo > 0) {
CONSCRYPT_LOG_VERBOSE("FOO: %d", foo);
}
} while (errno == EINTR);
}
}
Expand Down Expand Up @@ -7817,7 +7843,12 @@ static void sslNotify(AppData* appData) {
char token = '*';
do {
errno = 0;
(void)write(appData->fdsEmergency[1], &token, 1);
// TEMP - fixes build error
int foo = 0;
foo = write(appData->fdsEmergency[1], &token, 1);
if (foo > 0) {
CONSCRYPT_LOG_VERBOSE("FOO: %d", foo);
}
} while (errno == EINTR);
errno = errnoBackup;
#endif
Expand Down Expand Up @@ -8269,6 +8300,7 @@ static jlong NativeCrypto_SSL_CTX_new(JNIEnv* env, jclass) {
bssl::UniquePtr<SSL_CTX> sslCtx(SSL_CTX_new(TLS_with_buffers_method()));
if (sslCtx.get() == nullptr) {
conscrypt::jniutil::throwExceptionFromBoringSSLError(env, "SSL_CTX_new");
ERR_clear_error();
return 0;
}
SSL_CTX_set_options(
Expand Down Expand Up @@ -11776,6 +11808,191 @@ 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) {
conscrypt::jniutil::throwNullPointerException(env, "Null pointer, ssl address");
ERR_clear_error();
return JNI_FALSE;
}
ScopedByteArrayRO configBytes(env, configJavaBytes);
if (configBytes.get() == nullptr) {
conscrypt::jniutil::throwNullPointerException(env, "Null pointer, ech config");
ERR_clear_error();
JNI_TRACE("NativeCrypto_SSL_set1_ech_config_list => could not read config bytes");
return JNI_FALSE;
}
int ret = SSL_set1_ech_config_list(ssl, reinterpret_cast<const uint8_t*>(configBytes.get()),
configBytes.size());
if (!ret) {
conscrypt::jniutil::throwParsingException(env, "Error parsing ECH config");
ERR_clear_error();
JNI_TRACE("ssl=%p NativeCrypto_SSL_set1_ech_config_list(%p) => threw exception", ssl, configJavaBytes);
return JNI_FALSE;
} else {
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);
if (retry_configs_len <= 0) {
return nullptr;
}
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),
reinterpret_cast<const jbyte*>(retry_configs));
JNI_TRACE("ssl=%p NativeCrypto_SSL_get0_ech_retry_configs() => %p", ssl, result);
return result;
}

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();
}

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);
}

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);
}

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) {
conscrypt::jniutil::throwNullPointerException(env, "Null pointer, ssl address");
ERR_clear_error();
return JNI_FALSE;
}
jboolean accepted = SSL_ech_accepted(ssl);

if (!accepted) {
conscrypt::jniutil::throwParsingException(env, "Invalid ECH config list");
ERR_clear_error();
JNI_TRACE("ssl=%p NativeCrypto_SSL_ech_accepted => threw exception", ssl);
return JNI_FALSE;
} else {
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 @@ -12133,6 +12350,17 @@ 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 TESTING
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_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_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
26 changes: 26 additions & 0 deletions common/src/main/java/org/conscrypt/NativeCrypto.java
Original file line number Diff line number Diff line change
Expand Up @@ -1622,6 +1622,32 @@ static native byte[] Scrypt_generate_key(
*/
static native boolean usesBoringSsl_FIPS_mode();

/* ECH */

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 byte[] SSL_ECH_KEYS_marshal_retry_configs(byte[] key);

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.
*/
Expand Down
9 changes: 9 additions & 0 deletions openjdk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,15 @@ def testInterop = tasks.register("testInterop", Test) {
}
check.dependsOn testInterop

// Added to see results of new ECH tests when running tests from the command line
tasks.withType(Test).configureEach {
testLogging {
exceptionFormat "full"
events "started", "skipped", "passed", "failed"
showStandardStreams true
}
}

jacocoTestReport {
additionalSourceDirs.from files("$rootDir/openjdk/src/test/java", "$rootDir/common/src/main/java")
executionData tasks.withType(Test)
Expand Down
Loading
Loading