Skip to content

Commit 18baf53

Browse files
committed
implement ECH Retry Config handling
This introduces a new Exception so that clients can respond only to the ECH retry request without having to parse SSLHandshakeExceptions in general. This exception should probably be implemented in boringssl or native_crypto.cc.
1 parent 2645b84 commit 18baf53

13 files changed

+264
-1
lines changed

common/src/jni/main/cpp/conscrypt/native_crypto.cc

+46
Original file line numberDiff line numberDiff line change
@@ -10474,6 +10474,50 @@ static jboolean NativeCrypto_SSL_set1_ech_config_list(JNIEnv* env, jclass, jlong
1047410474
return !!ret;
1047510475
}
1047610476

10477+
static jstring NativeCrypto_SSL_get0_ech_name_override(JNIEnv* env, jclass, jlong ssl_address,
10478+
CONSCRYPT_UNUSED jobject ssl_holder) {
10479+
CHECK_ERROR_QUEUE_ON_RETURN;
10480+
SSL* ssl = to_SSL(env, ssl_address, true);
10481+
JNI_TRACE("ssl=%p NativeCrypto_SSL_get0_ech_name_override()", ssl);
10482+
if (ssl == nullptr) {
10483+
JNI_TRACE("ssl=%p NativeCrypto_SSL_get0_ech_name_override() => nullptr", ssl);
10484+
return nullptr;
10485+
}
10486+
const char* ech_name_override;
10487+
size_t ech_name_override_len;
10488+
SSL_get0_ech_name_override(ssl, &ech_name_override, &ech_name_override_len);
10489+
if (ech_name_override_len > 0) {
10490+
jstring name = env->NewStringUTF(ech_name_override);
10491+
return name;
10492+
}
10493+
return nullptr;
10494+
}
10495+
10496+
static jbyteArray NativeCrypto_SSL_get0_ech_retry_configs(JNIEnv* env, jclass, jlong ssl_address,
10497+
CONSCRYPT_UNUSED jobject ssl_holder) {
10498+
CHECK_ERROR_QUEUE_ON_RETURN;
10499+
SSL* ssl = to_SSL(env, ssl_address, true);
10500+
JNI_TRACE("ssl=%p NativeCrypto_SSL_get0_ech_retry_configs()", ssl);
10501+
if (ssl == nullptr) {
10502+
return nullptr;
10503+
}
10504+
10505+
const uint8_t *retry_configs;
10506+
size_t retry_configs_len;
10507+
SSL_get0_ech_retry_configs(ssl, &retry_configs, &retry_configs_len);
10508+
10509+
jbyteArray result = env->NewByteArray(static_cast<jsize>(retry_configs_len));
10510+
if (result == nullptr) {
10511+
JNI_TRACE("ssl=%p NativeCrypto_SSL_get0_ech_retry_configs() => creating byte array failed",
10512+
ssl);
10513+
return nullptr;
10514+
}
10515+
env->SetByteArrayRegion(result, 0, static_cast<jsize>(retry_configs_len),
10516+
(const jbyte*) retry_configs);
10517+
JNI_TRACE("ssl=%p NativeCrypto_SSL_get0_ech_retry_configs(%p) => %p", ssl, ssl, result);
10518+
return result;
10519+
}
10520+
1047710521
/**
1047810522
* public static native long SSL_ech_accepted(long ssl);
1047910523
*/
@@ -10851,6 +10895,8 @@ static JNINativeMethod sNativeCryptoMethods[] = {
1085110895
CONSCRYPT_NATIVE_METHOD(usesBoringSsl_FIPS_mode, "()Z"),
1085210896
CONSCRYPT_NATIVE_METHOD(SSL_set_enable_ech_grease, "(J" REF_SSL "Z)V"),
1085310897
CONSCRYPT_NATIVE_METHOD(SSL_set1_ech_config_list, "(J" REF_SSL "[B)Z"),
10898+
CONSCRYPT_NATIVE_METHOD(SSL_get0_ech_name_override, "(J" REF_SSL ")Ljava/lang/String;"),
10899+
CONSCRYPT_NATIVE_METHOD(SSL_get0_ech_retry_configs, "(J" REF_SSL ")[B"),
1085410900
CONSCRYPT_NATIVE_METHOD(SSL_ech_accepted, "(J" REF_SSL ")Z"),
1085510901
CONSCRYPT_NATIVE_METHOD(SSL_CTX_ech_enable_server, "(J" REF_SSL_CTX "[B[B)Z"),
1085610902

common/src/main/java/org/conscrypt/AbstractConscryptEngine.java

+4
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ abstract class AbstractConscryptEngine extends SSLEngine {
100100

101101
public abstract byte[] getEchConfigList();
102102

103+
public abstract String getEchNameOverride();
104+
105+
public abstract byte[] getEchRetryConfig();
106+
103107
public abstract boolean echAccepted();
104108

105109
/* @Override */

common/src/main/java/org/conscrypt/AbstractConscryptSocket.java

+4
Original file line numberDiff line numberDiff line change
@@ -761,5 +761,9 @@ abstract byte[] exportKeyingMaterial(String label, byte[] context, int length)
761761

762762
public abstract byte[] getEchConfigList();
763763

764+
public abstract String getEchNameOverride();
765+
766+
public abstract byte[] getEchRetryConfig();
767+
764768
public abstract boolean echAccepted();
765769
}

common/src/main/java/org/conscrypt/Conscrypt.java

+8
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,14 @@ public static byte[] getEchConfigList(SSLSocket socket) {
510510
return toConscrypt(socket).getEchConfigList();
511511
}
512512

513+
public static String getEchNameOverride(SSLSocket socket) {
514+
return toConscrypt(socket).getEchNameOverride();
515+
}
516+
517+
public static byte[] getEchRetryConfig(SSLSocket socket) {
518+
return toConscrypt(socket).getEchConfigList();
519+
}
520+
513521
public static boolean echAccepted(SSLSocket socket) {
514522
return toConscrypt(socket).echAccepted();
515523
}

common/src/main/java/org/conscrypt/ConscryptEngine.java

+10
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,16 @@ public byte[] getEchConfigList() {
415415
return sslParameters.getEchConfigList();
416416
}
417417

418+
@Override
419+
public String getEchNameOverride() {
420+
return ssl.getEchNameOverride();
421+
}
422+
423+
@Override
424+
public byte[] getEchRetryConfig() {
425+
return ssl.getEchRetryConfig();
426+
}
427+
418428
@Override
419429
public boolean echAccepted() {
420430
return ssl.echAccepted();

common/src/main/java/org/conscrypt/ConscryptEngineSocket.java

+10
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,16 @@ public byte[] getEchConfigList() {
420420
return engine.getEchConfigList();
421421
}
422422

423+
@Override
424+
public String getEchNameOverride() {
425+
return engine.getEchNameOverride();
426+
}
427+
428+
@Override
429+
public byte[] getEchRetryConfig() {
430+
return engine.getEchRetryConfig();
431+
}
432+
423433
@Override
424434
public boolean echAccepted() {
425435
return engine.echAccepted();

common/src/main/java/org/conscrypt/ConscryptFileDescriptorSocket.java

+10
Original file line numberDiff line numberDiff line change
@@ -931,6 +931,16 @@ public byte[] getEchConfigList() {
931931
return sslParameters.getEchConfigList();
932932
}
933933

934+
@Override
935+
public String getEchNameOverride() {
936+
return ssl.getEchNameOverride();
937+
}
938+
939+
@Override
940+
public byte[] getEchRetryConfig() {
941+
return ssl.getEchRetryConfig();
942+
}
943+
934944
@Override
935945
public boolean echAccepted() {
936946
return ssl.echAccepted();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package org.conscrypt;
2+
3+
import javax.net.ssl.SSLHandshakeException;
4+
5+
/**
6+
* The server rejected the ECH Config List, and might have supplied an ECH
7+
* Retry Config.
8+
*
9+
* @see NativeCrypto#SSL_get0_ech_retry_configs(long, NativeSsl)
10+
*/
11+
public class EchRejectedException extends SSLHandshakeException {
12+
private static final long serialVersionUID = 98723498273473923L;
13+
14+
EchRejectedException(String message) {
15+
super(message);
16+
}
17+
}
18+

common/src/main/java/org/conscrypt/Java8EngineWrapper.java

+10
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,16 @@ public byte[] getEchConfigList() {
136136
return delegate.getEchConfigList();
137137
}
138138

139+
@Override
140+
public String getEchNameOverride() {
141+
return delegate.getEchNameOverride();
142+
}
143+
144+
@Override
145+
public byte[] getEchRetryConfig() {
146+
return delegate.getEchRetryConfig();
147+
}
148+
139149
@Override
140150
public boolean echAccepted() {
141151
return delegate.echAccepted();

common/src/main/java/org/conscrypt/NativeCrypto.java

+4
Original file line numberDiff line numberDiff line change
@@ -1454,6 +1454,10 @@ static native void ENGINE_SSL_shutdown(long ssl, NativeSsl ssl_holder, SSLHandsh
14541454

14551455
static native boolean SSL_set1_ech_config_list(long ssl, NativeSsl ssl_holder, byte[] echConfig);
14561456

1457+
static native String SSL_get0_ech_name_override(long ssl, NativeSsl ssl_holder);
1458+
1459+
static native byte[] SSL_get0_ech_retry_configs(long ssl, NativeSsl ssl_holder);
1460+
14571461
static native boolean SSL_ech_accepted(long ssl, NativeSsl ssl_holder);
14581462

14591463
static native boolean SSL_CTX_ech_enable_server(long sslCtx, AbstractSessionContext holder,

common/src/main/java/org/conscrypt/NativeSsl.java

+8
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,14 @@ byte[] getTlsChannelId() throws SSLException {
279279
return NativeCrypto.SSL_get_tls_channel_id(ssl, this);
280280
}
281281

282+
String getEchNameOverride() {
283+
return NativeCrypto.SSL_get0_ech_name_override(ssl, this);
284+
}
285+
286+
byte[] getEchRetryConfig() {
287+
return NativeCrypto.SSL_get0_ech_retry_configs(ssl, this);
288+
}
289+
282290
boolean echAccepted() {
283291
return NativeCrypto.SSL_ech_accepted(ssl, this);
284292
}

common/src/main/java/org/conscrypt/SSLUtils.java

+5
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,11 @@ static SSLHandshakeException toSSLHandshakeException(Throwable e) {
359359
return (SSLHandshakeException) e;
360360
}
361361

362+
if (e.getMessage().contains(":ECH_REJECTED ")) {
363+
// TODO this should probably be implemented in boringssl
364+
return (SSLHandshakeException) new EchRejectedException(e.getMessage()).initCause(e);
365+
}
366+
362367
return (SSLHandshakeException) new SSLHandshakeException(e.getMessage()).initCause(e);
363368
}
364369

openjdk/src/test/java/org/conscrypt/NativeCryptoTest.java

+127-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import static org.conscrypt.NativeConstants.TLS1_VERSION;
3030
import static org.conscrypt.TestUtils.openTestFile;
3131
import static org.conscrypt.TestUtils.readTestFile;
32+
import static org.junit.Assert.assertArrayEquals;
3233
import static org.junit.Assert.assertEquals;
3334
import static org.junit.Assert.assertFalse;
3435
import static org.junit.Assert.assertNotNull;
@@ -528,6 +529,9 @@ public long beforeHandshake(long c) throws SSLException {
528529
public void afterHandshake(long session, long ssl, long context, Socket socket,
529530
FileDescriptor fd, SSLHandshakeCallbacks callback) throws Exception {
530531
assertFalse(NativeCrypto.SSL_ech_accepted(ssl, null));
532+
assertNull(NativeCrypto.SSL_get0_ech_name_override(ssl, null));
533+
byte[] retryConfigs = NativeCrypto.SSL_get0_ech_retry_configs(ssl, null);
534+
assertEquals(5, retryConfigs.length); // should be the invalid ECH Config List
531535
super.afterHandshake(session, ssl, context, socket, fd, callback);
532536
}
533537
};
@@ -626,6 +630,103 @@ public void afterHandshake(long session, long ssl, long context, Socket socket,
626630
assertTrue(serverCallback.serverCertificateRequestedInvoked);
627631
}
628632

633+
@Test
634+
public void test_SSL_do_handshake_ech_retry_configs() throws Exception {
635+
final ServerSocket listener = newServerSocket();
636+
637+
final byte[] key = readTestFile("boringssl-ech-private-key.bin");
638+
final byte[] serverConfig = readTestFile("boringssl-server-ech-config.bin");
639+
final byte[] originalClientConfigList = readTestFile("boringssl-ech-config-list.bin");
640+
final byte[] clientConfigList = originalClientConfigList.clone();
641+
clientConfigList[20] = (byte) (clientConfigList[20] % 255 + 1); // corrupt it
642+
643+
Hooks cHooks = new ClientHooks() {
644+
@Override
645+
public long beforeHandshake(long c) throws SSLException {
646+
long ssl = super.beforeHandshake(c);
647+
assertEquals(1, NativeCrypto.SSL_set_protocol_versions(ssl, null, TLS1_VERSION, TLS1_3_VERSION));
648+
assertTrue(NativeCrypto.SSL_set1_ech_config_list(ssl, null, clientConfigList));
649+
return ssl;
650+
}
651+
652+
@Override
653+
public void afterHandshake(long session, long ssl, long context, Socket socket,
654+
FileDescriptor fd, SSLHandshakeCallbacks callback) {
655+
fail();
656+
}
657+
};
658+
Hooks sHooks = new ServerHooks(getServerPrivateKey(), getEncodedServerCertificates()) {
659+
@Override
660+
public long beforeHandshake(long c) throws SSLException {
661+
long ssl = super.beforeHandshake(c);
662+
assertEquals(1, NativeCrypto.SSL_set_protocol_versions(ssl, null, TLS1_VERSION, TLS1_3_VERSION));
663+
assertTrue(NativeCrypto.SSL_CTX_ech_enable_server(c, null, key, serverConfig));
664+
return ssl;
665+
}
666+
667+
@Override
668+
public void afterHandshake(long session, long ssl, long context, Socket socket,
669+
FileDescriptor fd, SSLHandshakeCallbacks callback) throws Exception {
670+
assertTrue(NativeCrypto.SSL_ech_accepted(ssl, null));
671+
super.afterHandshake(session, ssl, context, socket, fd, callback);
672+
}
673+
};
674+
Future<TestSSLHandshakeCallbacks> client = handshake(listener, 0, true, cHooks, null, null, true);
675+
Future<TestSSLHandshakeCallbacks> server = handshake(listener, 0, false, sHooks, null, null, true);
676+
TestSSLHandshakeCallbacks clientCallback = null;
677+
TestSSLHandshakeCallbacks serverCallback = null;
678+
try {
679+
clientCallback = client.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
680+
serverCallback = server.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
681+
} catch (ExecutionException e) {
682+
// caused by SSLProtocolException
683+
}
684+
assertNull(clientCallback);
685+
assertNull(serverCallback);
686+
assertArrayEquals(originalClientConfigList, cHooks.echRetryConfigs);
687+
assertEquals("example.com", cHooks.echNameOverride);
688+
assertNotNull(cHooks.echRetryConfigs);
689+
assertNull(sHooks.echNameOverride);
690+
assertNull(sHooks.echRetryConfigs);
691+
692+
final byte[] echRetryConfigsFromPrevious = cHooks.echRetryConfigs;
693+
cHooks = new ClientHooks() {
694+
@Override
695+
public long beforeHandshake(long c) throws SSLException {
696+
long ssl = super.beforeHandshake(c);
697+
assertEquals(1, NativeCrypto.SSL_set_protocol_versions(ssl, null, TLS1_VERSION, TLS1_3_VERSION));
698+
assertTrue(NativeCrypto.SSL_set1_ech_config_list(ssl, null, echRetryConfigsFromPrevious));
699+
return ssl;
700+
}
701+
702+
@Override
703+
public void afterHandshake(long session, long ssl, long context, Socket socket,
704+
FileDescriptor fd, SSLHandshakeCallbacks callback) throws Exception {
705+
assertTrue(NativeCrypto.SSL_ech_accepted(ssl, null));
706+
super.afterHandshake(session, ssl, context, socket, fd, callback);
707+
}
708+
};
709+
710+
client = handshake(listener, 0, true, cHooks, null, null);
711+
server = handshake(listener, 0, false, sHooks, null, null);
712+
clientCallback = client.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
713+
serverCallback = server.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
714+
assertTrue(clientCallback.verifyCertificateChainCalled);
715+
assertEqualCertificateChains(
716+
getServerCertificateRefs(), clientCallback.certificateChainRefs);
717+
assertFalse(serverCallback.verifyCertificateChainCalled);
718+
assertFalse(clientCallback.clientCertificateRequestedCalled);
719+
assertFalse(serverCallback.clientCertificateRequestedCalled);
720+
assertFalse(clientCallback.clientPSKKeyRequestedInvoked);
721+
assertFalse(serverCallback.clientPSKKeyRequestedInvoked);
722+
assertFalse(clientCallback.serverPSKKeyRequestedInvoked);
723+
assertFalse(serverCallback.serverPSKKeyRequestedInvoked);
724+
assertTrue(clientCallback.handshakeCompletedCalled);
725+
assertTrue(serverCallback.handshakeCompletedCalled);
726+
assertFalse(clientCallback.serverCertificateRequestedInvoked);
727+
assertTrue(serverCallback.serverCertificateRequestedInvoked);
728+
}
729+
629730
@Test
630731
public void test_SSL_set_enable_ech_grease() throws Exception {
631732
long c = NativeCrypto.SSL_CTX_new();
@@ -688,6 +789,11 @@ public void test_SSL_CTX_ech_enable_server() throws Exception {
688789
NativeCrypto.SSL_CTX_free(c, null);
689790
}
690791

792+
@Test(expected = NullPointerException.class)
793+
public void test_SSL_get0_ech_retry_configs_withNullShouldThrow() throws Exception {
794+
NativeCrypto.SSL_get0_ech_retry_configs(NULL, null);
795+
}
796+
691797
@Test(expected = NullPointerException.class)
692798
public void test_SSL_CTX_ech_enable_server_NULL_SSL_CTX() throws Exception {
693799
NativeCrypto.SSL_CTX_ech_enable_server(NULL, null, null, null);
@@ -933,6 +1039,8 @@ public static class Hooks {
9331039
boolean pskEnabled;
9341040
byte[] pskKey;
9351041
List<String> enabledCipherSuites;
1042+
byte[] echRetryConfigs;
1043+
String echNameOverride;
9361044

9371045
/**
9381046
* @throws SSLException if an error occurs creating the context.
@@ -1254,6 +1362,12 @@ public void clientCertificateRequested(long s) {
12541362
public static Future<TestSSLHandshakeCallbacks> handshake(final ServerSocket listener,
12551363
final int timeout, final boolean client, final Hooks hooks, final byte[] alpnProtocols,
12561364
final ApplicationProtocolSelectorAdapter alpnSelector) {
1365+
return handshake(listener, timeout, client, hooks, alpnProtocols, alpnSelector, false);
1366+
}
1367+
1368+
public static Future<TestSSLHandshakeCallbacks> handshake(final ServerSocket listener,
1369+
final int timeout, final boolean client, final Hooks hooks, final byte[] alpnProtocols,
1370+
final ApplicationProtocolSelectorAdapter alpnSelector, final boolean useEchRetryConfig) {
12571371
ExecutorService executor = Executors.newSingleThreadExecutor();
12581372
Future<TestSSLHandshakeCallbacks> future =
12591373
executor.submit(new Callable<TestSSLHandshakeCallbacks>() {
@@ -1294,7 +1408,19 @@ public TestSSLHandshakeCallbacks call() throws Exception {
12941408
if (!client && alpnSelector != null) {
12951409
NativeCrypto.setHasApplicationProtocolSelector(s, null, true);
12961410
}
1297-
NativeCrypto.SSL_do_handshake(s, null, fd, callback, timeout);
1411+
if (useEchRetryConfig) {
1412+
try {
1413+
NativeCrypto.SSL_do_handshake(s, null, fd, callback, timeout);
1414+
} catch (SSLProtocolException e) {
1415+
hooks.echRetryConfigs =
1416+
NativeCrypto.SSL_get0_ech_retry_configs(s, null);
1417+
hooks.echNameOverride =
1418+
NativeCrypto.SSL_get0_ech_name_override(s, null);
1419+
throw e;
1420+
}
1421+
} else {
1422+
NativeCrypto.SSL_do_handshake(s, null, fd, callback, timeout);
1423+
}
12981424
session = NativeCrypto.SSL_get1_session(s, null);
12991425
if (DEBUG) {
13001426
System.out.println("ssl=0x" + Long.toString(s, 16)

0 commit comments

Comments
 (0)