Add most of an ECH client implementation.

Based on an initial implementation by Dan McArdle at
https://boringssl-review.googlesource.com/c/boringssl/+/46784

This CL contains most of a client implementation for
draft-ietf-tls-esni-10. The pieces missing so far, which will be done in
follow-up CLs are:

1. While the ClientHelloInner is padded, the server Certificate message
   is not. I'll add that once we resolve the spec discussions on how to
   do that. (We were originally going to use TLS record-level padding,
   but that doesn't work well with QUIC.)

2. The client should check the public name is a valid DNS name before
   copying it into ClientHelloOuter.server_name.

3. The ClientHelloOuter handshake flow is not yet implemented. This CL
   can detect when the server selects ClientHelloOuter, but for now the
   handshake immediately fails. A follow-up CL will remove that logic
   and instead add the APIs and extra checks needed.

Otherwise, this should be complete, including padding and compression.

The main interesting point design-wise is that we run through
ClientHello construction multiple times. We need to construct
ClientHelloInner and ClientHelloOuter. Then each of those has slight
variants: EncodedClientHelloInner is the compressed form, and
ClientHelloOuterAAD just has the ECH extension erased to avoid a
circular dependency.

I've computed ClientHelloInner and EncodedClientHelloInner concurrently
because the compression scheme requires shifting the extensions around
to be contiguous. However, I've computed ClientHelloOuterAAD and
ClientHelloOuter by running through the logic twice. This probably can
be done better, but the next draft revises the construction anyway, so
I'm thinking I'll rework it then. (In the next draft, we use a
placeholder payload of the same length, so we can construct the
ClientHello once and fill in the payload.)

Additionally, now that we have a client available in ssl_test, this adds
a threading test to confirm that SSL_CTX_set1_ech_keys is properly
synchronized. (Confirmed that, if I drop the lock in
SSL_CTX_set1_ech_keys, TSan notices.)

Change-Id: Icaff68b595035bdcc73c468ff638e67c84239ef4
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/48004
Reviewed-by: Adam Langley <agl@google.com>
diff --git a/crypto/err/ssl.errordata b/crypto/err/ssl.errordata
index 1e5b1c0..d3efb73 100644
--- a/crypto/err/ssl.errordata
+++ b/crypto/err/ssl.errordata
@@ -85,6 +85,7 @@
 SSL,158,INVALID_COMMAND
 SSL,256,INVALID_COMPRESSION_LIST
 SSL,301,INVALID_DELEGATED_CREDENTIAL
+SSL,318,INVALID_ECH_CONFIG_LIST
 SSL,317,INVALID_ECH_PUBLIC_NAME
 SSL,159,INVALID_MESSAGE
 SSL,251,INVALID_OUTER_RECORD_TYPE
diff --git a/crypto/hpke/hpke.c b/crypto/hpke/hpke.c
index fe50bc2..444494f 100644
--- a/crypto/hpke/hpke.c
+++ b/crypto/hpke/hpke.c
@@ -318,6 +318,10 @@
 
 uint16_t EVP_HPKE_AEAD_id(const EVP_HPKE_AEAD *aead) { return aead->id; }
 
+const EVP_AEAD *EVP_HPKE_AEAD_aead(const EVP_HPKE_AEAD *aead) {
+  return aead->aead_func();
+}
+
 
 // HPKE implementation.
 
@@ -390,7 +394,7 @@
   }
 
   // key = LabeledExpand(secret, "key", key_schedule_context, Nk)
-  const EVP_AEAD *aead = ctx->aead->aead_func();
+  const EVP_AEAD *aead = EVP_HPKE_AEAD_aead(ctx->aead);
   uint8_t key[EVP_AEAD_MAX_KEY_LENGTH];
   const size_t kKeyLen = EVP_AEAD_key_length(aead);
   if (!hpke_labeled_expand(hkdf_md, key, kKeyLen, secret, secret_len, suite_id,
diff --git a/include/openssl/hpke.h b/include/openssl/hpke.h
index 2f14f77..85a597d 100644
--- a/include/openssl/hpke.h
+++ b/include/openssl/hpke.h
@@ -73,6 +73,9 @@
 // EVP_HPKE_AEAD_id returns the HPKE AEAD identifier for |aead|.
 OPENSSL_EXPORT uint16_t EVP_HPKE_AEAD_id(const EVP_HPKE_AEAD *aead);
 
+// EVP_HPKE_AEAD_aead returns the |EVP_AEAD| corresponding to |aead|.
+OPENSSL_EXPORT const EVP_AEAD *EVP_HPKE_AEAD_aead(const EVP_HPKE_AEAD *aead);
+
 
 // Recipient keys.
 //
diff --git a/include/openssl/ssl.h b/include/openssl/ssl.h
index b487a12..1ff9379 100644
--- a/include/openssl/ssl.h
+++ b/include/openssl/ssl.h
@@ -3561,10 +3561,28 @@
 //
 // See https://tools.ietf.org/html/draft-ietf-tls-esni-10.
 
-// SSL_set_enable_ech_grease configures whether the client may send ECH GREASE
-// as part of this connection.
+// SSL_set_enable_ech_grease configures whether the client will send a GREASE
+// ECH extension when no supported ECHConfig is available.
 OPENSSL_EXPORT void SSL_set_enable_ech_grease(SSL *ssl, int enable);
 
+// SSL_set1_ech_config_list configures |ssl| to, as a client, offer ECH with the
+// specified configuration. |ech_config_list| should contain a serialized
+// ECHConfigList structure. It returns one on success and zero on error.
+//
+// This function returns an error if the input is malformed. If the input is
+// valid but none of the ECHConfigs implement supported parameters, it will
+// return success and proceed without ECH.
+//
+// WARNING: Client ECH support is still incomplete and does not yet implement
+// the recovery flow. It currently treats ECH rejection as a fatal error. Do not
+// use this API yet.
+//
+// TODO(https://crbug.com/boringssl/275): When the recovery flow is implemented,
+// fill in the remaining docs.
+OPENSSL_EXPORT int SSL_set1_ech_config_list(SSL *ssl,
+                                            const uint8_t *ech_config_list,
+                                            size_t ech_config_list_len);
+
 // SSL_marshal_ech_config constructs a new serialized ECHConfig. On success, it
 // sets |*out| to a newly-allocated buffer containing the result and |*out_len|
 // to the size of the buffer. The caller must call |OPENSSL_free| on |*out| to
@@ -5475,6 +5493,7 @@
 #define SSL_R_INVALID_ALPN_PROTOCOL_LIST 315
 #define SSL_R_COULD_NOT_PARSE_HINTS 316
 #define SSL_R_INVALID_ECH_PUBLIC_NAME 317
+#define SSL_R_INVALID_ECH_CONFIG_LIST 318
 #define SSL_R_SSLV3_ALERT_CLOSE_NOTIFY 1000
 #define SSL_R_SSLV3_ALERT_UNEXPECTED_MESSAGE 1010
 #define SSL_R_SSLV3_ALERT_BAD_RECORD_MAC 1020
diff --git a/ssl/encrypted_client_hello.cc b/ssl/encrypted_client_hello.cc
index 8b606f0..b6a79a2 100644
--- a/ssl/encrypted_client_hello.cc
+++ b/ssl/encrypted_client_hello.cc
@@ -17,11 +17,15 @@
 #include <assert.h>
 #include <string.h>
 
+#include <utility>
+
+#include <openssl/aead.h>
 #include <openssl/bytestring.h>
 #include <openssl/curve25519.h>
 #include <openssl/err.h>
 #include <openssl/hkdf.h>
 #include <openssl/hpke.h>
+#include <openssl/rand.h>
 
 #include "internal.h"
 
@@ -278,15 +282,15 @@
 
   // Compute the ClientHello portion of the ClientHelloOuterAAD value. See
   // draft-ietf-tls-esni-10, section 5.2.
-  ScopedCBB ch_outer_aad_cbb;
+  ScopedCBB aad;
   CBB enc_cbb, outer_hello_cbb, extensions_cbb;
-  if (!CBB_init(ch_outer_aad_cbb.get(), 0) ||
-      !CBB_add_u16(ch_outer_aad_cbb.get(), kdf_id) ||
-      !CBB_add_u16(ch_outer_aad_cbb.get(), aead_id) ||
-      !CBB_add_u8(ch_outer_aad_cbb.get(), config_id) ||
-      !CBB_add_u16_length_prefixed(ch_outer_aad_cbb.get(), &enc_cbb) ||
+  if (!CBB_init(aad.get(), 256) ||
+      !CBB_add_u16(aad.get(), kdf_id) ||
+      !CBB_add_u16(aad.get(), aead_id) ||
+      !CBB_add_u8(aad.get(), config_id) ||
+      !CBB_add_u16_length_prefixed(aad.get(), &enc_cbb) ||
       !CBB_add_bytes(&enc_cbb, enc.data(), enc.size()) ||
-      !CBB_add_u24_length_prefixed(ch_outer_aad_cbb.get(), &outer_hello_cbb) ||
+      !CBB_add_u24_length_prefixed(aad.get(), &outer_hello_cbb) ||
       !ssl_client_hello_write_without_extensions(client_hello_outer,
                                                  &outer_hello_cbb) ||
       !CBB_add_u16_length_prefixed(&outer_hello_cbb, &extensions_cbb)) {
@@ -315,7 +319,7 @@
       return false;
     }
   }
-  if (!CBB_flush(ch_outer_aad_cbb.get())) {
+  if (!CBB_flush(aad.get())) {
     OPENSSL_PUT_ERROR(SSL, ERR_R_MALLOC_FAILURE);
     return false;
   }
@@ -343,8 +347,8 @@
   if (!EVP_HPKE_CTX_open(hpke_ctx, out_encoded_client_hello_inner->data(),
                          &encoded_client_hello_inner_len,
                          out_encoded_client_hello_inner->size(), payload.data(),
-                         payload.size(), CBB_data(ch_outer_aad_cbb.get()),
-                         CBB_len(ch_outer_aad_cbb.get()))) {
+                         payload.size(), CBB_data(aad.get()),
+                         CBB_len(aad.get()))) {
     *out_is_decrypt_error = true;
     OPENSSL_PUT_ERROR(SSL, SSL_R_DECRYPTION_FAILED);
     return false;
@@ -354,60 +358,103 @@
   return true;
 }
 
-
-bool ECHServerConfig::Init(Span<const uint8_t> ech_config,
-                           const EVP_HPKE_KEY *key, bool is_retry_config) {
-  assert(!initialized_);
-  is_retry_config_ = is_retry_config;
-
-  if (!ech_config_.CopyFrom(ech_config)) {
-    OPENSSL_PUT_ERROR(SSL, ERR_R_MALLOC_FAILURE);
-    return false;
-  }
-  // Read from |ech_config_| so we can save Spans with the same lifetime as |this|.
-  CBS reader(ech_config_);
-
+static bool parse_ech_config(CBS *cbs, ECHConfig *out, bool *out_supported,
+                             bool all_extensions_mandatory) {
   uint16_t version;
-  if (!CBS_get_u16(&reader, &version)) {
+  CBS orig = *cbs;
+  CBS contents;
+  if (!CBS_get_u16(cbs, &version) ||
+      !CBS_get_u16_length_prefixed(cbs, &contents)) {
     OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
     return false;
   }
+
+  if (version != kECHConfigVersion) {
+    *out_supported = false;
+    return true;
+  }
+
+  // Make a copy of the ECHConfig and parse from it, so the results alias into
+  // the saved copy.
+  if (!out->raw.CopyFrom(
+          MakeConstSpan(CBS_data(&orig), CBS_len(&orig) - CBS_len(cbs)))) {
+    return false;
+  }
+
+  CBS ech_config(out->raw);
+  CBS public_name, public_key, cipher_suites, extensions;
+  if (!CBS_skip(&ech_config, 2) || // version
+      !CBS_get_u16_length_prefixed(&ech_config, &contents) ||
+      !CBS_get_u8(&contents, &out->config_id) ||
+      !CBS_get_u16(&contents, &out->kem_id) ||
+      !CBS_get_u16_length_prefixed(&contents, &public_key) ||
+      CBS_len(&public_key) == 0 ||
+      !CBS_get_u16_length_prefixed(&contents, &cipher_suites) ||
+      CBS_len(&cipher_suites) == 0 || CBS_len(&cipher_suites) % 4 != 0 ||
+      !CBS_get_u16(&contents, &out->maximum_name_length) ||
+      !CBS_get_u16_length_prefixed(&contents, &public_name) ||
+      CBS_len(&public_name) == 0 ||
+      !CBS_get_u16_length_prefixed(&contents, &extensions) ||
+      CBS_len(&contents) != 0) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+    return false;
+  }
+
+  // TODO(https://crbug.com/boringssl/275): Check validity of |public_name|.
+
+  out->public_key = public_key;
+  out->public_name = public_name;
+  // This function does not ensure |out->kem_id| and |out->cipher_suites| use
+  // supported algorithms. The caller must do this.
+  out->cipher_suites = cipher_suites;
+
+  bool has_unknown_mandatory_extension = false;
+  while (CBS_len(&extensions) != 0) {
+    uint16_t type;
+    CBS body;
+    if (!CBS_get_u16(&extensions, &type) ||
+        !CBS_get_u16_length_prefixed(&extensions, &body)) {
+      OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+      return false;
+    }
+    // We currently do not support any extensions.
+    if (type & 0x8000 || all_extensions_mandatory) {
+      // Extension numbers with the high bit set are mandatory. Continue parsing
+      // to enforce syntax, but we will ultimately ignore this ECHConfig as a
+      // client and reject it as a server.
+      has_unknown_mandatory_extension = true;
+    }
+  }
+
+  *out_supported = !has_unknown_mandatory_extension;
+  return true;
+}
+
+bool ECHServerConfig::Init(Span<const uint8_t> ech_config,
+                           const EVP_HPKE_KEY *key, bool is_retry_config) {
+  is_retry_config_ = is_retry_config;
+
   // Parse the ECHConfig, rejecting all unsupported parameters and extensions.
   // Unlike most server options, ECH's server configuration is serialized and
   // configured in both the server and DNS. If the caller configures an
   // unsupported parameter, this is a deployment error. To catch these errors,
   // we fail early.
-  if (version != kECHConfigVersion) {
+  CBS cbs = ech_config;
+  bool supported;
+  if (!parse_ech_config(&cbs, &ech_config_, &supported,
+                        /*all_extensions_mandatory=*/true)) {
+    return false;
+  }
+  if (CBS_len(&cbs) != 0) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+    return false;
+  }
+  if (!supported) {
     OPENSSL_PUT_ERROR(SSL, SSL_R_UNSUPPORTED_ECH_SERVER_CONFIG);
     return false;
   }
 
-  CBS ech_config_contents, public_name, public_key, cipher_suites, extensions;
-  uint16_t kem_id, max_name_len;
-  if (!CBS_get_u16_length_prefixed(&reader, &ech_config_contents) ||
-      !CBS_get_u8(&ech_config_contents, &config_id_) ||
-      !CBS_get_u16(&ech_config_contents, &kem_id) ||
-      !CBS_get_u16_length_prefixed(&ech_config_contents, &public_key) ||
-      CBS_len(&public_key) == 0 ||
-      !CBS_get_u16_length_prefixed(&ech_config_contents, &cipher_suites) ||
-      CBS_len(&cipher_suites) == 0 ||
-      !CBS_get_u16(&ech_config_contents, &max_name_len) ||
-      !CBS_get_u16_length_prefixed(&ech_config_contents, &public_name) ||
-      CBS_len(&public_name) == 0 ||
-      !CBS_get_u16_length_prefixed(&ech_config_contents, &extensions) ||
-      CBS_len(&ech_config_contents) != 0 ||  //
-      CBS_len(&reader) != 0) {
-    OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
-    return false;
-  }
-
-  // We do not support any ECHConfig extensions, so |extensions| must be empty.
-  if (CBS_len(&extensions) != 0) {
-    OPENSSL_PUT_ERROR(SSL, SSL_R_ECH_SERVER_CONFIG_UNSUPPORTED_EXTENSION);
-    return false;
-  }
-
-  cipher_suites_ = cipher_suites;
+  CBS cipher_suites = ech_config_.cipher_suites;
   while (CBS_len(&cipher_suites) > 0) {
     uint16_t kdf_id, aead_id;
     if (!CBS_get_u16(&cipher_suites, &kdf_id) ||
@@ -431,9 +478,9 @@
                                sizeof(expected_public_key))) {
     return false;
   }
-  if (kem_id != EVP_HPKE_KEM_id(EVP_HPKE_KEY_kem(key)) ||
+  if (ech_config_.kem_id != EVP_HPKE_KEM_id(EVP_HPKE_KEY_kem(key)) ||
       MakeConstSpan(expected_public_key, expected_public_key_len) !=
-          public_key) {
+          ech_config_.public_key) {
     OPENSSL_PUT_ERROR(SSL, SSL_R_ECH_SERVER_CONFIG_AND_PRIVATE_KEY_MISMATCH);
     return false;
   }
@@ -442,17 +489,14 @@
     return false;
   }
 
-  initialized_ = true;
   return true;
 }
 
 bool ECHServerConfig::SetupContext(EVP_HPKE_CTX *ctx, uint16_t kdf_id,
                                    uint16_t aead_id,
                                    Span<const uint8_t> enc) const {
-  assert(initialized_);
-
   // Check the cipher suite is supported by this ECHServerConfig.
-  CBS cbs(cipher_suites_);
+  CBS cbs(ech_config_.cipher_suites);
   bool cipher_ok = false;
   while (CBS_len(&cbs) != 0) {
     uint16_t supported_kdf_id, supported_aead_id;
@@ -471,10 +515,11 @@
 
   static const uint8_t kInfoLabel[] = "tls ech";
   ScopedCBB info_cbb;
-  if (!CBB_init(info_cbb.get(), sizeof(kInfoLabel) + ech_config_.size()) ||
+  if (!CBB_init(info_cbb.get(), sizeof(kInfoLabel) + ech_config_.raw.size()) ||
       !CBB_add_bytes(info_cbb.get(), kInfoLabel,
                      sizeof(kInfoLabel) /* includes trailing NUL */) ||
-      !CBB_add_bytes(info_cbb.get(), ech_config_.data(), ech_config_.size())) {
+      !CBB_add_bytes(info_cbb.get(), ech_config_.raw.data(),
+                     ech_config_.raw.size())) {
     OPENSSL_PUT_ERROR(SSL, ERR_R_MALLOC_FAILURE);
     return false;
   }
@@ -486,6 +531,321 @@
       enc.size(), CBB_data(info_cbb.get()), CBB_len(info_cbb.get()));
 }
 
+bool ssl_is_valid_ech_config_list(Span<const uint8_t> ech_config_list) {
+  CBS cbs = ech_config_list, child;
+  if (!CBS_get_u16_length_prefixed(&cbs, &child) ||  //
+      CBS_len(&child) == 0 ||                        //
+      CBS_len(&cbs) > 0) {
+    return false;
+  }
+  while (CBS_len(&child) > 0) {
+    ECHConfig ech_config;
+    bool supported;
+    if (!parse_ech_config(&child, &ech_config, &supported,
+                          /*all_extensions_mandatory=*/false)) {
+      return false;
+    }
+  }
+  return true;
+}
+
+static bool select_ech_cipher_suite(const EVP_HPKE_KDF **out_kdf,
+                                    const EVP_HPKE_AEAD **out_aead,
+                                    Span<const uint8_t> cipher_suites) {
+  const bool has_aes_hardware = EVP_has_aes_hardware();
+  const EVP_HPKE_AEAD *aead = nullptr;
+  CBS cbs = cipher_suites;
+  while (CBS_len(&cbs) != 0) {
+    uint16_t kdf_id, aead_id;
+    if (!CBS_get_u16(&cbs, &kdf_id) ||  //
+        !CBS_get_u16(&cbs, &aead_id)) {
+      return false;
+    }
+    // Pick the first common cipher suite, but prefer ChaCha20-Poly1305 if we
+    // don't have AES hardware.
+    const EVP_HPKE_AEAD *candidate = get_ech_aead(aead_id);
+    if (kdf_id != EVP_HPKE_HKDF_SHA256 || candidate == nullptr) {
+      continue;
+    }
+    if (aead == nullptr ||
+        (!has_aes_hardware && aead_id == EVP_HPKE_CHACHA20_POLY1305)) {
+      aead = candidate;
+    }
+  }
+  if (aead == nullptr) {
+    return false;
+  }
+
+  *out_kdf = EVP_hpke_hkdf_sha256();
+  *out_aead = aead;
+  return true;
+}
+
+bool ssl_select_ech_config(SSL_HANDSHAKE *hs, Span<uint8_t> out_enc,
+                           size_t *out_enc_len) {
+  *out_enc_len = 0;
+  if (hs->max_version < TLS1_3_VERSION) {
+    // ECH requires TLS 1.3.
+    return true;
+  }
+
+  if (!hs->config->client_ech_config_list.empty()) {
+    CBS cbs = MakeConstSpan(hs->config->client_ech_config_list);
+    CBS child;
+    if (!CBS_get_u16_length_prefixed(&cbs, &child) ||  //
+        CBS_len(&child) == 0 ||                        //
+        CBS_len(&cbs) > 0) {
+      return false;
+    }
+    // Look for the first ECHConfig with supported parameters.
+    while (CBS_len(&child) > 0) {
+      ECHConfig ech_config;
+      bool supported;
+      if (!parse_ech_config(&child, &ech_config, &supported,
+                            /*all_extensions_mandatory=*/false)) {
+        return false;
+      }
+      const EVP_HPKE_KEM *kem = EVP_hpke_x25519_hkdf_sha256();
+      const EVP_HPKE_KDF *kdf;
+      const EVP_HPKE_AEAD *aead;
+      if (supported &&  //
+          ech_config.kem_id == EVP_HPKE_DHKEM_X25519_HKDF_SHA256 &&
+          select_ech_cipher_suite(&kdf, &aead, ech_config.cipher_suites)) {
+        ScopedCBB info;
+        static const uint8_t kInfoLabel[] = "tls ech";  // includes trailing NUL
+        if (!CBB_init(info.get(), sizeof(kInfoLabel) + ech_config.raw.size()) ||
+            !CBB_add_bytes(info.get(), kInfoLabel, sizeof(kInfoLabel)) ||
+            !CBB_add_bytes(info.get(), ech_config.raw.data(),
+                           ech_config.raw.size())) {
+          OPENSSL_PUT_ERROR(SSL, ERR_R_MALLOC_FAILURE);
+          return false;
+        }
+
+        if (!EVP_HPKE_CTX_setup_sender(
+                hs->ech_hpke_ctx.get(), out_enc.data(), out_enc_len,
+                out_enc.size(), kem, kdf, aead, ech_config.public_key.data(),
+                ech_config.public_key.size(), CBB_data(info.get()),
+                CBB_len(info.get())) ||
+            !hs->inner_transcript.Init()) {
+          return false;
+        }
+
+        hs->selected_ech_config = MakeUnique<ECHConfig>(std::move(ech_config));
+        return hs->selected_ech_config != nullptr;
+      }
+    }
+  }
+
+  return true;
+}
+
+static size_t aead_overhead(const EVP_HPKE_AEAD *aead) {
+#if defined(BORINGSSL_UNSAFE_FUZZER_MODE)
+  // TODO(https://crbug.com/boringssl/275): Having to adjust the overhead
+  // everywhere is tedious. Change fuzzer mode to append a fake tag but still
+  // otherwise be cleartext, refresh corpora, and then inline this function.
+  return 0;
+#else
+  return EVP_AEAD_max_overhead(EVP_HPKE_AEAD_aead(aead));
+#endif
+}
+
+static size_t compute_extension_length(const EVP_HPKE_AEAD *aead,
+                                       size_t enc_len, size_t in_len) {
+  size_t ret = 4;      // HpkeSymmetricCipherSuite cipher_suite
+  ret++;               // uint8 config_id
+  ret += 2 + enc_len;  // opaque enc<1..2^16-1>
+  ret += 2 + in_len + aead_overhead(aead);  // opaque payload<1..2^16-1>
+  return ret;
+}
+
+// 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 setup_ech_grease(SSL_HANDSHAKE *hs) {
+  assert(!hs->selected_ech_config);
+  if (hs->max_version < TLS1_3_VERSION || !hs->config->ech_grease_enabled) {
+    return true;
+  }
+
+  const uint16_t kdf_id = EVP_HPKE_HKDF_SHA256;
+  const EVP_HPKE_AEAD *aead = EVP_has_aes_hardware()
+                                  ? EVP_hpke_aes_128_gcm()
+                                  : EVP_hpke_chacha20_poly1305();
+  static_assert(ssl_grease_ech_config_id < sizeof(hs->grease_seed),
+                "hs->grease_seed is too small");
+  uint8_t config_id = hs->grease_seed[ssl_grease_ech_config_id];
+
+  uint8_t enc[X25519_PUBLIC_VALUE_LEN];
+  uint8_t private_key_unused[X25519_PRIVATE_KEY_LEN];
+  X25519_keypair(enc, private_key_unused);
+
+  // To determine a plausible length for the payload, we estimate the size of a
+  // typical EncodedClientHelloInner without resumption:
+  //
+  //   2+32+1+2   version, random, legacy_session_id, legacy_compression_methods
+  //   2+4*2      cipher_suites (three TLS 1.3 ciphers, GREASE)
+  //   2          extensions prefix
+  //   4          ech_is_inner
+  //   4+1+2*2    supported_versions (TLS 1.3, GREASE)
+  //   4+1+10*2   outer_extensions (key_share, sigalgs, sct, alpn,
+  //              supported_groups, status_request, psk_key_exchange_modes,
+  //              compress_certificate, GREASE x2)
+  //
+  // The server_name extension has an overhead of 9 bytes. For now, arbitrarily
+  // estimate maximum_name_length to be between 32 and 100 bytes.
+  //
+  // TODO(https://crbug.com/boringssl/275): If the padding scheme changes to
+  // also round the entire payload, adjust this to match. See
+  // https://github.com/tlswg/draft-ietf-tls-esni/issues/433
+  const size_t overhead = aead_overhead(aead);
+  const size_t in_len = random_size(128, 196);
+  const size_t extension_len =
+      compute_extension_length(aead, sizeof(enc), in_len);
+  bssl::ScopedCBB cbb;
+  CBB enc_cbb, payload_cbb;
+  uint8_t *payload;
+  if (!CBB_init(cbb.get(), extension_len) ||
+      !CBB_add_u16(cbb.get(), kdf_id) ||
+      !CBB_add_u16(cbb.get(), EVP_HPKE_AEAD_id(aead)) ||
+      !CBB_add_u8(cbb.get(), config_id) ||
+      !CBB_add_u16_length_prefixed(cbb.get(), &enc_cbb) ||
+      !CBB_add_bytes(&enc_cbb, enc, sizeof(enc)) ||
+      !CBB_add_u16_length_prefixed(cbb.get(), &payload_cbb) ||
+      !CBB_add_space(&payload_cbb, &payload, in_len + overhead) ||
+      !RAND_bytes(payload, in_len + overhead) ||
+      !CBBFinishArray(cbb.get(), &hs->ech_client_bytes)) {
+    return false;
+  }
+  assert(hs->ech_client_bytes.size() == extension_len);
+  return true;
+}
+
+bool ssl_encrypt_client_hello(SSL_HANDSHAKE *hs, Span<const uint8_t> enc) {
+  SSL *const ssl = hs->ssl;
+  if (!hs->selected_ech_config) {
+    return setup_ech_grease(hs);
+  }
+
+  // Construct ClientHelloInner and EncodedClientHelloInner. See
+  // draft-ietf-tls-esni-10, sections 5.1 and 6.1.
+  bssl::ScopedCBB cbb, encoded;
+  CBB body;
+  bool needs_psk_binder;
+  bssl::Array<uint8_t> hello_inner;
+  if (!ssl->method->init_message(ssl, cbb.get(), &body, SSL3_MT_CLIENT_HELLO) ||
+      !CBB_init(encoded.get(), 256) ||
+      !ssl_write_client_hello_without_extensions(hs, &body,
+                                                 ssl_client_hello_inner,
+                                                 /*empty_session_id=*/false) ||
+      !ssl_write_client_hello_without_extensions(hs, encoded.get(),
+                                                 ssl_client_hello_inner,
+                                                 /*empty_session_id=*/true) ||
+      !ssl_add_clienthello_tlsext(hs, &body, encoded.get(), &needs_psk_binder,
+                                  ssl_client_hello_inner, CBB_len(&body),
+                                  /*omit_ech_len=*/0) ||
+      !ssl->method->finish_message(ssl, cbb.get(), &hello_inner)) {
+    OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
+    return false;
+  }
+
+  if (needs_psk_binder) {
+    size_t binder_len;
+    if (!tls13_write_psk_binder(hs, hs->inner_transcript, MakeSpan(hello_inner),
+                                &binder_len)) {
+      return false;
+    }
+    // Also update the EncodedClientHelloInner.
+    if (CBB_len(encoded.get()) < binder_len) {
+      OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
+      return false;
+    }
+    OPENSSL_memcpy(const_cast<uint8_t *>(CBB_data(encoded.get())) +
+                       CBB_len(encoded.get()) - binder_len,
+                   hello_inner.data() + hello_inner.size() - binder_len,
+                   binder_len);
+  }
+
+  if (!hs->inner_transcript.Update(hello_inner)) {
+    return false;
+  }
+
+  // Construct ClientHelloOuterAAD. See draft-ietf-tls-esni-10, section 5.2.
+  // TODO(https://crbug.com/boringssl/275): This ends up constructing the
+  // ClientHelloOuter twice. Revisit this in the next draft, which uses a more
+  // forgiving construction.
+  const EVP_HPKE_KDF *kdf = EVP_HPKE_CTX_kdf(hs->ech_hpke_ctx.get());
+  const EVP_HPKE_AEAD *aead = EVP_HPKE_CTX_aead(hs->ech_hpke_ctx.get());
+  const size_t extension_len =
+      compute_extension_length(aead, enc.size(), CBB_len(encoded.get()));
+  bssl::ScopedCBB aad;
+  CBB outer_hello;
+  CBB enc_cbb;
+  if (!CBB_init(aad.get(), 256) ||
+      !CBB_add_u16(aad.get(), EVP_HPKE_KDF_id(kdf)) ||
+      !CBB_add_u16(aad.get(), EVP_HPKE_AEAD_id(aead)) ||
+      !CBB_add_u8(aad.get(), hs->selected_ech_config->config_id) ||
+      !CBB_add_u16_length_prefixed(aad.get(), &enc_cbb) ||
+      !CBB_add_bytes(&enc_cbb, enc.data(), enc.size()) ||
+      !CBB_add_u24_length_prefixed(aad.get(), &outer_hello) ||
+      !ssl_write_client_hello_without_extensions(hs, &outer_hello,
+                                                 ssl_client_hello_outer,
+                                                 /*empty_session_id=*/false) ||
+      !ssl_add_clienthello_tlsext(hs, &outer_hello, /*out_encoded=*/nullptr,
+                                  &needs_psk_binder, ssl_client_hello_outer,
+                                  CBB_len(&outer_hello),
+                                  /*omit_ech_len=*/4 + extension_len) ||
+      !CBB_flush(aad.get())) {
+    OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
+    return false;
+  }
+  // ClientHelloOuter may not require a PSK binder. Otherwise, we have a
+  // circular dependency.
+  assert(!needs_psk_binder);
+
+  CBB payload_cbb;
+  if (!CBB_init(cbb.get(), extension_len) ||
+      !CBB_add_u16(cbb.get(), EVP_HPKE_KDF_id(kdf)) ||
+      !CBB_add_u16(cbb.get(), EVP_HPKE_AEAD_id(aead)) ||
+      !CBB_add_u8(cbb.get(), hs->selected_ech_config->config_id) ||
+      !CBB_add_u16_length_prefixed(cbb.get(), &enc_cbb) ||
+      !CBB_add_bytes(&enc_cbb, enc.data(), enc.size()) ||
+      !CBB_add_u16_length_prefixed(cbb.get(), &payload_cbb)) {
+    return false;
+  }
+#if defined(BORINGSSL_UNSAFE_FUZZER_MODE)
+  // In fuzzer mode, the server expects a cleartext payload.
+  if (!CBB_add_bytes(&payload_cbb, CBB_data(encoded.get()),
+                     CBB_len(encoded.get()))) {
+    return false;
+  }
+#else
+  uint8_t *payload;
+  size_t payload_len =
+      CBB_len(encoded.get()) + EVP_AEAD_max_overhead(EVP_HPKE_AEAD_aead(aead));
+  if (!CBB_reserve(&payload_cbb, &payload, payload_len) ||
+      !EVP_HPKE_CTX_seal(hs->ech_hpke_ctx.get(), payload, &payload_len,
+                         payload_len, CBB_data(encoded.get()),
+                         CBB_len(encoded.get()), CBB_data(aad.get()),
+                         CBB_len(aad.get())) ||
+      !CBB_did_write(&payload_cbb, payload_len)) {
+    return false;
+  }
+#endif // BORINGSSL_UNSAFE_FUZZER_MODE
+  if (!CBBFinishArray(cbb.get(), &hs->ech_client_bytes)) {
+    return false;
+  }
+
+  // The |aad| calculation relies on |extension_length| being correct.
+  assert(hs->ech_client_bytes.size() == extension_len);
+  return true;
+}
+
 BSSL_NAMESPACE_END
 
 using namespace bssl;
@@ -497,6 +857,20 @@
   ssl->config->ech_grease_enabled = !!enable;
 }
 
+int SSL_set1_ech_config_list(SSL *ssl, const uint8_t *ech_config_list,
+                             size_t ech_config_list_len) {
+  if (!ssl->config) {
+    return 0;
+  }
+
+  auto span = MakeConstSpan(ech_config_list, ech_config_list_len);
+  if (!ssl_is_valid_ech_config_list(span)) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_INVALID_ECH_CONFIG_LIST);
+    return 0;
+  }
+  return ssl->config->client_ech_config_list.CopyFrom(span);
+}
+
 int SSL_marshal_ech_config(uint8_t **out, size_t *out_len, uint8_t config_id,
                            const EVP_HPKE_KEY *key, const char *public_name,
                            size_t max_name_len) {
@@ -581,10 +955,10 @@
 int SSL_ECH_KEYS_has_duplicate_config_id(const SSL_ECH_KEYS *keys) {
   bool seen[256] = {false};
   for (const auto &config : keys->configs) {
-    if (seen[config->config_id()]) {
+    if (seen[config->ech_config().config_id]) {
       return 1;
     }
-    seen[config->config_id()] = true;
+    seen[config->ech_config().config_id] = true;
   }
   return 0;
 }
@@ -600,8 +974,8 @@
   }
   for (const auto &config : keys->configs) {
     if (config->is_retry_config() &&
-        !CBB_add_bytes(&child, config->ech_config().data(),
-                       config->ech_config().size())) {
+        !CBB_add_bytes(&child, config->ech_config().raw.data(),
+                       config->ech_config().raw.size())) {
       OPENSSL_PUT_ERROR(SSL, ERR_R_MALLOC_FAILURE);
       return false;
     }
@@ -628,5 +1002,12 @@
 }
 
 int SSL_ech_accepted(const SSL *ssl) {
+  if (SSL_in_early_data(ssl) && !ssl->server) {
+    // In the client early data state, we report properties as if the server
+    // accepted early data. The server can only accept early data with
+    // ClientHelloInner.
+    return ssl->s3->hs->selected_ech_config != nullptr;
+  }
+
   return ssl->s3->ech_accept;
 }
diff --git a/ssl/handshake_client.cc b/ssl/handshake_client.cc
index 1d6bc0a..ae8aabd 100644
--- a/ssl/handshake_client.cc
+++ b/ssl/handshake_client.cc
@@ -162,6 +162,7 @@
 #include <openssl/ecdsa.h>
 #include <openssl/err.h>
 #include <openssl/evp.h>
+#include <openssl/hpke.h>
 #include <openssl/md5.h>
 #include <openssl/mem.h>
 #include <openssl/rand.h>
@@ -201,7 +202,8 @@
 
 // ssl_get_client_disabled sets |*out_mask_a| and |*out_mask_k| to masks of
 // disabled algorithms.
-static void ssl_get_client_disabled(SSL_HANDSHAKE *hs, uint32_t *out_mask_a,
+static void ssl_get_client_disabled(const SSL_HANDSHAKE *hs,
+                                    uint32_t *out_mask_a,
                                     uint32_t *out_mask_k) {
   *out_mask_a = 0;
   *out_mask_k = 0;
@@ -213,8 +215,9 @@
   }
 }
 
-static bool ssl_write_client_cipher_list(SSL_HANDSHAKE *hs, CBB *out) {
-  SSL *const ssl = hs->ssl;
+static bool ssl_write_client_cipher_list(const SSL_HANDSHAKE *hs, CBB *out,
+                                         ssl_client_hello_type_t type) {
+  const SSL *const ssl = hs->ssl;
   uint32_t mask_a, mask_k;
   ssl_get_client_disabled(hs, &mask_a, &mask_k);
 
@@ -246,7 +249,7 @@
     }
   }
 
-  if (hs->min_version < TLS1_3_VERSION) {
+  if (hs->min_version < TLS1_3_VERSION && type != ssl_client_hello_inner) {
     bool any_enabled = false;
     for (const SSL_CIPHER *cipher : SSL_get_ciphers(ssl)) {
       // Skip disabled ciphers
@@ -280,53 +283,72 @@
   return CBB_flush(out);
 }
 
-bool ssl_write_client_hello(SSL_HANDSHAKE *hs) {
-  SSL *const ssl = hs->ssl;
-  ScopedCBB cbb;
-  CBB body;
-  if (!ssl->method->init_message(ssl, cbb.get(), &body, SSL3_MT_CLIENT_HELLO)) {
-    return false;
-  }
-
+bool ssl_write_client_hello_without_extensions(const SSL_HANDSHAKE *hs,
+                                               CBB *cbb,
+                                               ssl_client_hello_type_t type,
+                                               bool empty_session_id) {
+  const SSL *const ssl = hs->ssl;
   CBB child;
-  if (!CBB_add_u16(&body, hs->client_version) ||
-      !CBB_add_bytes(&body, ssl->s3->client_random, SSL3_RANDOM_SIZE) ||
-      !CBB_add_u8_length_prefixed(&body, &child)) {
+  if (!CBB_add_u16(cbb, hs->client_version) ||
+      !CBB_add_bytes(cbb,
+                     type == ssl_client_hello_inner ? hs->inner_client_random
+                                                    : ssl->s3->client_random,
+                     SSL3_RANDOM_SIZE) ||
+      !CBB_add_u8_length_prefixed(cbb, &child)) {
     return false;
   }
 
   // Do not send a session ID on renegotiation.
   if (!ssl->s3->initial_handshake_complete &&
+      !empty_session_id &&
       !CBB_add_bytes(&child, hs->session_id, hs->session_id_len)) {
     return false;
   }
 
   if (SSL_is_dtls(ssl)) {
-    if (!CBB_add_u8_length_prefixed(&body, &child) ||
+    if (!CBB_add_u8_length_prefixed(cbb, &child) ||
         !CBB_add_bytes(&child, ssl->d1->cookie, ssl->d1->cookie_len)) {
       return false;
     }
   }
 
-  bool needs_psk_binder;
-  if (!ssl_write_client_cipher_list(hs, &body) ||
-      !CBB_add_u8(&body, 1 /* one compression method */) ||
-      !CBB_add_u8(&body, 0 /* null compression */) ||
-      !ssl_add_clienthello_tlsext(hs, &body, &needs_psk_binder,
-                                  CBB_len(&body))) {
+  if (!ssl_write_client_cipher_list(hs, cbb, type) ||
+      !CBB_add_u8(cbb, 1 /* one compression method */) ||
+      !CBB_add_u8(cbb, 0 /* null compression */)) {
     return false;
   }
+  return true;
+}
 
+bool ssl_add_client_hello(SSL_HANDSHAKE *hs) {
+  SSL *const ssl = hs->ssl;
+  ScopedCBB cbb;
+  CBB body;
+  ssl_client_hello_type_t type = hs->selected_ech_config
+                                     ? ssl_client_hello_outer
+                                     : ssl_client_hello_unencrypted;
+  bool needs_psk_binder;
   Array<uint8_t> msg;
-  if (!ssl->method->finish_message(ssl, cbb.get(), &msg)) {
+  if (!ssl->method->init_message(ssl, cbb.get(), &body, SSL3_MT_CLIENT_HELLO) ||
+      !ssl_write_client_hello_without_extensions(hs, &body, type,
+                                                 /*empty_session_id*/ false) ||
+      !ssl_add_clienthello_tlsext(hs, &body, /*out_encoded=*/nullptr,
+                                  &needs_psk_binder, type, CBB_len(&body),
+                                  /*omit_ech_len=*/0) ||
+      !ssl->method->finish_message(ssl, cbb.get(), &msg)) {
     return false;
   }
 
   // Now that the length prefixes have been computed, fill in the placeholder
   // PSK binder.
-  if (needs_psk_binder &&
-      !tls13_write_psk_binder(hs, MakeSpan(msg))) {
-    return false;
+  if (needs_psk_binder) {
+    // ClientHelloOuter cannot have a PSK binder. Otherwise the
+    // ClientHellOuterAAD computation would break.
+    assert(type != ssl_client_hello_outer);
+    if (!tls13_write_psk_binder(hs, hs->transcript, MakeSpan(msg),
+                                /*out_binder_len=*/0)) {
+      return false;
+    }
   }
 
   return ssl->method->add_message(ssl, std::move(msg));
@@ -423,7 +445,7 @@
 }
 
 void ssl_done_writing_client_hello(SSL_HANDSHAKE *hs) {
-  hs->ech_grease.Reset();
+  hs->ech_client_bytes.Reset();
   hs->cookie.Reset();
   hs->key_share_bytes.Reset();
 }
@@ -440,6 +462,12 @@
     return ssl_hs_error;
   }
 
+  uint8_t ech_enc[EVP_HPKE_MAX_ENC_LENGTH];
+  size_t ech_enc_len;
+  if (!ssl_select_ech_config(hs, ech_enc, &ech_enc_len)) {
+    return ssl_hs_error;
+  }
+
   // Always advertise the ClientHello version from the original maximum version,
   // even on renegotiation. The static RSA key exchange uses this field, and
   // some servers fail when it changes across handshakes.
@@ -456,6 +484,11 @@
   if (ssl->session != nullptr) {
     if (ssl->session->is_server ||
         !ssl_supports_version(hs, ssl->session->ssl_version) ||
+        // Do not offer TLS 1.2 sessions with ECH. ClientHelloInner does not
+        // offer TLS 1.2, and the cleartext session ID may leak the server
+        // identity.
+        (hs->selected_ech_config &&
+         ssl_session_protocol_version(ssl->session.get()) < TLS1_3_VERSION) ||
         !SSL_SESSION_is_resumable(ssl->session.get()) ||
         !ssl_session_is_time_valid(ssl, ssl->session.get()) ||
         (ssl->quic_method != nullptr) != ssl->session->is_quic ||
@@ -467,6 +500,10 @@
   if (!RAND_bytes(ssl->s3->client_random, sizeof(ssl->s3->client_random))) {
     return ssl_hs_error;
   }
+  if (hs->selected_ech_config &&
+      !RAND_bytes(hs->inner_client_random, sizeof(hs->inner_client_random))) {
+    return ssl_hs_error;
+  }
 
   // Never send a session ID in QUIC. QUIC uses TLS 1.3 at a minimum and
   // disables TLS 1.3 middlebox compatibility mode.
@@ -498,8 +535,8 @@
   }
 
   if (!ssl_setup_key_shares(hs, /*override_group_id=*/0) ||
-      !ssl_setup_ech_grease(hs) ||
-      !ssl_write_client_hello(hs)) {
+      !ssl_encrypt_client_hello(hs, MakeConstSpan(ech_enc, ech_enc_len)) ||
+      !ssl_add_client_hello(hs)) {
     return ssl_hs_error;
   }
 
@@ -525,9 +562,7 @@
     return ssl_hs_error;
   }
 
-  if (!tls13_init_early_key_schedule(
-          hs,
-          MakeConstSpan(ssl->session->secret, ssl->session->secret_length)) ||
+  if (!tls13_init_early_key_schedule(hs, ssl->session.get()) ||
       !tls13_derive_early_secret(hs)) {
     return ssl_hs_error;
   }
@@ -578,6 +613,10 @@
 
   assert(SSL_is_dtls(ssl));
 
+  // When implementing DTLS 1.3, we need to handle the interactions between
+  // HelloVerifyRequest, DTLS 1.3's HelloVerifyRequest removal, and ECH.
+  assert(hs->max_version < TLS1_3_VERSION);
+
   SSLMessage msg;
   if (!ssl->method->get_message(ssl, &msg)) {
     return ssl_hs_read_message;
@@ -609,7 +648,7 @@
     return ssl_hs_error;
   }
 
-  if (!ssl_write_client_hello(hs)) {
+  if (!ssl_add_client_hello(hs)) {
     return ssl_hs_error;
   }
 
@@ -685,6 +724,15 @@
     return ssl_hs_error;
   }
 
+  // TODO(https://crbug.com/boringssl/275): If the server negotiates TLS 1.2 and
+  // we offer ECH, we handshake with ClientHelloOuter instead of
+  // ClientHelloInner. That path is not yet implemented. For now, terminate the
+  // handshake with a distinguishable error for testing.
+  if (hs->selected_ech_config) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_CONNECTION_REJECTED);
+    return ssl_hs_error;
+  }
+
   // Copy over the server random.
   OPENSSL_memcpy(ssl->s3->server_random, CBS_data(&server_random),
                  SSL3_RANDOM_SIZE);
diff --git a/ssl/handshake_server.cc b/ssl/handshake_server.cc
index c7d45f3..f6feb87 100644
--- a/ssl/handshake_server.cc
+++ b/ssl/handshake_server.cc
@@ -618,11 +618,11 @@
     }
 
     if (hs->ech_keys) {
-      for (const auto &ech_config : hs->ech_keys->configs) {
+      for (const auto &config : hs->ech_keys->configs) {
         hs->ech_hpke_ctx.Reset();
-        if (config_id != ech_config->config_id() ||
-            !ech_config->SetupContext(hs->ech_hpke_ctx.get(), kdf_id, aead_id,
-                                      enc)) {
+        if (config_id != config->ech_config().config_id ||
+            !config->SetupContext(hs->ech_hpke_ctx.get(), kdf_id, aead_id,
+                                  enc)) {
           // Ignore the error and try another ECHConfig.
           ERR_clear_error();
           continue;
diff --git a/ssl/internal.h b/ssl/internal.h
index 5b8ff68..3728f70 100644
--- a/ssl/internal.h
+++ b/ssl/internal.h
@@ -495,8 +495,10 @@
                                  uint16_t version);
 
 // ssl_add_supported_versions writes the supported versions of |hs| to |cbb|, in
-// decreasing preference order.
-bool ssl_add_supported_versions(const SSL_HANDSHAKE *hs, CBB *cbb);
+// decreasing preference order. The version list is filtered to those whose
+// protocol version is at least |extra_min_version|.
+bool ssl_add_supported_versions(const SSL_HANDSHAKE *hs, CBB *cbb,
+                                uint16_t extra_min_version);
 
 // ssl_negotiate_version negotiates a common version based on |hs|'s preferences
 // and the peer preference list in |peer_versions|. On success, it returns true
@@ -679,6 +681,9 @@
   SSLTranscript();
   ~SSLTranscript();
 
+  SSLTranscript(SSLTranscript &&other) = default;
+  SSLTranscript &operator=(SSLTranscript &&other) = default;
+
   // Init initializes the handshake transcript. If called on an existing
   // transcript, it resets the transcript and hash. It returns true on success
   // and false on failure.
@@ -1360,9 +1365,10 @@
 bool tls13_init_key_schedule(SSL_HANDSHAKE *hs, Span<const uint8_t> psk);
 
 // tls13_init_early_key_schedule initializes the handshake hash and key
-// derivation state from the resumption secret and incorporates the PSK to
-// derive the early secrets. It returns one on success and zero on error.
-bool tls13_init_early_key_schedule(SSL_HANDSHAKE *hs, Span<const uint8_t> psk);
+// derivation state from |session| for use with 0-RTT. It returns one on success
+// and zero on error.
+bool tls13_init_early_key_schedule(SSL_HANDSHAKE *hs,
+                                   const SSL_SESSION *session);
 
 // tls13_advance_key_schedule incorporates |in| into the key schedule with
 // HKDF-Extract. It returns true on success and false on error.
@@ -1415,10 +1421,13 @@
 // on failure.
 bool tls13_derive_session_psk(SSL_SESSION *session, Span<const uint8_t> nonce);
 
-// tls13_write_psk_binder calculates the PSK binder value and replaces the last
-// bytes of |msg| with the resulting value. It returns true on success, and
-// false on failure.
-bool tls13_write_psk_binder(const SSL_HANDSHAKE *hs, Span<uint8_t> msg);
+// tls13_write_psk_binder calculates the PSK binder value over |transcript| and
+// |msg|, and replaces the last bytes of |msg| with the resulting value. It
+// returns true on success, and false on failure. If |out_binder_len| is
+// non-NULL, it sets |*out_binder_len| to the length of the value computed.
+bool tls13_write_psk_binder(const SSL_HANDSHAKE *hs,
+                            const SSLTranscript &transcript, Span<uint8_t> msg,
+                            size_t *out_binder_len);
 
 // tls13_verify_psk_binder verifies that the handshake transcript, truncated up
 // to the binders has a valid signature using the value of |session|'s
@@ -1430,12 +1439,24 @@
 
 // Encrypted ClientHello.
 
+struct ECHConfig {
+  static constexpr bool kAllowUniquePtr = true;
+  // raw contains the serialized ECHConfig.
+  Array<uint8_t> raw;
+  // The following fields alias into |raw|.
+  Span<const uint8_t> public_key;
+  Span<const uint8_t> public_name;
+  Span<const uint8_t> cipher_suites;
+  uint16_t kem_id = 0;
+  uint16_t maximum_name_length = 0;
+  uint8_t config_id = 0;
+};
+
 class ECHServerConfig {
  public:
   static constexpr bool kAllowUniquePtr = true;
-  ECHServerConfig() : is_retry_config_(false), initialized_(false) {}
+  ECHServerConfig() = default;
   ECHServerConfig(const ECHServerConfig &other) = delete;
-  ~ECHServerConfig() = default;
   ECHServerConfig &operator=(ECHServerConfig &&) = delete;
 
   // Init parses |ech_config| as an ECHConfig and saves a copy of |key|.
@@ -1449,28 +1470,19 @@
   bool SetupContext(EVP_HPKE_CTX *ctx, uint16_t kdf_id, uint16_t aead_id,
                     Span<const uint8_t> enc) const;
 
-  Span<const uint8_t> ech_config() const {
-    assert(initialized_);
-    return ech_config_;
-  }
-  bool is_retry_config() const {
-    assert(initialized_);
-    return is_retry_config_;
-  }
-  uint8_t config_id() const {
-    assert(initialized_);
-    return config_id_;
-  }
+  const ECHConfig &ech_config() const { return ech_config_; }
+  bool is_retry_config() const { return is_retry_config_; }
 
  private:
-  Array<uint8_t> ech_config_;
-  Span<const uint8_t> cipher_suites_;
+  ECHConfig ech_config_;
   ScopedEVP_HPKE_KEY key_;
+  bool is_retry_config_ = false;
+};
 
-  uint8_t config_id_;
-
-  bool is_retry_config_ : 1;
-  bool initialized_ : 1;
+enum ssl_client_hello_type_t {
+  ssl_client_hello_unencrypted,
+  ssl_client_hello_inner,
+  ssl_client_hello_outer,
 };
 
 // ssl_decode_client_hello_inner recovers the full ClientHelloInner from the
@@ -1498,15 +1510,51 @@
                               uint8_t config_id, Span<const uint8_t> enc,
                               Span<const uint8_t> payload);
 
-// tls13_ech_accept_confirmation computes the server's ECH acceptance signal,
-// writing it to |out|. It returns true on success, and false on failure.
-bool tls13_ech_accept_confirmation(
-    SSL_HANDSHAKE *hs, bssl::Span<uint8_t> out,
-    bssl::Span<const uint8_t> server_hello_ech_conf);
+#define ECH_CONFIRMATION_SIGNAL_LEN 8
 
-// ssl_setup_ech_grease computes a GREASE ECH extension to offer instead of a
-// real one. It returns true on success and false otherwise.
-bool ssl_setup_ech_grease(SSL_HANDSHAKE *hs);
+// ssl_ech_confirmation_signal_hello_offset returns the offset of the ECH
+// confirmation signal in a ServerHello message, including the handshake header.
+size_t ssl_ech_confirmation_signal_hello_offset(const SSL *ssl);
+
+// ssl_ech_accept_confirmation computes the server's ECH acceptance signal,
+// writing it to |out|. The signal is computed by concatenating |transcript|
+// with |server_hello|. This function handles the fact that eight bytes of
+// |server_hello| need to be replaced with zeros before hashing. It returns true
+// on success, and false on failure.
+bool ssl_ech_accept_confirmation(const SSL_HANDSHAKE *hs, Span<uint8_t> out,
+                                 const SSLTranscript &transcript,
+                                 Span<const uint8_t> server_hello);
+
+// ssl_is_valid_ech_config_list returns true if |ech_config_list| is a valid
+// ECHConfigList structure and false otherwise.
+bool ssl_is_valid_ech_config_list(Span<const uint8_t> ech_config_list);
+
+// ssl_select_ech_config selects an ECHConfig and associated parameters to offer
+// on the client and updates |hs|. It returns true on success, whether an
+// ECHConfig was found or not, and false on internal error. On success, the
+// encapsulated key is written to |out_enc| and |*out_enc_len| is set to the
+// number of bytes written. If the function did not select an ECHConfig, the
+// encapsulated key is the empty string.
+bool ssl_select_ech_config(SSL_HANDSHAKE *hs, Span<uint8_t> out_enc,
+                           size_t *out_enc_len);
+
+// ssl_ech_extension_body_length returns the length of the body of a ClientHello
+// ECH extension that encrypts |in_len| bytes with |aead| and an 'enc' value of
+// length |enc_len|. The result does not include the four-byte extension header.
+size_t ssl_ech_extension_body_length(const EVP_HPKE_AEAD *aead, size_t enc_len,
+                                     size_t in_len);
+
+// ssl_encrypt_client_hello constructs a new ClientHelloInner, adds it to the
+// inner transcript, and encrypts for inclusion in the ClientHelloOuter. |enc|
+// is the encapsulated key to include in the extension. It returns true on
+// success and false on error. If not offering ECH, |enc| is ignored and the
+// function will compute a GREASE ECH extension if necessary, and otherwise
+// return success while doing nothing.
+//
+// Encrypting the ClientHelloInner incorporates all extensions in the
+// ClientHelloOuter, so all other state necessary for |ssl_add_client_hello|
+// must already be computed.
+bool ssl_encrypt_client_hello(SSL_HANDSHAKE *hs, Span<const uint8_t> enc);
 
 
 // Delegated credentials.
@@ -1717,6 +1765,9 @@
   bool GetClientHello(SSLMessage *out_msg, SSL_CLIENT_HELLO *out_client_hello);
 
   Span<uint8_t> secret() { return MakeSpan(secret_, hash_len_); }
+  Span<const uint8_t> secret() const {
+    return MakeConstSpan(secret_, hash_len_);
+  }
   Span<uint8_t> early_traffic_secret() {
     return MakeSpan(early_traffic_secret_, hash_len_);
   }
@@ -1746,6 +1797,10 @@
     uint32_t received;
   } extensions;
 
+  // inner_extensions_sent, on clients that offer ECH, is |extensions.sent| for
+  // the ClientHelloInner.
+  uint32_t inner_extensions_sent = 0;
+
   // error, if |wait| is |ssl_hs_error|, is the error the handshake failed on.
   UniquePtr<ERR_SAVE_STATE> error;
 
@@ -1757,11 +1812,20 @@
   // transcript is the current handshake transcript.
   SSLTranscript transcript;
 
+  // inner_transcript, on the client, is the handshake transcript for the
+  // ClientHelloInner handshake. It is moved to |transcript| if the server
+  // accepts ECH.
+  SSLTranscript inner_transcript;
+
+  // inner_client_random is the ClientHello random value used with
+  // ClientHelloInner.
+  uint8_t inner_client_random[SSL3_RANDOM_SIZE] = {0};
+
   // cookie is the value of the cookie received from the server, if any.
   Array<uint8_t> cookie;
 
-  // ech_grease contains the GREASE ECH extension to send in the ClientHello.
-  Array<uint8_t> ech_grease;
+  // ech_client_bytes contains the ECH extension to send in the ClientHello.
+  Array<uint8_t> ech_client_bytes;
 
   // ech_client_hello_buf, on the server, contains the bytes of the
   // reconstructed ClientHelloInner message.
@@ -1796,8 +1860,9 @@
   // |cert_compression_negotiated| is true.
   uint16_t cert_compression_alg_id;
 
-  // ech_hpke_ctx, on the server, is the HPKE context used to decrypt the
-  // client's ECH payloads.
+  // ech_hpke_ctx is the HPKE context used in ECH. On the server, it is
+  // initialized if |ech_accept| is true. On the client, it is initialized if
+  // |selected_ech_config| is not nullptr.
   ScopedEVP_HPKE_CTX ech_hpke_ctx;
 
   // server_params, in a TLS 1.2 server, stores the ServerKeyExchange
@@ -1841,6 +1906,11 @@
   // |SSL_CTX| rotates keys.
   UniquePtr<SSL_ECH_KEYS> ech_keys;
 
+  // selected_ech_config, for clients, is the ECHConfig the client uses to offer
+  // ECH, or nullptr if ECH is not being offered. If non-NULL, |ech_hpke_ctx|
+  // will be initialized.
+  UniquePtr<ECHConfig> selected_ech_config;
+
   // new_cipher is the cipher being negotiated in this handshake.
   const SSL_CIPHER *new_cipher = nullptr;
 
@@ -2058,7 +2128,17 @@
 // returns whether it's valid.
 bool ssl_is_sct_list_valid(const CBS *contents);
 
-bool ssl_write_client_hello(SSL_HANDSHAKE *hs);
+// ssl_write_client_hello_without_extensions writes a ClientHello to |out|,
+// up to the extensions field. |type| determines the type of ClientHello to
+// write. If |omit_session_id| is true, the session ID is empty.
+bool ssl_write_client_hello_without_extensions(const SSL_HANDSHAKE *hs,
+                                               CBB *cbb,
+                                               ssl_client_hello_type_t type,
+                                               bool empty_session_id);
+
+// ssl_add_client_hello constructs a ClientHello and adds it to the outgoing
+// flight. It returns true on success and false on error.
+bool ssl_add_client_hello(SSL_HANDSHAKE *hs);
 
 enum ssl_cert_verify_context_t {
   ssl_cert_verify_server,
@@ -2899,6 +2979,10 @@
   // DTLS-SRTP.
   UniquePtr<STACK_OF(SRTP_PROTECTION_PROFILE)> srtp_profiles;
 
+  // client_ech_config_list, if not empty, is a serialized ECHConfigList
+  // structure for the client to use when negotiating ECH.
+  Array<uint8_t> client_ech_config_list;
+
   // verify_mode is a bitmask of |SSL_VERIFY_*| values.
   uint8_t verify_mode = SSL_VERIFY_NONE;
 
@@ -3159,14 +3243,27 @@
 // false.
 bool tls1_set_curves_list(Array<uint16_t> *out_group_ids, const char *curves);
 
-// ssl_add_clienthello_tlsext writes ClientHello extensions to |out|. It returns
-// true on success and false on failure. The |header_len| argument is the length
-// of the ClientHello written so far and is used to compute the padding length.
-// (It does not include the record header or handshake headers.) On success, if
-// |*out_needs_psk_binder| is true, the last ClientHello extension was the
-// pre_shared_key extension and needs a PSK binder filled in.
-bool ssl_add_clienthello_tlsext(SSL_HANDSHAKE *hs, CBB *out,
-                                bool *out_needs_psk_binder, size_t header_len);
+// ssl_add_clienthello_tlsext writes ClientHello extensions to |out| for |type|.
+// It returns true on success and false on failure. The |header_len| argument is
+// the length of the ClientHello written so far and is used to compute the
+// padding length. (It does not include the record header or handshake headers.)
+//
+// If |type| is |ssl_client_hello_inner|, this function also writes the
+// compressed extensions to |out_encoded|. Otherwise, |out_encoded| should be
+// nullptr.
+//
+// On success, the function sets |*out_needs_psk_binder| to whether the last
+// ClientHello extension was the pre_shared_key extension and needs a PSK binder
+// filled in. The caller should then update |out| and, if applicable,
+// |out_encoded| with the binder after completing the whole message.
+//
+// If |omit_ech_len| is non-zero, the ECH extension is omitted, but padding is
+// computed as if there were an extension of length |omit_ech_len|. This is used
+// to compute ClientHelloOuterAAD.
+bool ssl_add_clienthello_tlsext(SSL_HANDSHAKE *hs, CBB *out, CBB *out_encoded,
+                                bool *out_needs_psk_binder,
+                                ssl_client_hello_type_t type, size_t header_len,
+                                size_t omit_ech_len);
 
 bool ssl_add_serverhello_tlsext(SSL_HANDSHAKE *hs, CBB *out);
 bool ssl_parse_clienthello_tlsext(SSL_HANDSHAKE *hs,
diff --git a/ssl/ssl_test.cc b/ssl/ssl_test.cc
index cdd2e97..e2708b1 100644
--- a/ssl/ssl_test.cc
+++ b/ssl/ssl_test.cc
@@ -1406,6 +1406,132 @@
   return KeyFromPEM(kKeyPEM);
 }
 
+static bool CompleteHandshakes(SSL *client, SSL *server) {
+  // Drive both their handshakes to completion.
+  for (;;) {
+    int client_ret = SSL_do_handshake(client);
+    int client_err = SSL_get_error(client, client_ret);
+    if (client_err != SSL_ERROR_NONE &&
+        client_err != SSL_ERROR_WANT_READ &&
+        client_err != SSL_ERROR_WANT_WRITE &&
+        client_err != SSL_ERROR_PENDING_TICKET) {
+      fprintf(stderr, "Client error: %s\n", SSL_error_description(client_err));
+      return false;
+    }
+
+    int server_ret = SSL_do_handshake(server);
+    int server_err = SSL_get_error(server, server_ret);
+    if (server_err != SSL_ERROR_NONE &&
+        server_err != SSL_ERROR_WANT_READ &&
+        server_err != SSL_ERROR_WANT_WRITE &&
+        server_err != SSL_ERROR_PENDING_TICKET) {
+      fprintf(stderr, "Server error: %s\n", SSL_error_description(server_err));
+      return false;
+    }
+
+    if (client_ret == 1 && server_ret == 1) {
+      break;
+    }
+  }
+
+  return true;
+}
+
+static bool FlushNewSessionTickets(SSL *client, SSL *server) {
+  // NewSessionTickets are deferred on the server to |SSL_write|, and clients do
+  // not pick them up until |SSL_read|.
+  for (;;) {
+    int server_ret = SSL_write(server, nullptr, 0);
+    int server_err = SSL_get_error(server, server_ret);
+    // The server may either succeed (|server_ret| is zero) or block on write
+    // (|server_ret| is -1 and |server_err| is |SSL_ERROR_WANT_WRITE|).
+    if (server_ret > 0 ||
+        (server_ret < 0 && server_err != SSL_ERROR_WANT_WRITE)) {
+      fprintf(stderr, "Unexpected server result: %d %d\n", server_ret,
+              server_err);
+      return false;
+    }
+
+    int client_ret = SSL_read(client, nullptr, 0);
+    int client_err = SSL_get_error(client, client_ret);
+    // The client must always block on read.
+    if (client_ret != -1 || client_err != SSL_ERROR_WANT_READ) {
+      fprintf(stderr, "Unexpected client result: %d %d\n", client_ret,
+              client_err);
+      return false;
+    }
+
+    // The server flushed everything it had to write.
+    if (server_ret == 0) {
+      return true;
+    }
+  }
+}
+
+// CreateClientAndServer creates a client and server |SSL| objects whose |BIO|s
+// are paired with each other. It does not run the handshake. The caller is
+// expected to configure the objects and drive the handshake as needed.
+static bool CreateClientAndServer(bssl::UniquePtr<SSL> *out_client,
+                                  bssl::UniquePtr<SSL> *out_server,
+                                  SSL_CTX *client_ctx, SSL_CTX *server_ctx) {
+  bssl::UniquePtr<SSL> client(SSL_new(client_ctx)), server(SSL_new(server_ctx));
+  if (!client || !server) {
+    return false;
+  }
+  SSL_set_connect_state(client.get());
+  SSL_set_accept_state(server.get());
+
+  BIO *bio1, *bio2;
+  if (!BIO_new_bio_pair(&bio1, 0, &bio2, 0)) {
+    return false;
+  }
+  // SSL_set_bio takes ownership.
+  SSL_set_bio(client.get(), bio1, bio1);
+  SSL_set_bio(server.get(), bio2, bio2);
+
+  *out_client = std::move(client);
+  *out_server = std::move(server);
+  return true;
+}
+
+struct ClientConfig {
+  SSL_SESSION *session = nullptr;
+  std::string servername;
+  bool early_data = false;
+};
+
+static bool ConnectClientAndServer(bssl::UniquePtr<SSL> *out_client,
+                                   bssl::UniquePtr<SSL> *out_server,
+                                   SSL_CTX *client_ctx, SSL_CTX *server_ctx,
+                                   const ClientConfig &config = ClientConfig(),
+                                   bool shed_handshake_config = true) {
+  bssl::UniquePtr<SSL> client, server;
+  if (!CreateClientAndServer(&client, &server, client_ctx, server_ctx)) {
+    return false;
+  }
+  if (config.early_data) {
+    SSL_set_early_data_enabled(client.get(), 1);
+  }
+  if (config.session) {
+    SSL_set_session(client.get(), config.session);
+  }
+  if (!config.servername.empty() &&
+      !SSL_set_tlsext_host_name(client.get(), config.servername.c_str())) {
+    return false;
+  }
+
+  SSL_set_shed_handshake_config(client.get(), shed_handshake_config);
+  SSL_set_shed_handshake_config(server.get(), shed_handshake_config);
+
+  if (!CompleteHandshakes(client.get(), server.get())) {
+    return false;
+  }
+
+  *out_client = std::move(client);
+  *out_server = std::move(server);
+  return true;
+}
+
 // Test that |SSL_get_client_CA_list| echoes back the configured parameter even
 // before configuring as a server.
 TEST(SSLTest, ClientCAList) {
@@ -1532,6 +1658,38 @@
   return true;
 }
 
+static bssl::UniquePtr<SSL_ECH_KEYS> MakeTestECHKeys() {
+  bssl::ScopedEVP_HPKE_KEY key;
+  uint8_t *ech_config;
+  size_t ech_config_len;
+  if (!EVP_HPKE_KEY_generate(key.get(), EVP_hpke_x25519_hkdf_sha256()) ||
+      !SSL_marshal_ech_config(&ech_config, &ech_config_len,
+                              /*config_id=*/1, key.get(), "public.example",
+                              16)) {
+    return nullptr;
+  }
+  bssl::UniquePtr<uint8_t> free_ech_config(ech_config);
+
+  // Install a non-retry config.
+  bssl::UniquePtr<SSL_ECH_KEYS> keys(SSL_ECH_KEYS_new());
+  if (!keys || !SSL_ECH_KEYS_add(keys.get(), /*is_retry_config=*/1, ech_config,
+                                 ech_config_len, key.get())) {
+    return nullptr;
+  }
+  return keys;
+}
+
+static bool InstallECHConfigList(SSL *client, const SSL_ECH_KEYS *keys) {
+  uint8_t *ech_config_list;
+  size_t ech_config_list_len;
+  if (!SSL_ECH_KEYS_marshal_retry_configs(keys, &ech_config_list,
+                                          &ech_config_list_len)) {
+    return false;
+  }
+  bssl::UniquePtr<uint8_t> free_ech_config_list(ech_config_list);
+  return SSL_set1_ech_config_list(client, ech_config_list, ech_config_list_len);
+}
+
 // Test that |SSL_marshal_ech_config| and |SSL_ECH_KEYS_marshal_retry_configs|
 // output values as expected.
 TEST(SSLTest, MarshalECHConfig) {
@@ -1604,9 +1762,6 @@
   expected.insert(expected.end(), ech_config, ech_config + ech_config_len);
   expected.insert(expected.end(), ech_config2, ech_config2 + ech_config2_len);
   EXPECT_EQ(Bytes(expected), Bytes(ech_config_list, ech_config_list_len));
-
-  // TODO(https://crbug.com/boringssl/275): When the client is implemented, test
-  // that we are able to use our own ECHConfigs.
 }
 
 TEST(SSLTest, ECHHasDuplicateConfigID) {
@@ -1763,6 +1918,196 @@
                                 key.get()));
 }
 
+// Test that |SSL_get_client_random| reports the correct value on both client
+// and server in ECH. The client sends two different random values. When ECH is
+// accepted, we should report the inner one.
+TEST(SSLTest, ECHClientRandomsMatch) {
+  bssl::UniquePtr<SSL_CTX> server_ctx =
+      CreateContextWithTestCertificate(TLS_method());
+  bssl::UniquePtr<SSL_ECH_KEYS> keys = MakeTestECHKeys();
+  ASSERT_TRUE(keys);
+  ASSERT_TRUE(SSL_CTX_set1_ech_keys(server_ctx.get(), keys.get()));
+
+  bssl::UniquePtr<SSL_CTX> client_ctx(SSL_CTX_new(TLS_method()));
+  ASSERT_TRUE(client_ctx);
+  bssl::UniquePtr<SSL> client, server;
+  ASSERT_TRUE(CreateClientAndServer(&client, &server, client_ctx.get(),
+                                    server_ctx.get()));
+  ASSERT_TRUE(InstallECHConfigList(client.get(), keys.get()));
+  ASSERT_TRUE(CompleteHandshakes(client.get(), server.get()));
+
+  EXPECT_TRUE(SSL_ech_accepted(client.get()));
+  EXPECT_TRUE(SSL_ech_accepted(server.get()));
+
+  // An ECH server will fairly naturally record the inner ClientHello random,
+  // but an ECH client may forget to update the random once ClientHelloInner is
+  // selected.
+  uint8_t client_random1[SSL3_RANDOM_SIZE];
+  uint8_t client_random2[SSL3_RANDOM_SIZE];
+  ASSERT_EQ(sizeof(client_random1),
+            SSL_get_client_random(client.get(), client_random1,
+                                  sizeof(client_random1)));
+  ASSERT_EQ(sizeof(client_random2),
+            SSL_get_client_random(server.get(), client_random2,
+                                  sizeof(client_random2)));
+  EXPECT_EQ(Bytes(client_random1), Bytes(client_random2));
+}
+
+// GetECHLength sets |*out_client_hello_len| and |*out_ech_len| to the lengths
+// of the ClientHello and ECH extension, respectively, when a client created
+// from |ctx| constructs a ClientHello with name |name| and an ECHConfig with
+// maximum name length |max_name_len|.
+static bool GetECHLength(SSL_CTX *ctx, size_t *out_client_hello_len,
+                         size_t *out_ech_len, size_t max_name_len,
+                         const char *name) {
+  bssl::ScopedEVP_HPKE_KEY key;
+  uint8_t *ech_config;
+  size_t ech_config_len;
+  if (!EVP_HPKE_KEY_generate(key.get(), EVP_hpke_x25519_hkdf_sha256()) ||
+      !SSL_marshal_ech_config(&ech_config, &ech_config_len,
+                              /*config_id=*/1, key.get(), "public.example",
+                              max_name_len)) {
+    return false;
+  }
+  bssl::UniquePtr<uint8_t> free_ech_config(ech_config);
+
+  bssl::UniquePtr<SSL_ECH_KEYS> keys(SSL_ECH_KEYS_new());
+  if (!keys || !SSL_ECH_KEYS_add(keys.get(), /*is_retry_config=*/1, ech_config,
+                                 ech_config_len, key.get())) {
+    return false;
+  }
+
+  bssl::UniquePtr<SSL> ssl(SSL_new(ctx));
+  if (!ssl || !InstallECHConfigList(ssl.get(), keys.get()) ||
+      (name != nullptr && !SSL_set_tlsext_host_name(ssl.get(), name))) {
+    return false;
+  }
+  SSL_set_connect_state(ssl.get());
+
+  std::vector<uint8_t> client_hello;
+  SSL_CLIENT_HELLO parsed;
+  const uint8_t *unused;
+  if (!GetClientHello(ssl.get(), &client_hello) ||
+      !ssl_client_hello_init(
+          ssl.get(), &parsed,
+          // Skip record and handshake headers. This assumes the ClientHello
+          // fits in one record.
+          MakeConstSpan(client_hello)
+              .subspan(SSL3_RT_HEADER_LENGTH + SSL3_HM_HEADER_LENGTH)) ||
+      !SSL_early_callback_ctx_extension_get(
+          &parsed, TLSEXT_TYPE_encrypted_client_hello, &unused, out_ech_len)) {
+    return false;
+  }
+  *out_client_hello_len = client_hello.size();
+  return true;
+}
+
+TEST(SSLTest, ECHPadding) {
+  bssl::UniquePtr<SSL_CTX> ctx(SSL_CTX_new(TLS_method()));
+  ASSERT_TRUE(ctx);
+
+  // Sample lengths with max_name_len = 128 as baseline.
+  size_t client_hello_len_baseline, ech_len_baseline;
+  ASSERT_TRUE(GetECHLength(ctx.get(), &client_hello_len_baseline,
+                           &ech_len_baseline, 128, "example.com"));
+
+  // Check that all name lengths under the server's maximum look the same.
+  for (size_t name_len : {1, 2, 32, 64, 127, 128}) {
+    SCOPED_TRACE(name_len);
+    size_t client_hello_len, ech_len;
+    ASSERT_TRUE(GetECHLength(ctx.get(), &client_hello_len, &ech_len, 128,
+                             std::string(name_len, 'a').c_str()));
+    EXPECT_EQ(client_hello_len, client_hello_len_baseline);
+    EXPECT_EQ(ech_len, ech_len_baseline);
+  }
+
+  // When sending no SNI, we must still pad as if we are sending one.
+  size_t client_hello_len, ech_len;
+  ASSERT_TRUE(
+      GetECHLength(ctx.get(), &client_hello_len, &ech_len, 128, nullptr));
+  EXPECT_EQ(client_hello_len, client_hello_len_baseline);
+  EXPECT_EQ(ech_len, ech_len_baseline);
+
+  size_t client_hello_len_129, ech_len_129;
+  ASSERT_TRUE(GetECHLength(ctx.get(), &client_hello_len_129, &ech_len_129, 128,
+                           std::string(129, 'a').c_str()));
+  // The padding calculation should not pad beyond the maximum.
+  EXPECT_GT(ech_len_129, ech_len_baseline);
+
+  // If the SNI exceeds the maximum name length, we apply some generic padding,
+  // so close name lengths still match.
+  for (size_t name_len : {129, 130, 131, 132}) {
+    SCOPED_TRACE(name_len);
+    ASSERT_TRUE(GetECHLength(ctx.get(), &client_hello_len, &ech_len, 128,
+                             std::string(name_len, 'a').c_str()));
+    EXPECT_EQ(client_hello_len, client_hello_len_129);
+    EXPECT_EQ(ech_len, ech_len_129);
+  }
+}
+
+#if defined(OPENSSL_THREADS)
+// Test that the server ECH config can be swapped out while the |SSL_CTX| is
+// in use on other threads. This test is intended to be run with TSan.
+TEST(SSLTest, ECHThreads) {
+  // Generate a pair of ECHConfigs.
+  bssl::ScopedEVP_HPKE_KEY key1;
+  ASSERT_TRUE(EVP_HPKE_KEY_generate(key1.get(), EVP_hpke_x25519_hkdf_sha256()));
+  uint8_t *ech_config1;
+  size_t ech_config1_len;
+  ASSERT_TRUE(SSL_marshal_ech_config(&ech_config1, &ech_config1_len,
+                                     /*config_id=*/1, key1.get(),
+                                     "public.example", 16));
+  bssl::UniquePtr<uint8_t> free_ech_config1(ech_config1);
+  bssl::ScopedEVP_HPKE_KEY key2;
+  ASSERT_TRUE(EVP_HPKE_KEY_generate(key2.get(), EVP_hpke_x25519_hkdf_sha256()));
+  uint8_t *ech_config2;
+  size_t ech_config2_len;
+  ASSERT_TRUE(SSL_marshal_ech_config(&ech_config2, &ech_config2_len,
+                                     /*config_id=*/2, key2.get(),
+                                     "public.example", 16));
+  bssl::UniquePtr<uint8_t> free_ech_config2(ech_config2);
+
+  // |keys1| contains the first config. |keys12| contains both.
+  bssl::UniquePtr<SSL_ECH_KEYS> keys1(SSL_ECH_KEYS_new());
+  ASSERT_TRUE(keys1);
+  ASSERT_TRUE(SSL_ECH_KEYS_add(keys1.get(), /*is_retry_config=*/1, ech_config1,
+                               ech_config1_len, key1.get()));
+  bssl::UniquePtr<SSL_ECH_KEYS> keys12(SSL_ECH_KEYS_new());
+  ASSERT_TRUE(keys12);
+  ASSERT_TRUE(SSL_ECH_KEYS_add(keys12.get(), /*is_retry_config=*/1, ech_config2,
+                               ech_config2_len, key2.get()));
+  ASSERT_TRUE(SSL_ECH_KEYS_add(keys12.get(), /*is_retry_config=*/0, ech_config1,
+                               ech_config1_len, key1.get()));
+
+  bssl::UniquePtr<SSL_CTX> server_ctx =
+      CreateContextWithTestCertificate(TLS_method());
+  ASSERT_TRUE(SSL_CTX_set1_ech_keys(server_ctx.get(), keys1.get()));
+
+  bssl::UniquePtr<SSL_CTX> client_ctx(SSL_CTX_new(TLS_method()));
+  ASSERT_TRUE(client_ctx);
+  bssl::UniquePtr<SSL> client, server;
+  ASSERT_TRUE(CreateClientAndServer(&client, &server, client_ctx.get(),
+                                    server_ctx.get()));
+  ASSERT_TRUE(InstallECHConfigList(client.get(), keys1.get()));
+
+  // In parallel, complete the connection and reconfigure the ECHConfig. Note
+  // |keys12| supports all the keys in |keys1|, so the handshake should complete
+  // the same whichever the server uses.
+  std::vector<std::thread> threads;
+  threads.emplace_back([&] {
+    ASSERT_TRUE(CompleteHandshakes(client.get(), server.get()));
+    EXPECT_TRUE(SSL_ech_accepted(client.get()));
+    EXPECT_TRUE(SSL_ech_accepted(server.get()));
+  });
+  threads.emplace_back([&] {
+    EXPECT_TRUE(SSL_CTX_set1_ech_keys(server_ctx.get(), keys12.get()));
+  });
+  for (auto &thread : threads) {
+    thread.join();
+  }
+}
+#endif  // OPENSSL_THREADS
+
 static void AppendSession(SSL_SESSION *session, void *arg) {
   std::vector<SSL_SESSION*> *out =
       reinterpret_cast<std::vector<SSL_SESSION*>*>(arg);
@@ -1888,132 +2233,6 @@
     0x69, 0x74, 0x73, 0x20, 0x50, 0x74, 0x79, 0x20, 0x4c, 0x74, 0x64,
 };
 
-static bool CompleteHandshakes(SSL *client, SSL *server) {
-  // Drive both their handshakes to completion.
-  for (;;) {
-    int client_ret = SSL_do_handshake(client);
-    int client_err = SSL_get_error(client, client_ret);
-    if (client_err != SSL_ERROR_NONE &&
-        client_err != SSL_ERROR_WANT_READ &&
-        client_err != SSL_ERROR_WANT_WRITE &&
-        client_err != SSL_ERROR_PENDING_TICKET) {
-      fprintf(stderr, "Client error: %s\n", SSL_error_description(client_err));
-      return false;
-    }
-
-    int server_ret = SSL_do_handshake(server);
-    int server_err = SSL_get_error(server, server_ret);
-    if (server_err != SSL_ERROR_NONE &&
-        server_err != SSL_ERROR_WANT_READ &&
-        server_err != SSL_ERROR_WANT_WRITE &&
-        server_err != SSL_ERROR_PENDING_TICKET) {
-      fprintf(stderr, "Server error: %s\n", SSL_error_description(server_err));
-      return false;
-    }
-
-    if (client_ret == 1 && server_ret == 1) {
-      break;
-    }
-  }
-
-  return true;
-}
-
-static bool FlushNewSessionTickets(SSL *client, SSL *server) {
-  // NewSessionTickets are deferred on the server to |SSL_write|, and clients do
-  // not pick them up until |SSL_read|.
-  for (;;) {
-    int server_ret = SSL_write(server, nullptr, 0);
-    int server_err = SSL_get_error(server, server_ret);
-    // The server may either succeed (|server_ret| is zero) or block on write
-    // (|server_ret| is -1 and |server_err| is |SSL_ERROR_WANT_WRITE|).
-    if (server_ret > 0 ||
-        (server_ret < 0 && server_err != SSL_ERROR_WANT_WRITE)) {
-      fprintf(stderr, "Unexpected server result: %d %d\n", server_ret,
-              server_err);
-      return false;
-    }
-
-    int client_ret = SSL_read(client, nullptr, 0);
-    int client_err = SSL_get_error(client, client_ret);
-    // The client must always block on read.
-    if (client_ret != -1 || client_err != SSL_ERROR_WANT_READ) {
-      fprintf(stderr, "Unexpected client result: %d %d\n", client_ret,
-              client_err);
-      return false;
-    }
-
-    // The server flushed everything it had to write.
-    if (server_ret == 0) {
-      return true;
-    }
-  }
-}
-
-// CreateClientAndServer creates a client and server |SSL| objects whose |BIO|s
-// are paired with each other. It does not run the handshake. The caller is
-// expected to configure the objects and drive the handshake as needed.
-static bool CreateClientAndServer(bssl::UniquePtr<SSL> *out_client,
-                                  bssl::UniquePtr<SSL> *out_server,
-                                  SSL_CTX *client_ctx, SSL_CTX *server_ctx) {
-  bssl::UniquePtr<SSL> client(SSL_new(client_ctx)), server(SSL_new(server_ctx));
-  if (!client || !server) {
-    return false;
-  }
-  SSL_set_connect_state(client.get());
-  SSL_set_accept_state(server.get());
-
-  BIO *bio1, *bio2;
-  if (!BIO_new_bio_pair(&bio1, 0, &bio2, 0)) {
-    return false;
-  }
-  // SSL_set_bio takes ownership.
-  SSL_set_bio(client.get(), bio1, bio1);
-  SSL_set_bio(server.get(), bio2, bio2);
-
-  *out_client = std::move(client);
-  *out_server = std::move(server);
-  return true;
-}
-
-struct ClientConfig {
-  SSL_SESSION *session = nullptr;
-  std::string servername;
-  bool early_data = false;
-};
-
-static bool ConnectClientAndServer(bssl::UniquePtr<SSL> *out_client,
-                                   bssl::UniquePtr<SSL> *out_server,
-                                   SSL_CTX *client_ctx, SSL_CTX *server_ctx,
-                                   const ClientConfig &config = ClientConfig(),
-                                   bool shed_handshake_config = true) {
-  bssl::UniquePtr<SSL> client, server;
-  if (!CreateClientAndServer(&client, &server, client_ctx, server_ctx)) {
-    return false;
-  }
-  if (config.early_data) {
-    SSL_set_early_data_enabled(client.get(), 1);
-  }
-  if (config.session) {
-    SSL_set_session(client.get(), config.session);
-  }
-  if (!config.servername.empty() &&
-      !SSL_set_tlsext_host_name(client.get(), config.servername.c_str())) {
-    return false;
-  }
-
-  SSL_set_shed_handshake_config(client.get(), shed_handshake_config);
-  SSL_set_shed_handshake_config(server.get(), shed_handshake_config);
-
-  if (!CompleteHandshakes(client.get(), server.get())) {
-    return false;
-  }
-
-  *out_client = std::move(client);
-  *out_server = std::move(server);
-  return true;
-}
-
 // SSLVersionTest executes its test cases under all available protocol versions.
 // Test cases call |Connect| to create a connection using context objects with
 // the protocol version fixed to the current version under test.
diff --git a/ssl/ssl_versions.cc b/ssl/ssl_versions.cc
index 6caf070..df499c7 100644
--- a/ssl/ssl_versions.cc
+++ b/ssl/ssl_versions.cc
@@ -273,9 +273,13 @@
   return true;
 }
 
-bool ssl_add_supported_versions(const SSL_HANDSHAKE *hs, CBB *cbb) {
+bool ssl_add_supported_versions(const SSL_HANDSHAKE *hs, CBB *cbb,
+                                uint16_t extra_min_version) {
   for (uint16_t version : get_method_versions(hs->ssl->method)) {
+    uint16_t protocol_version;
     if (ssl_supports_version(hs, version) &&
+        ssl_protocol_version_from_wire(&protocol_version, version) &&
+        protocol_version >= extra_min_version &&  //
         !CBB_add_u16(cbb, version)) {
       return false;
     }
diff --git a/ssl/t1_lib.cc b/ssl/t1_lib.cc
index 106c99a..90ad5eb 100644
--- a/ssl/t1_lib.cc
+++ b/ssl/t1_lib.cc
@@ -127,7 +127,6 @@
 #include <openssl/hpke.h>
 #include <openssl/mem.h>
 #include <openssl/nid.h>
-#include <openssl/rand.h>
 
 #include "../crypto/internal.h"
 #include "internal.h"
@@ -502,7 +501,15 @@
 //
 // The add callbacks receive a |CBB| to which the extension can be appended but
 // the function is responsible for appending the type and length bytes too.
-// |add_clienthello| may be called multiple times and must not mutate |hs|.
+//
+// |add_clienthello| may be called multiple times and must not mutate |hs|. It
+// is additionally passed two output |CBB|s. If the extension is the same
+// independent of the value of |type|, the callback may write to
+// |out_compressible| instead of |out|. When serializing the ClientHelloInner,
+// all compressible extensions will be made continguous and replaced with
+// ech_outer_extensions when encrypted. When serializing the ClientHelloOuter
+// or not offering ECH, |out| will be equal to |out_compressible|, so writing to
+// |out_compressible| still works.
 //
 // Note the |parse_serverhello| and |add_serverhello| callbacks refer to the
 // TLS 1.2 ServerHello. In TLS 1.3, these callbacks act on EncryptedExtensions,
@@ -514,7 +521,8 @@
 struct tls_extension {
   uint16_t value;
 
-  bool (*add_clienthello)(const SSL_HANDSHAKE *hs, CBB *out);
+  bool (*add_clienthello)(const SSL_HANDSHAKE *hs, CBB *out,
+                          CBB *out_compressible, ssl_client_hello_type_t type);
   bool (*parse_serverhello)(SSL_HANDSHAKE *hs, uint8_t *out_alert,
                             CBS *contents);
 
@@ -549,10 +557,21 @@
 //
 // https://tools.ietf.org/html/rfc6066#section-3.
 
-static bool ext_sni_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out) {
+static bool ext_sni_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out,
+                                    CBB *out_compressible,
+                                    ssl_client_hello_type_t type) {
   const SSL *const ssl = hs->ssl;
-  if (ssl->hostname == nullptr) {
-    return true;
+  // If offering ECH, send the public name instead of the configured name.
+  Span<const uint8_t> hostname;
+  if (type == ssl_client_hello_outer) {
+    hostname = hs->selected_ech_config->public_name;
+  } else {
+    if (ssl->hostname == nullptr) {
+      return true;
+    }
+    hostname =
+        MakeConstSpan(reinterpret_cast<const uint8_t *>(ssl->hostname.get()),
+                      strlen(ssl->hostname.get()));
   }
 
   CBB contents, server_name_list, name;
@@ -561,8 +580,7 @@
       !CBB_add_u16_length_prefixed(&contents, &server_name_list) ||
       !CBB_add_u8(&server_name_list, TLSEXT_NAMETYPE_host_name) ||
       !CBB_add_u16_length_prefixed(&server_name_list, &name) ||
-      !CBB_add_bytes(&name, (const uint8_t *)ssl->hostname.get(),
-                     strlen(ssl->hostname.get())) ||
+      !CBB_add_bytes(&name, hostname.data(), hostname.size()) ||
       !CBB_flush(out)) {
     return false;
   }
@@ -602,88 +620,24 @@
 //
 // https://tools.ietf.org/html/draft-ietf-tls-esni-10
 
-// 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;
-}
-
-bool ssl_setup_ech_grease(SSL_HANDSHAKE *hs) {
-  if (hs->max_version < TLS1_3_VERSION || !hs->config->ech_grease_enabled) {
+static bool ext_ech_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out,
+                                    CBB *out_compressible,
+                                    ssl_client_hello_type_t type) {
+  if (type == ssl_client_hello_inner || hs->ech_client_bytes.empty()) {
     return true;
   }
 
-  const uint16_t kdf_id = EVP_HPKE_HKDF_SHA256;
-  const uint16_t aead_id = EVP_has_aes_hardware() ? EVP_HPKE_AES_128_GCM
-                                                  : EVP_HPKE_CHACHA20_POLY1305;
-  constexpr size_t kAEADOverhead = 16;  // Both AEADs have a 16-byte tag.
-  static_assert(ssl_grease_ech_config_id < sizeof(hs->grease_seed),
-                "hs->grease_seed is too small");
-  uint8_t ech_config_id = hs->grease_seed[ssl_grease_ech_config_id];
-
-  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 estimate the size of a
-  // typical EncodedClientHelloInner without resumption:
-  //
-  //   2+32+1+2   version, random, legacy_session_id, legacy_compression_methods
-  //   2+4*2      cipher_suites (three TLS 1.3 ciphers, GREASE)
-  //   2          extensions prefix
-  //   4          ech_is_inner
-  //   4+1+2*2    supported_versions (TLS 1.3, GREASE)
-  //   4+1+10*2   outer_extensions (key_share, sigalgs, sct, alpn,
-  //              supported_groups, status_request, psk_key_exchange_modes,
-  //              compress_certificate, GREASE x2)
-  //
-  // The server_name extension has an overhead of 9 bytes. For now, arbitrarily
-  // estimate maximum_name_length to be between 32 and 100 bytes.
-  //
-  // TODO(davidben): If the padding scheme changes to also round the entire
-  // payload, adjust this to match. See
-  // https://github.com/tlswg/draft-ietf-tls-esni/issues/433
-  const size_t payload_len = random_size(128, 196) + kAEADOverhead;
-  bssl::ScopedCBB cbb;
-  CBB enc_cbb, payload_cbb;
-  uint8_t *payload;
-  if (!CBB_init(cbb.get(), 64 + payload_len) ||
-      !CBB_add_u16(cbb.get(), kdf_id) ||  //
-      !CBB_add_u16(cbb.get(), aead_id) ||
-      !CBB_add_u8(cbb.get(), ech_config_id) ||
-      !CBB_add_u16_length_prefixed(cbb.get(), &enc_cbb) ||
-      !CBB_add_bytes(&enc_cbb, ech_enc, OPENSSL_ARRAY_SIZE(ech_enc)) ||
-      !CBB_add_u16_length_prefixed(cbb.get(), &payload_cbb) ||
-      !CBB_add_space(&payload_cbb, &payload, payload_len) ||
-      !RAND_bytes(payload, payload_len) ||
-      !CBBFinishArray(cbb.get(), &hs->ech_grease)) {
+  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_client_bytes.data(),
+                     hs->ech_client_bytes.size()) ||
+      !CBB_flush(out)) {
     return false;
   }
   return true;
 }
 
-static bool ext_ech_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out) {
-  if (hs->max_version < TLS1_3_VERSION) {
-    return true;
-  }
-  if (hs->config->ech_grease_enabled) {
-    assert(!hs->ech_grease.empty());
-    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;
-  }
-  // 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) {
   SSL *const ssl = hs->ssl;
@@ -699,24 +653,23 @@
     return false;
   }
 
-  // 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) {
+  // The server may only send retry configs in response to ClientHelloOuter (or
+  // ECH GREASE), not ClientHelloInner. The unsolicited extension rule checks
+  // this implicitly because the ClientHelloInner has no encrypted_client_hello
+  // extension.
+  //
+  // TODO(https://crbug.com/boringssl/275): If
+  // https://github.com/tlswg/draft-ietf-tls-esni/pull/422 is merged, a later
+  // draft will fold encrypted_client_hello and ech_is_inner together. Then this
+  // assert should become a runtime check.
+  assert(!ssl->s3->ech_accept);
+
+  // TODO(https://crbug.com/boringssl/275): When the implementing the
+  // ClientHelloOuter flow, save the retry configs.
+  if (!ssl_is_valid_ech_config_list(*contents)) {
     *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;
 }
 
@@ -749,16 +702,23 @@
     if (!config->is_retry_config()) {
       continue;
     }
-    if (!CBB_add_bytes(&retry_configs, config->ech_config().data(),
-                       config->ech_config().size())) {
+    if (!CBB_add_bytes(&retry_configs, config->ech_config().raw.data(),
+                       config->ech_config().raw.size())) {
       return false;
     }
   }
   return CBB_flush(out);
 }
 
-static bool ext_ech_is_inner_add_clienthello(const SSL_HANDSHAKE *hs,
-                                             CBB *out) {
+static bool ext_ech_is_inner_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out,
+                                             CBB *out_compressible,
+                                             ssl_client_hello_type_t type) {
+  if (type == ssl_client_hello_inner) {
+    if (!CBB_add_u16(out, TLSEXT_TYPE_ech_is_inner) ||
+        !CBB_add_u16(out, 0 /* empty extension */)) {
+      return false;
+    }
+  }
   return true;
 }
 
@@ -781,10 +741,13 @@
 //
 // https://tools.ietf.org/html/rfc5746
 
-static bool ext_ri_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out) {
+static bool ext_ri_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out,
+                                   CBB *out_compressible,
+                                   ssl_client_hello_type_t type) {
   const SSL *const ssl = hs->ssl;
   // Renegotiation indication is not necessary in TLS 1.3.
-  if (hs->min_version >= TLS1_3_VERSION) {
+  if (hs->min_version >= TLS1_3_VERSION ||
+     type == ssl_client_hello_inner) {
     return true;
   }
 
@@ -946,9 +909,11 @@
 //
 // https://tools.ietf.org/html/rfc7627
 
-static bool ext_ems_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out) {
+static bool ext_ems_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out,
+                                    CBB *out_compressible,
+                                    ssl_client_hello_type_t type) {
   // Extended master secret is not necessary in TLS 1.3.
-  if (hs->min_version >= TLS1_3_VERSION) {
+  if (hs->min_version >= TLS1_3_VERSION || type == ssl_client_hello_inner) {
     return true;
   }
 
@@ -1021,10 +986,12 @@
 //
 // https://tools.ietf.org/html/rfc5077
 
-static bool ext_ticket_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out) {
+static bool ext_ticket_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out,
+                                       CBB *out_compressible,
+                                       ssl_client_hello_type_t type) {
   const SSL *const ssl = hs->ssl;
   // TLS 1.3 uses a different ticket extension.
-  if (hs->min_version >= TLS1_3_VERSION ||
+  if (hs->min_version >= TLS1_3_VERSION || type == ssl_client_hello_inner ||
       SSL_get_options(ssl) & SSL_OP_NO_TICKET) {
     return true;
   }
@@ -1099,17 +1066,19 @@
 //
 // https://tools.ietf.org/html/rfc5246#section-7.4.1.4.1
 
-static bool ext_sigalgs_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out) {
+static bool ext_sigalgs_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out,
+                                        CBB *out_compressible,
+                                        ssl_client_hello_type_t type) {
   if (hs->max_version < TLS1_2_VERSION) {
     return true;
   }
 
   CBB contents, sigalgs_cbb;
-  if (!CBB_add_u16(out, TLSEXT_TYPE_signature_algorithms) ||
-      !CBB_add_u16_length_prefixed(out, &contents) ||
+  if (!CBB_add_u16(out_compressible, TLSEXT_TYPE_signature_algorithms) ||
+      !CBB_add_u16_length_prefixed(out_compressible, &contents) ||
       !CBB_add_u16_length_prefixed(&contents, &sigalgs_cbb) ||
       !tls12_add_verify_sigalgs(hs, &sigalgs_cbb) ||
-      !CBB_flush(out)) {
+      !CBB_flush(out_compressible)) {
     return false;
   }
 
@@ -1138,18 +1107,20 @@
 //
 // https://tools.ietf.org/html/rfc6066#section-8
 
-static bool ext_ocsp_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out) {
+static bool ext_ocsp_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out,
+                                     CBB *out_compressible,
+                                     ssl_client_hello_type_t type) {
   if (!hs->config->ocsp_stapling_enabled) {
     return true;
   }
 
   CBB contents;
-  if (!CBB_add_u16(out, TLSEXT_TYPE_status_request) ||
-      !CBB_add_u16_length_prefixed(out, &contents) ||
+  if (!CBB_add_u16(out_compressible, TLSEXT_TYPE_status_request) ||
+      !CBB_add_u16_length_prefixed(out_compressible, &contents) ||
       !CBB_add_u8(&contents, TLSEXT_STATUSTYPE_ocsp) ||
       !CBB_add_u16(&contents, 0 /* empty responder ID list */) ||
       !CBB_add_u16(&contents, 0 /* empty request extensions */) ||
-      !CBB_flush(out)) {
+      !CBB_flush(out_compressible)) {
     return false;
   }
 
@@ -1220,11 +1191,16 @@
 //
 // https://htmlpreview.github.io/?https://github.com/agl/technotes/blob/master/nextprotoneg.html
 
-static bool ext_npn_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out) {
+static bool ext_npn_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out,
+                                    CBB *out_compressible,
+                                    ssl_client_hello_type_t type) {
   const SSL *const ssl = hs->ssl;
-  if (ssl->s3->initial_handshake_complete ||
-      ssl->ctx->next_proto_select_cb == NULL ||
-      SSL_is_dtls(ssl)) {
+  if (ssl->ctx->next_proto_select_cb == NULL ||
+      // Do not allow NPN to change on renegotiation.
+      ssl->s3->initial_handshake_complete ||
+      // NPN is not defined in DTLS or TLS 1.3.
+      SSL_is_dtls(ssl) || hs->min_version >= TLS1_3_VERSION ||
+      type == ssl_client_hello_inner) {
     return true;
   }
 
@@ -1343,13 +1319,15 @@
 //
 // https://tools.ietf.org/html/rfc6962#section-3.3.1
 
-static bool ext_sct_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out) {
+static bool ext_sct_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out,
+                                    CBB *out_compressible,
+                                    ssl_client_hello_type_t type) {
   if (!hs->config->signed_cert_timestamps_enabled) {
     return true;
   }
 
-  if (!CBB_add_u16(out, TLSEXT_TYPE_certificate_timestamp) ||
-      !CBB_add_u16(out, 0 /* length */)) {
+  if (!CBB_add_u16(out_compressible, TLSEXT_TYPE_certificate_timestamp) ||
+      !CBB_add_u16(out_compressible, 0 /* length */)) {
     return false;
   }
 
@@ -1434,7 +1412,9 @@
 //
 // https://tools.ietf.org/html/rfc7301
 
-static bool ext_alpn_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out) {
+static bool ext_alpn_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out,
+                                     CBB *out_compressible,
+                                     ssl_client_hello_type_t type) {
   const SSL *const ssl = hs->ssl;
   if (hs->config->alpn_client_proto_list.empty() && ssl->quic_method) {
     // ALPN MUST be used with QUIC.
@@ -1448,12 +1428,13 @@
   }
 
   CBB contents, proto_list;
-  if (!CBB_add_u16(out, TLSEXT_TYPE_application_layer_protocol_negotiation) ||
-      !CBB_add_u16_length_prefixed(out, &contents) ||
+  if (!CBB_add_u16(out_compressible,
+                   TLSEXT_TYPE_application_layer_protocol_negotiation) ||
+      !CBB_add_u16_length_prefixed(out_compressible, &contents) ||
       !CBB_add_u16_length_prefixed(&contents, &proto_list) ||
       !CBB_add_bytes(&proto_list, hs->config->alpn_client_proto_list.data(),
                      hs->config->alpn_client_proto_list.size()) ||
-      !CBB_flush(out)) {
+      !CBB_flush(out_compressible)) {
     return false;
   }
 
@@ -1648,14 +1629,16 @@
 //
 // https://tools.ietf.org/html/draft-balfanz-tls-channelid-01
 
-static bool ext_channel_id_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out) {
+static bool ext_channel_id_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out,
+                                           CBB *out_compressible,
+                                           ssl_client_hello_type_t type) {
   const SSL *const ssl = hs->ssl;
   if (!hs->config->channel_id_private || SSL_is_dtls(ssl)) {
     return true;
   }
 
-  if (!CBB_add_u16(out, TLSEXT_TYPE_channel_id) ||
-      !CBB_add_u16(out, 0 /* length */)) {
+  if (!CBB_add_u16(out_compressible, TLSEXT_TYPE_channel_id) ||
+      !CBB_add_u16(out_compressible, 0 /* length */)) {
     return false;
   }
 
@@ -1714,7 +1697,9 @@
 //
 // https://tools.ietf.org/html/rfc5764
 
-static bool ext_srtp_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out) {
+static bool ext_srtp_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out,
+                                     CBB *out_compressible,
+                                     ssl_client_hello_type_t type) {
   const SSL *const ssl = hs->ssl;
   const STACK_OF(SRTP_PROTECTION_PROFILE) *profiles =
       SSL_get_srtp_profiles(ssl);
@@ -1725,8 +1710,8 @@
   }
 
   CBB contents, profile_ids;
-  if (!CBB_add_u16(out, TLSEXT_TYPE_srtp) ||
-      !CBB_add_u16_length_prefixed(out, &contents) ||
+  if (!CBB_add_u16(out_compressible, TLSEXT_TYPE_srtp) ||
+      !CBB_add_u16_length_prefixed(out_compressible, &contents) ||
       !CBB_add_u16_length_prefixed(&contents, &profile_ids)) {
     return false;
   }
@@ -1738,7 +1723,7 @@
   }
 
   if (!CBB_add_u8(&contents, 0 /* empty use_mki value */) ||
-      !CBB_flush(out)) {
+      !CBB_flush(out_compressible)) {
     return false;
   }
 
@@ -1868,9 +1853,11 @@
   return true;
 }
 
-static bool ext_ec_point_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out) {
+static bool ext_ec_point_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out,
+                                         CBB *out_compressible,
+                                         ssl_client_hello_type_t type) {
   // The point format extension is unnecessary in TLS 1.3.
-  if (hs->min_version >= TLS1_3_VERSION) {
+  if (hs->min_version >= TLS1_3_VERSION || type == ssl_client_hello_inner) {
     return true;
   }
 
@@ -1936,10 +1923,19 @@
 //
 // https://tools.ietf.org/html/rfc8446#section-4.2.11
 
-static bool should_offer_psk(const SSL_HANDSHAKE *hs) {
+static bool should_offer_psk(const SSL_HANDSHAKE *hs,
+                             ssl_client_hello_type_t type) {
   const SSL *const ssl = hs->ssl;
   if (hs->max_version < TLS1_3_VERSION || ssl->session == nullptr ||
-      ssl_session_protocol_version(ssl->session.get()) < TLS1_3_VERSION) {
+      ssl_session_protocol_version(ssl->session.get()) < TLS1_3_VERSION ||
+      // The ClientHelloOuter cannot include the PSK extension.
+      //
+      // TODO(https://crbug.com/boringssl/275): draft-ietf-tls-esni-10 mandates
+      // this, but it risks breaking the ClientHelloOuter flow on 0-RTT reject.
+      // Later drafts will recommend including a placeholder one, at which point
+      // we will need to synthesize a ticket. See
+      // https://github.com/tlswg/draft-ietf-tls-esni/issues/408
+      type == ssl_client_hello_outer) {
     return false;
   }
 
@@ -1954,9 +1950,10 @@
   return true;
 }
 
-static size_t ext_pre_shared_key_clienthello_length(const SSL_HANDSHAKE *hs) {
+static size_t ext_pre_shared_key_clienthello_length(
+    const SSL_HANDSHAKE *hs, ssl_client_hello_type_t type) {
   const SSL *const ssl = hs->ssl;
-  if (!should_offer_psk(hs)) {
+  if (!should_offer_psk(hs, type)) {
     return 0;
   }
 
@@ -1965,11 +1962,11 @@
 }
 
 static bool ext_pre_shared_key_add_clienthello(const SSL_HANDSHAKE *hs,
-                                               CBB *out,
-                                               bool *out_needs_binder) {
+                                               CBB *out, bool *out_needs_binder,
+                                               ssl_client_hello_type_t type) {
   const SSL *const ssl = hs->ssl;
   *out_needs_binder = false;
-  if (!should_offer_psk(hs)) {
+  if (!should_offer_psk(hs, type)) {
     return true;
   }
 
@@ -2110,21 +2107,22 @@
 //
 // https://tools.ietf.org/html/rfc8446#section-4.2.9
 
-static bool ext_psk_key_exchange_modes_add_clienthello(const SSL_HANDSHAKE *hs,
-                                                       CBB *out) {
+static bool ext_psk_key_exchange_modes_add_clienthello(
+    const SSL_HANDSHAKE *hs, CBB *out, CBB *out_compressible,
+    ssl_client_hello_type_t type) {
   if (hs->max_version < TLS1_3_VERSION) {
     return true;
   }
 
   CBB contents, ke_modes;
-  if (!CBB_add_u16(out, TLSEXT_TYPE_psk_key_exchange_modes) ||
-      !CBB_add_u16_length_prefixed(out, &contents) ||
+  if (!CBB_add_u16(out_compressible, TLSEXT_TYPE_psk_key_exchange_modes) ||
+      !CBB_add_u16_length_prefixed(out_compressible, &contents) ||
       !CBB_add_u8_length_prefixed(&contents, &ke_modes) ||
       !CBB_add_u8(&ke_modes, SSL_PSK_DHE_KE)) {
     return false;
   }
 
-  return CBB_flush(out);
+  return CBB_flush(out_compressible);
 }
 
 static bool ext_psk_key_exchange_modes_parse_clienthello(SSL_HANDSHAKE *hs,
@@ -2154,7 +2152,9 @@
 //
 // https://tools.ietf.org/html/rfc8446#section-4.2.10
 
-static bool ext_early_data_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out) {
+static bool ext_early_data_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out,
+                                           CBB *out_compressible,
+                                           ssl_client_hello_type_t type) {
   const SSL *const ssl = hs->ssl;
   // The second ClientHello never offers early data, and we must have already
   // filled in |early_data_reason| by this point.
@@ -2167,9 +2167,16 @@
     return true;
   }
 
-  if (!CBB_add_u16(out, TLSEXT_TYPE_early_data) ||
-      !CBB_add_u16(out, 0) ||
-      !CBB_flush(out)) {
+  // If offering ECH, the extension only applies to ClientHelloInner, but we
+  // send the extension in both ClientHellos. This ensures that, if the server
+  // handshakes with ClientHelloOuter, it can skip past early data. See
+  // https://github.com/tlswg/draft-ietf-tls-esni/pull/415
+  //
+  // TODO(https://crbug.com/boringssl/275): Replace this with a reference to the
+  // right section in the next draft.
+  if (!CBB_add_u16(out_compressible, TLSEXT_TYPE_early_data) ||
+      !CBB_add_u16(out_compressible, 0) ||
+      !CBB_flush(out_compressible)) {
     return false;
   }
 
@@ -2316,19 +2323,21 @@
   return CBBFinishArray(cbb.get(), &hs->key_share_bytes);
 }
 
-static bool ext_key_share_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out) {
+static bool ext_key_share_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out,
+                                          CBB *out_compressible,
+                                          ssl_client_hello_type_t type) {
   if (hs->max_version < TLS1_3_VERSION) {
     return true;
   }
 
   assert(!hs->key_share_bytes.empty());
   CBB contents, kse_bytes;
-  if (!CBB_add_u16(out, TLSEXT_TYPE_key_share) ||
-      !CBB_add_u16_length_prefixed(out, &contents) ||
+  if (!CBB_add_u16(out_compressible, TLSEXT_TYPE_key_share) ||
+      !CBB_add_u16_length_prefixed(out_compressible, &contents) ||
       !CBB_add_u16_length_prefixed(&contents, &kse_bytes) ||
       !CBB_add_bytes(&kse_bytes, hs->key_share_bytes.data(),
                      hs->key_share_bytes.size()) ||
-      !CBB_flush(out)) {
+      !CBB_flush(out_compressible)) {
     return false;
   }
 
@@ -2441,13 +2450,20 @@
 //
 // https://tools.ietf.org/html/rfc8446#section-4.2.1
 
-static bool ext_supported_versions_add_clienthello(const SSL_HANDSHAKE *hs,
-                                                   CBB *out) {
+static bool ext_supported_versions_add_clienthello(
+    const SSL_HANDSHAKE *hs, CBB *out, CBB *out_compressible,
+    ssl_client_hello_type_t type) {
   const SSL *const ssl = hs->ssl;
   if (hs->max_version <= TLS1_2_VERSION) {
     return true;
   }
 
+  // supported_versions is compressible in ECH if ClientHelloOuter already
+  // requires TLS 1.3. Otherwise the extensions differ in the older versions.
+  if (hs->min_version >= TLS1_3_VERSION) {
+    out = out_compressible;
+  }
+
   CBB contents, versions;
   if (!CBB_add_u16(out, TLSEXT_TYPE_supported_versions) ||
       !CBB_add_u16_length_prefixed(out, &contents) ||
@@ -2461,7 +2477,10 @@
     return false;
   }
 
-  if (!ssl_add_supported_versions(hs, &versions) ||
+  // Encrypted ClientHellos requires TLS 1.3 or later.
+  uint16_t extra_min_version =
+      type == ssl_client_hello_inner ? TLS1_3_VERSION : 0;
+  if (!ssl_add_supported_versions(hs, &versions, extra_min_version) ||
       !CBB_flush(out)) {
     return false;
   }
@@ -2474,17 +2493,19 @@
 //
 // https://tools.ietf.org/html/rfc8446#section-4.2.2
 
-static bool ext_cookie_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out) {
+static bool ext_cookie_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out,
+                                       CBB *out_compressible,
+                                       ssl_client_hello_type_t type) {
   if (hs->cookie.empty()) {
     return true;
   }
 
   CBB contents, cookie;
-  if (!CBB_add_u16(out, TLSEXT_TYPE_cookie) ||
-      !CBB_add_u16_length_prefixed(out, &contents) ||
+  if (!CBB_add_u16(out_compressible, TLSEXT_TYPE_cookie) ||
+      !CBB_add_u16_length_prefixed(out_compressible, &contents) ||
       !CBB_add_u16_length_prefixed(&contents, &cookie) ||
       !CBB_add_bytes(&cookie, hs->cookie.data(), hs->cookie.size()) ||
-      !CBB_flush(out)) {
+      !CBB_flush(out_compressible)) {
     return false;
   }
 
@@ -2498,11 +2519,13 @@
 // https://tools.ietf.org/html/rfc8446#section-4.2.7
 
 static bool ext_supported_groups_add_clienthello(const SSL_HANDSHAKE *hs,
-                                                 CBB *out) {
+                                                 CBB *out,
+                                                 CBB *out_compressible,
+                                                 ssl_client_hello_type_t type) {
   const SSL *const ssl = hs->ssl;
   CBB contents, groups_bytes;
-  if (!CBB_add_u16(out, TLSEXT_TYPE_supported_groups) ||
-      !CBB_add_u16_length_prefixed(out, &contents) ||
+  if (!CBB_add_u16(out_compressible, TLSEXT_TYPE_supported_groups) ||
+      !CBB_add_u16_length_prefixed(out_compressible, &contents) ||
       !CBB_add_u16_length_prefixed(&contents, &groups_bytes)) {
     return false;
   }
@@ -2524,7 +2547,7 @@
     }
   }
 
-  return CBB_flush(out);
+  return CBB_flush(out_compressible);
 }
 
 static bool ext_supported_groups_parse_serverhello(SSL_HANDSHAKE *hs,
@@ -2613,16 +2636,18 @@
   return true;
 }
 
-static bool ext_quic_transport_params_add_clienthello(const SSL_HANDSHAKE *hs,
-                                                      CBB *out) {
+static bool ext_quic_transport_params_add_clienthello(
+    const SSL_HANDSHAKE *hs, CBB *out, CBB *out_compressible,
+    ssl_client_hello_type_t type) {
   return ext_quic_transport_params_add_clienthello_impl(
-      hs, out, /*use_legacy_codepoint=*/false);
+      hs, out_compressible, /*use_legacy_codepoint=*/false);
 }
 
 static bool ext_quic_transport_params_add_clienthello_legacy(
-    const SSL_HANDSHAKE *hs, CBB *out) {
+    const SSL_HANDSHAKE *hs, CBB *out, CBB *out_compressible,
+    ssl_client_hello_type_t type) {
   return ext_quic_transport_params_add_clienthello_impl(
-      hs, out, /*use_legacy_codepoint=*/true);
+      hs, out_compressible, /*use_legacy_codepoint=*/true);
 }
 
 static bool ext_quic_transport_params_parse_serverhello_impl(
@@ -2766,8 +2791,9 @@
 //
 // https://tools.ietf.org/html/draft-ietf-tls-subcerts
 
-static bool ext_delegated_credential_add_clienthello(const SSL_HANDSHAKE *hs,
-                                                     CBB *out) {
+static bool ext_delegated_credential_add_clienthello(
+    const SSL_HANDSHAKE *hs, CBB *out, CBB *out_compressible,
+    ssl_client_hello_type_t type) {
   return true;
 }
 
@@ -2796,8 +2822,9 @@
 
 // Certificate compression
 
-static bool cert_compression_add_clienthello(const SSL_HANDSHAKE *hs,
-                                             CBB *out) {
+static bool cert_compression_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out,
+                                             CBB *out_compressible,
+                                             ssl_client_hello_type_t type) {
   bool first = true;
   CBB contents, algs;
 
@@ -2806,9 +2833,10 @@
       continue;
     }
 
-    if (first && (!CBB_add_u16(out, TLSEXT_TYPE_cert_compression) ||
-                  !CBB_add_u16_length_prefixed(out, &contents) ||
-                  !CBB_add_u8_length_prefixed(&contents, &algs))) {
+    if (first &&
+        (!CBB_add_u16(out_compressible, TLSEXT_TYPE_cert_compression) ||
+         !CBB_add_u16_length_prefixed(out_compressible, &contents) ||
+         !CBB_add_u8_length_prefixed(&contents, &algs))) {
       return false;
     }
     first = false;
@@ -2817,7 +2845,7 @@
     }
   }
 
-  return first || CBB_flush(out);
+  return first || CBB_flush(out_compressible);
 }
 
 static bool cert_compression_parse_serverhello(SSL_HANDSHAKE *hs,
@@ -2915,7 +2943,9 @@
   return false;
 }
 
-static bool ext_alps_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out) {
+static bool ext_alps_add_clienthello(const SSL_HANDSHAKE *hs, CBB *out,
+                                     CBB *out_compressible,
+                                     ssl_client_hello_type_t type) {
   const SSL *const ssl = hs->ssl;
   if (// ALPS requires TLS 1.3.
       hs->max_version < TLS1_3_VERSION ||
@@ -2929,8 +2959,8 @@
   }
 
   CBB contents, proto_list, proto;
-  if (!CBB_add_u16(out, TLSEXT_TYPE_application_settings) ||
-      !CBB_add_u16_length_prefixed(out, &contents) ||
+  if (!CBB_add_u16(out_compressible, TLSEXT_TYPE_application_settings) ||
+      !CBB_add_u16_length_prefixed(out_compressible, &contents) ||
       !CBB_add_u16_length_prefixed(&contents, &proto_list)) {
     return false;
   }
@@ -2943,7 +2973,7 @@
     }
   }
 
-  return CBB_flush(out);
+  return CBB_flush(out_compressible);
 }
 
 static bool ext_alps_parse_serverhello(SSL_HANDSHAKE *hs, uint8_t *out_alert,
@@ -3267,11 +3297,153 @@
   return CBB_flush(cbb);
 }
 
-bool ssl_add_clienthello_tlsext(SSL_HANDSHAKE *hs, CBB *out,
-                                bool *out_needs_psk_binder, size_t header_len) {
+static bool ssl_add_clienthello_tlsext_inner(SSL_HANDSHAKE *hs, CBB *out,
+                                             CBB *out_encoded,
+                                             bool *out_needs_psk_binder) {
+  // When writing ClientHelloInner, we construct the real and encoded
+  // ClientHellos concurrently, to handle compression. Uncompressed extensions
+  // are written to |extensions| and copied to |extensions_encoded|. Compressed
+  // extensions are buffered in |compressed| and written to the end. (ECH can
+  // only compress continguous extensions.)
+  SSL *const ssl = hs->ssl;
+  bssl::ScopedCBB compressed, outer_extensions;
+  CBB extensions, extensions_encoded;
+  if (!CBB_add_u16_length_prefixed(out, &extensions) ||
+      !CBB_add_u16_length_prefixed(out_encoded, &extensions_encoded) ||
+      !CBB_init(compressed.get(), 64) ||
+      !CBB_init(outer_extensions.get(), 64)) {
+    OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
+    return false;
+  }
+
+  hs->inner_extensions_sent = 0;
+
+  if (ssl->ctx->grease_enabled) {
+    // Add a fake empty extension. See RFC 8701. This always matches
+    // |ssl_add_clienthello_tlsext|, so compress it.
+    uint16_t grease_ext = ssl_get_grease_value(hs, ssl_grease_extension1);
+    if (!add_padding_extension(compressed.get(), grease_ext, 0) ||
+        !CBB_add_u16(outer_extensions.get(), grease_ext)) {
+      return false;
+    }
+  }
+
+  for (size_t i = 0; i < kNumExtensions; i++) {
+    const size_t len_before = CBB_len(&extensions);
+    const size_t len_compressed_before = CBB_len(compressed.get());
+    if (!kExtensions[i].add_clienthello(hs, &extensions, compressed.get(),
+                                        ssl_client_hello_inner)) {
+      OPENSSL_PUT_ERROR(SSL, SSL_R_ERROR_ADDING_EXTENSION);
+      ERR_add_error_dataf("extension %u", (unsigned)kExtensions[i].value);
+      return false;
+    }
+
+    const size_t bytes_written = CBB_len(&extensions) - len_before;
+    const size_t bytes_written_compressed =
+        CBB_len(compressed.get()) - len_compressed_before;
+    // The callback may write to at most one output.
+    assert(bytes_written == 0 || bytes_written_compressed == 0);
+    if (bytes_written != 0 || bytes_written_compressed != 0) {
+      hs->inner_extensions_sent |= (1u << i);
+    }
+    // If compressed, update the running ech_outer_extensions extension.
+    if (bytes_written_compressed != 0 &&
+        !CBB_add_u16(outer_extensions.get(), kExtensions[i].value)) {
+      return false;
+    }
+  }
+
+  if (ssl->ctx->grease_enabled) {
+    // Add a fake non-empty extension. See RFC 8701. This always matches
+    // |ssl_add_clienthello_tlsext|, so compress it.
+    uint16_t grease_ext = ssl_get_grease_value(hs, ssl_grease_extension2);
+    if (!add_padding_extension(compressed.get(), grease_ext, 1) ||
+        !CBB_add_u16(outer_extensions.get(), grease_ext)) {
+      return false;
+    }
+  }
+
+  // Pad the server name. See draft-ietf-tls-esni-10, section 6.1.2.
+  // TODO(https://crbug.com/boringssl/275): Ideally we'd pad the whole thing to
+  // reduce the output range. See
+  // https://github.com/tlswg/draft-ietf-tls-esni/issues/433
+  size_t padding_len = 0;
+  size_t maximum_name_length = hs->selected_ech_config->maximum_name_length;
+  if (ssl->hostname) {
+    size_t hostname_len = strlen(ssl->hostname.get());
+    if (hostname_len <= maximum_name_length) {
+      padding_len = maximum_name_length - hostname_len;
+    } else {
+      // If the server underestimated the maximum size, pad to a multiple of 32.
+      padding_len = 31 - (hostname_len - 1) % 32;
+      // If the input is close to |maximum_name_length|, pad to the next
+      // multiple for at least 32 bytes of length ambiguity.
+      if (hostname_len + padding_len < maximum_name_length + 32) {
+        padding_len += 32;
+      }
+    }
+  } else {
+    // No SNI. Pad up to |maximum_name_length|, including server_name extension
+    // overhead.
+    padding_len = 9 + maximum_name_length;
+  }
+  if (!add_padding_extension(&extensions, TLSEXT_TYPE_padding, padding_len)) {
+    return false;
+  }
+
+  // Uncompressed extensions are encoded as-is.
+  if (!CBB_add_bytes(&extensions_encoded, CBB_data(&extensions),
+                     CBB_len(&extensions))) {
+    return false;
+  }
+
+  // Flush all the compressed extensions.
+  if (CBB_len(compressed.get()) != 0) {
+    CBB extension, child;
+    // Copy them as-is in the real ClientHelloInner.
+    if (!CBB_add_bytes(&extensions, CBB_data(compressed.get()),
+                       CBB_len(compressed.get())) ||
+        // Replace with ech_outer_extensions in the encoded form.
+        !CBB_add_u16(&extensions_encoded, TLSEXT_TYPE_ech_outer_extensions) ||
+        !CBB_add_u16_length_prefixed(&extensions_encoded, &extension) ||
+        !CBB_add_u8_length_prefixed(&extension, &child) ||
+        !CBB_add_bytes(&child, CBB_data(outer_extensions.get()),
+                       CBB_len(outer_extensions.get())) ||
+        !CBB_flush(&extensions_encoded)) {
+      return false;
+    }
+  }
+
+  // The PSK extension must be last. It is never compressed. Note, if there is a
+  // binder, the caller will need to update both ClientHelloInner and
+  // EncodedClientHelloInner after computing it.
+  const size_t len_before = CBB_len(&extensions);
+  if (!ext_pre_shared_key_add_clienthello(hs, &extensions, out_needs_psk_binder,
+                                          ssl_client_hello_inner) ||
+      !CBB_add_bytes(&extensions_encoded, CBB_data(&extensions) + len_before,
+                     CBB_len(&extensions) - len_before) ||
+      !CBB_flush(out) ||  //
+      !CBB_flush(out_encoded)) {
+    return false;
+  }
+
+  return true;
+}
+
+bool ssl_add_clienthello_tlsext(SSL_HANDSHAKE *hs, CBB *out, CBB *out_encoded,
+                                bool *out_needs_psk_binder,
+                                ssl_client_hello_type_t type, size_t header_len,
+                                size_t omit_ech_len) {
+  *out_needs_psk_binder = false;
+
+  if (type == ssl_client_hello_inner) {
+    return ssl_add_clienthello_tlsext_inner(hs, out, out_encoded,
+                                            out_needs_psk_binder);
+  }
+
+  assert(out_encoded == nullptr);  // Only ClientHelloInner needs two outputs.
   SSL *const ssl = hs->ssl;
   CBB extensions;
-  *out_needs_psk_binder = false;
   if (!CBB_add_u16_length_prefixed(out, &extensions)) {
     OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
     return false;
@@ -3291,14 +3463,20 @@
 
   bool last_was_empty = false;
   for (size_t i = 0; i < kNumExtensions; i++) {
-    const size_t len_before = CBB_len(&extensions);
-    if (!kExtensions[i].add_clienthello(hs, &extensions)) {
-      OPENSSL_PUT_ERROR(SSL, SSL_R_ERROR_ADDING_EXTENSION);
-      ERR_add_error_dataf("extension %u", (unsigned)kExtensions[i].value);
-      return false;
-    }
+    size_t bytes_written;
+    if (omit_ech_len != 0 &&
+        kExtensions[i].value == TLSEXT_TYPE_encrypted_client_hello) {
+      bytes_written = omit_ech_len;
+    } else {
+      const size_t len_before = CBB_len(&extensions);
+      if (!kExtensions[i].add_clienthello(hs, &extensions, &extensions, type)) {
+        OPENSSL_PUT_ERROR(SSL, SSL_R_ERROR_ADDING_EXTENSION);
+        ERR_add_error_dataf("extension %u", (unsigned)kExtensions[i].value);
+        return false;
+      }
 
-    const size_t bytes_written = CBB_len(&extensions) - len_before;
+      bytes_written = CBB_len(&extensions) - len_before;
+    }
     if (bytes_written != 0) {
       hs->extensions.sent |= (1u << i);
     }
@@ -3316,11 +3494,14 @@
     last_was_empty = false;
   }
 
-  size_t psk_extension_len = ext_pre_shared_key_clienthello_length(hs);
+  // In cleartext ClientHellos, we add the padding extension to work around
+  // bugs. We also apply this padding to ClientHelloOuter, to keep the wire
+  // images aligned.
+  size_t psk_extension_len = ext_pre_shared_key_clienthello_length(hs, type);
   if (!SSL_is_dtls(ssl) && !ssl->quic_method &&
       !ssl->s3->used_hello_retry_request) {
-    header_len +=
-        SSL3_HM_HEADER_LENGTH + 2 + CBB_len(&extensions) + psk_extension_len;
+    header_len += SSL3_HM_HEADER_LENGTH + 2 + CBB_len(&extensions) +
+                  omit_ech_len + psk_extension_len;
     size_t padding_len = 0;
 
     // The final extension must be non-empty. WebSphere Application
@@ -3362,8 +3543,8 @@
 
   // The PSK extension must be last, including after the padding.
   const size_t len_before = CBB_len(&extensions);
-  if (!ext_pre_shared_key_add_clienthello(hs, &extensions,
-                                          out_needs_psk_binder)) {
+  if (!ext_pre_shared_key_add_clienthello(hs, &extensions, out_needs_psk_binder,
+                                          type)) {
     OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
     return false;
   }
diff --git a/ssl/test/runner/common.go b/ssl/test/runner/common.go
index 4c4ddff..645b964 100644
--- a/ssl/test/runner/common.go
+++ b/ssl/test/runner/common.go
@@ -843,13 +843,19 @@
 	AlertBeforeFalseStartTest alert
 
 	// ExpectServerName, if not empty, is the hostname the client
-	// must specify in the server_name extension.
+	// must specify in the selected ClientHello's server_name extension.
 	ExpectServerName string
 
-	// ExpectClientECH causes the server to expect the peer to send an
-	// encrypted_client_hello extension containing a ClientECH structure.
+	// ExpectServerName, if not empty, is the hostname the client
+	// must specify in the ClientHelloOuter's server_name extension.
+	ExpectOuterServerName string
+
+	// ExpectClientECH causes the server to require that the client offer ECH.
 	ExpectClientECH bool
 
+	// ExpectNoClientECH causes the server to require that the client not offer ECH.
+	ExpectNoClientECH bool
+
 	// IgnoreECHConfigCipherPreferences, when true, causes the client to ignore
 	// the cipher preferences in the ECHConfig and select the most preferred ECH
 	// cipher suite unconditionally.
@@ -927,6 +933,19 @@
 	// success.
 	MinimalClientHelloOuter bool
 
+	// ExpectECHOuterExtensions is a list of extension IDs which the server
+	// will require to be present in ech_outer_extensions.
+	ExpectECHOuterExtensions []uint16
+
+	// ExpectECHOuterExtensions is a list of extension IDs which the server
+	// will require to be omitted in ech_outer_extensions.
+	ExpectECHUncompressedExtensions []uint16
+
+	// UseInnerSessionWithClientHelloOuter, if true, causes the server to
+	// handshake with ClientHelloOuter, but resume the session from
+	// ClientHelloInner.
+	UseInnerSessionWithClientHelloOuter bool
+
 	// RecordClientHelloInner, when non-nil, is called whenever the client
 	// generates an encrypted ClientHello. The byte strings do not include the
 	// ClientHello header.
@@ -1004,8 +1023,14 @@
 	// ClientHello session ID, even in TLS 1.2 full handshakes.
 	EchoSessionIDInFullHandshake bool
 
+	// ExpectNoSessionID, if true, causes the server to fail the connection if
+	// the session ID field is present.
+	ExpectNoSessionID bool
+
 	// ExpectNoTLS12Session, if true, causes the server to fail the
-	// connection if either a session ID or TLS 1.2 ticket is offered.
+	// connection if the server offered a TLS 1.2 session. TLS 1.3 clients
+	// always offer session IDs for compatibility, so the session ID check
+	// checks for sessions the server issued.
 	ExpectNoTLS12Session bool
 
 	// ExpectNoTLS13PSK, if true, causes the server to fail the connection
@@ -1335,11 +1360,6 @@
 	// client.
 	SendTicketAge time.Duration
 
-	// FailIfSessionOffered, if true, causes the server to fail any
-	// connections where the client offers a non-empty session ID or session
-	// ticket.
-	FailIfSessionOffered bool
-
 	// SendHelloRequestBeforeEveryAppDataRecord, if true, causes a
 	// HelloRequest handshake message to be sent before each application
 	// data record. This only makes sense for a server.
diff --git a/ssl/test/runner/handshake_messages.go b/ssl/test/runner/handshake_messages.go
index 469cf30..d666a87 100644
--- a/ssl/test/runner/handshake_messages.go
+++ b/ssl/test/runner/handshake_messages.go
@@ -263,6 +263,9 @@
 	MaxNameLen   uint16
 	PublicName   string
 	CipherSuites []HPKECipherSuite
+	// The following fields are only used by CreateECHConfig().
+	UnsupportedExtension          bool
+	UnsupportedMandatoryExtension bool
 }
 
 func CreateECHConfig(template *ECHConfig) *ECHConfig {
@@ -281,7 +284,16 @@
 	}
 	contents.addU16(template.MaxNameLen)
 	contents.addU16LengthPrefixed().addBytes([]byte(template.PublicName))
-	contents.addU16(0) // Empty extensions field
+	extensions := contents.addU16LengthPrefixed()
+	// Mandatory extensions have the high bit set.
+	if template.UnsupportedExtension {
+		extensions.addU16(0x1111)
+		extensions.addU16LengthPrefixed().addBytes([]byte("test"))
+	}
+	if template.UnsupportedMandatoryExtension {
+		extensions.addU16(0xaaaa)
+		extensions.addU16LengthPrefixed().addBytes([]byte("test"))
+	}
 
 	// This ought to be a call to a function like ParseECHConfig(bb.finish()),
 	// but this constrains us to constructing ECHConfigs we are willing to
@@ -989,8 +1001,8 @@
 				m.supportedCurves = append(m.supportedCurves, CurveID(v))
 			}
 		case extensionSupportedPoints:
-			// http://tools.ietf.org/html/rfc4492#section-5.5.2
-			if !body.readU8LengthPrefixedBytes(&m.supportedPoints) || len(body) != 0 {
+			// http://tools.ietf.org/html/rfc4492#section-5.1.2
+			if !body.readU8LengthPrefixedBytes(&m.supportedPoints) || len(m.supportedPoints) == 0 || len(body) != 0 {
 				return false
 			}
 		case extensionSessionTicket:
@@ -1193,7 +1205,7 @@
 	return true
 }
 
-func decodeClientHelloInner(encoded []byte, helloOuter *clientHelloMsg) (*clientHelloMsg, error) {
+func decodeClientHelloInner(config *Config, encoded []byte, helloOuter *clientHelloMsg) (*clientHelloMsg, error) {
 	reader := byteReader(encoded)
 	var versAndRandom, sessionID, cipherSuites, compressionMethods []byte
 	var extensions byteReader
@@ -1251,7 +1263,7 @@
 			}
 			newExtBody, ok := helloOuter.rawExtensions[newExtType]
 			if !ok {
-				return nil, fmt.Errorf("tls: extension %04x not found in ClientHelloOuter", newExtType)
+				return nil, fmt.Errorf("tls: extension %d not found in ClientHelloOuter", newExtType)
 			}
 			newExtensions.addU16(newExtType)
 			newExtensions.addU16LengthPrefixed().addBytes(newExtBody)
@@ -1259,6 +1271,17 @@
 		}
 	}
 
+	for _, expected := range config.Bugs.ExpectECHOuterExtensions {
+		if _, ok := copied[expected]; !ok {
+			return nil, fmt.Errorf("tls: extension %d not found in ech_outer_extensions", expected)
+		}
+	}
+	for _, expected := range config.Bugs.ExpectECHUncompressedExtensions {
+		if _, ok := copied[expected]; ok {
+			return nil, fmt.Errorf("tls: extension %d unexpectedly found in ech_outer_extensions", expected)
+		}
+	}
+
 	ret := new(clientHelloMsg)
 	if !ret.unmarshal(builder.finish()) {
 		return nil, errors.New("tls: error parsing reconstructed ClientHello")
diff --git a/ssl/test/runner/handshake_server.go b/ssl/test/runner/handshake_server.go
index 00bfd41..1bfc584 100644
--- a/ssl/test/runner/handshake_server.go
+++ b/ssl/test/runner/handshake_server.go
@@ -177,6 +177,15 @@
 		return errors.New("tls: ClientHello random was all zero")
 	}
 
+	if expected := config.Bugs.ExpectOuterServerName; len(expected) != 0 && expected != hs.clientHello.serverName {
+		return fmt.Errorf("tls: unexpected ClientHelloOuter server name: wanted %q, got %q", expected, hs.clientHello.serverName)
+	}
+
+	// We check this both before and after decrypting ECH.
+	if !hs.clientHello.hasGREASEExtension && config.Bugs.ExpectGREASE {
+		return errors.New("tls: no GREASE extension found")
+	}
+
 	if clientECH := hs.clientHello.clientECH; clientECH != nil {
 		for _, candidate := range config.ServerECHConfigs {
 			if candidate.ECHConfig.ConfigID != clientECH.configID {
@@ -206,9 +215,13 @@
 				c.sendAlert(alertDecryptError)
 				return fmt.Errorf("tls: error decrypting ClientHello: %s", err)
 			}
-			c.echAccepted = true
-			hs.clientHello = clientHelloInner
-			hs.echConfigID = clientECH.configID
+			if config.Bugs.UseInnerSessionWithClientHelloOuter {
+				hs.clientHello.pskIdentities = clientHelloInner.pskIdentities
+			} else {
+				c.echAccepted = true
+				hs.clientHello = clientHelloInner
+				hs.echConfigID = clientECH.configID
+			}
 		}
 	}
 
@@ -348,9 +361,14 @@
 	if config.Bugs.MockQUICTransport != nil && len(hs.clientHello.sessionID) > 0 {
 		return fmt.Errorf("tls: QUIC client did not disable compatibility mode")
 	}
+	if config.Bugs.ExpectNoSessionID && len(hs.clientHello.sessionID) > 0 {
+		return fmt.Errorf("tls: client offered an unexpected session ID")
+	}
 	if config.Bugs.ExpectNoTLS12Session {
-		if len(hs.clientHello.sessionID) > 0 && c.vers >= VersionTLS13 {
-			return fmt.Errorf("tls: client offered an unexpected session ID")
+		if len(hs.clientHello.sessionID) > 0 {
+			if _, ok := config.ServerSessionCache.Get(string(hs.clientHello.sessionID)); ok {
+				return fmt.Errorf("tls: client offered an unexpected TLS 1.2 session")
+			}
 		}
 		if len(hs.clientHello.sessionTicket) > 0 {
 			return fmt.Errorf("tls: client offered an unexpected session ticket")
@@ -449,13 +467,18 @@
 	extensions.addBytes(helloOuter.raw[helloOuter.extensionStart+2 : helloOuter.echExtensionStart])
 	extensions.addBytes(helloOuter.raw[helloOuter.echExtensionEnd:])
 
-	encoded, err := hs.echHPKEContext.Open(helloOuter.clientECH.payload, aad.finish())
-	if err != nil {
-		// Wrap |err| so the caller can implement trial decryption.
-		return nil, &echDecryptError{err}
+	// In fuzzer mode, the payload is cleartext.
+	encoded := helloOuter.clientECH.payload
+	if !hs.c.config.Bugs.NullAllCiphers {
+		var err error
+		encoded, err = hs.echHPKEContext.Open(helloOuter.clientECH.payload, aad.finish())
+		if err != nil {
+			// Wrap |err| so the caller can implement trial decryption.
+			return nil, &echDecryptError{err}
+		}
 	}
 
-	helloInner, err = decodeClientHelloInner(encoded, helloOuter)
+	helloInner, err = decodeClientHelloInner(hs.c.config, encoded, helloOuter)
 	if err != nil {
 		return nil, err
 	}
@@ -468,6 +491,7 @@
 	if bytes.Equal(helloInner.random, helloOuter.random) {
 		return nil, errors.New("tls: ClientHelloOuter and ClientHelloInner have the same random values")
 	}
+	// ClientHelloInner should not offer TLS 1.2 and below.
 	if len(helloInner.supportedVersions) == 0 {
 		return nil, errors.New("tls: ClientHelloInner did not offer supported_versions")
 	}
@@ -477,6 +501,10 @@
 			return nil, fmt.Errorf("tls: ClientHelloInner offered invalid version: %04x", vers)
 		}
 	}
+	// ClientHelloInner should omit TLS-1.2-only extensions.
+	if helloInner.nextProtoNeg || len(helloInner.supportedPoints) != 0 || helloInner.ticketSupported || helloInner.secureRenegotiation != nil || helloInner.extendedMasterSecret {
+		return nil, errors.New("tls: ClientHelloInner included a TLS-1.2-only extension")
+	}
 	if !helloInner.echIsInner {
 		return nil, errors.New("tls: ClientHelloInner missing ech_is_inner extension")
 	}
@@ -522,7 +550,10 @@
 	}
 
 	if config.Bugs.ExpectClientECH && hs.clientHello.clientECH == nil {
-		return errors.New("tls: expected client to send ClientECH")
+		return errors.New("tls: expected client to offer ECH")
+	}
+	if config.Bugs.ExpectNoClientECH && hs.clientHello.clientECH != nil {
+		return errors.New("tls: expected client not to offer ECH")
 	}
 
 	// Select the cipher suite.
@@ -588,6 +619,9 @@
 		pskKEModes = []byte{pskDHEKEMode}
 		replacedPSKIdentities = true
 	}
+	if config.Bugs.UseInnerSessionWithClientHelloOuter {
+		replacedPSKIdentities = true
+	}
 
 	var pskIndex int
 	foundKEMode := bytes.IndexByte(pskKEModes, pskDHEKEMode) >= 0
@@ -748,6 +782,10 @@
 			return unexpectedMessageError(newClientHello, newMsg)
 		}
 
+		if expected := config.Bugs.ExpectOuterServerName; len(expected) != 0 && expected != newClientHello.serverName {
+			return fmt.Errorf("tls: unexpected ClientHelloOuter server name: wanted %q, got %q", expected, newClientHello.serverName)
+		}
+
 		if c.echAccepted {
 			if newClientHello.clientECH == nil {
 				c.sendAlert(alertMissingExtension)
@@ -781,6 +819,11 @@
 		// Check that the new ClientHello matches the old ClientHello,
 		// except for relevant modifications. See RFC 8446, section 4.1.2.
 		ignoreExtensions := []uint16{extensionPadding}
+		// TODO(https://crbug.com/boringssl/275): draft-ietf-tls-esni-10 requires
+		// violating the RFC 8446 rules. See
+		// https://github.com/tlswg/draft-ietf-tls-esni/issues/358
+		// A later draft will likely fix this. Remove this line if it does.
+		ignoreExtensions = append(ignoreExtensions, extensionEncryptedClientHello)
 
 		if helloRetryRequest.hasSelectedGroup {
 			newKeyShares := newClientHello.keyShares
@@ -1451,10 +1494,6 @@
 		return false, errors.New("tls: offered resumption on renegotiation")
 	}
 
-	if c.config.Bugs.FailIfSessionOffered && (len(hs.clientHello.sessionTicket) > 0 || len(hs.clientHello.sessionID) > 0) {
-		return false, errors.New("tls: client offered a session ticket or ID")
-	}
-
 	if hs.checkForResumption() {
 		return true, nil
 	}
diff --git a/ssl/test/runner/hpke/hpke.go b/ssl/test/runner/hpke/hpke.go
index 4cd28b9..a08538b 100644
--- a/ssl/test/runner/hpke/hpke.go
+++ b/ssl/test/runner/hpke/hpke.go
@@ -30,6 +30,7 @@
 
 // KEM scheme IDs.
 const (
+	P256WithHKDFSHA256   uint16 = 0x0010
 	X25519WithHKDFSHA256 uint16 = 0x0020
 )
 
diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go
index f2ad64f..b6586de 100644
--- a/ssl/test/runner/runner.go
+++ b/ssl/test/runner/runner.go
@@ -13156,7 +13156,7 @@
 		config: Config{
 			MaxVersion: VersionTLS13,
 			Bugs: ProtocolBugs{
-				ExpectNoTLS12Session: true,
+				ExpectNoSessionID: true,
 			},
 		},
 		flags: []string{"-max-version", strconv.Itoa(VersionTLS12)},
@@ -16266,33 +16266,42 @@
 }
 
 // generateServerECHConfig constructs a ServerECHConfig with a fresh X25519
-// keypair and using |template| as a template for the ECHConfig. If
-// |template.CipherSuites| is empty, all ciphers are included. |template.KEM|
-// and |template.PublicKey| are ignored.
+// keypair and using |template| as a template for the ECHConfig. If fields are
+// omitted, defaults are used.
 func generateServerECHConfig(template *ECHConfig) ServerECHConfig {
 	publicKey, secretKey, err := hpke.GenerateKeyPairX25519()
 	if err != nil {
 		panic(err)
 	}
 	templateCopy := *template
-	templateCopy.KEM = hpke.X25519WithHKDFSHA256
-	templateCopy.PublicKey = publicKey
+	if templateCopy.KEM == 0 {
+		templateCopy.KEM = hpke.X25519WithHKDFSHA256
+	}
+	if len(templateCopy.PublicKey) == 0 {
+		templateCopy.PublicKey = publicKey
+	}
 	if len(templateCopy.CipherSuites) == 0 {
 		templateCopy.CipherSuites = make([]HPKECipherSuite, len(echCiphers))
 		for i, cipher := range echCiphers {
 			templateCopy.CipherSuites[i] = cipher.cipher
 		}
 	}
+	if len(templateCopy.PublicName) == 0 {
+		templateCopy.PublicName = "public.example"
+	}
+	if templateCopy.MaxNameLen == 0 {
+		templateCopy.MaxNameLen = 64
+	}
 	return ServerECHConfig{ECHConfig: CreateECHConfig(&templateCopy), Key: secretKey}
 }
 
 func addEncryptedClientHelloTests() {
 	// echConfig's ConfigID should match the one used in ssl/test/fuzzer.h.
-	echConfig := generateServerECHConfig(&ECHConfig{ConfigID: 42, PublicName: "public.example"})
-	echConfig1 := generateServerECHConfig(&ECHConfig{ConfigID: 43, PublicName: "public.example"})
-	echConfig2 := generateServerECHConfig(&ECHConfig{ConfigID: 44, PublicName: "public.example"})
-	echConfig3 := generateServerECHConfig(&ECHConfig{ConfigID: 45, PublicName: "public.example"})
-	echConfigRepeatID := generateServerECHConfig(&ECHConfig{ConfigID: 42, PublicName: "public.example"})
+	echConfig := generateServerECHConfig(&ECHConfig{ConfigID: 42})
+	echConfig1 := generateServerECHConfig(&ECHConfig{ConfigID: 43})
+	echConfig2 := generateServerECHConfig(&ECHConfig{ConfigID: 44})
+	echConfig3 := generateServerECHConfig(&ECHConfig{ConfigID: 45})
+	echConfigRepeatID := generateServerECHConfig(&ECHConfig{ConfigID: 42})
 
 	for _, protocol := range []protocol{tls, quic} {
 		prefix := protocol.String() + "-"
@@ -16666,11 +16675,38 @@
 				},
 			})
 
+			// Test that client can offer the specified cipher and skip over
+			// unrecognized ones.
+			cipherConfig := generateServerECHConfig(&ECHConfig{
+				ConfigID: 42,
+				CipherSuites: []HPKECipherSuite{
+					{KDF: 0x1111, AEAD: 0x2222},
+					{KDF: cipher.cipher.KDF, AEAD: 0x2222},
+					{KDF: 0x1111, AEAD: cipher.cipher.AEAD},
+					cipher.cipher,
+				},
+			})
+			testCases = append(testCases, testCase{
+				testType: clientTest,
+				protocol: protocol,
+				name:     prefix + "ECH-Client-Cipher-" + cipher.name,
+				config: Config{
+					ServerECHConfigs: []ServerECHConfig{cipherConfig},
+				},
+				flags: []string{
+					"-ech-config-list", base64.StdEncoding.EncodeToString(CreateECHConfigList(cipherConfig.ECHConfig.Raw)),
+					"-host-name", "secret.example",
+					"-expect-ech-accept",
+				},
+				expectations: connectionExpectations{
+					echAccepted: true,
+				},
+			})
+
 			// Test that the ECH server rejects the specified cipher if not
 			// listed in its ECHConfig.
-			config := generateServerECHConfig(&ECHConfig{
+			otherCipherConfig := generateServerECHConfig(&ECHConfig{
 				ConfigID:     42,
-				PublicName:   "public.name",
 				CipherSuites: []HPKECipherSuite{otherCipher.cipher},
 			})
 			testCases = append(testCases, testCase{
@@ -16682,12 +16718,12 @@
 					ClientECHConfig: echConfig.ECHConfig,
 					ECHCipherSuites: []HPKECipherSuite{cipher.cipher},
 					Bugs: ProtocolBugs{
-						ExpectECHRetryConfigs: CreateECHConfigList(config.ECHConfig.Raw),
+						ExpectECHRetryConfigs: CreateECHConfigList(otherCipherConfig.ECHConfig.Raw),
 					},
 				},
 				flags: []string{
-					"-ech-server-config", base64.StdEncoding.EncodeToString(config.ECHConfig.Raw),
-					"-ech-server-key", base64.StdEncoding.EncodeToString(config.Key),
+					"-ech-server-config", base64.StdEncoding.EncodeToString(otherCipherConfig.ECHConfig.Raw),
+					"-ech-server-key", base64.StdEncoding.EncodeToString(otherCipherConfig.Key),
 					"-ech-is-retry-config", "1",
 					"-expect-server-name", "public.example",
 				},
@@ -17070,6 +17106,609 @@
 			expectedLocalError: "remote error: illegal parameter",
 			expectedError:      ":UNEXPECTED_EXTENSION:",
 		})
+
+		// Test the client can negotiate ECH, with and without HelloRetryRequest.
+		testCases = append(testCases, testCase{
+			testType: clientTest,
+			protocol: protocol,
+			name:     prefix + "ECH-Client",
+			config: Config{
+				MinVersion:       VersionTLS13,
+				MaxVersion:       VersionTLS13,
+				ServerECHConfigs: []ServerECHConfig{echConfig},
+				Bugs: ProtocolBugs{
+					ExpectServerName:      "secret.example",
+					ExpectOuterServerName: "public.example",
+				},
+			},
+			flags: []string{
+				"-ech-config-list", base64.StdEncoding.EncodeToString(CreateECHConfigList(echConfig.ECHConfig.Raw)),
+				"-host-name", "secret.example",
+				"-expect-ech-accept",
+			},
+			resumeSession: true,
+			expectations:  connectionExpectations{echAccepted: true},
+		})
+		testCases = append(testCases, testCase{
+			testType: clientTest,
+			protocol: protocol,
+			name:     prefix + "ECH-Client-HelloRetryRequest",
+			config: Config{
+				MinVersion:       VersionTLS13,
+				MaxVersion:       VersionTLS13,
+				CurvePreferences: []CurveID{CurveP384},
+				ServerECHConfigs: []ServerECHConfig{echConfig},
+				Bugs: ProtocolBugs{
+					ExpectServerName:      "secret.example",
+					ExpectOuterServerName: "public.example",
+					ExpectMissingKeyShare: true, // Check we triggered HRR.
+				},
+			},
+			resumeSession: true,
+			flags: []string{
+				"-ech-config-list", base64.StdEncoding.EncodeToString(CreateECHConfigList(echConfig.ECHConfig.Raw)),
+				"-host-name", "secret.example",
+				"-expect-ech-accept",
+				"-expect-hrr", // Check we triggered HRR.
+			},
+			expectations: connectionExpectations{echAccepted: true},
+		})
+
+		// Test the client can negotiate ECH with early data.
+		testCases = append(testCases, testCase{
+			testType: clientTest,
+			protocol: protocol,
+			name:     prefix + "ECH-Client-EarlyData",
+			config: Config{
+				MinVersion:       VersionTLS13,
+				MaxVersion:       VersionTLS13,
+				ServerECHConfigs: []ServerECHConfig{echConfig},
+				Bugs: ProtocolBugs{
+					ExpectServerName: "secret.example",
+				},
+			},
+			flags: []string{
+				"-ech-config-list", base64.StdEncoding.EncodeToString(CreateECHConfigList(echConfig.ECHConfig.Raw)),
+				"-host-name", "secret.example",
+				"-expect-ech-accept",
+			},
+			resumeSession: true,
+			earlyData:     true,
+			expectations:  connectionExpectations{echAccepted: true},
+		})
+		testCases = append(testCases, testCase{
+			testType: clientTest,
+			protocol: protocol,
+			name:     prefix + "ECH-Client-EarlyDataRejected",
+			config: Config{
+				MinVersion:       VersionTLS13,
+				MaxVersion:       VersionTLS13,
+				ServerECHConfigs: []ServerECHConfig{echConfig},
+				Bugs: ProtocolBugs{
+					ExpectServerName:      "secret.example",
+					AlwaysRejectEarlyData: true,
+				},
+			},
+			flags: []string{
+				"-ech-config-list", base64.StdEncoding.EncodeToString(CreateECHConfigList(echConfig.ECHConfig.Raw)),
+				"-host-name", "secret.example",
+				"-expect-ech-accept",
+			},
+			resumeSession:           true,
+			earlyData:               true,
+			expectEarlyDataRejected: true,
+			expectations:            connectionExpectations{echAccepted: true},
+		})
+
+		if protocol != quic {
+			// Test that an ECH client does not offer a TLS 1.2 session.
+			testCases = append(testCases, testCase{
+				testType: clientTest,
+				protocol: protocol,
+				name:     prefix + "ECH-Client-TLS12SessionID",
+				config: Config{
+					MaxVersion:             VersionTLS12,
+					SessionTicketsDisabled: true,
+				},
+				resumeConfig: &Config{
+					ServerECHConfigs: []ServerECHConfig{echConfig},
+					Bugs: ProtocolBugs{
+						ExpectNoTLS12Session: true,
+					},
+				},
+				flags: []string{
+					"-on-resume-ech-config-list", base64.StdEncoding.EncodeToString(CreateECHConfigList(echConfig.ECHConfig.Raw)),
+					"-on-resume-expect-ech-accept",
+				},
+				resumeSession:        true,
+				expectResumeRejected: true,
+				resumeExpectations:   &connectionExpectations{echAccepted: true},
+			})
+			testCases = append(testCases, testCase{
+				testType: clientTest,
+				protocol: protocol,
+				name:     prefix + "ECH-Client-TLS12SessionTicket",
+				config: Config{
+					MaxVersion: VersionTLS12,
+				},
+				resumeConfig: &Config{
+					ServerECHConfigs: []ServerECHConfig{echConfig},
+					Bugs: ProtocolBugs{
+						ExpectNoTLS12Session: true,
+					},
+				},
+				flags: []string{
+					"-on-resume-ech-config-list", base64.StdEncoding.EncodeToString(CreateECHConfigList(echConfig.ECHConfig.Raw)),
+					"-on-resume-expect-ech-accept",
+				},
+				resumeSession:        true,
+				expectResumeRejected: true,
+				resumeExpectations:   &connectionExpectations{echAccepted: true},
+			})
+		}
+
+		// ClientHelloInner should not include NPN, which is a TLS 1.2-only
+		// extensions. The Go server will enforce this, so this test only needs
+		// to configure the feature on the shim. Other application extensions
+		// are sent implicitly.
+		testCases = append(testCases, testCase{
+			testType: clientTest,
+			protocol: protocol,
+			name:     prefix + "ECH-Client-NoNPN",
+			config: Config{
+				ServerECHConfigs: []ServerECHConfig{echConfig},
+			},
+			flags: []string{
+				"-ech-config-list", base64.StdEncoding.EncodeToString(CreateECHConfigList(echConfig.ECHConfig.Raw)),
+				"-expect-ech-accept",
+				// Enable NPN.
+				"-select-next-proto", "foo",
+			},
+			expectations: connectionExpectations{echAccepted: true},
+		})
+
+		// Test that the client iterates over configurations in the
+		// ECHConfigList and selects the first with supported parameters.
+		p256Key := ecdsaP256Certificate.PrivateKey.(*ecdsa.PrivateKey)
+		unsupportedKEM := generateServerECHConfig(&ECHConfig{
+			KEM:       hpke.P256WithHKDFSHA256,
+			PublicKey: elliptic.Marshal(elliptic.P256(), p256Key.X, p256Key.Y),
+		}).ECHConfig
+		unsupportedCipherSuites := generateServerECHConfig(&ECHConfig{
+			CipherSuites: []HPKECipherSuite{{0x1111, 0x2222}},
+		}).ECHConfig
+		unsupportedMandatoryExtension := generateServerECHConfig(&ECHConfig{
+			UnsupportedMandatoryExtension: true,
+		}).ECHConfig
+		testCases = append(testCases, testCase{
+			testType: clientTest,
+			protocol: protocol,
+			name:     prefix + "ECH-Client-SelectECHConfig",
+			config: Config{
+				ServerECHConfigs: []ServerECHConfig{echConfig},
+			},
+			flags: []string{
+				"-ech-config-list", base64.StdEncoding.EncodeToString(CreateECHConfigList(
+					unsupportedVersion,
+					unsupportedKEM.Raw,
+					unsupportedCipherSuites.Raw,
+					unsupportedMandatoryExtension.Raw,
+					echConfig.ECHConfig.Raw,
+					// |echConfig1| is also supported, but the client should
+					// select the first one.
+					echConfig1.ECHConfig.Raw,
+				)),
+				"-expect-ech-accept",
+			},
+			expectations: connectionExpectations{
+				echAccepted: true,
+			},
+		})
+
+		// Test that the client skips sending ECH if all ECHConfigs are
+		// unsupported.
+		testCases = append(testCases, testCase{
+			testType: clientTest,
+			protocol: protocol,
+			name:     prefix + "ECH-Client-NoSupportedConfigs",
+			config: Config{
+				Bugs: ProtocolBugs{
+					ExpectNoClientECH: true,
+				},
+			},
+			flags: []string{
+				"-ech-config-list", base64.StdEncoding.EncodeToString(CreateECHConfigList(
+					unsupportedVersion,
+					unsupportedKEM.Raw,
+					unsupportedCipherSuites.Raw,
+					unsupportedMandatoryExtension.Raw,
+				)),
+			},
+		})
+
+		// If ECH GREASE is enabled, the client should send ECH GREASE when no
+		// configured ECHConfig is suitable.
+		testCases = append(testCases, testCase{
+			testType: clientTest,
+			protocol: protocol,
+			name:     prefix + "ECH-Client-NoSupportedConfigs-GREASE",
+			config: Config{
+				Bugs: ProtocolBugs{
+					ExpectClientECH: true,
+				},
+			},
+			flags: []string{
+				"-ech-config-list", base64.StdEncoding.EncodeToString(CreateECHConfigList(
+					unsupportedVersion,
+					unsupportedKEM.Raw,
+					unsupportedCipherSuites.Raw,
+					unsupportedMandatoryExtension.Raw,
+				)),
+				"-enable-ech-grease",
+			},
+		})
+
+		// If both ECH GREASE and suitable ECHConfigs are available, the
+		// client should send normal ECH.
+		testCases = append(testCases, testCase{
+			testType: clientTest,
+			protocol: protocol,
+			name:     prefix + "ECH-Client-GREASE",
+			config: Config{
+				ServerECHConfigs: []ServerECHConfig{echConfig},
+			},
+			flags: []string{
+				"-ech-config-list", base64.StdEncoding.EncodeToString(CreateECHConfigList(echConfig.ECHConfig.Raw)),
+				"-expect-ech-accept",
+			},
+			resumeSession: true,
+			expectations:  connectionExpectations{echAccepted: true},
+		})
+
+		// Test that GREASE extensions correctly interact with ECH. Both the
+		// inner and outer ClientHellos should include GREASE extensions.
+		testCases = append(testCases, testCase{
+			testType: clientTest,
+			protocol: protocol,
+			name:     prefix + "ECH-Client-GREASEExtensions",
+			config: Config{
+				ServerECHConfigs: []ServerECHConfig{echConfig},
+				Bugs: ProtocolBugs{
+					ExpectGREASE: true,
+				},
+			},
+			flags: []string{
+				"-ech-config-list", base64.StdEncoding.EncodeToString(CreateECHConfigList(echConfig.ECHConfig.Raw)),
+				"-expect-ech-accept",
+				"-enable-grease",
+			},
+			resumeSession: true,
+			expectations:  connectionExpectations{echAccepted: true},
+		})
+
+		// Test that the client tolerates unsupported extensions if the
+		// mandatory bit is not set.
+		unsupportedExtension := generateServerECHConfig(&ECHConfig{UnsupportedExtension: true})
+		testCases = append(testCases, testCase{
+			testType: clientTest,
+			protocol: protocol,
+			name:     prefix + "ECH-Client-UnsupportedExtension",
+			config: Config{
+				ServerECHConfigs: []ServerECHConfig{unsupportedExtension},
+			},
+			flags: []string{
+				"-ech-config-list", base64.StdEncoding.EncodeToString(CreateECHConfigList(unsupportedExtension.ECHConfig.Raw)),
+				"-expect-ech-accept",
+			},
+			expectations: connectionExpectations{echAccepted: true},
+		})
+
+		// Syntax errors in the ECHConfigList should be rejected.
+		testCases = append(testCases, testCase{
+			testType: clientTest,
+			protocol: protocol,
+			name:     prefix + "ECH-Client-InvalidECHConfigList",
+			flags: []string{
+				"-ech-config-list", base64.StdEncoding.EncodeToString(CreateECHConfigList(echConfig.ECHConfig.Raw[1:])),
+			},
+			shouldFail:    true,
+			expectedError: ":INVALID_ECH_CONFIG_LIST:",
+		})
+
+		// If the ClientHelloInner has no server_name extension, while the
+		// ClientHelloOuter has one, the client must check for unsolicited
+		// extensions based on the selected ClientHello.
+		testCases = append(testCases, testCase{
+			testType: clientTest,
+			protocol: protocol,
+			name:     prefix + "ECH-Client-UnsolicitedInnerServerNameAck",
+			config: Config{
+				ServerECHConfigs: []ServerECHConfig{echConfig},
+				Bugs: ProtocolBugs{
+					// ClientHelloOuter should have a server name.
+					ExpectOuterServerName: "public.example",
+					// The server will acknowledge the server_name extension.
+					// This option runs whether or not the client requested the
+					// extension.
+					SendServerNameAck: true,
+				},
+			},
+			flags: []string{
+				"-ech-config-list", base64.StdEncoding.EncodeToString(CreateECHConfigList(echConfig.ECHConfig.Raw)),
+				// No -host-name flag.
+				"-expect-ech-accept",
+			},
+			shouldFail:         true,
+			expectedError:      ":UNEXPECTED_EXTENSION:",
+			expectedLocalError: "remote error: unsupported extension",
+			expectations:       connectionExpectations{echAccepted: true},
+		})
+
+		// Most extensions are the same between ClientHelloInner and
+		// ClientHelloOuter and can be compressed.
+		testCases = append(testCases, testCase{
+			testType: clientTest,
+			protocol: protocol,
+			name:     prefix + "ECH-Client-ExpectECHOuterExtensions",
+			config: Config{
+				ServerECHConfigs: []ServerECHConfig{echConfig},
+				NextProtos:       []string{"proto"},
+				Bugs: ProtocolBugs{
+					ExpectECHOuterExtensions: []uint16{
+						extensionALPN,
+						extensionKeyShare,
+						extensionPSKKeyExchangeModes,
+						extensionSignatureAlgorithms,
+						extensionSupportedCurves,
+					},
+				},
+			},
+			flags: []string{
+				"-ech-config-list", base64.StdEncoding.EncodeToString(CreateECHConfigList(echConfig.ECHConfig.Raw)),
+				"-expect-ech-accept",
+				"-advertise-alpn", "\x05proto",
+				"-expect-alpn", "proto",
+				"-host-name", "secret.example",
+			},
+			expectations: connectionExpectations{
+				echAccepted: true,
+				nextProto:   "proto",
+			},
+			skipQUICALPNConfig: true,
+		})
+
+		// If the server name happens to match the public name, it still should
+		// not be compressed. It is not publicly known that they match.
+		testCases = append(testCases, testCase{
+			testType: clientTest,
+			protocol: protocol,
+			name:     prefix + "ECH-Client-NeverCompressServerName",
+			config: Config{
+				ServerECHConfigs: []ServerECHConfig{echConfig},
+				NextProtos:       []string{"proto"},
+				Bugs: ProtocolBugs{
+					ExpectECHUncompressedExtensions: []uint16{extensionServerName},
+					ExpectServerName:                "public.example",
+					ExpectOuterServerName:           "public.example",
+				},
+			},
+			flags: []string{
+				"-ech-config-list", base64.StdEncoding.EncodeToString(CreateECHConfigList(echConfig.ECHConfig.Raw)),
+				"-expect-ech-accept",
+				"-host-name", "public.example",
+			},
+			expectations: connectionExpectations{echAccepted: true},
+		})
+
+		// If the ClientHelloOuter disables TLS 1.3, e.g. in QUIC, the client
+		// should also compress supported_versions.
+		testCases = append(testCases, testCase{
+			testType: clientTest,
+			protocol: protocol,
+			name:     prefix + "ECH-Client-CompressSupportedVersions",
+			config: Config{
+				ServerECHConfigs: []ServerECHConfig{echConfig},
+				Bugs: ProtocolBugs{
+					ExpectECHOuterExtensions: []uint16{
+						extensionSupportedVersions,
+					},
+				},
+			},
+			flags: []string{
+				"-ech-config-list", base64.StdEncoding.EncodeToString(CreateECHConfigList(echConfig.ECHConfig.Raw)),
+				"-host-name", "secret.example",
+				"-expect-ech-accept",
+				"-min-version", strconv.Itoa(int(VersionTLS13)),
+			},
+			expectations: connectionExpectations{echAccepted: true},
+		})
+
+		// Test that the client can still offer server names that exceed the
+		// maximum name length. It is only a padding hint.
+		maxNameLen10 := generateServerECHConfig(&ECHConfig{MaxNameLen: 10})
+		testCases = append(testCases, testCase{
+			testType: clientTest,
+			protocol: protocol,
+			name:     prefix + "ECH-Client-NameTooLong",
+			config: Config{
+				ServerECHConfigs: []ServerECHConfig{maxNameLen10},
+				Bugs: ProtocolBugs{
+					ExpectServerName: "test0123456789.example",
+				},
+			},
+			flags: []string{
+				"-ech-config-list", base64.StdEncoding.EncodeToString(CreateECHConfigList(maxNameLen10.ECHConfig.Raw)),
+				"-host-name", "test0123456789.example",
+				"-expect-ech-accept",
+			},
+			expectations: connectionExpectations{echAccepted: true},
+		})
+
+		// Test the client can recognize when ECH is rejected.
+		// TODO(https://crbug.com/boringssl/275): Once implemented, this
+		// handshake should complete.
+		testCases = append(testCases, testCase{
+			testType: clientTest,
+			protocol: protocol,
+			name:     prefix + "ECH-Client-Reject",
+			config: Config{
+				Bugs: ProtocolBugs{
+					ExpectServerName: "public.example",
+				},
+			},
+			flags: []string{
+				"-ech-config-list", base64.StdEncoding.EncodeToString(CreateECHConfigList(echConfig.ECHConfig.Raw)),
+			},
+			shouldFail:    true,
+			expectedError: ":CONNECTION_REJECTED:",
+		})
+		testCases = append(testCases, testCase{
+			testType: clientTest,
+			protocol: protocol,
+			name:     prefix + "ECH-Client-Reject-HelloRetryRequest",
+			config: Config{
+				CurvePreferences: []CurveID{CurveP384},
+				Bugs: ProtocolBugs{
+					ExpectServerName:      "public.example",
+					ExpectMissingKeyShare: true, // Check we triggered HRR.
+				},
+			},
+			flags: []string{
+				"-ech-config-list", base64.StdEncoding.EncodeToString(CreateECHConfigList(echConfig.ECHConfig.Raw)),
+				"-expect-hrr", // Check we triggered HRR.
+			},
+			shouldFail:    true,
+			expectedError: ":CONNECTION_REJECTED:",
+		})
+		if protocol != quic {
+			testCases = append(testCases, testCase{
+				testType: clientTest,
+				protocol: protocol,
+				name:     prefix + "ECH-Client-Reject-TLS12",
+				config: Config{
+					MaxVersion: VersionTLS12,
+					Bugs: ProtocolBugs{
+						ExpectServerName:      "public.example",
+						ExpectMissingKeyShare: true, // Check we triggered HRR.
+					},
+				},
+				flags: []string{
+					"-ech-config-list", base64.StdEncoding.EncodeToString(CreateECHConfigList(echConfig.ECHConfig.Raw)),
+					"-expect-hrr", // Check we triggered HRR.
+				},
+				shouldFail:    true,
+				expectedError: ":CONNECTION_REJECTED:",
+			})
+		}
+
+		// Test that the client rejects ClientHelloOuter handshakes that attempt
+		// to resume the ClientHelloInner's ticket. In draft-ietf-tls-esni-10,
+		// the confirmation signal is computed in an odd order, so this requires
+		// an explicit check on the client.
+		testCases = append(testCases, testCase{
+			testType: clientTest,
+			protocol: protocol,
+			name:     prefix + "ECH-Client-Reject-ResumeInnerSession",
+			config: Config{
+				ServerECHConfigs: []ServerECHConfig{echConfig},
+				Bugs: ProtocolBugs{
+					ExpectServerName: "secret.example",
+				},
+			},
+			resumeConfig: &Config{
+				ServerECHConfigs: []ServerECHConfig{echConfig},
+				Bugs: ProtocolBugs{
+					ExpectServerName:                    "public.example",
+					UseInnerSessionWithClientHelloOuter: true,
+				},
+			},
+			resumeSession: true,
+			flags: []string{
+				"-ech-config-list", base64.StdEncoding.EncodeToString(CreateECHConfigList(echConfig.ECHConfig.Raw)),
+				"-host-name", "secret.example",
+				"-on-initial-expect-ech-accept",
+			},
+			shouldFail:         true,
+			expectedError:      ":UNEXPECTED_EXTENSION:",
+			expectations:       connectionExpectations{echAccepted: true},
+			resumeExpectations: &connectionExpectations{echAccepted: false},
+		})
+
+		// Test that the client can process ECH rejects after an early data reject.
+		testCases = append(testCases, testCase{
+			testType: clientTest,
+			protocol: protocol,
+			name:     prefix + "ECH-Client-Reject-EarlyDataReject",
+			config: Config{
+				ServerECHConfigs: []ServerECHConfig{echConfig},
+				Bugs: ProtocolBugs{
+					ExpectServerName: "secret.example",
+				},
+			},
+			resumeConfig: &Config{
+				Bugs: ProtocolBugs{
+					ExpectServerName: "public.example",
+				},
+			},
+			flags: []string{
+				"-ech-config-list", base64.StdEncoding.EncodeToString(CreateECHConfigList(echConfig.ECHConfig.Raw)),
+				"-host-name", "secret.example",
+				// Although the resumption connection does not accept ECH, the
+				// API will report ECH was accepted at the 0-RTT point.
+				"-expect-ech-accept",
+			},
+			resumeSession:           true,
+			expectResumeRejected:    true,
+			earlyData:               true,
+			expectEarlyDataRejected: true,
+			expectations:            connectionExpectations{echAccepted: true},
+			resumeExpectations:      &connectionExpectations{echAccepted: false},
+			// TODO(https://crbug.com/boringssl/275): Once implemented, this
+			// should complete the handshake.
+			shouldFail:    true,
+			expectedError: ":CONNECTION_REJECTED:",
+		})
+		if protocol != quic {
+			testCases = append(testCases, testCase{
+				testType: clientTest,
+				protocol: protocol,
+				name:     prefix + "ECH-Client-Reject-EarlyDataReject-TLS12",
+				config: Config{
+					ServerECHConfigs: []ServerECHConfig{echConfig},
+					Bugs: ProtocolBugs{
+						ExpectServerName: "secret.example",
+					},
+				},
+				resumeConfig: &Config{
+					MaxVersion: VersionTLS12,
+					Bugs: ProtocolBugs{
+						ExpectServerName: "public.example",
+					},
+				},
+				flags: []string{
+					"-ech-config-list", base64.StdEncoding.EncodeToString(CreateECHConfigList(echConfig.ECHConfig.Raw)),
+					"-host-name", "secret.example",
+					// Although the resumption connection does not accept ECH, the
+					// API will report ECH was accepted at the 0-RTT point.
+					"-expect-ech-accept",
+				},
+				resumeSession:           true,
+				expectResumeRejected:    true,
+				earlyData:               true,
+				expectEarlyDataRejected: true,
+				expectations:            connectionExpectations{echAccepted: true},
+				resumeExpectations:      &connectionExpectations{echAccepted: false},
+				// ClientHellos with early data cannot negotiate TLS 1.2, with
+				// or without ECH. The shim should first report
+				// |SSL_R_WRONG_VERSION_ON_EARLY_DATA|. The caller will then
+				// repair the first error by retrying without early data. That
+				// will look like ECH-Client-Reject-TLS12 and select TLS 1.2
+				// and ClientHelloOuter. The caller will then trigger a third
+				// attempt, which will succeed.
+				shouldFail:    true,
+				expectedError: ":WRONG_VERSION_ON_EARLY_DATA:",
+			})
+		}
 	}
 }
 
diff --git a/ssl/test/test_config.cc b/ssl/test/test_config.cc
index 7978a70..c62c765 100644
--- a/ssl/test/test_config.cc
+++ b/ssl/test/test_config.cc
@@ -200,6 +200,7 @@
 };
 
 const Flag<std::string> kBase64Flags[] = {
+    {"-ech-config-list", &TestConfig::ech_config_list},
     {"-expect-certificate-types", &TestConfig::expect_certificate_types},
     {"-expect-channel-id", &TestConfig::expect_channel_id},
     {"-expect-ocsp-response", &TestConfig::expect_ocsp_response},
@@ -1722,6 +1723,12 @@
   if (enable_ech_grease) {
     SSL_set_enable_ech_grease(ssl.get(), 1);
   }
+  if (!ech_config_list.empty() &&
+      !SSL_set1_ech_config_list(
+          ssl.get(), reinterpret_cast<const uint8_t *>(ech_config_list.data()),
+          ech_config_list.size())) {
+    return nullptr;
+  }
   if (ech_server_configs.size() != ech_server_keys.size() ||
       ech_server_configs.size() != ech_is_retry_config.size()) {
     fprintf(stderr,
diff --git a/ssl/test/test_config.h b/ssl/test/test_config.h
index a1860c7..9ef0ced 100644
--- a/ssl/test/test_config.h
+++ b/ssl/test/test_config.h
@@ -44,6 +44,7 @@
   std::vector<std::string> ech_server_keys;
   std::vector<int> ech_is_retry_config;
   bool expect_ech_accept = false;
+  std::string ech_config_list;
   std::string expect_certificate_types;
   bool require_any_client_certificate = false;
   std::string advertise_npn;
diff --git a/ssl/tls13_client.cc b/ssl/tls13_client.cc
index 92ccf62..0292291 100644
--- a/ssl/tls13_client.cc
+++ b/ssl/tls13_client.cc
@@ -156,12 +156,6 @@
 
   hs->new_cipher = cipher;
 
-  if (!hs->transcript.InitHash(ssl_protocol_version(ssl), hs->new_cipher) ||
-      !hs->transcript.UpdateForHelloRetryRequest()) {
-    return ssl_hs_error;
-  }
-
-
   bool have_cookie, have_key_share, have_supported_versions;
   CBS cookie, key_share, supported_versions;
   SSL_EXTENSION_TYPE ext_types[] = {
@@ -227,9 +221,24 @@
     }
   }
 
-  if (!ssl_hash_message(hs, msg)) {
+  // We do not know whether ECH was chosen until ServerHello and must
+  // concurrently update both transcripts.
+  //
+  // TODO(https://crbug.com/boringssl/275): A later draft will likely add an ECH
+  // signal to HRR and change this.
+  if (!hs->transcript.InitHash(ssl_protocol_version(ssl), hs->new_cipher) ||
+      !hs->transcript.UpdateForHelloRetryRequest() ||
+      !ssl_hash_message(hs, msg)) {
     return ssl_hs_error;
   }
+  if (hs->selected_ech_config) {
+    if (!hs->inner_transcript.InitHash(ssl_protocol_version(ssl),
+                                       hs->new_cipher) ||
+        !hs->inner_transcript.UpdateForHelloRetryRequest() ||
+        !hs->inner_transcript.Update(msg.raw)) {
+      return ssl_hs_error;
+    }
+  }
 
   // HelloRetryRequest should be the end of the flight.
   if (ssl->method->has_unprocessed_handshake_data(ssl)) {
@@ -256,7 +265,13 @@
   // Any 0-RTT keys must have been discarded.
   assert(hs->ssl->s3->write_level == ssl_encryption_initial);
 
-  if (!ssl_write_client_hello(hs)) {
+  // Build the second ClientHelloInner, if applicable. The second ClientHello
+  // uses an empty string for |enc|.
+  if (hs->selected_ech_config && !ssl_encrypt_client_hello(hs, {})) {
+    return ssl_hs_error;
+  }
+
+  if (!ssl_add_client_hello(hs)) {
     return ssl_hs_error;
   }
 
@@ -414,13 +429,11 @@
       EVP_MD_size(ssl_get_handshake_digest(ssl_protocol_version(ssl), cipher));
 
   // Set up the key schedule and incorporate the PSK into the running secret.
-  if (ssl->s3->session_reused) {
-    if (!tls13_init_key_schedule(
-            hs, MakeConstSpan(hs->new_session->secret,
-                              hs->new_session->secret_length))) {
-      return ssl_hs_error;
-    }
-  } else if (!tls13_init_key_schedule(hs, MakeConstSpan(kZeroes, hash_len))) {
+  if (!tls13_init_key_schedule(
+          hs, ssl->s3->session_reused
+                  ? MakeConstSpan(hs->new_session->secret,
+                                  hs->new_session->secret_length)
+                  : MakeConstSpan(kZeroes, hash_len))) {
     return ssl_hs_error;
   }
 
@@ -440,8 +453,54 @@
     return ssl_hs_error;
   }
 
-  if (!tls13_advance_key_schedule(hs, dhe_secret) ||
-      !ssl_hash_message(hs, msg) ||
+  if (!tls13_advance_key_schedule(hs, dhe_secret)) {
+    return ssl_hs_error;
+  }
+
+  // Determine whether the server accepted ECH.
+  //
+  // TODO(https://crbug.com/boringssl/275): This is a bit late in the process of
+  // parsing ServerHello. |ssl->session| is only valid for ClientHelloInner, so
+  // the decisions made based on PSK need to be double-checked. draft-11 will
+  // fix this, at which point this logic can be moved before any processing.
+  if (hs->selected_ech_config) {
+    uint8_t ech_confirmation[ECH_CONFIRMATION_SIGNAL_LEN];
+    if (!hs->inner_transcript.InitHash(ssl_protocol_version(ssl),
+                                       hs->new_cipher) ||
+        !ssl_ech_accept_confirmation(hs, ech_confirmation, hs->inner_transcript,
+                                     msg.raw)) {
+      return ssl_hs_error;
+    }
+
+    if (CRYPTO_memcmp(ech_confirmation,
+                      ssl->s3->server_random + sizeof(ssl->s3->server_random) -
+                          sizeof(ech_confirmation),
+                      sizeof(ech_confirmation)) == 0) {
+      ssl->s3->ech_accept = true;
+      hs->transcript = std::move(hs->inner_transcript);
+      hs->extensions.sent = hs->inner_extensions_sent;
+      // Report the inner random value through |SSL_get_client_random|.
+      OPENSSL_memcpy(ssl->s3->client_random, hs->inner_client_random,
+                     SSL3_RANDOM_SIZE);
+    } else {
+      // Resuming against the ClientHelloOuter was an unsolicited extension.
+      if (have_pre_shared_key) {
+        OPENSSL_PUT_ERROR(SSL, SSL_R_UNEXPECTED_EXTENSION);
+        ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_UNSUPPORTED_EXTENSION);
+        return ssl_hs_error;
+      }
+
+      // TODO(https://crbug.com/boringssl/275): If the server declines ECH, we
+      // handshake with ClientHelloOuter instead of ClientHelloInner. That path
+      // is not yet implemented. For now, terminate the handshake with a
+      // distiguisable error for testing.
+      OPENSSL_PUT_ERROR(SSL, SSL_R_CONNECTION_REJECTED);
+      return ssl_hs_error;
+    }
+  }
+
+
+  if (!ssl_hash_message(hs, msg) ||
       !tls13_derive_handshake_secrets(hs)) {
     return ssl_hs_error;
   }
@@ -491,6 +550,13 @@
   }
 
   if (ssl->s3->early_data_accepted) {
+    // The extension parser checks the server resumed the session.
+    assert(ssl->s3->session_reused);
+    // If offering ECH, the server may not accept early data with
+    // ClientHelloOuter. We do not offer sessions with ClientHelloOuter, so this
+    // this should be implied by checking |session_reused|.
+    assert(hs->selected_ech_config == nullptr || ssl->s3->ech_accept);
+
     if (hs->early_session->cipher != hs->new_session->cipher) {
       OPENSSL_PUT_ERROR(SSL, SSL_R_CIPHER_MISMATCH_ON_EARLY_DATA);
       ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_ILLEGAL_PARAMETER);
diff --git a/ssl/tls13_enc.cc b/ssl/tls13_enc.cc
index 2ac9985..174f5f1 100644
--- a/ssl/tls13_enc.cc
+++ b/ssl/tls13_enc.cc
@@ -33,24 +33,25 @@
 
 BSSL_NAMESPACE_BEGIN
 
-static bool init_key_schedule(SSL_HANDSHAKE *hs, uint16_t version,
-                              const SSL_CIPHER *cipher) {
-  if (!hs->transcript.InitHash(version, cipher)) {
+static bool init_key_schedule(SSL_HANDSHAKE *hs, SSLTranscript *transcript,
+                              uint16_t version, const SSL_CIPHER *cipher) {
+  if (!transcript->InitHash(version, cipher)) {
     return false;
   }
 
   // Initialize the secret to the zero key.
-  hs->ResizeSecrets(hs->transcript.DigestLen());
+  hs->ResizeSecrets(transcript->DigestLen());
   OPENSSL_memset(hs->secret().data(), 0, hs->secret().size());
 
   return true;
 }
 
-static bool hkdf_extract_to_secret(SSL_HANDSHAKE *hs, Span<const uint8_t> in) {
+static bool hkdf_extract_to_secret(SSL_HANDSHAKE *hs,
+                                   const SSLTranscript &transcript,
+                                   Span<const uint8_t> in) {
   size_t len;
-  if (!HKDF_extract(hs->secret().data(), &len, hs->transcript.Digest(),
-                    in.data(), in.size(), hs->secret().data(),
-                    hs->secret().size())) {
+  if (!HKDF_extract(hs->secret().data(), &len, transcript.Digest(), in.data(),
+                    in.size(), hs->secret().data(), hs->secret().size())) {
     return false;
   }
   assert(len == hs->secret().size());
@@ -58,7 +59,8 @@
 }
 
 bool tls13_init_key_schedule(SSL_HANDSHAKE *hs, Span<const uint8_t> psk) {
-  if (!init_key_schedule(hs, ssl_protocol_version(hs->ssl), hs->new_cipher)) {
+  if (!init_key_schedule(hs, &hs->transcript, ssl_protocol_version(hs->ssl),
+                         hs->new_cipher)) {
     return false;
   }
 
@@ -67,14 +69,22 @@
   if (!hs->handback) {
     hs->transcript.FreeBuffer();
   }
-  return hkdf_extract_to_secret(hs, psk);
+  return hkdf_extract_to_secret(hs, hs->transcript, psk);
 }
 
-bool tls13_init_early_key_schedule(SSL_HANDSHAKE *hs, Span<const uint8_t> psk) {
-  SSL *const ssl = hs->ssl;
-  return init_key_schedule(hs, ssl_session_protocol_version(ssl->session.get()),
-                           ssl->session->cipher) &&
-         hkdf_extract_to_secret(hs, psk);
+bool tls13_init_early_key_schedule(SSL_HANDSHAKE *hs,
+                                   const SSL_SESSION *session) {
+  assert(!hs->ssl->server);
+  // When offering ECH, early data is associated with ClientHelloInner, not
+  // ClientHelloOuter.
+  SSLTranscript *transcript =
+      hs->selected_ech_config ? &hs->inner_transcript : &hs->transcript;
+  return init_key_schedule(hs, transcript,
+                           ssl_session_protocol_version(session),
+                           session->cipher) &&
+         hkdf_extract_to_secret(
+             hs, *transcript,
+             MakeConstSpan(session->secret, session->secret_length));
 }
 
 static Span<const char> label_to_span(const char *label) {
@@ -118,25 +128,31 @@
          hkdf_expand_label(hs->secret(), hs->transcript.Digest(), hs->secret(),
                            label_to_span(kTLS13LabelDerived),
                            MakeConstSpan(derive_context, derive_context_len)) &&
-         hkdf_extract_to_secret(hs, in);
+         hkdf_extract_to_secret(hs, hs->transcript, in);
 }
 
-// derive_secret derives a secret of length |out.size()| and writes the result
-// in |out| with the given label, the current base secret, and the most
-// recently-saved handshake context. It returns true on success and false on
-// error.
-static bool derive_secret(SSL_HANDSHAKE *hs, Span<uint8_t> out,
-                          Span<const char> label) {
+// derive_secret_with_transcript derives a secret of length |out.size()| and
+// writes the result in |out| with the given label, the current base secret, and
+// the state of |transcript|. It returns true on success and false on error.
+static bool derive_secret_with_transcript(const SSL_HANDSHAKE *hs,
+                                          Span<uint8_t> out,
+                                          const SSLTranscript &transcript,
+                                          Span<const char> label) {
   uint8_t context_hash[EVP_MAX_MD_SIZE];
   size_t context_hash_len;
-  if (!hs->transcript.GetHash(context_hash, &context_hash_len)) {
+  if (!transcript.GetHash(context_hash, &context_hash_len)) {
     return false;
   }
 
-  return hkdf_expand_label(out, hs->transcript.Digest(), hs->secret(), label,
+  return hkdf_expand_label(out, transcript.Digest(), hs->secret(), label,
                            MakeConstSpan(context_hash, context_hash_len));
 }
 
+static bool derive_secret(SSL_HANDSHAKE *hs, Span<uint8_t> out,
+                          Span<const char> label) {
+  return derive_secret_with_transcript(hs, out, hs->transcript, label);
+}
+
 bool tls13_set_traffic_key(SSL *ssl, enum ssl_encryption_level_t level,
                            enum evp_aead_direction_t direction,
                            const SSL_SESSION *session,
@@ -228,8 +244,14 @@
 
 bool tls13_derive_early_secret(SSL_HANDSHAKE *hs) {
   SSL *const ssl = hs->ssl;
-  if (!derive_secret(hs, hs->early_traffic_secret(),
-                     label_to_span(kTLS13LabelClientEarlyTraffic)) ||
+  // When offering ECH on the client, early data is associated with
+  // ClientHelloInner, not ClientHelloOuter.
+  const SSLTranscript &transcript = (!ssl->server && hs->selected_ech_config)
+                                        ? hs->inner_transcript
+                                        : hs->transcript;
+  if (!derive_secret_with_transcript(
+          hs, hs->early_traffic_secret(), transcript,
+          label_to_span(kTLS13LabelClientEarlyTraffic)) ||
       !ssl_log_secret(ssl, "CLIENT_EARLY_TRAFFIC_SECRET",
                       hs->early_traffic_secret())) {
     return false;
@@ -449,7 +471,8 @@
 }
 
 bool tls13_write_psk_binder(const SSL_HANDSHAKE *hs,
-                            Span<uint8_t> msg) {
+                            const SSLTranscript &transcript, Span<uint8_t> msg,
+                            size_t *out_binder_len) {
   const SSL *const ssl = hs->ssl;
   const EVP_MD *digest = ssl_session_get_digest(ssl->session.get());
   const size_t hash_len = EVP_MD_size(digest);
@@ -460,7 +483,7 @@
   uint8_t verify_data[EVP_MAX_MD_SIZE];
   size_t verify_data_len;
   if (!tls13_psk_binder(verify_data, &verify_data_len, ssl->session.get(),
-                        hs->transcript, msg, binders_len) ||
+                        transcript, msg, binders_len) ||
       verify_data_len != hash_len) {
     OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
     return false;
@@ -468,6 +491,9 @@
 
   OPENSSL_memcpy(msg.data() + msg.size() - verify_data_len, verify_data,
                  verify_data_len);
+  if (out_binder_len != nullptr) {
+    *out_binder_len = verify_data_len;
+  }
   return true;
 }
 
@@ -502,17 +528,39 @@
   return true;
 }
 
-bool tls13_ech_accept_confirmation(
-    SSL_HANDSHAKE *hs, bssl::Span<uint8_t> out,
-    bssl::Span<const uint8_t> server_hello_ech_conf) {
-  // Compute the hash of the transcript concatenated with
-  // |server_hello_ech_conf| without modifying |hs->transcript|.
+size_t ssl_ech_confirmation_signal_hello_offset(const SSL *ssl) {
+  static_assert(ECH_CONFIRMATION_SIGNAL_LEN < SSL3_RANDOM_SIZE,
+                "the confirmation signal is a suffix of the random");
+  const size_t header_len =
+      SSL_is_dtls(ssl) ? DTLS1_HM_HEADER_LENGTH : SSL3_HM_HEADER_LENGTH;
+  return header_len + 2 /* version */ + SSL3_RANDOM_SIZE -
+         ECH_CONFIRMATION_SIGNAL_LEN;
+}
+
+bool ssl_ech_accept_confirmation(
+    const SSL_HANDSHAKE *hs, bssl::Span<uint8_t> out,
+    const SSLTranscript &transcript,
+    bssl::Span<const uint8_t> server_hello) {
+  // We hash |server_hello|, with the last |ECH_CONFIRMATION_SIGNAL_LEN| bytes
+  // of the random value zeroed.
+  static const uint8_t kZeroes[ECH_CONFIRMATION_SIGNAL_LEN] = {0};
+  const size_t offset = ssl_ech_confirmation_signal_hello_offset(hs->ssl);
+  if (server_hello.size() < offset + ECH_CONFIRMATION_SIGNAL_LEN) {
+    OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
+    return false;
+  }
+
+  auto before_zeroes = server_hello.subspan(0, offset);
+  auto after_zeroes =
+      server_hello.subspan(offset + ECH_CONFIRMATION_SIGNAL_LEN);
   uint8_t context_hash[EVP_MAX_MD_SIZE];
   unsigned context_hash_len;
   ScopedEVP_MD_CTX ctx;
-  if (!hs->transcript.CopyToHashContext(ctx.get(), hs->transcript.Digest()) ||
-      !EVP_DigestUpdate(ctx.get(), server_hello_ech_conf.data(),
-                        server_hello_ech_conf.size()) ||
+  if (!transcript.CopyToHashContext(ctx.get(), transcript.Digest()) ||
+      !EVP_DigestUpdate(ctx.get(), before_zeroes.data(),
+                        before_zeroes.size()) ||
+      !EVP_DigestUpdate(ctx.get(), kZeroes, sizeof(kZeroes)) ||
+      !EVP_DigestUpdate(ctx.get(), after_zeroes.data(), after_zeroes.size()) ||
       !EVP_DigestFinal_ex(ctx.get(), context_hash, &context_hash_len)) {
     return false;
   }
@@ -521,16 +569,20 @@
   // Derive-Secret, which derives a secret of size Hash.length. That value is
   // then truncated to the first 8 bytes. Note this differs from deriving an
   // 8-byte secret because the target length is included in the derivation.
+  //
+  // TODO(https://crbug.com/boringssl/275): draft-11 will avoid this.
   uint8_t accept_confirmation_buf[EVP_MAX_MD_SIZE];
   bssl::Span<uint8_t> accept_confirmation =
-      MakeSpan(accept_confirmation_buf, hs->transcript.DigestLen());
-  if (!hkdf_expand_label(accept_confirmation, hs->transcript.Digest(),
+      MakeSpan(accept_confirmation_buf, transcript.DigestLen());
+  if (!hkdf_expand_label(accept_confirmation, transcript.Digest(),
                          hs->secret(), label_to_span("ech accept confirmation"),
                          MakeConstSpan(context_hash, context_hash_len))) {
     return false;
   }
 
-  if (out.size() > accept_confirmation.size()) {
+  static_assert(ECH_CONFIRMATION_SIGNAL_LEN < EVP_MAX_MD_SIZE,
+                "ECH confirmation signal too big");
+  if (out.size() != ECH_CONFIRMATION_SIGNAL_LEN) {
     OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
     return false;
   }
diff --git a/ssl/tls13_server.cc b/ssl/tls13_server.cc
index c772010..7f32b6c 100644
--- a/ssl/tls13_server.cc
+++ b/ssl/tls13_server.cc
@@ -512,17 +512,12 @@
       ssl_get_handshake_digest(ssl_protocol_version(ssl), hs->new_cipher));
 
   // Set up the key schedule and incorporate the PSK into the running secret.
-  if (ssl->s3->session_reused) {
-    if (!tls13_init_key_schedule(
-            hs, MakeConstSpan(hs->new_session->secret,
-                              hs->new_session->secret_length))) {
-      return ssl_hs_error;
-    }
-  } else if (!tls13_init_key_schedule(hs, MakeConstSpan(kZeroes, hash_len))) {
-    return ssl_hs_error;
-  }
-
-  if (!ssl_hash_message(hs, msg)) {
+  if (!tls13_init_key_schedule(
+          hs, ssl->s3->session_reused
+                  ? MakeConstSpan(hs->new_session->secret,
+                                  hs->new_session->secret_length)
+                  : MakeConstSpan(kZeroes, hash_len)) ||
+      !ssl_hash_message(hs, msg)) {
     return ssl_hs_error;
   }
 
@@ -730,28 +725,6 @@
   return ssl_hs_ok;
 }
 
-static bool make_server_hello(SSL_HANDSHAKE *hs, Array<uint8_t> *out) {
-  SSL *const ssl = hs->ssl;
-  ScopedCBB cbb;
-  CBB body, extensions, session_id;
-  if (!ssl->method->init_message(ssl, cbb.get(), &body, SSL3_MT_SERVER_HELLO) ||
-      !CBB_add_u16(&body, TLS1_2_VERSION) ||
-      !CBB_add_bytes(&body, ssl->s3->server_random,
-                     sizeof(ssl->s3->server_random)) ||
-      !CBB_add_u8_length_prefixed(&body, &session_id) ||
-      !CBB_add_bytes(&session_id, hs->session_id, hs->session_id_len) ||
-      !CBB_add_u16(&body, SSL_CIPHER_get_protocol_id(hs->new_cipher)) ||
-      !CBB_add_u8(&body, 0) ||
-      !CBB_add_u16_length_prefixed(&body, &extensions) ||
-      !ssl_ext_pre_shared_key_add_serverhello(hs, &extensions) ||
-      !ssl_ext_key_share_add_serverhello(hs, &extensions) ||
-      !ssl_ext_supported_versions_add_serverhello(hs, &extensions) ||
-      !ssl->method->finish_message(ssl, cbb.get(), out)) {
-    return false;
-  }
-  return true;
-}
-
 static enum ssl_hs_wait_t do_send_server_hello(SSL_HANDSHAKE *hs) {
   SSL *const ssl = hs->ssl;
 
@@ -769,24 +742,44 @@
     }
   }
 
-  assert(!ssl->s3->ech_accept || hs->ech_is_inner_present);
-  if (hs->ech_is_inner_present) {
-    // Construct the ServerHelloECHConf message, which is the same as
-    // ServerHello, except the last 8 bytes of its random field are zeroed out.
-    Span<uint8_t> random_suffix = random.subspan(24);
-    OPENSSL_memset(random_suffix.data(), 0, random_suffix.size());
-
-    Array<uint8_t> server_hello_ech_conf;
-    if (!make_server_hello(hs, &server_hello_ech_conf) ||
-        !tls13_ech_accept_confirmation(hs, random_suffix,
-                                       server_hello_ech_conf)) {
-      return ssl_hs_error;
-    }
+  Array<uint8_t> server_hello;
+  ScopedCBB cbb;
+  CBB body, extensions, session_id;
+  if (!ssl->method->init_message(ssl, cbb.get(), &body, SSL3_MT_SERVER_HELLO) ||
+      !CBB_add_u16(&body, TLS1_2_VERSION) ||
+      !CBB_add_bytes(&body, ssl->s3->server_random,
+                     sizeof(ssl->s3->server_random)) ||
+      !CBB_add_u8_length_prefixed(&body, &session_id) ||
+      !CBB_add_bytes(&session_id, hs->session_id, hs->session_id_len) ||
+      !CBB_add_u16(&body, SSL_CIPHER_get_protocol_id(hs->new_cipher)) ||
+      !CBB_add_u8(&body, 0) ||
+      !CBB_add_u16_length_prefixed(&body, &extensions) ||
+      !ssl_ext_pre_shared_key_add_serverhello(hs, &extensions) ||
+      !ssl_ext_key_share_add_serverhello(hs, &extensions) ||
+      !ssl_ext_supported_versions_add_serverhello(hs, &extensions) ||
+      !ssl->method->finish_message(ssl, cbb.get(), &server_hello)) {
+    return ssl_hs_error;
   }
 
-  Array<uint8_t> server_hello;
-  if (!make_server_hello(hs, &server_hello) ||
-      !ssl->method->add_message(ssl, std::move(server_hello))) {
+  assert(!ssl->s3->ech_accept || hs->ech_is_inner_present);
+  if (hs->ech_is_inner_present) {
+    // Fill in the ECH confirmation signal.
+    Span<uint8_t> random_suffix =
+        random.subspan(SSL3_RANDOM_SIZE - ECH_CONFIRMATION_SIGNAL_LEN);
+    if (!ssl_ech_accept_confirmation(hs, random_suffix, hs->transcript,
+                                     server_hello)) {
+      return ssl_hs_error;
+    }
+
+    // Update |server_hello|.
+    const size_t offset = ssl_ech_confirmation_signal_hello_offset(ssl);
+    Span<uint8_t> server_hello_out =
+        MakeSpan(server_hello).subspan(offset, ECH_CONFIRMATION_SIGNAL_LEN);
+    OPENSSL_memcpy(server_hello_out.data(), random_suffix.data(),
+                   ECH_CONFIRMATION_SIGNAL_LEN);
+  }
+
+  if (!ssl->method->add_message(ssl, std::move(server_hello))) {
     return ssl_hs_error;
   }
 
@@ -805,8 +798,6 @@
   }
 
   // Send EncryptedExtensions.
-  ScopedCBB cbb;
-  CBB body;
   if (!ssl->method->init_message(ssl, cbb.get(), &body,
                                  SSL3_MT_ENCRYPTED_EXTENSIONS) ||
       !ssl_add_serverhello_tlsext(hs, &body) ||
diff --git a/tool/client.cc b/tool/client.cc
index a36d7ea..6f738e3 100644
--- a/tool/client.cc
+++ b/tool/client.cc
@@ -64,6 +64,13 @@
         "-server-name", kOptionalArgument, "The server name to advertise",
     },
     {
+        "-ech-grease", kBooleanArgument, "Enable ECH GREASE",
+    },
+    {
+        "-ech-config-list", kOptionalArgument,
+        "Path to file containing serialized ECHConfigs",
+    },
+    {
         "-select-next-proto", kOptionalArgument,
         "An NPN protocol to select if the server supports NPN",
     },
@@ -265,6 +272,24 @@
     SSL_set_tlsext_host_name(ssl.get(), args_map["-server-name"].c_str());
   }
 
+  if (args_map.count("-ech-grease") != 0) {
+    SSL_set_enable_ech_grease(ssl.get(), 1);
+  }
+
+  if (args_map.count("-ech-config-list") != 0) {
+    const char *filename = args_map["-ech-config-list"].c_str();
+    ScopedFILE f(fopen(filename, "rb"));
+    std::vector<uint8_t> data;
+    if (f == nullptr || !ReadAll(&data, f.get())) {
+      fprintf(stderr, "Error reading %s.\n", filename);
+      return false;
+    }
+    if (!SSL_set1_ech_config_list(ssl.get(), data.data(), data.size())) {
+      fprintf(stderr, "Error setting ECHConfigList\n");
+      return false;
+    }
+  }
+
   if (args_map.count("-session-in") != 0) {
     bssl::UniquePtr<BIO> in(BIO_new_file(args_map["-session-in"].c_str(),
                                          "rb"));