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;