Implement GREASE for ECH (draft-ietf-tls-esni-08).
Bug: 275
Change-Id: I4927c0886e3acf5b39104e3d89ed51d67520a343
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/40204
Reviewed-by: David Benjamin <davidben@google.com>
Commit-Queue: David Benjamin <davidben@google.com>
diff --git a/crypto/hpke/hpke.c b/crypto/hpke/hpke.c
index 2e0a581..cbd6e09 100644
--- a/crypto/hpke/hpke.c
+++ b/crypto/hpke/hpke.c
@@ -125,7 +125,7 @@
return 1;
}
-static const EVP_AEAD *hpke_get_aead(uint16_t aead_id) {
+const EVP_AEAD *EVP_HPKE_get_aead(uint16_t aead_id) {
switch (aead_id) {
case EVP_HPKE_AEAD_AES_GCM_128:
return EVP_aead_aes_128_gcm();
@@ -138,7 +138,7 @@
return NULL;
}
-static const EVP_MD *hpke_get_kdf(uint16_t kdf_id) {
+const EVP_MD *EVP_HPKE_get_hkdf_md(uint16_t kdf_id) {
switch (kdf_id) {
case EVP_HPKE_HKDF_SHA256:
return EVP_sha256();
@@ -174,7 +174,7 @@
}
// Attempt to get an EVP_AEAD*.
- const EVP_AEAD *aead = hpke_get_aead(hpke->aead_id);
+ const EVP_AEAD *aead = EVP_HPKE_get_aead(hpke->aead_id);
if (aead == NULL) {
return 0;
}
@@ -351,7 +351,7 @@
hpke->is_sender = 1;
hpke->kdf_id = kdf_id;
hpke->aead_id = aead_id;
- hpke->hkdf_md = hpke_get_kdf(kdf_id);
+ hpke->hkdf_md = EVP_HPKE_get_hkdf_md(kdf_id);
if (hpke->hkdf_md == NULL) {
return 0;
}
@@ -375,7 +375,7 @@
hpke->is_sender = 0;
hpke->kdf_id = kdf_id;
hpke->aead_id = aead_id;
- hpke->hkdf_md = hpke_get_kdf(kdf_id);
+ hpke->hkdf_md = EVP_HPKE_get_hkdf_md(kdf_id);
if (hpke->hkdf_md == NULL) {
return 0;
}
@@ -415,7 +415,7 @@
hpke->is_sender = 1;
hpke->kdf_id = kdf_id;
hpke->aead_id = aead_id;
- hpke->hkdf_md = hpke_get_kdf(kdf_id);
+ hpke->hkdf_md = EVP_HPKE_get_hkdf_md(kdf_id);
if (hpke->hkdf_md == NULL) {
return 0;
}
@@ -440,7 +440,7 @@
hpke->is_sender = 0;
hpke->kdf_id = kdf_id;
hpke->aead_id = aead_id;
- hpke->hkdf_md = hpke_get_kdf(kdf_id);
+ hpke->hkdf_md = EVP_HPKE_get_hkdf_md(kdf_id);
if (hpke->hkdf_md == NULL) {
return 0;
}
diff --git a/crypto/hpke/internal.h b/crypto/hpke/internal.h
index 87c049a..d078887 100644
--- a/crypto/hpke/internal.h
+++ b/crypto/hpke/internal.h
@@ -18,6 +18,7 @@
#include <openssl/aead.h>
#include <openssl/base.h>
#include <openssl/curve25519.h>
+#include <openssl/digest.h>
#if defined(__cplusplus)
extern "C" {
@@ -77,8 +78,8 @@
// In each of the following functions, |hpke| must have been initialized with
// |EVP_HPKE_CTX_init|. |kdf_id| selects the KDF for non-KEM HPKE operations and
// must be one of the |EVP_HPKE_HKDF_*| constants. |aead_id| selects the AEAD
-// for the "open" and "seal" operations and must be one of the |EVP_HPKE_AEAD_*"
-// constants."
+// for the "open" and "seal" operations and must be one of the |EVP_HPKE_AEAD_*|
+// constants.
// EVP_HPKE_CTX_setup_base_s_x25519 sets up |hpke| as a sender context that can
// encrypt for the private key corresponding to |peer_public_value| (the
@@ -215,6 +216,14 @@
// set up as a sender.
OPENSSL_EXPORT size_t EVP_HPKE_CTX_max_overhead(const EVP_HPKE_CTX *hpke);
+// EVP_HPKE_get_aead returns the AEAD corresponding to |aead_id|, or NULL if
+// |aead_id| is not a known AEAD identifier.
+OPENSSL_EXPORT const EVP_AEAD *EVP_HPKE_get_aead(uint16_t aead_id);
+
+// EVP_HPKE_get_hkdf_md returns the hash function associated with |kdf_id|, or
+// NULL if |kdf_id| is not a known KDF identifier that uses HKDF.
+OPENSSL_EXPORT const EVP_MD *EVP_HPKE_get_hkdf_md(uint16_t kdf_id);
+
#if defined(__cplusplus)
} // extern C
diff --git a/include/openssl/ssl.h b/include/openssl/ssl.h
index d22c1e2..e40e2b2 100644
--- a/include/openssl/ssl.h
+++ b/include/openssl/ssl.h
@@ -3553,6 +3553,21 @@
enum ssl_early_data_reason_t reason);
+// Encrypted Client Hello.
+//
+// ECH is a mechanism for encrypting the entire ClientHello message in TLS 1.3.
+// This can prevent observers from seeing cleartext information about the
+// connection, such as the server_name extension.
+//
+// ECH support in BoringSSL is still experimental and under development.
+//
+// See https://tools.ietf.org/html/draft-ietf-tls-esni-08.
+
+// SSL_set_enable_ech_grease configures whether the client may send ECH GREASE
+// as part of this connection.
+OPENSSL_EXPORT void SSL_set_enable_ech_grease(SSL *ssl, int enable);
+
+
// Alerts.
//
// TLS uses alerts to signal error conditions. Alerts have a type (warning or
diff --git a/include/openssl/tls1.h b/include/openssl/tls1.h
index 0ab10c9..22689a2 100644
--- a/include/openssl/tls1.h
+++ b/include/openssl/tls1.h
@@ -238,6 +238,10 @@
// extension number.
#define TLSEXT_TYPE_application_settings 17513
+// ExtensionType value from draft-ietf-tls-esni-08. This is not an IANA defined
+// extension number.
+#define TLSEXT_TYPE_encrypted_client_hello 0xfe08
+
// ExtensionType value from RFC6962
#define TLSEXT_TYPE_certificate_timestamp 18
diff --git a/ssl/internal.h b/ssl/internal.h
index 61cbefe..5545bec 100644
--- a/ssl/internal.h
+++ b/ssl/internal.h
@@ -1638,6 +1638,10 @@
// cookie is the value of the cookie received from the server, if any.
Array<uint8_t> cookie;
+ // ech_grease contains the bytes of the GREASE ECH extension that was sent in
+ // the first ClientHello.
+ Array<uint8_t> ech_grease;
+
// key_share_bytes is the value of the previously sent KeyShare extension by
// the client in TLS 1.3.
Array<uint8_t> key_share_bytes;
@@ -2729,6 +2733,10 @@
// verify_mode is a bitmask of |SSL_VERIFY_*| values.
uint8_t verify_mode = SSL_VERIFY_NONE;
+ // ech_grease_enabled controls whether ECH GREASE may be sent in the
+ // ClientHello.
+ bool ech_grease_enabled : 1;
+
// Enable signed certificate time stamps. Currently client only.
bool signed_cert_timestamps_enabled : 1;
diff --git a/ssl/ssl_lib.cc b/ssl/ssl_lib.cc
index 79eaacb..8c31871 100644
--- a/ssl/ssl_lib.cc
+++ b/ssl/ssl_lib.cc
@@ -722,6 +722,7 @@
SSL_CONFIG::SSL_CONFIG(SSL *ssl_arg)
: ssl(ssl_arg),
+ ech_grease_enabled(false),
signed_cert_timestamps_enabled(false),
ocsp_stapling_enabled(false),
channel_id_enabled(false),
@@ -1466,6 +1467,13 @@
}
}
+void SSL_set_enable_ech_grease(SSL *ssl, int enable) {
+ if (!ssl->config) {
+ return;
+ }
+ ssl->config->ech_grease_enabled = !!enable;
+}
+
uint32_t SSL_CTX_set_options(SSL_CTX *ctx, uint32_t options) {
ctx->options |= options;
return ctx->options;
diff --git a/ssl/t1_lib.cc b/ssl/t1_lib.cc
index e48f1a1..955eae7 100644
--- a/ssl/t1_lib.cc
+++ b/ssl/t1_lib.cc
@@ -113,10 +113,13 @@
#include <stdlib.h>
#include <string.h>
+#include <algorithm>
#include <utility>
+#include <openssl/aead.h>
#include <openssl/bytestring.h>
#include <openssl/chacha.h>
+#include <openssl/curve25519.h>
#include <openssl/digest.h>
#include <openssl/err.h>
#include <openssl/evp.h>
@@ -125,6 +128,7 @@
#include <openssl/nid.h>
#include <openssl/rand.h>
+#include "../crypto/hpke/internal.h"
#include "../crypto/internal.h"
#include "internal.h"
@@ -587,6 +591,160 @@
}
+// Encrypted Client Hello (ECH)
+//
+// https://tools.ietf.org/html/draft-ietf-tls-esni-08
+
+// random_size returns a random value between |min| and |max|, inclusive.
+static size_t random_size(size_t min, size_t max) {
+ assert(min < max);
+ size_t value;
+ RAND_bytes(reinterpret_cast<uint8_t *>(&value), sizeof(value));
+ return value % (max - min + 1) + min;
+}
+
+static bool ext_ech_add_clienthello_grease(SSL_HANDSHAKE *hs, CBB *out) {
+ // If we are responding to the server's HelloRetryRequest, we repeat the bytes
+ // of the first ECH GREASE extension.
+ if (hs->ssl->s3->used_hello_retry_request) {
+ CBB ech_body;
+ if (!CBB_add_u16(out, TLSEXT_TYPE_encrypted_client_hello) ||
+ !CBB_add_u16_length_prefixed(out, &ech_body) ||
+ !CBB_add_bytes(&ech_body, hs->ech_grease.data(),
+ hs->ech_grease.size()) ||
+ !CBB_flush(out)) {
+ return false;
+ }
+ return true;
+ }
+
+ constexpr uint16_t kdf_id = EVP_HPKE_HKDF_SHA256;
+ const EVP_MD *kdf = EVP_HPKE_get_hkdf_md(kdf_id);
+ assert(kdf != nullptr);
+
+ const uint16_t aead_id = EVP_has_aes_hardware()
+ ? EVP_HPKE_AEAD_AES_GCM_128
+ : EVP_HPKE_AEAD_CHACHA20POLY1305;
+ const EVP_AEAD *aead = EVP_HPKE_get_aead(aead_id);
+ assert(aead != nullptr);
+
+ uint8_t ech_config_id_buf[EVP_MAX_MD_SIZE];
+ Span<uint8_t> ech_config_id(ech_config_id_buf, EVP_MD_size(kdf));
+ RAND_bytes(ech_config_id.data(), ech_config_id.size());
+
+ uint8_t ech_enc[X25519_PUBLIC_VALUE_LEN];
+ uint8_t private_key_unused[X25519_PRIVATE_KEY_LEN];
+ X25519_keypair(ech_enc, private_key_unused);
+
+ // To determine a plausible length for the payload, we first estimate the size
+ // of a typical EncodedClientHelloInner, with an expected use of
+ // outer_extensions. To limit the size, we only consider initial ClientHellos
+ // that do not offer resumption.
+ //
+ // Field/Extension Size
+ // ---------------------------------------------------------------------
+ // version 2
+ // random 32
+ // legacy_session_id 1
+ // - Has a U8 length prefix, but body is
+ // always empty string in inner CH.
+ // cipher_suites 2 (length prefix)
+ // - Only includes TLS 1.3 ciphers (3). 6
+ // - Maybe also include a GREASE suite. 2
+ // legacy_compression_methods 2 (length prefix)
+ // - Always has "null" compression method. 1
+ // extensions: 2 (length prefix)
+ // - encrypted_client_hello (empty). 4 (id + length prefix)
+ // - supported_versions. 4 (id + length prefix)
+ // - U8 length prefix 1
+ // - U16 protocol version (TLS 1.3) 2
+ // - outer_extensions. 4 (id + length prefix)
+ // - U8 length prefix 1
+ // - N extension IDs (2 bytes each):
+ // - key_share 2
+ // - sigalgs 2
+ // - sct 2
+ // - alpn 2
+ // - supported_groups. 2
+ // - status_request. 2
+ // - psk_key_exchange_modes. 2
+ // - compress_certificate. 2
+ //
+ // The server_name extension has an overhead of 9 bytes, plus up to an
+ // estimated 100 bytes of hostname. Rounding up to a multiple of 32 yields a
+ // range of 96 to 192. Note that this estimate does not fully capture
+ // optional extensions like GREASE, but the rounding gives some leeway.
+
+ uint8_t payload[EVP_AEAD_MAX_OVERHEAD + 192];
+ const size_t payload_len =
+ EVP_AEAD_max_overhead(aead) + 32 * random_size(96 / 32, 192 / 32);
+ assert(payload_len <= sizeof(payload));
+ RAND_bytes(payload, payload_len);
+
+ // Inside the TLS extension contents, write a serialized ClientEncryptedCH.
+ CBB ech_body, config_id_cbb, enc_cbb, payload_cbb;
+ if (!CBB_add_u16(out, TLSEXT_TYPE_encrypted_client_hello) ||
+ !CBB_add_u16_length_prefixed(out, &ech_body) ||
+ !CBB_add_u16(&ech_body, kdf_id) || //
+ !CBB_add_u16(&ech_body, aead_id) ||
+ !CBB_add_u8_length_prefixed(&ech_body, &config_id_cbb) ||
+ !CBB_add_bytes(&config_id_cbb, ech_config_id.data(),
+ ech_config_id.size()) ||
+ !CBB_add_u16_length_prefixed(&ech_body, &enc_cbb) ||
+ !CBB_add_bytes(&enc_cbb, ech_enc, OPENSSL_ARRAY_SIZE(ech_enc)) ||
+ !CBB_add_u16_length_prefixed(&ech_body, &payload_cbb) ||
+ !CBB_add_bytes(&payload_cbb, payload, payload_len) || //
+ !CBB_flush(&ech_body)) {
+ return false;
+ }
+ // Save the bytes of the newly-generated extension in case the server sends
+ // a HelloRetryRequest.
+ if (!hs->ech_grease.CopyFrom(
+ MakeConstSpan(CBB_data(&ech_body), CBB_len(&ech_body)))) {
+ return false;
+ }
+ return CBB_flush(out);
+}
+
+static bool ext_ech_add_clienthello(SSL_HANDSHAKE *hs, CBB *out) {
+ if (hs->max_version < TLS1_3_VERSION) {
+ return true;
+ }
+ if (hs->config->ech_grease_enabled) {
+ return ext_ech_add_clienthello_grease(hs, out);
+ }
+ // Nothing to do, since we don't yet implement the non-GREASE parts of ECH.
+ return true;
+}
+
+static bool ext_ech_parse_serverhello(SSL_HANDSHAKE *hs, uint8_t *out_alert,
+ CBS *contents) {
+ if (contents == NULL) {
+ return true;
+ }
+
+ // If the client only sent GREASE, we must check the extension syntactically.
+ CBS ech_configs;
+ if (!CBS_get_u16_length_prefixed(contents, &ech_configs) ||
+ CBS_len(&ech_configs) == 0 || //
+ CBS_len(contents) > 0) {
+ *out_alert = SSL_AD_DECODE_ERROR;
+ return false;
+ }
+ while (CBS_len(&ech_configs) > 0) {
+ // Do a top-level parse of the ECHConfig, stopping before ECHConfigContents.
+ uint16_t version;
+ CBS ech_config_contents;
+ if (!CBS_get_u16(&ech_configs, &version) ||
+ !CBS_get_u16_length_prefixed(&ech_configs, &ech_config_contents)) {
+ *out_alert = SSL_AD_DECODE_ERROR;
+ return false;
+ }
+ }
+ return true;
+}
+
+
// Renegotiation indication.
//
// https://tools.ietf.org/html/rfc5746
@@ -2971,6 +3129,14 @@
ext_sni_add_serverhello,
},
{
+ TLSEXT_TYPE_encrypted_client_hello,
+ NULL,
+ ext_ech_add_clienthello,
+ ext_ech_parse_serverhello,
+ ignore_parse_clienthello,
+ dont_add_serverhello,
+ },
+ {
TLSEXT_TYPE_extended_master_secret,
NULL,
ext_ems_add_clienthello,
diff --git a/ssl/test/runner/common.go b/ssl/test/runner/common.go
index 8a934b3..522f458 100644
--- a/ssl/test/runner/common.go
+++ b/ssl/test/runner/common.go
@@ -127,6 +127,7 @@
extensionChannelID uint16 = 30032 // not IANA assigned
extensionDelegatedCredentials uint16 = 0x22 // draft-ietf-tls-subcerts-06
extensionDuplicate uint16 = 0xffff // not IANA assigned
+ extensionEncryptedClientHello uint16 = 0xfe08 // not IANA assigned
)
// TLS signaling cipher suite values
@@ -794,6 +795,14 @@
// must specify in the server_name extension.
ExpectServerName string
+ // ExpectClientECH causes the server to expect the peer to send an
+ // encrypted_client_hello extension containing a ClientECH structure.
+ ExpectClientECH bool
+
+ // SendECHRetryConfigs, if not empty, contains the ECH server's serialized
+ // retry configs.
+ SendECHRetryConfigs []byte
+
// SwapNPNAndALPN switches the relative order between NPN and ALPN in
// both ClientHello and ServerHello.
SwapNPNAndALPN bool
diff --git a/ssl/test/runner/handshake_messages.go b/ssl/test/runner/handshake_messages.go
index 9164819..b175a93 100644
--- a/ssl/test/runner/handshake_messages.go
+++ b/ssl/test/runner/handshake_messages.go
@@ -249,6 +249,48 @@
obfuscatedTicketAge uint32
}
+type HPKECipherSuite struct {
+ KDF uint16
+ AEAD uint16
+}
+
+type ECHConfig struct {
+ PublicName string
+ PublicKey []byte
+ KEM uint16
+ CipherSuites []HPKECipherSuite
+ MaxNameLen uint16
+}
+
+func MarshalECHConfig(e *ECHConfig) []byte {
+ bb := newByteBuilder()
+ // ECHConfig's wire format reuses the encrypted_client_hello extension
+ // codepoint as a version identifier.
+ bb.addU16(extensionEncryptedClientHello)
+ contents := bb.addU16LengthPrefixed()
+ contents.addU16LengthPrefixed().addBytes([]byte(e.PublicName))
+ contents.addU16LengthPrefixed().addBytes(e.PublicKey)
+ contents.addU16(e.KEM)
+ cipherSuites := contents.addU16LengthPrefixed()
+ for _, suite := range e.CipherSuites {
+ cipherSuites.addU16(suite.KDF)
+ cipherSuites.addU16(suite.AEAD)
+ }
+ contents.addU16(e.MaxNameLen)
+ contents.addU16(0) // Empty extensions field
+ return bb.finish()
+}
+
+// The contents of a CH "encrypted_client_hello" extension.
+// https://tools.ietf.org/html/draft-ietf-tls-esni-08
+type clientECH struct {
+ hpkeKDF uint16
+ hpkeAEAD uint16
+ configID []byte
+ enc []byte
+ payload []byte
+}
+
type clientHelloMsg struct {
raw []byte
isDTLS bool
@@ -260,6 +302,7 @@
compressionMethods []uint8
nextProtoNeg bool
serverName string
+ clientECH *clientECH
ocspStapling bool
supportedCurves []CurveID
supportedPoints []uint8
@@ -378,6 +421,20 @@
body: serverNameList.finish(),
})
}
+ if m.clientECH != nil {
+ // https://tools.ietf.org/html/draft-ietf-tls-esni-08
+ body := newByteBuilder()
+ body.addU16(m.clientECH.hpkeKDF)
+ body.addU16(m.clientECH.hpkeAEAD)
+ body.addU8LengthPrefixed().addBytes(m.clientECH.configID)
+ body.addU16LengthPrefixed().addBytes(m.clientECH.enc)
+ body.addU16LengthPrefixed().addBytes(m.clientECH.payload)
+
+ extensions = append(extensions, extension{
+ id: extensionEncryptedClientHello,
+ body: body.finish(),
+ })
+ }
if m.ocspStapling {
certificateStatusRequest := newByteBuilder()
// RFC 4366, section 3.6
@@ -778,6 +835,19 @@
m.serverName = string(name)
}
}
+ case extensionEncryptedClientHello:
+ var ech clientECH
+ if !body.readU16(&ech.hpkeKDF) ||
+ !body.readU16(&ech.hpkeAEAD) ||
+ !body.readU8LengthPrefixedBytes(&ech.configID) ||
+ !body.readU16LengthPrefixedBytes(&ech.enc) ||
+ len(ech.enc) == 0 ||
+ !body.readU16LengthPrefixedBytes(&ech.payload) ||
+ len(ech.payload) == 0 ||
+ len(body) > 0 {
+ return false
+ }
+ m.clientECH = &ech
case extensionNextProtoNeg:
if len(body) != 0 {
return false
@@ -1260,6 +1330,7 @@
serverNameAck bool
applicationSettings []byte
hasApplicationSettings bool
+ echRetryConfigs []byte
}
func (m *serverExtensions) marshal(extensions *byteBuilder) {
@@ -1398,6 +1469,12 @@
extensions.addU16(extensionApplicationSettings)
extensions.addU16LengthPrefixed().addBytes(m.applicationSettings)
}
+ if len(m.echRetryConfigs) > 0 {
+ extensions.addU16(extensionEncryptedClientHello)
+ body := extensions.addU16LengthPrefixed()
+ echConfigs := body.addU16LengthPrefixed()
+ echConfigs.addBytes(m.echRetryConfigs)
+ }
}
func (m *serverExtensions) unmarshal(data byteReader, version uint16) bool {
@@ -1509,6 +1586,26 @@
case extensionApplicationSettings:
m.hasApplicationSettings = true
m.applicationSettings = body
+ case extensionEncryptedClientHello:
+ var echConfigs byteReader
+ if !body.readU16LengthPrefixed(&echConfigs) {
+ return false
+ }
+ for len(echConfigs) > 0 {
+ // Validate the ECHConfig with a top-level parse.
+ echConfigReader := echConfigs
+ var version uint16
+ var contents byteReader
+ if !echConfigReader.readU16(&version) ||
+ !echConfigReader.readU16LengthPrefixed(&contents) {
+ return false
+ }
+
+ m.echRetryConfigs = contents
+ }
+ if len(body) > 0 {
+ return false
+ }
default:
// Unknown extensions are illegal from the server.
return false
diff --git a/ssl/test/runner/handshake_server.go b/ssl/test/runner/handshake_server.go
index 1a4beef..e1bc2e8 100644
--- a/ssl/test/runner/handshake_server.go
+++ b/ssl/test/runner/handshake_server.go
@@ -403,6 +403,14 @@
return err
}
+ if config.Bugs.ExpectClientECH && hs.clientHello.clientECH == nil {
+ return errors.New("tls: expected client to send ClientECH")
+ }
+
+ if hs.clientHello.clientECH != nil && len(config.Bugs.SendECHRetryConfigs) > 0 {
+ encryptedExtensions.extensions.echRetryConfigs = config.Bugs.SendECHRetryConfigs
+ }
+
// Select the cipher suite.
var preferenceList, supportedList []uint16
if config.PreferServerCipherSuites {
diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go
index 1bbec86..3b0bd85 100644
--- a/ssl/test/runner/runner.go
+++ b/ssl/test/runner/runner.go
@@ -46,6 +46,7 @@
"syscall"
"time"
+ "boringssl.googlesource.com/boringssl/ssl/test/runner/hpke"
"boringssl.googlesource.com/boringssl/util/testresult"
)
@@ -16139,6 +16140,114 @@
})
}
+func addEncryptedClientHelloTests() {
+ // Test ECH GREASE.
+
+ // Test the client's behavior when the server ignores ECH GREASE.
+ testCases = append(testCases, testCase{
+ testType: clientTest,
+ name: "ECH-GREASE-Client-TLS13",
+ config: Config{
+ MinVersion: VersionTLS13,
+ MaxVersion: VersionTLS13,
+ Bugs: ProtocolBugs{
+ ExpectClientECH: true,
+ },
+ },
+ flags: []string{"-enable-ech-grease"},
+ })
+
+ // Test the client's ECH GREASE behavior when responding to server's
+ // HelloRetryRequest. This test implicitly checks that the first and second
+ // ClientHello messages have identical ECH extensions.
+ testCases = append(testCases, testCase{
+ testType: clientTest,
+ name: "ECH-GREASE-Client-TLS13-HelloRetryRequest",
+ config: Config{
+ MaxVersion: VersionTLS13,
+ MinVersion: VersionTLS13,
+ // P-384 requires a HelloRetryRequest against BoringSSL's default
+ // configuration. Assert this with ExpectMissingKeyShare.
+ CurvePreferences: []CurveID{CurveP384},
+ Bugs: ProtocolBugs{
+ ExpectMissingKeyShare: true,
+ ExpectClientECH: true,
+ },
+ },
+ flags: []string{"-enable-ech-grease", "-expect-hrr"},
+ })
+
+ retryConfigValid := ECHConfig{
+ PublicName: "example.com",
+ // A real X25519 public key obtained from hpke.GenerateKeyPair().
+ PublicKey: []byte{
+ 0x23, 0x1a, 0x96, 0x53, 0x52, 0x81, 0x1d, 0x7a,
+ 0x36, 0x76, 0xaa, 0x5e, 0xad, 0xdb, 0x66, 0x1c,
+ 0x92, 0x45, 0x8a, 0x60, 0xc7, 0x81, 0x93, 0xb0,
+ 0x47, 0x7b, 0x54, 0x18, 0x6b, 0x9a, 0x1d, 0x6d},
+ KEM: hpke.X25519WithHKDFSHA256,
+ CipherSuites: []HPKECipherSuite{
+ {
+ KDF: hpke.HKDFSHA256,
+ AEAD: hpke.AES256GCM,
+ },
+ },
+ MaxNameLen: 42,
+ }
+
+ retryConfigUnsupportedVersion := []byte{
+ // version
+ 0xba, 0xdd,
+ // length
+ 0x00, 0x05,
+ // contents
+ 0x05, 0x04, 0x03, 0x02, 0x01,
+ }
+
+ var validAndInvalidConfigs []byte
+ validAndInvalidConfigs = append(validAndInvalidConfigs, MarshalECHConfig(&retryConfigValid)...)
+ validAndInvalidConfigs = append(validAndInvalidConfigs, retryConfigUnsupportedVersion...)
+
+ // Test that the client accepts a well-formed encrypted_client_hello
+ // extension in response to ECH GREASE. The response includes one ECHConfig
+ // with a supported version and one with an unsupported version.
+ testCases = append(testCases, testCase{
+ testType: clientTest,
+ name: "ECH-GREASE-Client-TLS13-Retry-Configs",
+ config: Config{
+ MinVersion: VersionTLS13,
+ MaxVersion: VersionTLS13,
+ Bugs: ProtocolBugs{
+ ExpectClientECH: true,
+ // Include an additional well-formed ECHConfig with an invalid
+ // version. This ensures the client can iterate over the retry
+ // configs.
+ SendECHRetryConfigs: validAndInvalidConfigs,
+ },
+ },
+ flags: []string{"-enable-ech-grease"},
+ })
+
+ // Test that the client aborts with a decode_error alert when it receives a
+ // syntactically-invalid encrypted_client_hello extension from the server.
+ testCases = append(testCases, testCase{
+ testType: clientTest,
+ name: "ECH-GREASE-Client-TLS13-Invalid-Retry-Configs",
+ config: Config{
+ MinVersion: VersionTLS13,
+ MaxVersion: VersionTLS13,
+ Bugs: ProtocolBugs{
+ ExpectClientECH: true,
+ SendECHRetryConfigs: []byte{0xba, 0xdd, 0xec, 0xcc},
+ },
+ },
+ flags: []string{"-enable-ech-grease"},
+ shouldFail: true,
+ expectedLocalError: "remote error: error decoding message",
+ expectedError: ":ERROR_PARSING_EXTENSION:",
+ })
+}
+
func worker(statusChan chan statusMsg, c chan *testCase, shimPath string, wg *sync.WaitGroup) {
defer wg.Done()
@@ -16316,6 +16425,7 @@
addCertCompressionTests()
addJDK11WorkaroundTests()
addDelegatedCredentialTests()
+ addEncryptedClientHelloTests()
testCases = append(testCases, convertToSplitHandshakeTests(testCases)...)
diff --git a/ssl/test/test_config.cc b/ssl/test/test_config.cc
index 3c49b94..e321ff3 100644
--- a/ssl/test/test_config.cc
+++ b/ssl/test/test_config.cc
@@ -55,6 +55,7 @@
{"-dtls", &TestConfig::is_dtls},
{"-quic", &TestConfig::is_quic},
{"-fallback-scsv", &TestConfig::fallback_scsv},
+ {"-enable-ech-grease", &TestConfig::enable_ech_grease},
{"-require-any-client-certificate",
&TestConfig::require_any_client_certificate},
{"-false-start", &TestConfig::false_start},
@@ -1576,6 +1577,9 @@
if (!expect_channel_id.empty() || enable_channel_id) {
SSL_set_tls_channel_id_enabled(ssl.get(), 1);
}
+ if (enable_ech_grease) {
+ SSL_set_enable_ech_grease(ssl.get(), 1);
+ }
if (!send_channel_id.empty()) {
SSL_set_tls_channel_id_enabled(ssl.get(), 1);
if (!async) {
diff --git a/ssl/test/test_config.h b/ssl/test/test_config.h
index 35007b6..93aab24 100644
--- a/ssl/test/test_config.h
+++ b/ssl/test/test_config.h
@@ -39,6 +39,7 @@
std::string key_file;
std::string cert_file;
std::string expect_server_name;
+ bool enable_ech_grease = false;
std::string expect_certificate_types;
bool require_any_client_certificate = false;
std::string advertise_npn;