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;