Merge pull request #437 from h2o/kazuho/ech

[ech] rewrite ESNI to ECH draft 15
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 923ae3d..3af816d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -127,8 +127,6 @@
     TARGET_LINK_LIBRARIES(picotls-openssl ${OPENSSL_CRYPTO_LIBRARIES} picotls-core ${CMAKE_DL_LIBS})
     ADD_EXECUTABLE(cli t/cli.c lib/pembase64.c)
     TARGET_LINK_LIBRARIES(cli picotls-openssl picotls-core)
-    ADD_EXECUTABLE(picotls-esni src/esni.c)
-    TARGET_LINK_LIBRARIES(picotls-esni picotls-openssl picotls-core ${OPENSSL_CRYPTO_LIBRARIES} ${CMAKE_DL_LIBS})
 
     ADD_EXECUTABLE(test-openssl.t
         ${MINICRYPTO_LIBRARY_FILES}
diff --git a/include/picotls.h b/include/picotls.h
index 83913f9..972bc9d 100644
--- a/include/picotls.h
+++ b/include/picotls.h
@@ -171,12 +171,6 @@
 #define PTLS_HPKE_AEAD_AES_256_GCM 2
 #define PTLS_HPKE_AEAD_CHACHA20POLY1305 3
 
-/* ESNI */
-#define PTLS_ESNI_VERSION_DRAFT03 0xff02
-
-#define PTLS_ESNI_RESPONSE_TYPE_ACCEPT 0
-#define PTLS_ESNI_RESPONSE_TYPE_RETRY_REQUEST 1
-
 /* error classes and macros */
 #define PTLS_ERROR_CLASS_SELF_ALERT 0
 #define PTLS_ERROR_CLASS_PEER_ALERT 0x100
@@ -212,9 +206,11 @@
 #define PTLS_ALERT_INTERNAL_ERROR 80
 #define PTLS_ALERT_USER_CANCELED 90
 #define PTLS_ALERT_MISSING_EXTENSION 109
+#define PTLS_ALERT_UNSUPPORTED_EXTENSION 110
 #define PTLS_ALERT_UNRECOGNIZED_NAME 112
 #define PTLS_ALERT_CERTIFICATE_REQUIRED 116
 #define PTLS_ALERT_NO_APPLICATION_PROTOCOL 120
+#define PTLS_ALERT_ECH_REQUIRED 121
 
 /* TLS 1.2 */
 #define PTLS_TLS12_MASTER_SECRET_SIZE 48
@@ -233,7 +229,6 @@
 #define PTLS_ERROR_STATELESS_RETRY (PTLS_ERROR_CLASS_INTERNAL + 6)
 #define PTLS_ERROR_NOT_AVAILABLE (PTLS_ERROR_CLASS_INTERNAL + 7)
 #define PTLS_ERROR_COMPRESSION_FAILURE (PTLS_ERROR_CLASS_INTERNAL + 8)
-#define PTLS_ERROR_ESNI_RETRY (PTLS_ERROR_CLASS_INTERNAL + 8)
 #define PTLS_ERROR_REJECT_EARLY_DATA (PTLS_ERROR_CLASS_INTERNAL + 9)
 #define PTLS_ERROR_DELEGATE (PTLS_ERROR_CLASS_INTERNAL + 10)
 #define PTLS_ERROR_ASYNC_OPERATION (PTLS_ERROR_CLASS_INTERNAL + 11)
@@ -588,41 +583,6 @@
     ptls_aead_algorithm_t *aead;
 } ptls_hpke_cipher_suite_t;
 
-/**
- * holds ESNIKeys and the private key (instantiated by ptls_esni_parse, freed using ptls_esni_dispose)
- */
-typedef struct st_ptls_esni_context_t {
-    ptls_key_exchange_context_t **key_exchanges;
-    struct {
-        ptls_cipher_suite_t *cipher_suite;
-        uint8_t record_digest[PTLS_MAX_DIGEST_SIZE];
-    } * cipher_suites;
-    uint16_t padded_length;
-    uint64_t not_before;
-    uint64_t not_after;
-    uint16_t version;
-} ptls_esni_context_t;
-
-/**
- * holds the ESNI secret, as exchanged during the handshake
- */
-
-#define PTLS_ESNI_NONCE_SIZE 16
-
-typedef struct st_ptls_esni_secret_t {
-    ptls_iovec_t secret;
-    uint8_t nonce[PTLS_ESNI_NONCE_SIZE];
-    uint8_t esni_contents_hash[PTLS_MAX_DIGEST_SIZE];
-    struct {
-        ptls_key_exchange_algorithm_t *key_share;
-        ptls_cipher_suite_t *cipher;
-        ptls_iovec_t pubkey;
-        uint8_t record_digest[PTLS_MAX_DIGEST_SIZE];
-        uint16_t padded_length;
-    } client;
-    uint16_t version;
-} ptls_esni_secret_t;
-
 #define PTLS_CALLBACK_TYPE0(ret, name)                                                                                             \
     typedef struct st_ptls_##name##_t {                                                                                            \
         ret (*cb)(struct st_ptls_##name##_t * self);                                                                               \
@@ -669,10 +629,6 @@
         size_t count;
     } server_certificate_types;
     /**
-     * if ESNI was used
-     */
-    unsigned esni : 1;
-    /**
      * set to 1 if ClientHello is too old (or too new) to be handled by picotls
      */
     unsigned incompatible_version : 1;
@@ -710,9 +666,11 @@
  * callback to the invocation of the verify_sign callback, verify_sign is called with both data and sign set to an empty buffer.
  * The implementor of the callback should use that as the opportunity to free any temporary data allocated for the verify_sign
  * callback.
+ * The name of the server to be verified, if any, is provided explicitly as `server_name`. When ECH is offered by the client but
+ * the was rejected by the server, this value can be different from that being sent via `ptls_get_server_name`.
  */
 typedef struct st_ptls_verify_certificate_t {
-    int (*cb)(struct st_ptls_verify_certificate_t *self, ptls_t *tls,
+    int (*cb)(struct st_ptls_verify_certificate_t *self, ptls_t *tls, const char *server_name,
               int (**verify_sign)(void *verify_ctx, uint16_t algo, ptls_iovec_t data, ptls_iovec_t sign), void **verify_data,
               ptls_iovec_t *certs, size_t num_certs);
     /**
@@ -766,10 +724,12 @@
               ptls_iovec_t input);
 } ptls_decompress_certificate_t;
 /**
- * provides access to the ESNI shared secret (Zx).  API is subject to change.
+ * ECH: creates the AEAD context to be used for "Open"-ing inner CH. Given `config_id`, the callback looks up the ECH config and the
+ * corresponding private key, invokes `ptls_hpke_setup_base_r` with provided `cipher`, `enc`, and `info_prefix` (which will be
+ * "tls ech" || 00).
  */
-PTLS_CALLBACK_TYPE(int, update_esni_key, ptls_t *tls, ptls_iovec_t secret, ptls_hash_algorithm_t *hash,
-                   const void *hashed_esni_contents);
+PTLS_CALLBACK_TYPE(ptls_aead_context_t *, ech_create_opener, ptls_hpke_kem_t **kem, ptls_hpke_cipher_suite_t **cipher, ptls_t *tls,
+                   uint8_t config_id, ptls_hpke_cipher_suite_id_t cipher_id, ptls_iovec_t enc, ptls_iovec_t info_prefix);
 
 /**
  * the configuration
@@ -799,9 +759,30 @@
         size_t count;
     } certificates;
     /**
-     * list of ESNI data terminated by NULL
+     * ECH
      */
-    ptls_esni_context_t **esni;
+    struct {
+        struct {
+            /**
+             * list of HPKE symmetric cipher-suites (set to NULL to disable ECH altogether)
+             */
+            ptls_hpke_cipher_suite_t **ciphers;
+            /**
+             * KEMs being supported
+             */
+            ptls_hpke_kem_t **kems;
+        } client;
+        struct {
+            /**
+             * callback that does ECDH key exchange and returns the AEAD context
+             */
+            ptls_ech_create_opener_t *create_opener;
+            /**
+             * ECHConfigList to be sent to the client when there is mismatch (or when the client sends a grease)
+             */
+            ptls_iovec_t retry_configs;
+        } server;
+    } ech;
     /**
      *
      */
@@ -900,10 +881,6 @@
     /**
      *
      */
-    ptls_update_esni_key_t *update_esni_key;
-    /**
-     *
-     */
     ptls_on_extension_t *on_extension;
     /**
      * (optional) list of supported tls12 cipher-suites terminated by NULL
@@ -961,9 +938,18 @@
              */
             unsigned negotiate_before_key_exchange : 1;
             /**
-             * ESNIKeys (the value of the TXT record, after being base64-"decoded")
+             * ECH
              */
-            ptls_iovec_t esni_keys;
+            struct {
+                /**
+                 * config offered by server e.g., by HTTPS RR
+                 */
+                ptls_iovec_t configs;
+                /**
+                 * slot to save the config obtained from server on mismatch; user must free the returned blob by calling `free`
+                 */
+                ptls_iovec_t *retry_configs;
+            } ech;
         } client;
         struct {
             /**
@@ -1159,7 +1145,7 @@
         ptls_buffer_push(_buf, (type));                                                                                            \
         ptls_buffer_push_block(_buf, 3, block);                                                                                    \
         if (_key_sched != NULL)                                                                                                    \
-            ptls__key_schedule_update_hash(_key_sched, _buf->base + mess_start, _buf->off - mess_start);                           \
+            ptls__key_schedule_update_hash(_key_sched, _buf->base + mess_start, _buf->off - mess_start, 0);                        \
     } while (0)
 
 #define ptls_push_message(emitter, key_sched, type, block)                                                                         \
@@ -1482,6 +1468,10 @@
  */
 int ptls_is_psk_handshake(ptls_t *tls);
 /**
+ * return if a ECH handshake was performed, as well as optionally the kem and cipher-suite being used
+ */
+int ptls_is_ech_handshake(ptls_t *tls, ptls_hpke_kem_t **kem, ptls_hpke_cipher_suite_t **cipher);
+/**
  * returns a pointer to user data pointer (client is reponsible for freeing the associated data prior to calling ptls_free)
  */
 void **ptls_get_data_ptr(ptls_t *tls);
@@ -1683,7 +1673,7 @@
 /**
  * internal
  */
-void ptls__key_schedule_update_hash(ptls_key_schedule_t *sched, const uint8_t *msg, size_t msglen);
+void ptls__key_schedule_update_hash(ptls_key_schedule_t *sched, const uint8_t *msg, size_t msglen, int use_outer);
 /**
  * clears memory
  */
@@ -1697,6 +1687,11 @@
  */
 int ptls_server_name_is_ipaddr(const char *name);
 /**
+ * encodes one ECH Config
+ */
+int ptls_ech_encode_config(ptls_buffer_t *buf, uint8_t config_id, ptls_hpke_kem_t *kem, ptls_iovec_t public_key,
+                           ptls_hpke_cipher_suite_t **ciphers, uint8_t max_name_length, const char *public_name);
+/**
  * loads a certificate chain to ptls_context_t::certificates. `certificate.list` and each element of the list is allocated by
  * malloc.  It is the responsibility of the user to free them when discarding the TLS context.
  */
@@ -1716,19 +1711,6 @@
 /**
  *
  */
-int ptls_esni_init_context(ptls_context_t *ctx, ptls_esni_context_t *esni, ptls_iovec_t esni_keys,
-                           ptls_key_exchange_context_t **key_exchanges);
-/**
- *
- */
-void ptls_esni_dispose_context(ptls_esni_context_t *esni);
-/**
- * Obtain the ESNI secrets negotiated during the handshake.
- */
-ptls_esni_secret_t *ptls_get_esni_secret(ptls_t *ctx);
-/**
- *
- */
 char *ptls_hexdump(char *dst, const void *src, size_t len);
 /**
  * Builds a JSON-safe string without double quotes. Supplied buffer MUST be at least 6x + 1 bytes larger than the input.
diff --git a/lib/openssl.c b/lib/openssl.c
index 3a64cb4..db3f98b 100644
--- a/lib/openssl.c
+++ b/lib/openssl.c
@@ -1420,7 +1420,7 @@
     return ret;
 }
 
-static int verify_cert(ptls_verify_certificate_t *_self, ptls_t *tls,
+static int verify_cert(ptls_verify_certificate_t *_self, ptls_t *tls, const char *server_name,
                        int (**verifier)(void *, uint16_t, ptls_iovec_t, ptls_iovec_t), void **verify_data, ptls_iovec_t *certs,
                        size_t num_certs)
 {
@@ -1445,7 +1445,7 @@
             }
             sk_X509_push(chain, interm);
         }
-        ret = verify_cert_chain(self->cert_store, cert, chain, ptls_is_server(tls), ptls_get_server_name(tls), &ossl_x509_err);
+        ret = verify_cert_chain(self->cert_store, cert, chain, ptls_is_server(tls), server_name, &ossl_x509_err);
     } else {
         ret = PTLS_ALERT_CERTIFICATE_REQUIRED;
         ossl_x509_err = 0;
@@ -1515,7 +1515,7 @@
     return NULL;
 }
 
-static int verify_raw_cert(ptls_verify_certificate_t *_self, ptls_t *tls,
+static int verify_raw_cert(ptls_verify_certificate_t *_self, ptls_t *tls, const char *server_name,
                            int (**verifier)(void *, uint16_t algo, ptls_iovec_t, ptls_iovec_t), void **verify_data,
                            ptls_iovec_t *certs, size_t num_certs)
 {
diff --git a/lib/picotls.c b/lib/picotls.c
index eb77e67..976212f 100644
--- a/lib/picotls.c
+++ b/lib/picotls.c
@@ -68,10 +68,19 @@
 #define PTLS_EXTENSION_TYPE_COOKIE 44
 #define PTLS_EXTENSION_TYPE_PSK_KEY_EXCHANGE_MODES 45
 #define PTLS_EXTENSION_TYPE_KEY_SHARE 51
-#define PTLS_EXTENSION_TYPE_ENCRYPTED_SERVER_NAME 0xffce
+#define PTLS_EXTENSION_TYPE_ECH_OUTER_EXTENSIONS 0xfd00
+#define PTLS_EXTENSION_TYPE_ENCRYPTED_CLIENT_HELLO 0xfe0d
 
 #define PTLS_SERVER_NAME_TYPE_HOSTNAME 0
 
+#define PTLS_ECH_CONFIG_VERSION 0xfe0d
+#define PTLS_ECH_CLIENT_HELLO_TYPE_OUTER 0
+#define PTLS_ECH_CLIENT_HELLO_TYPE_INNER 1
+
+#define PTLS_ECH_CONFIRM_LENGTH 8
+
+static const char ech_info_prefix[8] = "tls ech";
+
 #define PTLS_SERVER_CERTIFICATE_VERIFY_CONTEXT_STRING "TLS 1.3, server CertificateVerify"
 #define PTLS_CLIENT_CERTIFICATE_VERIFY_CONTEXT_STRING "TLS 1.3, client CertificateVerify"
 #define PTLS_MAX_CERTIFICATE_VERIFY_SIGNDATA_SIZE                                                                                  \
@@ -148,6 +157,38 @@
     struct st_ptls_signature_algorithms_t signature_algorithms;
 };
 
+struct st_decoded_ech_config_t {
+    uint8_t id;
+    ptls_hpke_kem_t *kem;
+    ptls_iovec_t public_key;
+    ptls_hpke_cipher_suite_t *cipher;
+    uint8_t max_name_length;
+    ptls_iovec_t public_name;
+    ptls_iovec_t bytes;
+};
+
+/**
+ * Properties for ECH. Iff ECH is used and not rejected, `aead` is non-NULL.
+ */
+struct st_ptls_ech_t {
+    uint8_t offered : 1;
+    uint8_t accepted : 1;
+    uint8_t config_id;
+    ptls_hpke_kem_t *kem;
+    ptls_hpke_cipher_suite_t *cipher;
+    ptls_aead_context_t *aead;
+    uint8_t inner_client_random[PTLS_HELLO_RANDOM_SIZE];
+    struct {
+        ptls_iovec_t enc;
+        uint8_t max_name_length;
+        char *public_name;
+        /**
+         * retains a copy of entire ECH extension so that it can be replayed in the 2nd CH when ECH is rejected via HRR
+         */
+        ptls_iovec_t first_ech;
+    } client;
+};
+
 struct st_ptls_t {
     /**
      * the context
@@ -212,20 +253,20 @@
      */
     ptls_cipher_suite_t *cipher_suite;
     /**
-     * clienthello.random
+     * ClientHello.random that appears on the wire. When ECH is used, that of inner CH is retained separately.
      */
     uint8_t client_random[PTLS_HELLO_RANDOM_SIZE];
     /**
-     * esni
-     */
-    ptls_esni_secret_t *esni;
-    /**
      * exporter master secret (either 0rtt or 1rtt)
      */
     struct {
         uint8_t *early;
         uint8_t *one_rtt;
     } exporter_master_secret;
+    /**
+     * ECH
+     */
+    struct st_ptls_ech_t ech;
     /* flags */
     unsigned is_server : 1;
     unsigned is_psk_handshake : 1;
@@ -304,13 +345,6 @@
     struct st_ptls_signature_algorithms_t signature_algorithms;
     ptls_iovec_t server_name;
     struct {
-        ptls_cipher_suite_t *cipher; /* selected cipher-suite, or NULL if esni extension is not used */
-        ptls_key_exchange_algorithm_t *key_share;
-        ptls_iovec_t peer_key;
-        const uint8_t *record_digest;
-        ptls_iovec_t encrypted_sni;
-    } esni;
-    struct {
         ptls_iovec_t list[16];
         size_t count;
     } alpn;
@@ -330,6 +364,21 @@
         unsigned sent_key_share : 1;
     } cookie;
     struct {
+        uint8_t list[MAX_CERTIFICATE_TYPES];
+        size_t count;
+    } server_certificate_types;
+    unsigned status_request : 1;
+    /**
+     * ECH: payload.base != NULL indicates that the extension was received
+     */
+    struct {
+        uint8_t type;
+        uint8_t config_id;
+        ptls_hpke_cipher_suite_id_t cipher_suite;
+        ptls_iovec_t enc;
+        ptls_iovec_t payload;
+    } ech;
+    struct {
         const uint8_t *hash_end;
         struct {
             struct st_ptls_client_hello_psk_t list[4];
@@ -339,12 +388,8 @@
         unsigned early_data_indication : 1;
         unsigned is_last_extension : 1;
     } psk;
-    struct {
-        uint8_t list[MAX_CERTIFICATE_TYPES];
-        size_t count;
-    } server_certificate_types;
     ptls_raw_extension_t unknown_extensions[MAX_UNKNOWN_EXTENSIONS + 1];
-    unsigned status_request : 1;
+    size_t first_extension_at;
 };
 
 struct st_ptls_server_hello_t {
@@ -356,6 +401,7 @@
         struct {
             uint16_t selected_group;
             ptls_iovec_t cookie;
+            const uint8_t *ech;
         } retry_request;
     };
 };
@@ -366,7 +412,7 @@
     size_t num_hashes;
     struct {
         ptls_hash_algorithm_t *algo;
-        ptls_hash_context_t *ctx;
+        ptls_hash_context_t *ctx, *ctx_outer;
     } hashes[1];
 };
 
@@ -376,7 +422,7 @@
 };
 
 struct st_ptls_extension_bitmap_t {
-    uint8_t bits[8]; /* only ids below 64 is tracked */
+    uint64_t bits;
 };
 
 static const uint8_t zeroes_of_max_digest_size[PTLS_MAX_DIGEST_SIZE] = {0};
@@ -395,27 +441,17 @@
     return 0;
 }
 
-static inline int extension_bitmap_is_set(struct st_ptls_extension_bitmap_t *bitmap, uint16_t id)
-{
-    if (id < sizeof(bitmap->bits) * 8)
-        return (bitmap->bits[id / 8] & (1 << (id % 8))) != 0;
-    return 0;
-}
-
-static inline void extension_bitmap_set(struct st_ptls_extension_bitmap_t *bitmap, uint16_t id)
-{
-    if (id < sizeof(bitmap->bits) * 8)
-        bitmap->bits[id / 8] |= 1 << (id % 8);
-}
-
-static inline void init_extension_bitmap(struct st_ptls_extension_bitmap_t *bitmap, unsigned hstype)
+static int extension_bitmap_testandset(struct st_ptls_extension_bitmap_t *bitmap, int hstype, uint16_t extid)
 {
 #define HSTYPE_TO_BIT(hstype) ((uint64_t)1 << ((hstype) + 1)) /* min(hstype) is -1 (PSEUDO_HRR) */
 #define DEFINE_BIT(abbrev, hstype) static const uint64_t abbrev = HSTYPE_TO_BIT(PTLS_HANDSHAKE_TYPE_##hstype)
-#define EXT(extid, allowed_bits)                                                                                                   \
+#define EXT(candext, allowed_bits)                                                                                                 \
     do {                                                                                                                           \
-        if (((allowed_bits)&HSTYPE_TO_BIT(hstype)) == 0)                                                                           \
-            extension_bitmap_set(bitmap, PTLS_EXTENSION_TYPE_##extid);                                                             \
+        if (PTLS_UNLIKELY(extid == PTLS_EXTENSION_TYPE_##candext)) {                                                               \
+            allowed_hs_bits = allowed_bits;                                                                                        \
+            goto Found;                                                                                                            \
+        }                                                                                                                          \
+        ext_bitmap_mask <<= 1;                                                                                                     \
     } while (0)
 
     DEFINE_BIT(CH, CLIENT_HELLO);
@@ -426,7 +462,7 @@
     DEFINE_BIT(CT, CERTIFICATE);
     DEFINE_BIT(NST, NEW_SESSION_TICKET);
 
-    *bitmap = (struct st_ptls_extension_bitmap_t){{0}};
+    uint64_t allowed_hs_bits, ext_bitmap_mask = 1;
 
     /* clang-format off */
     /* RFC 8446 section 4.2: "The table below indicates the messages where a given extension may appear... If an implementation
@@ -449,9 +485,21 @@
     EXT( COOKIE                  , CH + HRR      );
     EXT( SUPPORTED_VERSIONS      , CH + SH + HRR );
     EXT( COMPRESS_CERTIFICATE    , CH + CR       ); /* from RFC 8879 */
+    EXT( ENCRYPTED_CLIENT_HELLO  , CH + HRR + EE ); /* from draft-ietf-tls-esni-15 */
+    EXT( ECH_OUTER_EXTENSIONS    , 0             );
     /* +-----------------------------------------+ */
     /* clang-format on */
 
+    return 1;
+
+Found:
+    if ((allowed_hs_bits & HSTYPE_TO_BIT(hstype)) == 0)
+        return 0;
+    if ((bitmap->bits & ext_bitmap_mask) != 0)
+        return 0;
+    bitmap->bits |= ext_bitmap_mask;
+    return 1;
+
 #undef HSTYPE_TO_BIT
 #undef DEFINE_ABBREV
 #undef EXT
@@ -492,6 +540,17 @@
         dst[i] = (uint8_t)(v >> (56 - 8 * i));
 }
 
+static char *duplicate_as_str(const void *src, size_t len)
+{
+    char *dst;
+
+    if ((dst = malloc(len + 1)) == NULL)
+        return NULL;
+    memcpy(dst, src, len);
+    dst[len] = '\0';
+    return dst;
+}
+
 void ptls_buffer__release_memory(ptls_buffer_t *buf)
 {
     ptls_clear_memory(buf->base, buf->off);
@@ -818,17 +877,15 @@
 
 #define decode_open_extensions(src, end, hstype, exttype, block)                                                                   \
     do {                                                                                                                           \
-        struct st_ptls_extension_bitmap_t bitmap;                                                                                  \
-        init_extension_bitmap(&bitmap, (hstype));                                                                                  \
+        struct st_ptls_extension_bitmap_t bitmap = {0};                                                                            \
         ptls_decode_open_block((src), end, 2, {                                                                                    \
             while ((src) != end) {                                                                                                 \
                 if ((ret = ptls_decode16((exttype), &(src), end)) != 0)                                                            \
                     goto Exit;                                                                                                     \
-                if (extension_bitmap_is_set(&bitmap, *(exttype)) != 0) {                                                           \
+                if (!extension_bitmap_testandset(&bitmap, (hstype), *(exttype))) {                                                 \
                     ret = PTLS_ALERT_ILLEGAL_PARAMETER;                                                                            \
                     goto Exit;                                                                                                     \
                 }                                                                                                                  \
-                extension_bitmap_set(&bitmap, *(exttype));                                                                         \
                 ptls_decode_open_block((src), end, 2, block);                                                                      \
             }                                                                                                                      \
         });                                                                                                                        \
@@ -915,16 +972,242 @@
         tls->ctx->log_event->cb(tls->ctx->log_event, tls, type, "%s", ptls_hexdump(hexbuf, secret.base, secret.len));
 }
 
+/**
+ * This function preserves the flags and  modes (e.g., `offered`, `accepted`, `cipher`), they can be used afterwards.
+ */
+static void clear_ech(struct st_ptls_ech_t *ech, int is_server)
+{
+    if (ech->aead != NULL) {
+        ptls_aead_free(ech->aead);
+        ech->aead = NULL;
+    }
+    ptls_clear_memory(ech->inner_client_random, PTLS_HELLO_RANDOM_SIZE);
+    if (!is_server) {
+        free(ech->client.enc.base);
+        ech->client.enc = ptls_iovec_init(NULL, 0);
+        if (ech->client.public_name != NULL) {
+            free(ech->client.public_name);
+            ech->client.public_name = NULL;
+        }
+        free(ech->client.first_ech.base);
+        ech->client.first_ech = ptls_iovec_init(NULL, 0);
+    }
+}
+
+/**
+ * Decodes one ECHConfigContents (tls-esni-15 section 4). `decoded->kem` and `cipher` may be NULL even when the function returns
+ * zero, if the corresponding entries are not found.
+ */
+static int decode_one_ech_config(ptls_hpke_kem_t **kems, ptls_hpke_cipher_suite_t **ciphers,
+                                 struct st_decoded_ech_config_t *decoded, const uint8_t **src, const uint8_t *const end)
+{
+    char *public_name_buf = NULL;
+    int ret;
+
+    *decoded = (struct st_decoded_ech_config_t){0};
+
+    if ((ret = ptls_decode8(&decoded->id, src, end)) != 0)
+        goto Exit;
+    uint16_t kem_id;
+    if ((ret = ptls_decode16(&kem_id, src, end)) != 0)
+        goto Exit;
+    for (size_t i = 0; kems[i] != NULL; ++i) {
+        if (kems[i]->id == kem_id) {
+            decoded->kem = kems[i];
+            break;
+        }
+    }
+    ptls_decode_open_block(*src, end, 2, {
+        if (*src == end) {
+            ret = PTLS_ALERT_DECODE_ERROR;
+            goto Exit;
+        }
+        decoded->public_key = ptls_iovec_init(*src, end - *src);
+        *src = end;
+    });
+    ptls_decode_open_block(*src, end, 2, {
+        do {
+            uint16_t kdf_id;
+            uint16_t aead_id;
+            if ((ret = ptls_decode16(&kdf_id, src, end)) != 0)
+                goto Exit;
+            if ((ret = ptls_decode16(&aead_id, src, end)) != 0)
+                goto Exit;
+            if (decoded->cipher == NULL) {
+                for (size_t i = 0; ciphers[i] != NULL; ++i) {
+                    if (ciphers[i]->id.kdf == kdf_id && ciphers[i]->id.aead == aead_id) {
+                        decoded->cipher = ciphers[i];
+                        break;
+                    }
+                }
+            }
+        } while (*src != end);
+    });
+    if ((ret = ptls_decode8(&decoded->max_name_length, src, end)) != 0)
+        goto Exit;
+
+#define SKIP_DECODED()                                                                                                             \
+    do {                                                                                                                           \
+        decoded->kem = NULL;                                                                                                       \
+        decoded->cipher = NULL;                                                                                                    \
+    } while (0)
+
+    /* Decode public_name. The specification requires clients to ignore (upon parsing ESNIConfigList) or reject (upon handshake)
+     * public names that are not DNS names or IPv4 addresses. We ignore IPv4 and v6 addresses during parsing (IPv6 addresses never
+     * looks like DNS names), and delegate the responsibility of rejecting non-DNS names to the certificate verify callback. */
+    ptls_decode_open_block(*src, end, 1, {
+        if (*src == end) {
+            ret = PTLS_ALERT_DECODE_ERROR;
+            goto Exit;
+        }
+        if ((public_name_buf = duplicate_as_str(*src, end - *src)) == NULL) {
+            ret = PTLS_ERROR_NO_MEMORY;
+            goto Exit;
+        }
+        if (ptls_server_name_is_ipaddr(public_name_buf)) {
+            SKIP_DECODED();
+        } else {
+            decoded->public_name = ptls_iovec_init(*src, end - *src);
+        }
+        *src = end;
+    });
+
+    ptls_decode_block(*src, end, 2, {
+        while (*src < end) {
+            uint16_t type;
+            if ((ret = ptls_decode16(&type, src, end)) != 0)
+                goto Exit;
+            ptls_decode_open_block(*src, end, 2, { *src = end; });
+            /* if a critital extension is found, indicate that the config cannot be used */
+            if ((type & 0x8000) != 0)
+                SKIP_DECODED();
+        }
+    });
+
+#undef SKIP_DECODED
+
+Exit:
+    free(public_name_buf);
+    return ret;
+}
+
+static int client_decode_ech_config_list(ptls_context_t *ctx, struct st_decoded_ech_config_t *decoded, ptls_iovec_t config_list)
+{
+    const uint8_t *src = config_list.base, *const end = src + config_list.len;
+    int match_found = 0, ret;
+
+    *decoded = (struct st_decoded_ech_config_t){0};
+
+    ptls_decode_block(src, end, 2, {
+        do {
+            const uint8_t *config_start = src;
+            uint16_t version;
+            if ((ret = ptls_decode16(&version, &src, end)) != 0)
+                goto Exit;
+            ptls_decode_open_block(src, end, 2, {
+                /* If the block is the one that we recognize, parse it, then adopt if if possible. Otherwise, skip. */
+                if (version == PTLS_ECH_CONFIG_VERSION) {
+                    struct st_decoded_ech_config_t thisconf;
+                    if ((ret = decode_one_ech_config(ctx->ech.client.kems, ctx->ech.client.ciphers, &thisconf, &src, end)) != 0)
+                        goto Exit;
+                    if (!match_found && thisconf.kem != NULL && thisconf.cipher != NULL) {
+                        *decoded = thisconf;
+                        decoded->bytes = ptls_iovec_init(config_start, end - config_start);
+                        match_found = 1;
+                    }
+                } else {
+                    src = end;
+                }
+            });
+        } while (src != end);
+    });
+    ret = 0;
+
+Exit:
+    if (ret != 0)
+        *decoded = (struct st_decoded_ech_config_t){0};
+    return ret;
+}
+
+static int client_setup_ech(struct st_ptls_ech_t *ech, struct st_decoded_ech_config_t *decoded,
+                            void (*random_bytes)(void *, size_t))
+{
+    ptls_buffer_t infobuf;
+    uint8_t infobuf_smallbuf[256];
+    int ret;
+
+    /* setup `enc` and `aead` by running HPKE */
+    ptls_buffer_init(&infobuf, infobuf_smallbuf, sizeof(infobuf_smallbuf));
+    ptls_buffer_pushv(&infobuf, ech_info_prefix, sizeof(ech_info_prefix));
+    ptls_buffer_pushv(&infobuf, decoded->bytes.base, decoded->bytes.len);
+    if ((ret = ptls_hpke_setup_base_s(decoded->kem, decoded->cipher, &ech->client.enc, &ech->aead, decoded->public_key,
+                                      ptls_iovec_init(infobuf.base, infobuf.off))) != 0)
+        goto Exit;
+
+    /* setup the rest */
+    ech->config_id = decoded->id;
+    ech->kem = decoded->kem;
+    ech->cipher = decoded->cipher;
+    random_bytes(ech->inner_client_random, PTLS_HELLO_RANDOM_SIZE);
+    ech->client.max_name_length = decoded->max_name_length;
+    if ((ech->client.public_name = duplicate_as_str(decoded->public_name.base, decoded->public_name.len)) == NULL) {
+        ret = PTLS_ERROR_NO_MEMORY;
+        goto Exit;
+    }
+
+Exit:
+    if (ret != 0)
+        clear_ech(ech, 0);
+    return ret;
+}
+
+#define ECH_CONFIRMATION_SERVER_HELLO "ech accept confirmation"
+#define ECH_CONFIRMATION_HRR "hrr ech accept confirmation"
+static int ech_calc_confirmation(ptls_key_schedule_t *sched, void *dst, const uint8_t *inner_random, const char *label,
+                                 ptls_iovec_t message)
+{
+    ptls_hash_context_t *hash = NULL;
+    uint8_t secret[PTLS_MAX_DIGEST_SIZE], transcript_hash[PTLS_MAX_DIGEST_SIZE];
+    int ret;
+
+    /* calc transcript hash using the modified ServerHello / HRR */
+    if ((hash = sched->hashes[0].ctx->clone_(sched->hashes[0].ctx)) == NULL) {
+        ret = PTLS_ERROR_NO_MEMORY;
+        goto Exit;
+    }
+    hash->update(hash, message.base, message.len);
+    hash->final(hash, transcript_hash, PTLS_HASH_FINAL_MODE_FREE);
+    hash = NULL;
+
+    /* HKDF extract and expand */
+    if ((ret = ptls_hkdf_extract(sched->hashes[0].algo, secret, ptls_iovec_init(NULL, 0),
+                                 ptls_iovec_init(inner_random, PTLS_HELLO_RANDOM_SIZE))) != 0)
+        goto Exit;
+    if ((ret = ptls_hkdf_expand_label(sched->hashes[0].algo, dst, 8, ptls_iovec_init(secret, sched->hashes[0].algo->digest_size),
+                                      label, ptls_iovec_init(transcript_hash, sched->hashes[0].algo->digest_size), NULL)) != 0)
+        goto Exit;
+
+Exit:
+    ptls_clear_memory(secret, sizeof(secret));
+    ptls_clear_memory(transcript_hash, sizeof(transcript_hash));
+    if (hash != NULL)
+        hash->final(hash, NULL, PTLS_HASH_FINAL_MODE_FREE);
+    return ret;
+}
+
 static void key_schedule_free(ptls_key_schedule_t *sched)
 {
     size_t i;
     ptls_clear_memory(sched->secret, sizeof(sched->secret));
-    for (i = 0; i != sched->num_hashes; ++i)
+    for (i = 0; i != sched->num_hashes; ++i) {
         sched->hashes[i].ctx->final(sched->hashes[i].ctx, NULL, PTLS_HASH_FINAL_MODE_FREE);
+        if (sched->hashes[i].ctx_outer != NULL)
+            sched->hashes[i].ctx_outer->final(sched->hashes[i].ctx_outer, NULL, PTLS_HASH_FINAL_MODE_FREE);
+    }
     free(sched);
 }
 
-static ptls_key_schedule_t *key_schedule_new(ptls_cipher_suite_t *preferred, ptls_cipher_suite_t **offered)
+static ptls_key_schedule_t *key_schedule_new(ptls_cipher_suite_t *preferred, ptls_cipher_suite_t **offered, int use_outer)
 {
 #define FOREACH_HASH(block)                                                                                                        \
     do {                                                                                                                           \
@@ -962,6 +1245,12 @@
         sched->hashes[sched->num_hashes].algo = cs->hash;
         if ((sched->hashes[sched->num_hashes].ctx = cs->hash->create()) == NULL)
             goto Fail;
+        if (use_outer) {
+            if ((sched->hashes[sched->num_hashes].ctx_outer = cs->hash->create()) == NULL)
+                goto Fail;
+        } else {
+            sched->hashes[sched->num_hashes].ctx_outer = NULL;
+        }
         ++sched->num_hashes;
     });
 
@@ -994,7 +1283,7 @@
     return ret;
 }
 
-static int key_schedule_select_one(ptls_key_schedule_t *sched, ptls_cipher_suite_t *cs, int reset)
+static int key_schedule_select_cipher(ptls_key_schedule_t *sched, ptls_cipher_suite_t *cs, int reset)
 {
     size_t found_slot = SIZE_MAX, i;
     int ret;
@@ -1008,6 +1297,8 @@
             found_slot = i;
         } else {
             sched->hashes[i].ctx->final(sched->hashes[i].ctx, NULL, PTLS_HASH_FINAL_MODE_FREE);
+            if (sched->hashes[i].ctx_outer != NULL)
+                sched->hashes[i].ctx_outer->final(sched->hashes[i].ctx_outer, NULL, PTLS_HASH_FINAL_MODE_FREE);
         }
     }
     if (found_slot != 0) {
@@ -1029,34 +1320,56 @@
     return ret;
 }
 
-void ptls__key_schedule_update_hash(ptls_key_schedule_t *sched, const uint8_t *msg, size_t msglen)
+static void key_schedule_select_outer(ptls_key_schedule_t *sched)
+{
+    /* This function is called when receiving a cleartext message (Server Hello), after the cipher-suite is determined (and hence
+     * the hash also), if ECH was offered */
+    assert(sched->generation == 1);
+    assert(sched->num_hashes == 1);
+    assert(sched->hashes[0].ctx_outer != NULL);
+
+    sched->hashes[0].ctx->final(sched->hashes[0].ctx, NULL, PTLS_HASH_FINAL_MODE_FREE);
+    sched->hashes[0].ctx = sched->hashes[0].ctx_outer;
+    sched->hashes[0].ctx_outer = NULL;
+}
+
+void ptls__key_schedule_update_hash(ptls_key_schedule_t *sched, const uint8_t *msg, size_t msglen, int use_outer)
 {
     size_t i;
 
     PTLS_DEBUGF("%s:%zu\n", __FUNCTION__, msglen);
-    for (i = 0; i != sched->num_hashes; ++i)
-        sched->hashes[i].ctx->update(sched->hashes[i].ctx, msg, msglen);
+    for (i = 0; i != sched->num_hashes; ++i) {
+        ptls_hash_context_t *ctx = use_outer ? sched->hashes[i].ctx_outer : sched->hashes[i].ctx;
+        ctx->update(ctx, msg, msglen);
+    }
 }
 
 static void key_schedule_update_ch1hash_prefix(ptls_key_schedule_t *sched)
 {
     uint8_t prefix[4] = {PTLS_HANDSHAKE_TYPE_MESSAGE_HASH, 0, 0, (uint8_t)sched->hashes[0].algo->digest_size};
-    ptls__key_schedule_update_hash(sched, prefix, sizeof(prefix));
+    ptls__key_schedule_update_hash(sched, prefix, sizeof(prefix), 0);
 }
 
 static void key_schedule_extract_ch1hash(ptls_key_schedule_t *sched, uint8_t *hash)
 {
+    assert(sched->hashes[0].ctx_outer == NULL);
     sched->hashes[0].ctx->final(sched->hashes[0].ctx, hash, PTLS_HASH_FINAL_MODE_RESET);
 }
 
 static void key_schedule_transform_post_ch1hash(ptls_key_schedule_t *sched)
 {
+    size_t digest_size = sched->hashes[0].algo->digest_size;
+    ptls_hash_context_t *hashes[3] = {sched->hashes[0].ctx, sched->hashes[0].ctx_outer, NULL};
     uint8_t ch1hash[PTLS_MAX_DIGEST_SIZE];
+    uint8_t prefix[4] = {PTLS_HANDSHAKE_TYPE_MESSAGE_HASH, 0, 0, (uint8_t)digest_size};
 
-    key_schedule_extract_ch1hash(sched, ch1hash);
+    for (size_t i = 0; hashes[i] != NULL; ++i) {
+        hashes[i]->final(hashes[i], ch1hash, PTLS_HASH_FINAL_MODE_RESET);
+        hashes[i]->update(hashes[i], prefix, sizeof(prefix));
+        hashes[i]->update(hashes[i], ch1hash, digest_size);
+    }
 
-    key_schedule_update_ch1hash_prefix(sched);
-    ptls__key_schedule_update_hash(sched, ch1hash, sched->hashes[0].algo->digest_size);
+    ptls_clear_memory(ch1hash, sizeof(ch1hash));
 }
 
 static int derive_secret_with_hash(ptls_key_schedule_t *sched, void *secret, const char *label, const uint8_t *hash)
@@ -1623,21 +1936,6 @@
     return ret;
 }
 
-static ptls_hash_context_t *create_sha256_context(ptls_context_t *ctx)
-{
-    ptls_cipher_suite_t **cs;
-
-    for (cs = ctx->cipher_suites; *cs != NULL; ++cs) {
-        switch ((*cs)->id) {
-        case PTLS_CIPHER_SUITE_AES_128_GCM_SHA256:
-        case PTLS_CIPHER_SUITE_CHACHA20_POLY1305_SHA256:
-            return (*cs)->hash->create();
-        }
-    }
-
-    return NULL;
-}
-
 static int select_cipher(ptls_cipher_suite_t **selected, ptls_cipher_suite_t **candidates, const uint8_t *src,
                          const uint8_t *const end, int server_preference)
 {
@@ -1752,265 +2050,227 @@
     return ret;
 }
 
-static int parse_esni_keys(ptls_context_t *ctx, uint16_t *esni_version, ptls_key_exchange_algorithm_t **selected_key_share,
-                           ptls_cipher_suite_t **selected_cipher, ptls_iovec_t *peer_key, uint16_t *padded_length,
-                           char **published_sni, ptls_iovec_t input)
+/**
+ * Within the outer ECH extension, returns the number of bytes that preceeds the AEAD-encrypted payload.
+ */
+static inline size_t outer_ech_header_size(size_t enc_size)
 {
-    const uint8_t *src = input.base, *const end = input.base + input.len;
-    uint16_t version;
-    uint64_t not_before, not_after, now;
-    int ret = 0;
+    return 10 + enc_size;
+}
 
-    /* version */
-    if ((ret = ptls_decode16(&version, &src, end)) != 0)
-        goto Exit;
-    if (version != PTLS_ESNI_VERSION_DRAFT03) {
-        ret = PTLS_ALERT_DECODE_ERROR;
-        goto Exit;
-    }
+/**
+ * Flag to indicate which of ClientHelloInner, EncodedClientHelloInner, ClientHelloOuter is to be generated. When ECH is inactive,
+ * only ClientHelloInner is used.
+ */
+enum encode_ch_mode { ENCODE_CH_MODE_INNER, ENCODE_CH_MODE_ENCODED_INNER, ENCODE_CH_MODE_OUTER };
 
-    { /* verify checksum */
-        ptls_hash_context_t *hctx;
-        uint8_t digest[PTLS_SHA256_DIGEST_SIZE];
-        if (end - src < 4) {
-            ret = PTLS_ALERT_DECODE_ERROR;
-            goto Exit;
-        }
-        if ((hctx = create_sha256_context(ctx)) == NULL) {
-            ret = PTLS_ERROR_LIBRARY;
-            goto Exit;
-        }
-        hctx->update(hctx, input.base, src - input.base);
-        hctx->update(hctx, "\0\0\0\0", 4);
-        hctx->update(hctx, src + 4, end - (src + 4));
-        hctx->final(hctx, digest, PTLS_HASH_FINAL_MODE_FREE);
-        if (memcmp(src, digest, 4) != 0) {
-            ret = PTLS_ALERT_DECODE_ERROR;
-            goto Exit;
-        }
-        src += 4;
-    }
-    *esni_version = version;
-    /* published sni */
-    ptls_decode_open_block(src, end, 2, {
-        size_t len = end - src;
-        *published_sni = malloc(len + 1);
-        if (*published_sni == NULL) {
-            ret = PTLS_ERROR_NO_MEMORY;
-            goto Exit;
-        }
-        if (len > 0) {
-            memcpy(*published_sni, src, len);
-        }
-        (*published_sni)[len] = 0;
-        src = end;
-    });
-    /* key-shares */
-    ptls_decode_open_block(src, end, 2, {
-        if ((ret = select_key_share(selected_key_share, peer_key, ctx->key_exchanges, &src, end, 0)) != 0)
-            goto Exit;
-    });
-    /* cipher-suite */
-    ptls_decode_open_block(src, end, 2, {
-        if ((ret = select_cipher(selected_cipher, ctx->cipher_suites, src, end, ctx->server_cipher_preference)) != 0)
-            goto Exit;
-        src = end;
-    });
-    /* padded-length */
-    if ((ret = ptls_decode16(padded_length, &src, end)) != 0)
-        goto Exit;
-    if (padded_length == 0)
-        goto Exit;
-    /* not-before, not_after */
-    if ((ret = ptls_decode64(&not_before, &src, end)) != 0 || (ret = ptls_decode64(&not_after, &src, end)) != 0)
-        goto Exit;
-    /* extensions */
-    ptls_decode_block(src, end, 2, {
-        while (src != end) {
-            uint16_t id;
-            if ((ret = ptls_decode16(&id, &src, end)) != 0)
+static int encode_client_hello(ptls_context_t *ctx, ptls_buffer_t *sendbuf, enum encode_ch_mode mode, int is_second_flight,
+                               ptls_handshake_properties_t *properties, const void *client_random,
+                               ptls_key_exchange_context_t *key_share_ctx, const char *sni_name, ptls_iovec_t legacy_session_id,
+                               struct st_ptls_ech_t *ech, size_t *ech_size_offset, ptls_iovec_t ech_replay,
+                               ptls_iovec_t resumption_secret, ptls_iovec_t resumption_ticket, uint32_t obfuscated_ticket_age,
+                               size_t psk_binder_size, ptls_iovec_t *cookie, int using_early_data)
+{
+    int ret;
+
+    assert(mode == ENCODE_CH_MODE_INNER || ech != NULL);
+
+    ptls_buffer_push_message_body(sendbuf, NULL, PTLS_HANDSHAKE_TYPE_CLIENT_HELLO, {
+        /* legacy_version */
+        ptls_buffer_push16(sendbuf, 0x0303);
+        /* random_bytes */
+        ptls_buffer_pushv(sendbuf, client_random, PTLS_HELLO_RANDOM_SIZE);
+        /* lecagy_session_id */
+        ptls_buffer_push_block(sendbuf, 1, {
+            if (mode != ENCODE_CH_MODE_ENCODED_INNER)
+                ptls_buffer_pushv(sendbuf, legacy_session_id.base, legacy_session_id.len);
+        });
+        /* cipher_suites */
+        ptls_buffer_push_block(sendbuf, 2, {
+            ptls_cipher_suite_t **cs = ctx->cipher_suites;
+            for (; *cs != NULL; ++cs)
+                ptls_buffer_push16(sendbuf, (*cs)->id);
+        });
+        /* legacy_compression_methods */
+        ptls_buffer_push_block(sendbuf, 1, { ptls_buffer_push(sendbuf, 0); });
+        /* extensions */
+        ptls_buffer_push_block(sendbuf, 2, {
+            if (mode == ENCODE_CH_MODE_OUTER) {
+                buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_ENCRYPTED_CLIENT_HELLO, {
+                    size_t ext_payload_from = sendbuf->off;
+                    ptls_buffer_push(sendbuf, PTLS_ECH_CLIENT_HELLO_TYPE_OUTER);
+                    ptls_buffer_push16(sendbuf, ech->cipher->id.kdf);
+                    ptls_buffer_push16(sendbuf, ech->cipher->id.aead);
+                    ptls_buffer_push(sendbuf, ech->config_id);
+                    ptls_buffer_push_block(sendbuf, 2, {
+                        if (!is_second_flight)
+                            ptls_buffer_pushv(sendbuf, ech->client.enc.base, ech->client.enc.len);
+                    });
+                    ptls_buffer_push_block(sendbuf, 2, {
+                        assert(sendbuf->off - ext_payload_from ==
+                               outer_ech_header_size(is_second_flight ? 0 : ech->client.enc.len));
+                        if ((ret = ptls_buffer_reserve(sendbuf, *ech_size_offset)) != 0)
+                            goto Exit;
+                        memset(sendbuf->base + sendbuf->off, 0, *ech_size_offset);
+                        sendbuf->off += *ech_size_offset;
+                        *ech_size_offset = sendbuf->off - *ech_size_offset;
+                    });
+                });
+            } else if (ech->aead != NULL) {
+                buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_ENCRYPTED_CLIENT_HELLO,
+                                      { ptls_buffer_push(sendbuf, PTLS_ECH_CLIENT_HELLO_TYPE_INNER); });
+            } else if (ech_replay.base != NULL) {
+                buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_ENCRYPTED_CLIENT_HELLO,
+                                      { ptls_buffer_pushv(sendbuf, ech_replay.base, ech_replay.len); });
+            }
+            if (mode == ENCODE_CH_MODE_ENCODED_INNER) {
+                buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_ECH_OUTER_EXTENSIONS, {
+                    ptls_buffer_push_block(sendbuf, 1, { ptls_buffer_push16(sendbuf, PTLS_EXTENSION_TYPE_KEY_SHARE); });
+                });
+            } else {
+                buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_KEY_SHARE, {
+                    ptls_buffer_push_block(sendbuf, 2, {
+                        if (key_share_ctx != NULL &&
+                            (ret = push_key_share_entry(sendbuf, key_share_ctx->algo->id, key_share_ctx->pubkey)) != 0)
+                            goto Exit;
+                    });
+                });
+            }
+            if (sni_name != NULL) {
+                buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_SERVER_NAME, {
+                    if ((ret = emit_server_name_extension(sendbuf, sni_name)) != 0)
+                        goto Exit;
+                });
+            }
+            if (properties != NULL && properties->client.negotiated_protocols.count != 0) {
+                buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_ALPN, {
+                    ptls_buffer_push_block(sendbuf, 2, {
+                        size_t i;
+                        for (i = 0; i != properties->client.negotiated_protocols.count; ++i) {
+                            ptls_buffer_push_block(sendbuf, 1, {
+                                ptls_iovec_t p = properties->client.negotiated_protocols.list[i];
+                                ptls_buffer_pushv(sendbuf, p.base, p.len);
+                            });
+                        }
+                    });
+                });
+            }
+            if (ctx->decompress_certificate != NULL) {
+                buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_COMPRESS_CERTIFICATE, {
+                    ptls_buffer_push_block(sendbuf, 1, {
+                        const uint16_t *algo = ctx->decompress_certificate->supported_algorithms;
+                        assert(*algo != UINT16_MAX);
+                        for (; *algo != UINT16_MAX; ++algo)
+                            ptls_buffer_push16(sendbuf, *algo);
+                    });
+                });
+            }
+            buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_SUPPORTED_VERSIONS, {
+                ptls_buffer_push_block(sendbuf, 1, {
+                    size_t i;
+                    for (i = 0; i != PTLS_ELEMENTSOF(supported_versions); ++i)
+                        ptls_buffer_push16(sendbuf, supported_versions[i]);
+                });
+            });
+            buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_SIGNATURE_ALGORITHMS, {
+                if ((ret = push_signature_algorithms(ctx->verify_certificate, sendbuf)) != 0)
+                    goto Exit;
+            });
+            buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_SUPPORTED_GROUPS, {
+                ptls_key_exchange_algorithm_t **algo = ctx->key_exchanges;
+                ptls_buffer_push_block(sendbuf, 2, {
+                    for (; *algo != NULL; ++algo)
+                        ptls_buffer_push16(sendbuf, (*algo)->id);
+                });
+            });
+            if (cookie != NULL && cookie->base != NULL) {
+                buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_COOKIE, {
+                    ptls_buffer_push_block(sendbuf, 2, { ptls_buffer_pushv(sendbuf, cookie->base, cookie->len); });
+                });
+            }
+            if (ctx->use_raw_public_keys) {
+                buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_SERVER_CERTIFICATE_TYPE, {
+                    ptls_buffer_push_block(sendbuf, 1, { ptls_buffer_push(sendbuf, PTLS_CERTIFICATE_TYPE_RAW_PUBLIC_KEY); });
+                });
+            }
+            if ((ret = push_additional_extensions(properties, sendbuf)) != 0)
                 goto Exit;
-            ptls_decode_open_block(src, end, 2, { src = end; });
-        }
+            if (ctx->save_ticket != NULL || resumption_secret.base != NULL) {
+                buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_PSK_KEY_EXCHANGE_MODES, {
+                    ptls_buffer_push_block(sendbuf, 1, {
+                        if (!ctx->require_dhe_on_psk)
+                            ptls_buffer_push(sendbuf, PTLS_PSK_KE_MODE_PSK);
+                        ptls_buffer_push(sendbuf, PTLS_PSK_KE_MODE_PSK_DHE);
+                    });
+                });
+            }
+            if (resumption_secret.base != NULL) {
+                if (using_early_data && !is_second_flight)
+                    buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_EARLY_DATA, {});
+                /* pre-shared key "MUST be the last extension in the ClientHello" (draft-17 section 4.2.6) */
+                buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_PRE_SHARED_KEY, {
+                    ptls_buffer_push_block(sendbuf, 2, {
+                        ptls_buffer_push_block(sendbuf, 2, {
+                            if (mode == ENCODE_CH_MODE_OUTER) {
+                                if ((ret = ptls_buffer_reserve(sendbuf, resumption_ticket.len)) != 0)
+                                    goto Exit;
+                                ctx->random_bytes(sendbuf->base + sendbuf->off, resumption_ticket.len);
+                                sendbuf->off += resumption_ticket.len;
+                            } else {
+                                ptls_buffer_pushv(sendbuf, resumption_ticket.base, resumption_ticket.len);
+                            }
+                        });
+                        uint32_t age;
+                        if (mode == ENCODE_CH_MODE_OUTER) {
+                            ctx->random_bytes(&age, sizeof(age));
+                        } else {
+                            age = obfuscated_ticket_age;
+                        }
+                        ptls_buffer_push32(sendbuf, age);
+                    });
+                    /* allocate space for PSK binder. The space is filled initially filled by a random value (meeting the
+                     * requirement of ClientHelloOuter), and later gets filled with the correct binder value if necessary. */
+                    ptls_buffer_push_block(sendbuf, 2, {
+                        ptls_buffer_push_block(sendbuf, 1, {
+                            if ((ret = ptls_buffer_reserve(sendbuf, psk_binder_size)) != 0)
+                                goto Exit;
+                            ctx->random_bytes(sendbuf->base + sendbuf->off, psk_binder_size);
+                            sendbuf->off += psk_binder_size;
+                        });
+                    });
+                });
+            }
+        });
     });
 
-    /* check validity period */
-    now = ctx->get_time->cb(ctx->get_time);
-    if (!(not_before * 1000 <= now && now <= not_after * 1000)) {
-        ret = PTLS_ALERT_DECODE_ERROR;
-        goto Exit;
-    }
-
-    ret = 0;
 Exit:
     return ret;
 }
 
-static int create_esni_aead(ptls_aead_context_t **aead_ctx, int is_enc, ptls_cipher_suite_t *cipher, ptls_iovec_t ecdh_secret,
-                            const uint8_t *esni_contents_hash)
-{
-    uint8_t aead_secret[PTLS_MAX_DIGEST_SIZE];
-    int ret;
-
-    if ((ret = ptls_hkdf_extract(cipher->hash, aead_secret, ptls_iovec_init(NULL, 0), ecdh_secret)) != 0)
-        goto Exit;
-    if ((*aead_ctx = new_aead(cipher->aead, cipher->hash, is_enc, aead_secret,
-                              ptls_iovec_init(esni_contents_hash, cipher->hash->digest_size), "tls13 esni ")) == NULL) {
-        ret = PTLS_ERROR_NO_MEMORY;
-        goto Exit;
-    }
-
-    ret = 0;
-Exit:
-    ptls_clear_memory(aead_secret, sizeof(aead_secret));
-    return ret;
-}
-
-static int build_esni_contents_hash(ptls_hash_algorithm_t *hash, uint8_t *digest, const uint8_t *record_digest, uint16_t group,
-                                    ptls_iovec_t pubkey, const uint8_t *client_random)
-{
-    ptls_buffer_t buf;
-    uint8_t smallbuf[256];
-    int ret;
-
-    /* build ESNIContents */
-    ptls_buffer_init(&buf, smallbuf, sizeof(smallbuf));
-    ptls_buffer_push_block(&buf, 2, { ptls_buffer_pushv(&buf, record_digest, hash->digest_size); });
-    if ((ret = push_key_share_entry(&buf, group, pubkey)) != 0)
-        goto Exit;
-    ptls_buffer_pushv(&buf, client_random, PTLS_HELLO_RANDOM_SIZE);
-
-    /* calculate digest */
-    if ((ret = ptls_calc_hash(hash, digest, buf.base, buf.off)) != 0)
-        goto Exit;
-
-    ret = 0;
-Exit:
-    ptls_buffer_dispose(&buf);
-    return ret;
-}
-
-static void free_esni_secret(ptls_esni_secret_t **esni, int is_server)
-{
-    assert(*esni != NULL);
-    if ((*esni)->secret.base != NULL) {
-        ptls_clear_memory((*esni)->secret.base, (*esni)->secret.len);
-        free((*esni)->secret.base);
-    }
-    if (!is_server)
-        free((*esni)->client.pubkey.base);
-    ptls_clear_memory((*esni), sizeof(**esni));
-    free(*esni);
-    *esni = NULL;
-}
-
-static int client_setup_esni(ptls_context_t *ctx, ptls_esni_secret_t **esni, ptls_iovec_t esni_keys, char **published_sni,
-                             const uint8_t *client_random)
-{
-    ptls_iovec_t peer_key;
-    int ret;
-
-    if ((*esni = malloc(sizeof(**esni))) == NULL)
-        return PTLS_ERROR_NO_MEMORY;
-    memset(*esni, 0, sizeof(**esni));
-
-    /* parse ESNI_Keys (and return success while keeping *esni NULL) */
-    if (parse_esni_keys(ctx, &(*esni)->version, &(*esni)->client.key_share, &(*esni)->client.cipher, &peer_key,
-                        &(*esni)->client.padded_length, published_sni, esni_keys) != 0) {
-        free(*esni);
-        *esni = NULL;
-        return 0;
-    }
-
-    ctx->random_bytes((*esni)->nonce, sizeof((*esni)->nonce));
-
-    /* calc record digest */
-    if ((ret = ptls_calc_hash((*esni)->client.cipher->hash, (*esni)->client.record_digest, esni_keys.base, esni_keys.len)) != 0)
-        goto Exit;
-    /* derive ECDH secret */
-    if ((ret = (*esni)->client.key_share->exchange((*esni)->client.key_share, &(*esni)->client.pubkey, &(*esni)->secret,
-                                                   peer_key)) != 0)
-        goto Exit;
-    /* calc H(ESNIContents) */
-    if ((ret = build_esni_contents_hash((*esni)->client.cipher->hash, (*esni)->esni_contents_hash, (*esni)->client.record_digest,
-                                        (*esni)->client.key_share->id, (*esni)->client.pubkey, client_random)) != 0)
-        goto Exit;
-
-    ret = 0;
-Exit:
-    if (ret != 0)
-        free_esni_secret(esni, 0);
-    return ret;
-}
-
-static int emit_esni_extension(ptls_esni_secret_t *esni, ptls_buffer_t *buf, ptls_iovec_t esni_keys, const char *server_name,
-                               size_t key_share_ch_off, size_t key_share_ch_len)
-{
-    ptls_aead_context_t *aead = NULL;
-    int ret;
-
-    if ((ret = create_esni_aead(&aead, 1, esni->client.cipher, esni->secret, esni->esni_contents_hash)) != 0)
-        goto Exit;
-
-    /* cipher-suite id */
-    ptls_buffer_push16(buf, esni->client.cipher->id);
-    /* key-share */
-    if ((ret = push_key_share_entry(buf, esni->client.key_share->id, esni->client.pubkey)) != 0)
-        goto Exit;
-    /* record-digest */
-    ptls_buffer_push_block(buf, 2, { ptls_buffer_pushv(buf, esni->client.record_digest, esni->client.cipher->hash->digest_size); });
-    /* encrypted sni */
-    ptls_buffer_push_block(buf, 2, {
-        size_t start_off = buf->off;
-        /* nonce */
-        ptls_buffer_pushv(buf, esni->nonce, PTLS_ESNI_NONCE_SIZE);
-        /* emit server-name extension */
-        if ((ret = emit_server_name_extension(buf, server_name)) != 0)
-            goto Exit;
-        /* pad */
-        if (buf->off - start_off < (size_t)(esni->client.padded_length + PTLS_ESNI_NONCE_SIZE)) {
-            size_t bytes_to_pad = esni->client.padded_length + PTLS_ESNI_NONCE_SIZE - (buf->off - start_off);
-            if ((ret = ptls_buffer_reserve(buf, bytes_to_pad)) != 0)
-                goto Exit;
-            memset(buf->base + buf->off, 0, bytes_to_pad);
-            buf->off += bytes_to_pad;
-        }
-        /* encrypt */
-        if ((ret = ptls_buffer_reserve_aligned(buf, aead->algo->tag_size, aead->algo->align_bits)) != 0)
-            goto Exit;
-        ptls_aead_encrypt(aead, buf->base + start_off, buf->base + start_off, buf->off - start_off, 0, buf->base + key_share_ch_off,
-                          key_share_ch_len);
-        buf->off += aead->algo->tag_size;
-    });
-
-    ret = 0;
-Exit:
-    if (aead != NULL)
-        ptls_aead_free(aead);
-    return ret;
-}
-
 static int send_client_hello(ptls_t *tls, ptls_message_emitter_t *emitter, ptls_handshake_properties_t *properties,
                              ptls_iovec_t *cookie)
 {
-    ptls_iovec_t resumption_secret = {NULL}, resumption_ticket;
-    char *published_sni = NULL;
+    ptls_iovec_t resumption_secret = {NULL}, resumption_ticket = {NULL};
     uint32_t obfuscated_ticket_age = 0;
-    size_t msghash_off;
+    const char *sni_name = NULL;
+    size_t mess_start, msghash_off;
     uint8_t binder_key[PTLS_MAX_DIGEST_SIZE];
-    int ret, is_second_flight = tls->key_schedule != NULL,
-             send_sni = tls->server_name != NULL && !ptls_server_name_is_ipaddr(tls->server_name);
+    ptls_buffer_t encoded_ch_inner;
+    int ret, is_second_flight = tls->key_schedule != NULL;
+
+    ptls_buffer_init(&encoded_ch_inner, "", 0);
+
+    if (tls->server_name != NULL && !ptls_server_name_is_ipaddr(tls->server_name))
+        sni_name = tls->server_name;
 
     if (properties != NULL) {
-        /* try to use ESNI */
-        if (!is_second_flight && send_sni && properties->client.esni_keys.base != NULL) {
-            if ((ret = client_setup_esni(tls->ctx, &tls->esni, properties->client.esni_keys, &published_sni, tls->client_random)) !=
-                0) {
-                goto Exit;
-            }
-            if (tls->ctx->update_esni_key != NULL) {
-                if ((ret = tls->ctx->update_esni_key->cb(tls->ctx->update_esni_key, tls, tls->esni->secret,
-                                                         tls->esni->client.cipher->hash, tls->esni->esni_contents_hash)) != 0)
+        /* try to use ECH (ignore broken ECHConfigList; it is delivered insecurely) */
+        if (!is_second_flight && sni_name != NULL && tls->ctx->ech.client.ciphers != NULL &&
+            properties->client.ech.configs.len != 0) {
+            struct st_decoded_ech_config_t decoded;
+            client_decode_ech_config_list(tls->ctx, &decoded, properties->client.ech.configs);
+            if (decoded.kem != NULL && decoded.cipher != NULL) {
+                if ((ret = client_setup_ech(&tls->ech, &decoded, tls->ctx->random_bytes)) != 0)
                     goto Exit;
             }
         }
@@ -2048,164 +2308,111 @@
     if (tls->key_share == NULL && !(properties != NULL && properties->client.negotiate_before_key_exchange))
         tls->key_share = tls->ctx->key_exchanges[0];
 
+    /* instantiate key share context */
+    assert(tls->client.key_share_ctx == NULL);
+    if (tls->key_share != NULL) {
+        if ((ret = tls->key_share->create(tls->key_share, &tls->client.key_share_ctx)) != 0)
+            goto Exit;
+    }
+
+    /* initialize key schedule */
     if (!is_second_flight) {
-        tls->key_schedule = key_schedule_new(tls->cipher_suite, tls->ctx->cipher_suites);
+        tls->key_schedule = key_schedule_new(tls->cipher_suite, tls->ctx->cipher_suites, tls->ech.aead != NULL);
         if ((ret = key_schedule_extract(tls->key_schedule, resumption_secret)) != 0)
             goto Exit;
     }
 
-    msghash_off = emitter->buf->off + emitter->record_header_length;
-    ptls_push_message(emitter, NULL, PTLS_HANDSHAKE_TYPE_CLIENT_HELLO, {
-        ptls_buffer_t *sendbuf = emitter->buf;
-        /* legacy_version */
-        ptls_buffer_push16(sendbuf, 0x0303);
-        /* random_bytes */
-        ptls_buffer_pushv(sendbuf, tls->client_random, sizeof(tls->client_random));
-        /* lecagy_session_id */
-        ptls_buffer_push_block(
-            sendbuf, 1, { ptls_buffer_pushv(sendbuf, tls->client.legacy_session_id.base, tls->client.legacy_session_id.len); });
-        /* cipher_suites */
-        ptls_buffer_push_block(sendbuf, 2, {
-            ptls_cipher_suite_t **cs = tls->ctx->cipher_suites;
-            for (; *cs != NULL; ++cs)
-                ptls_buffer_push16(sendbuf, (*cs)->id);
-        });
-        /* legacy_compression_methods */
-        ptls_buffer_push_block(sendbuf, 1, { ptls_buffer_push(sendbuf, 0); });
-        /* extensions */
-        ptls_buffer_push_block(sendbuf, 2, {
-            struct {
-                size_t off;
-                size_t len;
-            } key_share_client_hello;
-            buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_KEY_SHARE, {
-                key_share_client_hello.off = sendbuf->off;
-                ptls_buffer_push_block(sendbuf, 2, {
-                    if (tls->key_share != NULL) {
-                        if ((ret = tls->key_share->create(tls->key_share, &tls->client.key_share_ctx)) != 0)
-                            goto Exit;
-                        if ((ret = push_key_share_entry(sendbuf, tls->key_share->id, tls->client.key_share_ctx->pubkey)) != 0)
-                            goto Exit;
-                    }
-                });
-                key_share_client_hello.len = sendbuf->off - key_share_client_hello.off;
-            });
-            if (send_sni) {
-                if (tls->esni != NULL) {
-                    if (published_sni != NULL) {
-                        buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_SERVER_NAME, {
-                            if ((ret = emit_server_name_extension(sendbuf, published_sni)) != 0)
-                                goto Exit;
-                        });
-                    }
-                    buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_ENCRYPTED_SERVER_NAME, {
-                        if ((ret = emit_esni_extension(tls->esni, sendbuf, properties->client.esni_keys, tls->server_name,
-                                                       key_share_client_hello.off, key_share_client_hello.len)) != 0)
-                            goto Exit;
-                    });
-                } else {
-                    buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_SERVER_NAME, {
-                        if ((ret = emit_server_name_extension(sendbuf, tls->server_name)) != 0)
-                            goto Exit;
-                    });
-                }
-            }
-            if (properties != NULL && properties->client.negotiated_protocols.count != 0) {
-                buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_ALPN, {
-                    ptls_buffer_push_block(sendbuf, 2, {
-                        size_t i;
-                        for (i = 0; i != properties->client.negotiated_protocols.count; ++i) {
-                            ptls_buffer_push_block(sendbuf, 1, {
-                                ptls_iovec_t p = properties->client.negotiated_protocols.list[i];
-                                ptls_buffer_pushv(sendbuf, p.base, p.len);
-                            });
-                        }
-                    });
-                });
-            }
-            if (tls->ctx->decompress_certificate != NULL) {
-                buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_COMPRESS_CERTIFICATE, {
-                    ptls_buffer_push_block(sendbuf, 1, {
-                        const uint16_t *algo = tls->ctx->decompress_certificate->supported_algorithms;
-                        assert(*algo != UINT16_MAX);
-                        for (; *algo != UINT16_MAX; ++algo)
-                            ptls_buffer_push16(sendbuf, *algo);
-                    });
-                });
-            }
-            buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_SUPPORTED_VERSIONS, {
-                ptls_buffer_push_block(sendbuf, 1, {
-                    size_t i;
-                    for (i = 0; i != PTLS_ELEMENTSOF(supported_versions); ++i)
-                        ptls_buffer_push16(sendbuf, supported_versions[i]);
-                });
-            });
-            buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_SIGNATURE_ALGORITHMS, {
-                if ((ret = push_signature_algorithms(tls->ctx->verify_certificate, sendbuf)) != 0)
-                    goto Exit;
-            });
-            buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_SUPPORTED_GROUPS, {
-                ptls_key_exchange_algorithm_t **algo = tls->ctx->key_exchanges;
-                ptls_buffer_push_block(sendbuf, 2, {
-                    for (; *algo != NULL; ++algo)
-                        ptls_buffer_push16(sendbuf, (*algo)->id);
-                });
-            });
-            if (cookie != NULL && cookie->base != NULL) {
-                buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_COOKIE, {
-                    ptls_buffer_push_block(sendbuf, 2, { ptls_buffer_pushv(sendbuf, cookie->base, cookie->len); });
-                });
-            }
-            if (tls->ctx->use_raw_public_keys) {
-                buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_SERVER_CERTIFICATE_TYPE, {
-                    ptls_buffer_push_block(sendbuf, 1, { ptls_buffer_push(sendbuf, PTLS_CERTIFICATE_TYPE_RAW_PUBLIC_KEY); });
-                });
-            }
-            if ((ret = push_additional_extensions(properties, sendbuf)) != 0)
-                goto Exit;
-            if (tls->ctx->save_ticket != NULL || resumption_secret.base != NULL) {
-                buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_PSK_KEY_EXCHANGE_MODES, {
-                    ptls_buffer_push_block(sendbuf, 1, {
-                        if (!tls->ctx->require_dhe_on_psk)
-                            ptls_buffer_push(sendbuf, PTLS_PSK_KE_MODE_PSK);
-                        ptls_buffer_push(sendbuf, PTLS_PSK_KE_MODE_PSK_DHE);
-                    });
-                });
-            }
-            if (resumption_secret.base != NULL) {
-                if (tls->client.using_early_data && !is_second_flight)
-                    buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_EARLY_DATA, {});
-                /* pre-shared key "MUST be the last extension in the ClientHello" (draft-17 section 4.2.6) */
-                buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_PRE_SHARED_KEY, {
-                    ptls_buffer_push_block(sendbuf, 2, {
-                        ptls_buffer_push_block(sendbuf, 2,
-                                               { ptls_buffer_pushv(sendbuf, resumption_ticket.base, resumption_ticket.len); });
-                        ptls_buffer_push32(sendbuf, obfuscated_ticket_age);
-                    });
-                    /* allocate space for PSK binder. the space is filled at the bottom of the function */
-                    ptls_buffer_push_block(sendbuf, 2, {
-                        ptls_buffer_push_block(sendbuf, 1, {
-                            if ((ret = ptls_buffer_reserve(sendbuf, tls->key_schedule->hashes[0].algo->digest_size)) != 0)
-                                goto Exit;
-                            sendbuf->off += tls->key_schedule->hashes[0].algo->digest_size;
-                        });
-                    });
-                });
-            }
-        });
-    });
+    /* start generating CH */
+    if ((ret = emitter->begin_message(emitter)) != 0)
+        goto Exit;
+    mess_start = msghash_off = emitter->buf->off;
+
+    /* generate true (inner) CH */
+    if ((ret = encode_client_hello(tls->ctx, emitter->buf, ENCODE_CH_MODE_INNER, is_second_flight, properties,
+                                   tls->ech.aead != NULL ? tls->ech.inner_client_random : tls->client_random,
+                                   tls->client.key_share_ctx, sni_name, tls->client.legacy_session_id, &tls->ech, NULL,
+                                   tls->ech.client.first_ech, resumption_secret, resumption_ticket, obfuscated_ticket_age,
+                                   tls->key_schedule->hashes[0].algo->digest_size, cookie, tls->client.using_early_data)) != 0)
+        goto Exit;
 
     /* update the message hash, filling in the PSK binder HMAC if necessary */
     if (resumption_secret.base != NULL) {
         size_t psk_binder_off = emitter->buf->off - (3 + tls->key_schedule->hashes[0].algo->digest_size);
         if ((ret = derive_secret_with_empty_digest(tls->key_schedule, binder_key, "res binder")) != 0)
             goto Exit;
-        ptls__key_schedule_update_hash(tls->key_schedule, emitter->buf->base + msghash_off, psk_binder_off - msghash_off);
+        ptls__key_schedule_update_hash(tls->key_schedule, emitter->buf->base + msghash_off, psk_binder_off - msghash_off, 0);
         msghash_off = psk_binder_off;
         if ((ret = calc_verify_data(emitter->buf->base + psk_binder_off + 3, tls->key_schedule, binder_key)) != 0)
             goto Exit;
     }
-    ptls__key_schedule_update_hash(tls->key_schedule, emitter->buf->base + msghash_off, emitter->buf->off - msghash_off);
+    ptls__key_schedule_update_hash(tls->key_schedule, emitter->buf->base + msghash_off, emitter->buf->off - msghash_off, 0);
+
+    /* ECH */
+    if (tls->ech.aead != NULL) {
+        /* build EncodedCHInner */
+        if ((ret = encode_client_hello(tls->ctx, &encoded_ch_inner, ENCODE_CH_MODE_ENCODED_INNER, is_second_flight, properties,
+                                       tls->ech.inner_client_random, tls->client.key_share_ctx, sni_name,
+                                       tls->client.legacy_session_id, &tls->ech, NULL, ptls_iovec_init(NULL, 0), resumption_secret,
+                                       resumption_ticket, obfuscated_ticket_age, tls->key_schedule->hashes[0].algo->digest_size,
+                                       cookie, tls->client.using_early_data)) != 0)
+            goto Exit;
+        if (resumption_secret.base != NULL)
+            memcpy(encoded_ch_inner.base + encoded_ch_inner.off - tls->key_schedule->hashes[0].algo->digest_size,
+                   emitter->buf->base + emitter->buf->off - tls->key_schedule->hashes[0].algo->digest_size,
+                   tls->key_schedule->hashes[0].algo->digest_size);
+        { /* pad EncodedCHInner (following draft-ietf-tls-esni-15 6.1.3) */
+            size_t padding_len;
+            if (sni_name != NULL) {
+                padding_len = strlen(sni_name);
+                if (padding_len < tls->ech.client.max_name_length)
+                    padding_len = tls->ech.client.max_name_length;
+            } else {
+                padding_len = tls->ech.client.max_name_length + 9;
+            }
+            size_t final_len = encoded_ch_inner.off - PTLS_HANDSHAKE_HEADER_SIZE + padding_len;
+            final_len = (final_len + 31) / 32 * 32;
+            padding_len = final_len - (encoded_ch_inner.off - PTLS_HANDSHAKE_HEADER_SIZE);
+            if (padding_len != 0) {
+                if ((ret = ptls_buffer_reserve(&encoded_ch_inner, padding_len)) != 0)
+                    goto Exit;
+                memset(encoded_ch_inner.base + encoded_ch_inner.off, 0, padding_len);
+                encoded_ch_inner.off += padding_len;
+            }
+        }
+        /* flush CHInner, build CHOuterAAD */
+        emitter->buf->off = mess_start;
+        size_t ech_payload_size = encoded_ch_inner.off - PTLS_HANDSHAKE_HEADER_SIZE + tls->ech.aead->algo->tag_size,
+               ech_size_offset = ech_payload_size;
+        if ((ret = encode_client_hello(tls->ctx, emitter->buf, ENCODE_CH_MODE_OUTER, is_second_flight, properties,
+                                       tls->client_random, tls->client.key_share_ctx, tls->ech.client.public_name,
+                                       tls->client.legacy_session_id, &tls->ech, &ech_size_offset, ptls_iovec_init(NULL, 0),
+                                       resumption_secret, resumption_ticket, obfuscated_ticket_age,
+                                       tls->key_schedule->hashes[0].algo->digest_size, cookie, tls->client.using_early_data)) != 0)
+            goto Exit;
+        /* overwrite ECH payload */
+        ptls_aead_encrypt(tls->ech.aead, emitter->buf->base + ech_size_offset, encoded_ch_inner.base + PTLS_HANDSHAKE_HEADER_SIZE,
+                          encoded_ch_inner.off - PTLS_HANDSHAKE_HEADER_SIZE, is_second_flight,
+                          emitter->buf->base + mess_start + PTLS_HANDSHAKE_HEADER_SIZE,
+                          emitter->buf->off - (mess_start + PTLS_HANDSHAKE_HEADER_SIZE));
+        /* keep the copy of the 1st ECH extension so that we can send it again in 2nd CH in response to rejection with HRR */
+        if (!is_second_flight) {
+            size_t len = outer_ech_header_size(tls->ech.client.enc.len) + ech_payload_size;
+            if ((tls->ech.client.first_ech.base = malloc(len)) == NULL) {
+                ret = PTLS_ERROR_NO_MEMORY;
+                goto Exit;
+            }
+            memcpy(tls->ech.client.first_ech.base,
+                   emitter->buf->base + ech_size_offset - outer_ech_header_size(tls->ech.client.enc.len), len);
+            tls->ech.client.first_ech.len = len;
+            tls->ech.offered = 1;
+        }
+        /* update hash */
+        ptls__key_schedule_update_hash(tls->key_schedule, emitter->buf->base + mess_start, emitter->buf->off - mess_start, 1);
+    }
+
+    /* commit CH to the record layer */
+    if ((ret = emitter->commit_message(emitter)) != 0)
+        goto Exit;
 
     if (tls->client.using_early_data) {
         assert(!is_second_flight);
@@ -2222,9 +2429,7 @@
     ret = PTLS_ERROR_IN_PROGRESS;
 
 Exit:
-    if (published_sni != NULL) {
-        free(published_sni);
-    }
+    ptls_buffer_dispose(&encoded_ch_inner);
     ptls_clear_memory(binder_key, sizeof(binder_key));
     return ret;
 }
@@ -2339,6 +2544,19 @@
                               if ((ret = ptls_decode16(&selected_psk_identity, &src, end)) != 0)
                                   goto Exit;
                               break;
+                          case PTLS_EXTENSION_TYPE_ENCRYPTED_CLIENT_HELLO:
+                              assert(sh->is_retry_request);
+                              if (!tls->ech.offered) {
+                                  ret = PTLS_ALERT_UNSUPPORTED_EXTENSION;
+                                  goto Exit;
+                              }
+                              if (end - src != PTLS_ECH_CONFIRM_LENGTH) {
+                                  ret = PTLS_ALERT_DECODE_ERROR;
+                                  goto Exit;
+                              }
+                              sh->retry_request.ech = src;
+                              src = end;
+                              break;
                           default:
                               src = end;
                               break;
@@ -2410,14 +2628,41 @@
         goto Exit;
     }
 
-    key_schedule_transform_post_ch1hash(tls->key_schedule);
-    ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len);
     ret = send_client_hello(tls, emitter, properties, &sh->retry_request.cookie);
 
 Exit:
     return ret;
 }
 
+static int client_ech_select_hello(ptls_t *tls, ptls_iovec_t message, size_t confirm_hash_off, const char *label)
+{
+    uint8_t confirm_hash_delivered[PTLS_ECH_CONFIRM_LENGTH], confirm_hash_expected[PTLS_ECH_CONFIRM_LENGTH];
+    int ret = 0;
+
+    /* Determine if ECH has been accepted by checking the confirmation hash. `confirm_hash_off` set to zero indicates that HRR was
+     * received wo. ECH extension, which is an indication that ECH was rejected. */
+    if (confirm_hash_off != 0) {
+        memcpy(confirm_hash_delivered, message.base + confirm_hash_off, sizeof(confirm_hash_delivered));
+        memset(message.base + confirm_hash_off, 0, sizeof(confirm_hash_delivered));
+        if ((ret = ech_calc_confirmation(tls->key_schedule, confirm_hash_expected, tls->ech.inner_client_random, label, message)) !=
+            0)
+            goto Exit;
+        tls->ech.accepted = ptls_mem_equal(confirm_hash_delivered, confirm_hash_expected, sizeof(confirm_hash_delivered));
+        memcpy(message.base + confirm_hash_off, confirm_hash_delivered, sizeof(confirm_hash_delivered));
+        if (tls->ech.accepted)
+            goto Exit;
+    }
+
+    /* dispose ECH AEAD state to indicate rejection, adopting outer CH for the rest of the handshake */
+    ptls_aead_free(tls->ech.aead);
+    tls->ech.aead = NULL;
+    key_schedule_select_outer(tls->key_schedule);
+
+Exit:
+    ptls_clear_memory(confirm_hash_expected, sizeof(confirm_hash_expected));
+    return ret;
+}
+
 static int client_handle_hello(ptls_t *tls, ptls_message_emitter_t *emitter, ptls_iovec_t message,
                                ptls_handshake_properties_t *properties)
 {
@@ -2434,22 +2679,43 @@
     }
 
     if (sh.is_retry_request) {
-        if ((ret = key_schedule_select_one(tls->key_schedule, tls->cipher_suite, 0)) != 0)
+        if ((ret = key_schedule_select_cipher(tls->key_schedule, tls->cipher_suite, 0)) != 0)
             goto Exit;
+        key_schedule_transform_post_ch1hash(tls->key_schedule);
+        if (tls->ech.aead != NULL &&
+            (ret = client_ech_select_hello(tls, message, sh.retry_request.ech != NULL ? sh.retry_request.ech - message.base : 0,
+                                           ECH_CONFIRMATION_HRR)) != 0)
+            goto Exit;
+        ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len, 0);
         return handle_hello_retry_request(tls, emitter, &sh, message, properties);
     }
 
-    if ((ret = key_schedule_select_one(tls->key_schedule, tls->cipher_suite, tls->client.offered_psk && !tls->is_psk_handshake)) !=
-        0)
+    if ((ret = key_schedule_select_cipher(tls->key_schedule, tls->cipher_suite,
+                                          tls->client.offered_psk && !tls->is_psk_handshake)) != 0)
         goto Exit;
 
+    /* check if ECH is accepted */
+    static const size_t confirm_hash_off =
+        PTLS_HANDSHAKE_HEADER_SIZE + 2 /* legacy_version */ + PTLS_HELLO_RANDOM_SIZE - PTLS_ECH_CONFIRM_LENGTH;
+    if (tls->ech.aead != NULL) {
+        if ((ret = client_ech_select_hello(tls, message, confirm_hash_off, ECH_CONFIRMATION_SERVER_HELLO)) != 0)
+            goto Exit;
+    }
+
+    /* clear sensitive and space-consuming ECH state, now that are done with handling sending and decoding Hellos */
+    clear_ech(&tls->ech, 0);
+    if (tls->key_schedule->hashes[0].ctx_outer != NULL) {
+        tls->key_schedule->hashes[0].ctx_outer->final(tls->key_schedule->hashes[0].ctx_outer, NULL, PTLS_HASH_FINAL_MODE_FREE);
+        tls->key_schedule->hashes[0].ctx_outer = NULL;
+    }
+
+    ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len, 0);
+
     if (sh.peerkey.base != NULL) {
         if ((ret = tls->client.key_share_ctx->on_exchange(&tls->client.key_share_ctx, 1, &ecdh_secret, sh.peerkey)) != 0)
             goto Exit;
     }
 
-    ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len);
-
     if ((ret = key_schedule_extract(tls->key_schedule, ecdh_secret)) != 0)
         goto Exit;
     if ((ret = setup_traffic_protection(tls, 0, "s hs traffic", 2, 0)) != 0)
@@ -2514,7 +2780,7 @@
 
 static int client_handle_encrypted_extensions(ptls_t *tls, ptls_iovec_t message, ptls_handshake_properties_t *properties)
 {
-    const uint8_t *src = message.base + PTLS_HANDSHAKE_HEADER_SIZE, *const end = message.base + message.len, *esni_nonce = NULL;
+    const uint8_t *src = message.base + PTLS_HANDSHAKE_HEADER_SIZE, *const end = message.base + message.len;
     uint16_t type;
     static const ptls_raw_extension_t no_unknown_extensions = {UINT16_MAX};
     ptls_raw_extension_t *unknown_extensions = (ptls_raw_extension_t *)&no_unknown_extensions;
@@ -2537,19 +2803,6 @@
                 goto Exit;
             }
             break;
-        case PTLS_EXTENSION_TYPE_ENCRYPTED_SERVER_NAME:
-            if (*src == PTLS_ESNI_RESPONSE_TYPE_ACCEPT) {
-                if (end - src != PTLS_ESNI_NONCE_SIZE + 1) {
-                    ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-                    goto Exit;
-                }
-                esni_nonce = src + 1;
-            } else {
-                /* TODO: provide API to parse the RETRY REQUEST response */
-                ret = PTLS_ERROR_ESNI_RETRY;
-                goto Exit;
-            }
-            break;
         case PTLS_EXTENSION_TYPE_ALPN:
             ptls_decode_block(src, end, 2, {
                 ptls_decode_open_block(src, end, 1, {
@@ -2582,6 +2835,27 @@
             server_offered_cert_type = *src;
             src = end;
             break;
+        case PTLS_EXTENSION_TYPE_ENCRYPTED_CLIENT_HELLO: {
+            /* accept retry_configs only if we offered ECH but rejected */
+            if (!(tls->ech.offered && !ptls_is_ech_handshake(tls, NULL, NULL))) {
+                ret = PTLS_ALERT_UNSUPPORTED_EXTENSION;
+                goto Exit;
+            }
+            /* parse retry_config, and if it is applicable, provide that to the application */
+            struct st_decoded_ech_config_t decoded;
+            if ((ret = client_decode_ech_config_list(tls->ctx, &decoded, ptls_iovec_init(src, end - src))) != 0)
+                goto Exit;
+            if (decoded.kem != NULL && decoded.cipher != NULL && properties != NULL &&
+                properties->client.ech.retry_configs != NULL) {
+                if ((properties->client.ech.retry_configs->base = malloc(end - src)) == NULL) {
+                    ret = PTLS_ERROR_NO_MEMORY;
+                    goto Exit;
+                }
+                memcpy(properties->client.ech.retry_configs->base, src, end - src);
+                properties->client.ech.retry_configs->len = end - src;
+            }
+            src = end;
+        } break;
         default:
             if (should_collect_unknown_extension(tls, properties, type)) {
                 if (unknown_extensions == &no_unknown_extensions) {
@@ -2605,19 +2879,6 @@
         goto Exit;
     }
 
-    if (tls->esni != NULL) {
-        if (esni_nonce == NULL || !ptls_mem_equal(esni_nonce, tls->esni->nonce, PTLS_ESNI_NONCE_SIZE)) {
-            ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-            goto Exit;
-        }
-        free_esni_secret(&tls->esni, 0);
-    } else {
-        if (esni_nonce != NULL) {
-            ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-            goto Exit;
-        }
-    }
-
     if (tls->client.using_early_data) {
         if (skip_early_data)
             tls->client.using_early_data = 0;
@@ -2627,7 +2888,7 @@
     if ((ret = report_unknown_extensions(tls, properties, unknown_extensions)) != 0)
         goto Exit;
 
-    ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len);
+    ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len, 0);
     tls->state =
         tls->is_psk_handshake ? PTLS_STATE_CLIENT_EXPECT_FINISHED : PTLS_STATE_CLIENT_EXPECT_CERTIFICATE_REQUEST_OR_CERTIFICATE;
     ret = PTLS_ERROR_IN_PROGRESS;
@@ -2805,6 +3066,8 @@
     const uint8_t *src = message.base + PTLS_HANDSHAKE_HEADER_SIZE, *const end = message.base + message.len;
     int ret = 0;
 
+    assert(!tls->is_psk_handshake && "state machine asserts that this message is never delivered when PSK is used");
+
     if ((ret = decode_certificate_request(tls, &tls->client.certificate_request, src, end)) != 0)
         return ret;
 
@@ -2813,7 +3076,7 @@
         return PTLS_ALERT_ILLEGAL_PARAMETER;
 
     tls->state = PTLS_STATE_CLIENT_EXPECT_CERTIFICATE;
-    ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len);
+    ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len, 0);
 
     return PTLS_ERROR_IN_PROGRESS;
 }
@@ -2851,7 +3114,15 @@
     });
 
     if (tls->ctx->verify_certificate != NULL) {
-        if ((ret = tls->ctx->verify_certificate->cb(tls->ctx->verify_certificate, tls, &tls->certificate_verify.cb,
+        const char *server_name = NULL;
+        if (!ptls_is_server(tls)) {
+            if (tls->ech.offered && !ptls_is_ech_handshake(tls, NULL, NULL)) {
+                server_name = tls->ech.client.public_name;
+            } else {
+                server_name = tls->server_name;
+            }
+        }
+        if ((ret = tls->ctx->verify_certificate->cb(tls->ctx->verify_certificate, tls, server_name, &tls->certificate_verify.cb,
                                                     &tls->certificate_verify.verify_ctx, certs, num_certs)) != 0)
             goto Exit;
     }
@@ -2881,7 +3152,7 @@
     if ((ret = client_do_handle_certificate(tls, message.base + PTLS_HANDSHAKE_HEADER_SIZE, message.base + message.len)) != 0)
         return ret;
 
-    ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len);
+    ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len, 0);
 
     tls->state = PTLS_STATE_CLIENT_EXPECT_CERTIFICATE_VERIFY;
     return PTLS_ERROR_IN_PROGRESS;
@@ -2925,7 +3196,7 @@
     if ((ret = client_do_handle_certificate(tls, uncompressed, uncompressed + uncompressed_size)) != 0)
         goto Exit;
 
-    ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len);
+    ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len, 0);
     tls->state = PTLS_STATE_CLIENT_EXPECT_CERTIFICATE_VERIFY;
     ret = PTLS_ERROR_IN_PROGRESS;
 
@@ -2941,7 +3212,7 @@
     if ((ret = handle_certificate(tls, message.base + PTLS_HANDSHAKE_HEADER_SIZE, message.base + message.len, &got_certs)) != 0)
         return ret;
 
-    ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len);
+    ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len, 0);
 
     if (got_certs) {
         tls->state = PTLS_STATE_SERVER_EXPECT_CERTIFICATE_VERIFY;
@@ -2983,7 +3254,7 @@
         goto Exit;
     }
 
-    ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len);
+    ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len, 0);
 
 Exit:
     return ret;
@@ -3016,11 +3287,11 @@
 static int client_handle_finished(ptls_t *tls, ptls_message_emitter_t *emitter, ptls_iovec_t message)
 {
     uint8_t send_secret[PTLS_MAX_DIGEST_SIZE];
-    int ret;
+    int alert_ech_required = tls->ech.offered && !ptls_is_ech_handshake(tls, NULL, NULL), ret;
 
     if ((ret = verify_finished(tls, message)) != 0)
         goto Exit;
-    ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len);
+    ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len, 0);
 
     /* update traffic keys by using messages upto ServerFinished, but commission them after sending ClientFinished */
     if ((ret = key_schedule_extract(tls->key_schedule, ptls_iovec_init(NULL, 0))) != 0)
@@ -3045,12 +3316,7 @@
     if ((ret = push_change_cipher_spec(tls, emitter)) != 0)
         goto Exit;
 
-    if (tls->client.certificate_request.context.base != NULL) {
-        /* If this is a resumed session, the server must not send the certificate request in the handshake */
-        if (tls->is_psk_handshake) {
-            ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-            goto Exit;
-        }
+    if (!alert_ech_required && tls->client.certificate_request.context.base != NULL) {
         if ((ret = send_certificate(tls, emitter, &tls->client.certificate_request.signature_algorithms,
                                     tls->client.certificate_request.context, 0, NULL, 0)) == 0)
             ret = send_certificate_verify(tls, emitter, &tls->client.certificate_request.signature_algorithms,
@@ -3069,6 +3335,10 @@
 
     tls->state = PTLS_STATE_CLIENT_POST_HANDSHAKE;
 
+    /* if ECH was rejected, close the connection with ECH_REQUIRED alert after verifying messages up to Finished */
+    if (alert_ech_required)
+        ret = PTLS_ALERT_ECH_REQUIRED;
+
 Exit:
     ptls_clear_memory(send_secret, sizeof(send_secret));
     return ret;
@@ -3146,110 +3416,6 @@
     return ret;
 }
 
-static int client_hello_decrypt_esni(ptls_context_t *ctx, ptls_iovec_t *server_name, ptls_esni_secret_t **secret,
-                                     struct st_ptls_client_hello_t *ch)
-{
-    ptls_esni_context_t **esni;
-    ptls_key_exchange_context_t **key_share_ctx;
-    uint8_t *decrypted = NULL;
-    ptls_aead_context_t *aead = NULL;
-    int ret;
-
-    /* allocate secret */
-    assert(*secret == NULL);
-    if ((*secret = malloc(sizeof(**secret))) == NULL)
-        return PTLS_ERROR_NO_MEMORY;
-    memset(*secret, 0, sizeof(**secret));
-
-    /* find the matching esni structure */
-    for (esni = ctx->esni; *esni != NULL; ++esni) {
-        size_t i;
-        for (i = 0; (*esni)->cipher_suites[i].cipher_suite != NULL; ++i)
-            if ((*esni)->cipher_suites[i].cipher_suite->id == ch->esni.cipher->id)
-                break;
-        if ((*esni)->cipher_suites[i].cipher_suite == NULL) {
-            ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-            goto Exit;
-        }
-        if (memcmp((*esni)->cipher_suites[i].record_digest, ch->esni.record_digest, ch->esni.cipher->hash->digest_size) == 0) {
-            (*secret)->version = (*esni)->version;
-            break;
-        }
-    }
-    if (*esni == NULL) {
-        ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-        goto Exit;
-    }
-
-    /* find the matching private key for ESNI decryption */
-    for (key_share_ctx = (*esni)->key_exchanges; *key_share_ctx != NULL; ++key_share_ctx)
-        if ((*key_share_ctx)->algo->id == ch->esni.key_share->id)
-            break;
-    if (*key_share_ctx == NULL) {
-        ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-        goto Exit;
-    }
-
-    /* calculate ESNIContents */
-    if ((ret = build_esni_contents_hash(ch->esni.cipher->hash, (*secret)->esni_contents_hash, ch->esni.record_digest,
-                                        ch->esni.key_share->id, ch->esni.peer_key, ch->random_bytes)) != 0)
-        goto Exit;
-    /* derive the shared secret */
-    if ((ret = (*key_share_ctx)->on_exchange(key_share_ctx, 0, &(*secret)->secret, ch->esni.peer_key)) != 0)
-        goto Exit;
-    /* decrypt */
-    if (ch->esni.encrypted_sni.len - ch->esni.cipher->aead->tag_size != (*esni)->padded_length + PTLS_ESNI_NONCE_SIZE) {
-        ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-        goto Exit;
-    }
-    if ((decrypted = malloc((*esni)->padded_length + PTLS_ESNI_NONCE_SIZE)) == NULL) {
-        ret = PTLS_ERROR_NO_MEMORY;
-        goto Exit;
-    }
-    if ((ret = create_esni_aead(&aead, 0, ch->esni.cipher, (*secret)->secret, (*secret)->esni_contents_hash)) != 0)
-        goto Exit;
-    if (ptls_aead_decrypt(aead, decrypted, ch->esni.encrypted_sni.base, ch->esni.encrypted_sni.len, 0, ch->key_shares.base,
-                          ch->key_shares.len) != (*esni)->padded_length + PTLS_ESNI_NONCE_SIZE) {
-        ret = PTLS_ALERT_DECRYPT_ERROR;
-        goto Exit;
-    }
-    ptls_aead_free(aead);
-    aead = NULL;
-
-    { /* decode sni */
-        const uint8_t *src = decrypted, *const end = src + (*esni)->padded_length;
-        ptls_iovec_t found_name;
-        if (end - src < PTLS_ESNI_NONCE_SIZE) {
-            ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-            goto Exit;
-        }
-        memcpy((*secret)->nonce, src, PTLS_ESNI_NONCE_SIZE);
-        src += PTLS_ESNI_NONCE_SIZE;
-        if ((ret = client_hello_decode_server_name(&found_name, &src, end)) != 0)
-            goto Exit;
-        for (; src != end; ++src) {
-            if (*src != '\0') {
-                ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-                goto Exit;
-            }
-        }
-        /* if successful, reuse memory allocated for padded_server_name for storing the found name (freed by the caller) */
-        memmove(decrypted, found_name.base, found_name.len);
-        *server_name = ptls_iovec_init(decrypted, found_name.len);
-        decrypted = NULL;
-    }
-
-    ret = 0;
-Exit:
-    if (decrypted != NULL)
-        free(decrypted);
-    if (aead != NULL)
-        ptls_aead_free(aead);
-    if (ret != 0 && *secret != NULL)
-        free_esni_secret(secret, 1);
-    return ret;
-}
-
 static int select_negotiated_group(ptls_key_exchange_algorithm_t **selected, ptls_key_exchange_algorithm_t **candidates,
                                    const uint8_t *src, const uint8_t *const end)
 {
@@ -3276,9 +3442,10 @@
     return ret;
 }
 
-static int decode_client_hello(ptls_t *tls, struct st_ptls_client_hello_t *ch, const uint8_t *src, const uint8_t *const end,
-                               ptls_handshake_properties_t *properties)
+static int decode_client_hello(ptls_context_t *ctx, struct st_ptls_client_hello_t *ch, const uint8_t *src, const uint8_t *const end,
+                               ptls_handshake_properties_t *properties, ptls_t *tls_cbarg)
 {
+    const uint8_t *start = src;
     uint16_t exttype = 0;
     int ret;
 
@@ -3335,12 +3502,14 @@
         src = end;
     });
 
+    ch->first_extension_at = src - start + 2;
+
     /* decode extensions */
     decode_extensions(src, end, PTLS_HANDSHAKE_TYPE_CLIENT_HELLO, &exttype, {
         ch->psk.is_last_extension = 0;
-        if (tls->ctx->on_extension != NULL &&
-            (ret = tls->ctx->on_extension->cb(tls->ctx->on_extension, tls, PTLS_HANDSHAKE_TYPE_CLIENT_HELLO, exttype,
-                                              ptls_iovec_init(src, end - src)) != 0))
+        if (ctx->on_extension != NULL && tls_cbarg != NULL &&
+            (ret = ctx->on_extension->cb(ctx->on_extension, tls_cbarg, PTLS_HANDSHAKE_TYPE_CLIENT_HELLO, exttype,
+                                         ptls_iovec_init(src, end - src)) != 0))
             goto Exit;
         switch (exttype) {
         case PTLS_EXTENSION_TYPE_SERVER_NAME:
@@ -3351,47 +3520,6 @@
                 goto Exit;
             }
             break;
-        case PTLS_EXTENSION_TYPE_ENCRYPTED_SERVER_NAME: {
-            ptls_cipher_suite_t **cipher;
-            if (ch->esni.cipher != NULL) {
-                ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-                goto Exit;
-            }
-            { /* cipher-suite */
-                uint16_t csid;
-                if ((ret = ptls_decode16(&csid, &src, end)) != 0)
-                    goto Exit;
-                for (cipher = tls->ctx->cipher_suites; *cipher != NULL; ++cipher)
-                    if ((*cipher)->id == csid)
-                        break;
-                if (*cipher == NULL) {
-                    ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-                    goto Exit;
-                }
-            }
-            /* key-share (including peer-key) */
-            if ((ret = select_key_share(&ch->esni.key_share, &ch->esni.peer_key, tls->ctx->key_exchanges, &src, end, 1)) != 0)
-                goto Exit;
-            ptls_decode_open_block(src, end, 2, {
-                size_t len = end - src;
-                if (len != (*cipher)->hash->digest_size) {
-                    ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-                    goto Exit;
-                }
-                ch->esni.record_digest = src;
-                src += len;
-            });
-            ptls_decode_block(src, end, 2, {
-                size_t len = end - src;
-                if (len < (*cipher)->aead->tag_size) {
-                    ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-                    goto Exit;
-                }
-                ch->esni.encrypted_sni = ptls_iovec_init(src, len);
-                src += len;
-            });
-            ch->esni.cipher = *cipher; /* set only after successful parsing */
-        } break;
         case PTLS_EXTENSION_TYPE_ALPN:
             ptls_decode_block(src, end, 2, {
                 do {
@@ -3551,9 +3679,45 @@
         case PTLS_EXTENSION_TYPE_STATUS_REQUEST:
             ch->status_request = 1;
             break;
+        case PTLS_EXTENSION_TYPE_ENCRYPTED_CLIENT_HELLO:
+            if ((ret = ptls_decode8(&ch->ech.type, &src, end)) != 0)
+                goto Exit;
+            switch (ch->ech.type) {
+            case PTLS_ECH_CLIENT_HELLO_TYPE_OUTER:
+                if ((ret = ptls_decode16(&ch->ech.cipher_suite.kdf, &src, end)) != 0 ||
+                    (ret = ptls_decode16(&ch->ech.cipher_suite.aead, &src, end)) != 0)
+                    goto Exit;
+                if ((ret = ptls_decode8(&ch->ech.config_id, &src, end)) != 0)
+                    goto Exit;
+                ptls_decode_open_block(src, end, 2, {
+                    ch->ech.enc = ptls_iovec_init(src, end - src);
+                    src = end;
+                });
+                ptls_decode_open_block(src, end, 2, {
+                    if (src == end) {
+                        ret = PTLS_ALERT_DECODE_ERROR;
+                        goto Exit;
+                    }
+                    ch->ech.payload = ptls_iovec_init(src, end - src);
+                    src = end;
+                });
+                break;
+            case PTLS_ECH_CLIENT_HELLO_TYPE_INNER:
+                if (src != end) {
+                    ret = PTLS_ALERT_DECODE_ERROR;
+                    goto Exit;
+                }
+                ch->ech.payload = ptls_iovec_init("", 0); /* non-zero base indicates that the extension was received */
+                break;
+            default:
+                ret = PTLS_ALERT_ILLEGAL_PARAMETER;
+                goto Exit;
+            }
+            src = end;
+            break;
         default:
-            if (should_collect_unknown_extension(tls, properties, exttype)) {
-                if ((ret = collect_unknown_extension(tls, exttype, src, end, ch->unknown_extensions)) != 0)
+            if (tls_cbarg != NULL && should_collect_unknown_extension(tls_cbarg, properties, exttype)) {
+                if ((ret = collect_unknown_extension(tls_cbarg, exttype, src, end, ch->unknown_extensions)) != 0)
                     goto Exit;
             }
             break;
@@ -3566,6 +3730,198 @@
     return ret;
 }
 
+static int rebuild_ch_inner_extensions(ptls_buffer_t *buf, const uint8_t **src, const uint8_t *const end, const uint8_t *outer_ext,
+                                       const uint8_t *outer_ext_end)
+{
+    int ret;
+
+    ptls_buffer_push_block(buf, 2, {
+        ptls_decode_open_block(*src, end, 2, {
+            while (*src != end) {
+                uint16_t exttype;
+                if ((ret = ptls_decode16(&exttype, src, end)) != 0)
+                    goto Exit;
+                ptls_decode_open_block(*src, end, 2, {
+                    if (exttype == PTLS_EXTENSION_TYPE_ECH_OUTER_EXTENSIONS) {
+                        ptls_decode_open_block(*src, end, 1, {
+                            do {
+                                uint16_t reftype;
+                                uint16_t outertype;
+                                uint16_t outersize;
+                                if ((ret = ptls_decode16(&reftype, src, end)) != 0)
+                                    goto Exit;
+                                if (reftype == PTLS_EXTENSION_TYPE_ENCRYPTED_CLIENT_HELLO) {
+                                    ret = PTLS_ALERT_ILLEGAL_PARAMETER;
+                                    goto Exit;
+                                }
+                                while (1) {
+                                    if (ptls_decode16(&outertype, &outer_ext, outer_ext_end) != 0 ||
+                                        ptls_decode16(&outersize, &outer_ext, outer_ext_end) != 0) {
+                                        ret = PTLS_ALERT_ILLEGAL_PARAMETER;
+                                        goto Exit;
+                                    }
+                                    assert(outer_ext_end - outer_ext >= outersize);
+                                    if (outertype == reftype)
+                                        break;
+                                    outer_ext += outersize;
+                                }
+                                buffer_push_extension(buf, reftype, {
+                                    ptls_buffer_pushv(buf, outer_ext, outersize);
+                                    outer_ext += outersize;
+                                });
+                            } while (*src != end);
+                        });
+                    } else {
+                        buffer_push_extension(buf, exttype, {
+                            ptls_buffer_pushv(buf, *src, end - *src);
+                            *src = end;
+                        });
+                    }
+                });
+            }
+        });
+    });
+
+Exit:
+    return ret;
+}
+
+static int rebuild_ch_inner(ptls_buffer_t *buf, const uint8_t *src, const uint8_t *const end,
+                            struct st_ptls_client_hello_t *outer_ch, const uint8_t *outer_ext, const uint8_t *outer_ext_end)
+{
+#define COPY_BLOCK(capacity)                                                                                                       \
+    do {                                                                                                                           \
+        ptls_decode_open_block(src, end, (capacity), {                                                                             \
+            ptls_buffer_push_block(buf, (capacity), { ptls_buffer_pushv(buf, src, end - src); });                                  \
+            src = end;                                                                                                             \
+        });                                                                                                                        \
+    } while (0)
+
+    int ret;
+
+    ptls_buffer_push_message_body(buf, NULL, PTLS_HANDSHAKE_TYPE_CLIENT_HELLO, {
+        { /* legacy_version */
+            uint16_t legacy_version;
+            if ((ret = ptls_decode16(&legacy_version, &src, end)) != 0)
+                goto Exit;
+            ptls_buffer_push16(buf, legacy_version);
+        }
+
+        /* hello random */
+        if (end - src < PTLS_HELLO_RANDOM_SIZE) {
+            ret = PTLS_ALERT_DECODE_ERROR;
+            goto Exit;
+        }
+        ptls_buffer_pushv(buf, src, PTLS_HELLO_RANDOM_SIZE);
+        src += PTLS_HELLO_RANDOM_SIZE;
+
+        ptls_decode_open_block(src, end, 1, {
+            if (src != end) {
+                ret = PTLS_ALERT_ILLEGAL_PARAMETER;
+                goto Exit;
+            }
+        });
+        ptls_buffer_push_block(buf, 1,
+                               { ptls_buffer_pushv(buf, outer_ch->legacy_session_id.base, outer_ch->legacy_session_id.len); });
+
+        /* cipher-suites and legacy-compression-methods */
+        COPY_BLOCK(2);
+        COPY_BLOCK(1);
+
+        /* extensions */
+        if ((ret = rebuild_ch_inner_extensions(buf, &src, end, outer_ext, outer_ext_end)) != 0)
+            goto Exit;
+    });
+
+    /* padding must be all zero */
+    for (; src != end; ++src) {
+        if (*src != '\0') {
+            ret = PTLS_ALERT_ILLEGAL_PARAMETER;
+            goto Exit;
+        }
+    }
+
+Exit:
+    return ret;
+
+#undef COPY_BLOCK
+}
+
+/* Wrapper function for invoking the on_client_hello callback, taking an exhaustive list of parameters as arguments. The intention
+ * is to not miss setting them as we add new parameters to the struct. */
+static inline int call_on_client_hello_cb(ptls_t *tls, ptls_iovec_t server_name, ptls_iovec_t raw_message, ptls_iovec_t *alpns,
+                                          size_t num_alpns, const uint16_t *sig_algos, size_t num_sig_algos,
+                                          const uint16_t *cert_comp_algos, size_t num_cert_comp_algos,
+                                          const uint16_t *cipher_suites, size_t num_cipher_suites, const uint8_t *server_cert_types,
+                                          size_t num_server_cert_types, int incompatible_version)
+{
+    if (tls->ctx->on_client_hello == NULL)
+        return 0;
+
+    ptls_on_client_hello_parameters_t params = {server_name,
+                                                raw_message,
+                                                {alpns, num_alpns},
+                                                {sig_algos, num_sig_algos},
+                                                {cert_comp_algos, num_cert_comp_algos},
+                                                {cipher_suites, num_cipher_suites},
+                                                {server_cert_types, num_server_cert_types},
+                                                incompatible_version};
+    return tls->ctx->on_client_hello->cb(tls->ctx->on_client_hello, tls, &params);
+}
+
+static int check_client_hello_constraints(ptls_context_t *ctx, struct st_ptls_client_hello_t *ch, const void *prev_random,
+                                          int ech_is_inner_ch, ptls_iovec_t raw_message, ptls_t *tls_cbarg)
+{
+    int is_second_flight = prev_random != 0;
+
+    /* The following check is necessary so that we would be able to track the connection in SSLKEYLOGFILE, even though it might not
+     * be for the safety of the protocol. */
+    if (is_second_flight && !ptls_mem_equal(ch->random_bytes, prev_random, PTLS_HELLO_RANDOM_SIZE))
+        return PTLS_ALERT_HANDSHAKE_FAILURE;
+
+    /* bail out if CH cannot be handled as TLS 1.3 */
+    if (!is_supported_version(ch->selected_version)) {
+        /* ECH: server MUST abort with an "illegal_parameter" alert if the client offers TLS 1.2 or below (draft-15 7.1) */
+        if (ech_is_inner_ch)
+            return PTLS_ALERT_ILLEGAL_PARAMETER;
+        /* fail with PROTOCOL_VERSION alert, after providing the applications the raw CH and SNI to help them fallback */
+        if (!is_second_flight) {
+            int ret;
+            if ((ret = call_on_client_hello_cb(tls_cbarg, ch->server_name, raw_message, ch->alpn.list, ch->alpn.count, NULL, 0,
+                                               NULL, 0, NULL, 0, NULL, 0, 1)) != 0)
+                return ret;
+        }
+        return PTLS_ALERT_PROTOCOL_VERSION;
+    }
+
+    /* Check TLS 1.3-specific constraints. Hereafter, we might exit without calling on_client_hello. That's fine because this CH is
+     * ought to be rejected. */
+    if (ch->legacy_version <= 0x0300) {
+        /* RFC 8446 Appendix D.5: any endpoint receiving a Hello message with legacy_version set to 0x0300 MUST abort the handshake
+         * with a "protocol_version" alert. */
+        return PTLS_ALERT_PROTOCOL_VERSION;
+    }
+    if (!(ch->compression_methods.count == 1 && ch->compression_methods.ids[0] == 0))
+        return PTLS_ALERT_ILLEGAL_PARAMETER;
+    /* pre-shared key */
+    if (ch->psk.hash_end != NULL) {
+        /* PSK must be the last extension */
+        if (!ch->psk.is_last_extension)
+            return PTLS_ALERT_ILLEGAL_PARAMETER;
+    } else {
+        if (ch->psk.early_data_indication)
+            return PTLS_ALERT_ILLEGAL_PARAMETER;
+    }
+
+    if (ech_is_inner_ch && ch->ech.payload.base == NULL)
+        return PTLS_ALERT_ILLEGAL_PARAMETER;
+    if (ch->ech.payload.base != NULL &&
+        ch->ech.type != (ech_is_inner_ch ? PTLS_ECH_CLIENT_HELLO_TYPE_INNER : PTLS_ECH_CLIENT_HELLO_TYPE_OUTER))
+        return PTLS_ALERT_ILLEGAL_PARAMETER;
+
+    return 0;
+}
+
 static int vec_is_string(ptls_iovec_t x, const char *y)
 {
     return strncmp((const char *)x.base, y, x.len) == 0 && y[x.len] == '\0';
@@ -3665,7 +4021,7 @@
         goto Exit;
     if ((ret = derive_secret(tls->key_schedule, binder_key, "res binder")) != 0)
         goto Exit;
-    ptls__key_schedule_update_hash(tls->key_schedule, ch_trunc.base, ch_trunc.len);
+    ptls__key_schedule_update_hash(tls->key_schedule, ch_trunc.base, ch_trunc.len, 0);
     if ((ret = calc_verify_data(binder_key /* to conserve space, reuse binder_key for storing verify_data */, tls->key_schedule,
                                 binder_key)) != 0)
         goto Exit;
@@ -3736,29 +4092,37 @@
 static int server_handle_hello(ptls_t *tls, ptls_message_emitter_t *emitter, ptls_iovec_t message,
                                ptls_handshake_properties_t *properties)
 {
-#define EMIT_SERVER_HELLO(sched, fill_rand, extensions)                                                                            \
-    ptls_push_message(emitter, (sched), PTLS_HANDSHAKE_TYPE_SERVER_HELLO, {                                                        \
-        ptls_buffer_push16(emitter->buf, 0x0303 /* legacy version */);                                                             \
-        if ((ret = ptls_buffer_reserve(emitter->buf, PTLS_HELLO_RANDOM_SIZE)) != 0)                                                \
-            goto Exit;                                                                                                             \
-        do {                                                                                                                       \
-            fill_rand                                                                                                              \
-        } while (0);                                                                                                               \
-        emitter->buf->off += PTLS_HELLO_RANDOM_SIZE;                                                                               \
-        ptls_buffer_push_block(emitter->buf, 1,                                                                                    \
-                               { ptls_buffer_pushv(emitter->buf, ch->legacy_session_id.base, ch->legacy_session_id.len); });       \
-        ptls_buffer_push16(emitter->buf, tls->cipher_suite->id);                                                                   \
-        ptls_buffer_push(emitter->buf, 0);                                                                                         \
-        ptls_buffer_push_block(emitter->buf, 2, {                                                                                  \
-            buffer_push_extension(emitter->buf, PTLS_EXTENSION_TYPE_SUPPORTED_VERSIONS,                                            \
-                                  { ptls_buffer_push16(emitter->buf, ch->selected_version); });                                    \
+#define EMIT_SERVER_HELLO(sched, fill_rand, extensions, post_action)                                                               \
+    do {                                                                                                                           \
+        size_t sh_start_off;                                                                                                       \
+        ptls_push_message(emitter, NULL, PTLS_HANDSHAKE_TYPE_SERVER_HELLO, {                                                       \
+            sh_start_off = emitter->buf->off - PTLS_HANDSHAKE_HEADER_SIZE;                                                         \
+            ptls_buffer_push16(emitter->buf, 0x0303 /* legacy version */);                                                         \
+            if ((ret = ptls_buffer_reserve(emitter->buf, PTLS_HELLO_RANDOM_SIZE)) != 0)                                            \
+                goto Exit;                                                                                                         \
             do {                                                                                                                   \
-                extensions                                                                                                         \
+                fill_rand                                                                                                          \
             } while (0);                                                                                                           \
+            emitter->buf->off += PTLS_HELLO_RANDOM_SIZE;                                                                           \
+            ptls_buffer_push_block(emitter->buf, 1,                                                                                \
+                                   { ptls_buffer_pushv(emitter->buf, ch->legacy_session_id.base, ch->legacy_session_id.len); });   \
+            ptls_buffer_push16(emitter->buf, tls->cipher_suite->id);                                                               \
+            ptls_buffer_push(emitter->buf, 0);                                                                                     \
+            ptls_buffer_push_block(emitter->buf, 2, {                                                                              \
+                buffer_push_extension(emitter->buf, PTLS_EXTENSION_TYPE_SUPPORTED_VERSIONS,                                        \
+                                      { ptls_buffer_push16(emitter->buf, ch->selected_version); });                                \
+                do {                                                                                                               \
+                    extensions                                                                                                     \
+                } while (0);                                                                                                       \
+            });                                                                                                                    \
         });                                                                                                                        \
-    });
+        do {                                                                                                                       \
+            post_action                                                                                                            \
+        } while (0);                                                                                                               \
+        ptls__key_schedule_update_hash((sched), emitter->buf->base + sh_start_off, emitter->buf->off - sh_start_off, 0);           \
+    } while (0)
 
-#define EMIT_HELLO_RETRY_REQUEST(sched, negotiated_group, additional_extensions)                                                   \
+#define EMIT_HELLO_RETRY_REQUEST(sched, negotiated_group, additional_extensions, post_action)                                      \
     EMIT_SERVER_HELLO((sched), { memcpy(emitter->buf->base + emitter->buf->off, hello_retry_random, PTLS_HELLO_RANDOM_SIZE); },    \
                       {                                                                                                            \
                           ptls_key_exchange_algorithm_t *_negotiated_group = (negotiated_group);                                   \
@@ -3769,121 +4133,131 @@
                           do {                                                                                                     \
                               additional_extensions                                                                                \
                           } while (0);                                                                                             \
-                      })
+                      },                                                                                                           \
+                      post_action)
     struct st_ptls_client_hello_t *ch;
     struct {
         ptls_key_exchange_algorithm_t *algorithm;
         ptls_iovec_t peer_key;
     } key_share = {NULL};
+    struct {
+        uint8_t *encoded_ch_inner;
+        uint8_t *ch_outer_aad;
+        ptls_buffer_t ch_inner;
+    } ech = {NULL};
     enum { HANDSHAKE_MODE_FULL, HANDSHAKE_MODE_PSK, HANDSHAKE_MODE_PSK_DHE } mode;
     size_t psk_index = SIZE_MAX;
     ptls_iovec_t pubkey = {0}, ecdh_secret = {0};
     int accept_early_data = 0, is_second_flight = tls->state == PTLS_STATE_SERVER_EXPECT_SECOND_CLIENT_HELLO, ret;
 
+    ptls_buffer_init(&ech.ch_inner, "", 0);
+
     if ((ch = malloc(sizeof(*ch))) == NULL) {
         ret = PTLS_ERROR_NO_MEMORY;
         goto Exit;
     }
 
-    *ch = (struct st_ptls_client_hello_t){0,      NULL,   {NULL},     {NULL}, 0,     {NULL},   {NULL}, {NULL}, {{0}},
-                                          {NULL}, {NULL}, {{{NULL}}}, {{0}},  {{0}}, {{NULL}}, {NULL}, {{0}},  {{UINT16_MAX}}};
+    *ch = (struct st_ptls_client_hello_t){.unknown_extensions = {{UINT16_MAX}}};
 
     /* decode ClientHello */
-    if ((ret = decode_client_hello(tls, ch, message.base + PTLS_HANDSHAKE_HEADER_SIZE, message.base + message.len, properties)) !=
-        0)
+    if ((ret = decode_client_hello(tls->ctx, ch, message.base + PTLS_HANDSHAKE_HEADER_SIZE, message.base + message.len, properties,
+                                   tls)) != 0)
         goto Exit;
-
-    /* bail out if CH cannot be handled as TLS 1.3, providing the application the raw CH and SNI, to help them fallback */
-    if (!is_supported_version(ch->selected_version)) {
-        if (!is_second_flight && tls->ctx->on_client_hello != NULL) {
-            ptls_on_client_hello_parameters_t params = {
-                .server_name = ch->server_name,
-                .raw_message = message,
-                .negotiated_protocols = {ch->alpn.list, ch->alpn.count},
-                .incompatible_version = 1,
-            };
-            if ((ret = tls->ctx->on_client_hello->cb(tls->ctx->on_client_hello, tls, &params)) != 0)
+    if ((ret = check_client_hello_constraints(tls->ctx, ch, is_second_flight ? tls->client_random : NULL, 0, message, tls)) != 0)
+        goto Exit;
+    if (!is_second_flight) {
+        memcpy(tls->client_random, ch->random_bytes, PTLS_HELLO_RANDOM_SIZE);
+        log_client_random(tls);
+    } else {
+        /* consistency check for ECH extension in response to HRR */
+        if (tls->ech.aead != NULL) {
+            if (ch->ech.payload.base == NULL) {
+                ret = PTLS_ALERT_MISSING_EXTENSION;
                 goto Exit;
+            }
+            if (!(ch->ech.config_id == tls->ech.config_id && ch->ech.cipher_suite.kdf == tls->ech.cipher->id.kdf &&
+                  ch->ech.cipher_suite.aead == tls->ech.cipher->id.aead && ch->ech.enc.len == 0)) {
+                ret = PTLS_ALERT_ILLEGAL_PARAMETER;
+                goto Exit;
+            }
         }
-        ret = PTLS_ALERT_PROTOCOL_VERSION;
-        goto Exit;
     }
 
-    /* Check TLS 1.3-specific constraints. Hereafter, we might exit without calling on_client_hello. That's fine because this CH is
-     * ought to be rejected. */
-    if (ch->legacy_version <= 0x0300) {
-        /* RFC 8446 Appendix D.5: any endpoint receiving a Hello message with legacy_version set to 0x0300 MUST abort the handshake
-         * with a "protocol_version" alert. */
-        ret = PTLS_ALERT_PROTOCOL_VERSION;
-        goto Exit;
-    }
-    if (!(ch->compression_methods.count == 1 && ch->compression_methods.ids[0] == 0)) {
+    /* ECH */
+    if (ch->ech.payload.base != NULL) {
+        if (ch->ech.type != PTLS_ECH_CLIENT_HELLO_TYPE_OUTER) {
+            ret = PTLS_ALERT_ILLEGAL_PARAMETER;
+            goto Exit;
+        }
+        if (!is_second_flight)
+            tls->ech.offered = 1;
+        /* obtain AEAD context for opening inner CH */
+        if (!is_second_flight && ch->ech.payload.base != NULL && tls->ctx->ech.server.create_opener != NULL) {
+            if ((tls->ech.aead = tls->ctx->ech.server.create_opener->cb(
+                     tls->ctx->ech.server.create_opener, &tls->ech.kem, &tls->ech.cipher, tls, ch->ech.config_id,
+                     ch->ech.cipher_suite, ch->ech.enc, ptls_iovec_init(ech_info_prefix, sizeof(ech_info_prefix)))) != NULL)
+                tls->ech.config_id = ch->ech.config_id;
+        }
+        if (tls->ech.aead != NULL) {
+            /* now that AEAD context is available, create AAD and decrypt inner CH */
+            if ((ech.encoded_ch_inner = malloc(ch->ech.payload.len - tls->ech.aead->algo->tag_size)) == NULL ||
+                (ech.ch_outer_aad = malloc(message.len - PTLS_HANDSHAKE_HEADER_SIZE)) == NULL) {
+                ret = PTLS_ERROR_NO_MEMORY;
+                goto Exit;
+            }
+            memcpy(ech.ch_outer_aad, message.base + PTLS_HANDSHAKE_HEADER_SIZE, message.len - PTLS_HANDSHAKE_HEADER_SIZE);
+            memset(ech.ch_outer_aad + (ch->ech.payload.base - (message.base + PTLS_HANDSHAKE_HEADER_SIZE)), 0, ch->ech.payload.len);
+            if (ptls_aead_decrypt(tls->ech.aead, ech.encoded_ch_inner, ch->ech.payload.base, ch->ech.payload.len, is_second_flight,
+                                  ech.ch_outer_aad, message.len - PTLS_HANDSHAKE_HEADER_SIZE) != SIZE_MAX) {
+                tls->ech.accepted = 1;
+                /* successfully decrypted EncodedCHInner, build CHInner */
+                if ((ret = rebuild_ch_inner(&ech.ch_inner, ech.encoded_ch_inner,
+                                            ech.encoded_ch_inner + ch->ech.payload.len - tls->ech.aead->algo->tag_size, ch,
+                                            message.base + PTLS_HANDSHAKE_HEADER_SIZE + ch->first_extension_at,
+                                            message.base + message.len)) != 0)
+                    goto Exit;
+                /* treat inner ch as the message being received, re-decode it */
+                message = ptls_iovec_init(ech.ch_inner.base, ech.ch_inner.off);
+                *ch = (struct st_ptls_client_hello_t){.unknown_extensions = {{UINT16_MAX}}};
+                if ((ret = decode_client_hello(tls->ctx, ch, ech.ch_inner.base + PTLS_HANDSHAKE_HEADER_SIZE,
+                                               ech.ch_inner.base + ech.ch_inner.off, properties, tls)) != 0)
+                    goto Exit;
+                if ((ret = check_client_hello_constraints(tls->ctx, ch, is_second_flight ? tls->ech.inner_client_random : NULL, 1,
+                                                          message, tls)) != 0)
+                    goto Exit;
+                if (!is_second_flight)
+                    memcpy(tls->ech.inner_client_random, ch->random_bytes, PTLS_HELLO_RANDOM_SIZE);
+            } else if (is_second_flight) {
+                /* decryption failure of inner CH in 2nd CH is fatal */
+                ret = PTLS_ALERT_DECRYPT_ERROR;
+                goto Exit;
+            } else {
+                /* decryption failure of 1st CH indicates key mismatch; dispose of AEAD context to indicate adoption of outerCH */
+                ptls_aead_free(tls->ech.aead);
+                tls->ech.aead = NULL;
+            }
+        }
+    } else if (tls->ech.offered) {
+        assert(is_second_flight);
         ret = PTLS_ALERT_ILLEGAL_PARAMETER;
         goto Exit;
     }
-    /* esni */
-    if (ch->esni.cipher != NULL) {
-        if (ch->key_shares.base == NULL) {
-            ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-            goto Exit;
-        }
-    }
-    /* pre-shared key */
-    if (ch->psk.hash_end != NULL) {
-        /* PSK must be the last extension */
-        if (!ch->psk.is_last_extension) {
-            ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-            goto Exit;
-        }
-    } else {
-        if (ch->psk.early_data_indication) {
-            ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-            goto Exit;
-        }
-    }
 
     if (tls->ctx->require_dhe_on_psk)
         ch->psk.ke_modes &= ~(1u << PTLS_PSK_KE_MODE_PSK);
 
     /* handle client_random, legacy_session_id, SNI, ESNI */
     if (!is_second_flight) {
-        memcpy(tls->client_random, ch->random_bytes, sizeof(tls->client_random));
-        log_client_random(tls);
         if (ch->legacy_session_id.len != 0)
             tls->send_change_cipher_spec = 1;
         ptls_iovec_t server_name = {NULL};
-        int is_esni = 0;
-        if (ch->esni.cipher != NULL && tls->ctx->esni != NULL) {
-            if ((ret = client_hello_decrypt_esni(tls->ctx, &server_name, &tls->esni, ch)) != 0)
-                goto Exit;
-            if (tls->ctx->update_esni_key != NULL) {
-                if ((ret = tls->ctx->update_esni_key->cb(tls->ctx->update_esni_key, tls, tls->esni->secret, ch->esni.cipher->hash,
-                                                         tls->esni->esni_contents_hash)) != 0)
-                    goto Exit;
-            }
-            is_esni = 1;
-        } else if (ch->server_name.base != NULL) {
+        if (ch->server_name.base != NULL)
             server_name = ch->server_name;
-        }
-        if (tls->ctx->on_client_hello != NULL) {
-            ptls_on_client_hello_parameters_t params = {server_name,
-                                                        message,
-                                                        {ch->alpn.list, ch->alpn.count},
-                                                        {ch->signature_algorithms.list, ch->signature_algorithms.count},
-                                                        {ch->cert_compression_algos.list, ch->cert_compression_algos.count},
-                                                        {ch->client_ciphers.list, ch->client_ciphers.count},
-                                                        {ch->server_certificate_types.list, ch->server_certificate_types.count},
-                                                        is_esni};
-            ret = tls->ctx->on_client_hello->cb(tls->ctx->on_client_hello, tls, &params);
-        } else {
-            ret = 0;
-        }
-
-        if (is_esni)
-            free(server_name.base);
-        if (ret != 0)
+        if ((ret = call_on_client_hello_cb(tls, server_name, message, ch->alpn.list, ch->alpn.count, ch->signature_algorithms.list,
+                                           ch->signature_algorithms.count, ch->cert_compression_algos.list,
+                                           ch->cert_compression_algos.count, ch->client_ciphers.list, ch->client_ciphers.count,
+                                           ch->server_certificate_types.list, ch->server_certificate_types.count, 0)) != 0)
             goto Exit;
-
         if (!certificate_type_exists(ch->server_certificate_types.list, ch->server_certificate_types.count,
                                      tls->ctx->use_raw_public_keys ? PTLS_CERTIFICATE_TYPE_RAW_PUBLIC_KEY
                                                                    : PTLS_CERTIFICATE_TYPE_X509)) {
@@ -3895,12 +4269,6 @@
             ret = PTLS_ALERT_DECODE_ERROR;
             goto Exit;
         }
-        /* the following check is necessary so that we would be able to track the connection in SSLKEYLOGFILE, even though it
-         * might not be for the safety of the protocol */
-        if (!ptls_mem_equal(tls->client_random, ch->random_bytes, sizeof(tls->client_random))) {
-            ret = PTLS_ALERT_HANDSHAKE_FAILURE;
-            goto Exit;
-        }
         /* We compare SNI only when the value is saved by the on_client_hello callback. This should be OK because we are
          * ignoring the value unless the callback saves the server-name. */
         if (tls->server_name != NULL) {
@@ -3919,7 +4287,7 @@
             goto Exit;
         if (!is_second_flight) {
             tls->cipher_suite = cs;
-            tls->key_schedule = key_schedule_new(cs, NULL);
+            tls->key_schedule = key_schedule_new(cs, NULL, 0);
         } else {
             if (tls->cipher_suite != cs) {
                 ret = PTLS_ALERT_HANDSHAKE_FAILURE;
@@ -3951,14 +4319,17 @@
             }
             /* integrity check passed; update states */
             key_schedule_update_ch1hash_prefix(tls->key_schedule);
-            ptls__key_schedule_update_hash(tls->key_schedule, ch->cookie.ch1_hash.base, ch->cookie.ch1_hash.len);
+            ptls__key_schedule_update_hash(tls->key_schedule, ch->cookie.ch1_hash.base, ch->cookie.ch1_hash.len, 0);
             key_schedule_extract(tls->key_schedule, ptls_iovec_init(NULL, 0));
             /* ... reusing sendbuf to rebuild HRR for hash calculation */
             size_t hrr_start = emitter->buf->off;
-            EMIT_HELLO_RETRY_REQUEST(tls->key_schedule, ch->cookie.sent_key_share ? key_share.algorithm : NULL, {
-                buffer_push_extension(emitter->buf, PTLS_EXTENSION_TYPE_COOKIE,
-                                      { ptls_buffer_pushv(emitter->buf, ch->cookie.all.base, ch->cookie.all.len); });
-            });
+            EMIT_HELLO_RETRY_REQUEST(tls->key_schedule, ch->cookie.sent_key_share ? key_share.algorithm : NULL,
+                                     {
+                                         buffer_push_extension(emitter->buf, PTLS_EXTENSION_TYPE_COOKIE, {
+                                             ptls_buffer_pushv(emitter->buf, ch->cookie.all.base, ch->cookie.all.len);
+                                         });
+                                     },
+                                     {});
             emitter->buf->off = hrr_start;
             is_second_flight = 1;
 
@@ -3973,55 +4344,78 @@
             if ((ret = select_negotiated_group(&negotiated_group, tls->ctx->key_exchanges, ch->negotiated_groups.base,
                                                ch->negotiated_groups.base + ch->negotiated_groups.len)) != 0)
                 goto Exit;
-            ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len);
+            ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len, 0);
             assert(tls->key_schedule->generation == 0);
-            if (properties != NULL && properties->server.retry_uses_cookie) {
-                /* emit HRR with cookie (note: we MUST omit KeyShare if the client has specified the correct one; see 46554f0)
-                 */
-                EMIT_HELLO_RETRY_REQUEST(NULL, key_share.algorithm != NULL ? NULL : negotiated_group, {
+
+            /* Either send a stateless retry (w. cookies) or a stateful one. When sending the latter, run the state machine. At the
+             * moment, stateless retry is disabled when ECH is used (do we need to support it?). */
+            int retry_uses_cookie =
+                properties != NULL && properties->server.retry_uses_cookie && !ptls_is_ech_handshake(tls, NULL, NULL);
+            if (!retry_uses_cookie) {
+                key_schedule_transform_post_ch1hash(tls->key_schedule);
+                key_schedule_extract(tls->key_schedule, ptls_iovec_init(NULL, 0));
+            }
+            size_t ech_confirm_off = 0;
+            EMIT_HELLO_RETRY_REQUEST(
+                tls->key_schedule, key_share.algorithm != NULL ? NULL : negotiated_group,
+                {
                     ptls_buffer_t *sendbuf = emitter->buf;
-                    buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_COOKIE, {
-                        ptls_buffer_push_block(sendbuf, 2, {
-                            /* push to-be-signed data */
-                            size_t tbs_start = sendbuf->off;
+                    if (ptls_is_ech_handshake(tls, NULL, NULL)) {
+                        buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_ENCRYPTED_CLIENT_HELLO, {
+                            if ((ret = ptls_buffer_reserve(sendbuf, PTLS_ECH_CONFIRM_LENGTH)) != 0)
+                                goto Exit;
+                            memset(sendbuf->base + sendbuf->off, 0, PTLS_ECH_CONFIRM_LENGTH);
+                            ech_confirm_off = sendbuf->off;
+                            sendbuf->off += PTLS_ECH_CONFIRM_LENGTH;
+                        });
+                    }
+                    if (retry_uses_cookie) {
+                        buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_COOKIE, {
                             ptls_buffer_push_block(sendbuf, 2, {
-                                /* first block of the cookie data is the hash(ch1) */
+                                /* push to-be-signed data */
+                                size_t tbs_start = sendbuf->off;
+                                ptls_buffer_push_block(sendbuf, 2, {
+                                    /* first block of the cookie data is the hash(ch1) */
+                                    ptls_buffer_push_block(sendbuf, 1, {
+                                        size_t sz = tls->cipher_suite->hash->digest_size;
+                                        if ((ret = ptls_buffer_reserve(sendbuf, sz)) != 0)
+                                            goto Exit;
+                                        key_schedule_extract_ch1hash(tls->key_schedule, sendbuf->base + sendbuf->off);
+                                        sendbuf->off += sz;
+                                    });
+                                    /* second is if we have sent key_share extension */
+                                    ptls_buffer_push(sendbuf, key_share.algorithm == NULL);
+                                    /* we can add more data here */
+                                });
+                                size_t tbs_len = sendbuf->off - tbs_start;
+                                /* push the signature */
                                 ptls_buffer_push_block(sendbuf, 1, {
-                                    size_t sz = tls->cipher_suite->hash->digest_size;
+                                    size_t sz = tls->ctx->cipher_suites[0]->hash->digest_size;
                                     if ((ret = ptls_buffer_reserve(sendbuf, sz)) != 0)
                                         goto Exit;
-                                    key_schedule_extract_ch1hash(tls->key_schedule, sendbuf->base + sendbuf->off);
+                                    if ((ret = calc_cookie_signature(tls, properties, negotiated_group,
+                                                                     ptls_iovec_init(sendbuf->base + tbs_start, tbs_len),
+                                                                     sendbuf->base + sendbuf->off)) != 0)
+                                        goto Exit;
                                     sendbuf->off += sz;
                                 });
-                                /* second is if we have sent key_share extension */
-                                ptls_buffer_push(sendbuf, key_share.algorithm == NULL);
-                                /* we can add more data here */
-                            });
-                            size_t tbs_len = sendbuf->off - tbs_start;
-                            /* push the signature */
-                            ptls_buffer_push_block(sendbuf, 1, {
-                                size_t sz = tls->ctx->cipher_suites[0]->hash->digest_size;
-                                if ((ret = ptls_buffer_reserve(sendbuf, sz)) != 0)
-                                    goto Exit;
-                                if ((ret = calc_cookie_signature(tls, properties, negotiated_group,
-                                                                 ptls_iovec_init(sendbuf->base + tbs_start, tbs_len),
-                                                                 sendbuf->base + sendbuf->off)) != 0)
-                                    goto Exit;
-                                sendbuf->off += sz;
                             });
                         });
-                    });
+                    }
+                },
+                {
+                    if (ech_confirm_off != 0 &&
+                        (ret = ech_calc_confirmation(
+                             tls->key_schedule, emitter->buf->base + ech_confirm_off, tls->ech.inner_client_random,
+                             ECH_CONFIRMATION_HRR,
+                             ptls_iovec_init(emitter->buf->base + sh_start_off, emitter->buf->off - sh_start_off))) != 0)
+                        goto Exit;
                 });
+            if (retry_uses_cookie) {
                 if ((ret = push_change_cipher_spec(tls, emitter)) != 0)
                     goto Exit;
                 ret = PTLS_ERROR_STATELESS_RETRY;
             } else {
-                /* invoking stateful retry; roll the key schedule and emit HRR */
-                key_schedule_transform_post_ch1hash(tls->key_schedule);
-                key_schedule_extract(tls->key_schedule, ptls_iovec_init(NULL, 0));
-                EMIT_HELLO_RETRY_REQUEST(tls->key_schedule, key_share.algorithm != NULL ? NULL : negotiated_group, {});
-                if ((ret = push_change_cipher_spec(tls, emitter)) != 0)
-                    goto Exit;
                 tls->state = PTLS_STATE_SERVER_EXPECT_SECOND_CLIENT_HELLO;
                 if (ch->psk.early_data_indication)
                     tls->server.early_data_skipped_bytes = 0;
@@ -4052,7 +4446,7 @@
      * adjust key_schedule, determine handshake mode
      */
     if (psk_index == SIZE_MAX || tls->ctx->require_client_authentication) {
-        ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len);
+        ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len, 0);
         if (!is_second_flight) {
             assert(tls->key_schedule->generation == 0);
             key_schedule_extract(tls->key_schedule, ptls_iovec_init(NULL, 0));
@@ -4061,7 +4455,7 @@
         if (properties != NULL)
             properties->server.selected_psk_binder.len = 0;
     } else {
-        ptls__key_schedule_update_hash(tls->key_schedule, ch->psk.hash_end, message.base + message.len - ch->psk.hash_end);
+        ptls__key_schedule_update_hash(tls->key_schedule, ch->psk.hash_end, message.base + message.len - ch->psk.hash_end, 0);
         if ((ch->psk.ke_modes & (1u << PTLS_PSK_KE_MODE_PSK)) != 0) {
             mode = HANDSHAKE_MODE_PSK;
         } else {
@@ -4099,22 +4493,44 @@
         tls->key_share = key_share.algorithm;
     }
 
-    /* send ServerHello */
-    EMIT_SERVER_HELLO(
-        tls->key_schedule, { tls->ctx->random_bytes(emitter->buf->base + emitter->buf->off, PTLS_HELLO_RANDOM_SIZE); },
-        {
-            ptls_buffer_t *sendbuf = emitter->buf;
-            if (mode != HANDSHAKE_MODE_PSK) {
-                buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_KEY_SHARE, {
-                    ptls_buffer_push16(sendbuf, key_share.algorithm->id);
-                    ptls_buffer_push_block(sendbuf, 2, { ptls_buffer_pushv(sendbuf, pubkey.base, pubkey.len); });
-                });
-            }
-            if (mode != HANDSHAKE_MODE_FULL) {
-                buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_PRE_SHARED_KEY,
-                                      { ptls_buffer_push16(sendbuf, (uint16_t)psk_index); });
-            }
-        });
+    { /* send ServerHello */
+        size_t ech_confirm_off = 0;
+        EMIT_SERVER_HELLO(
+            tls->key_schedule,
+            {
+                tls->ctx->random_bytes(emitter->buf->base + emitter->buf->off, PTLS_HELLO_RANDOM_SIZE);
+                /* when accepting CHInner, last 8 byte of SH.random is zero for the handshake transcript */
+                if (ptls_is_ech_handshake(tls, NULL, NULL)) {
+                    ech_confirm_off = emitter->buf->off + PTLS_HELLO_RANDOM_SIZE - PTLS_ECH_CONFIRM_LENGTH;
+                    memset(emitter->buf->base + ech_confirm_off, 0, PTLS_ECH_CONFIRM_LENGTH);
+                }
+            },
+            {
+                ptls_buffer_t *sendbuf = emitter->buf;
+                if (mode != HANDSHAKE_MODE_PSK) {
+                    buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_KEY_SHARE, {
+                        ptls_buffer_push16(sendbuf, key_share.algorithm->id);
+                        ptls_buffer_push_block(sendbuf, 2, { ptls_buffer_pushv(sendbuf, pubkey.base, pubkey.len); });
+                    });
+                }
+                if (mode != HANDSHAKE_MODE_FULL) {
+                    buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_PRE_SHARED_KEY,
+                                          { ptls_buffer_push16(sendbuf, (uint16_t)psk_index); });
+                }
+            },
+            {
+                if (ech_confirm_off != 0 &&
+                    (ret = ech_calc_confirmation(
+                         tls->key_schedule, emitter->buf->base + ech_confirm_off, tls->ech.inner_client_random,
+                         ECH_CONFIRMATION_SERVER_HELLO,
+                         ptls_iovec_init(emitter->buf->base + sh_start_off, emitter->buf->off - sh_start_off))) != 0)
+                    goto Exit;
+            });
+    }
+
+    /* processing of ECH is complete; dispose state */
+    clear_ech(&tls->ech, 1);
+
     if ((ret = push_change_cipher_spec(tls, emitter)) != 0)
         goto Exit;
 
@@ -4140,16 +4556,7 @@
     ptls_push_message(emitter, tls->key_schedule, PTLS_HANDSHAKE_TYPE_ENCRYPTED_EXTENSIONS, {
         ptls_buffer_t *sendbuf = emitter->buf;
         ptls_buffer_push_block(sendbuf, 2, {
-            if (tls->esni != NULL) {
-                /* the extension is sent even if the application does not handle server name, because otherwise the handshake
-                 * would fail (FIXME ch->esni.nonce will be zero on HRR) */
-                buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_ENCRYPTED_SERVER_NAME, {
-                    uint8_t response_type = PTLS_ESNI_RESPONSE_TYPE_ACCEPT;
-                    ptls_buffer_pushv(sendbuf, &response_type, 1);
-                    ptls_buffer_pushv(sendbuf, tls->esni->nonce, PTLS_ESNI_NONCE_SIZE);
-                });
-                free_esni_secret(&tls->esni, 1);
-            } else if (tls->server_name != NULL) {
+            if (tls->server_name != NULL) {
                 /* In this event, the server SHALL include an extension of type "server_name" in the (extended) server hello.
                  * The "extension_data" field of this extension SHALL be empty. (RFC 6066 section 3) */
                 buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_SERVER_NAME, {});
@@ -4169,6 +4576,12 @@
             }
             if (tls->pending_handshake_secret != NULL)
                 buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_EARLY_DATA, {});
+            /* send ECH retry_configs, if ECH was offered by rejected, even though we (the server) could have accepted ECH */
+            if (tls->ech.offered && !ptls_is_ech_handshake(tls, NULL, NULL) && tls->ctx->ech.server.create_opener != NULL &&
+                tls->ctx->ech.server.retry_configs.len != 0)
+                buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_ENCRYPTED_CLIENT_HELLO, {
+                    ptls_buffer_pushv(sendbuf, tls->ctx->ech.server.retry_configs.base, tls->ctx->ech.server.retry_configs.len);
+                });
             if ((ret = push_additional_extensions(properties, sendbuf)) != 0)
                 goto Exit;
         });
@@ -4216,6 +4629,9 @@
         ptls_clear_memory(ecdh_secret.base, ecdh_secret.len);
         free(ecdh_secret.base);
     }
+    free(ech.encoded_ch_inner);
+    free(ech.ch_outer_aad);
+    ptls_buffer_dispose(&ech.ch_inner);
     free(ch);
     return ret;
 
@@ -4288,7 +4704,7 @@
     if ((ret = commission_handshake_secret(tls)) != 0)
         goto Exit;
 
-    ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len);
+    ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len, 0);
     tls->state = PTLS_STATE_SERVER_EXPECT_FINISHED;
     ret = PTLS_ERROR_IN_PROGRESS;
 
@@ -4308,7 +4724,7 @@
     if ((ret = setup_traffic_protection(tls, 0, NULL, 3, 0)) != 0)
         return ret;
 
-    ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len);
+    ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len, 0);
 
     tls->state = PTLS_STATE_SERVER_POST_HANDSHAKE;
     return 0;
@@ -4603,6 +5019,7 @@
 
     *tls = NULL;
 
+    /* TODO handle flags like psk_handshake, ech_handshake as we add support for TLS/1.3 import */
     ptls_decode_block(src, end, 2, {
         /* instantiate, based on the is_server flag */
         if (end - src < 2) {
@@ -4687,8 +5104,6 @@
     ptls_buffer_dispose(&tls->recvbuf.mess);
     free_exporter_master_secret(tls, 1);
     free_exporter_master_secret(tls, 0);
-    if (tls->esni != NULL)
-        free_esni_secret(&tls->esni, tls->is_server);
     if (tls->key_schedule != NULL)
         key_schedule_free(tls->key_schedule);
     if (tls->traffic_protection.dec.aead != NULL)
@@ -4697,6 +5112,7 @@
         ptls_aead_free(tls->traffic_protection.enc.aead);
     free(tls->server_name);
     free(tls->negotiated_protocol);
+    clear_ech(&tls->ech, tls->is_server);
     if (tls->is_server) {
         if (tls->server.async_job != NULL)
             tls->server.async_job->destroy_(tls->server.async_job);
@@ -4773,14 +5189,9 @@
 {
     char *duped = NULL;
 
-    if (server_name != NULL) {
-        if (server_name_len == 0)
-            server_name_len = strlen(server_name);
-        if ((duped = malloc(server_name_len + 1)) == NULL)
-            return PTLS_ERROR_NO_MEMORY;
-        memcpy(duped, server_name, server_name_len);
-        duped[server_name_len] = '\0';
-    }
+    if (server_name != NULL &&
+        (duped = duplicate_as_str(server_name, server_name_len != 0 ? server_name_len : strlen(server_name))) == NULL)
+        return PTLS_ERROR_NO_MEMORY;
 
     free(tls->server_name);
     tls->server_name = duped;
@@ -4797,14 +5208,8 @@
 {
     char *duped = NULL;
 
-    if (protocol != NULL) {
-        if (protocol_len == 0)
-            protocol_len = strlen(protocol);
-        if ((duped = malloc(protocol_len + 1)) == NULL)
-            return PTLS_ERROR_NO_MEMORY;
-        memcpy(duped, protocol, protocol_len);
-        duped[protocol_len] = '\0';
-    }
+    if (protocol != NULL && (duped = duplicate_as_str(protocol, protocol_len != 0 ? protocol_len : strlen(protocol))) == NULL)
+        return PTLS_ERROR_NO_MEMORY;
 
     free(tls->negotiated_protocol);
     tls->negotiated_protocol = duped;
@@ -4822,6 +5227,18 @@
     return tls->is_psk_handshake;
 }
 
+int ptls_is_ech_handshake(ptls_t *tls, ptls_hpke_kem_t **kem, ptls_hpke_cipher_suite_t **cipher)
+{
+    if (tls->ech.accepted) {
+        if (kem != NULL)
+            *kem = tls->ech.kem;
+        if (cipher != NULL)
+            *cipher = tls->ech.cipher;
+        return 1;
+    }
+    return 0;
+}
+
 void **ptls_get_data_ptr(ptls_t *tls)
 {
     return &tls->data_ptr;
@@ -5286,9 +5703,12 @@
     case PTLS_ERROR_ASYNC_OPERATION:
         break;
     default:
-        /* flush partially written response */
-        ptls_clear_memory(emitter.super.buf->base + sendbuf_orig_off, emitter.super.buf->off - sendbuf_orig_off);
-        emitter.super.buf->off = sendbuf_orig_off;
+        /* Flush handshake messages that have been written partially. ECH_REQUIRED sticks out because it is a message sent
+         * post-handshake compared to other alerts that are generating *during* the handshake. */
+        if (ret != PTLS_ALERT_ECH_REQUIRED) {
+            ptls_clear_memory(emitter.super.buf->base + sendbuf_orig_off, emitter.super.buf->off - sendbuf_orig_off);
+            emitter.super.buf->off = sendbuf_orig_off;
+        }
         /* send alert immediately */
         if (PTLS_ERROR_GET_CLASS(ret) != PTLS_ERROR_CLASS_PEER_ALERT)
             if (ptls_send_alert(tls, emitter.super.buf, PTLS_ALERT_LEVEL_FATAL,
@@ -5886,134 +6306,6 @@
     return handle_handshake_record(tls, handle_server_handshake_message, &emitter.super, &rec, properties);
 }
 
-int ptls_esni_init_context(ptls_context_t *ctx, ptls_esni_context_t *esni, ptls_iovec_t esni_keys,
-                           ptls_key_exchange_context_t **key_exchanges)
-{
-    const uint8_t *src = esni_keys.base, *const end = src + esni_keys.len;
-    size_t num_key_exchanges, num_cipher_suites = 0;
-    int ret;
-
-    for (num_key_exchanges = 0; key_exchanges[num_key_exchanges] != NULL; ++num_key_exchanges)
-        ;
-
-    memset(esni, 0, sizeof(*esni));
-    if ((esni->key_exchanges = malloc(sizeof(*esni->key_exchanges) * (num_key_exchanges + 1))) == NULL) {
-        ret = PTLS_ERROR_NO_MEMORY;
-        goto Exit;
-    }
-    memcpy(esni->key_exchanges, key_exchanges, sizeof(*esni->key_exchanges) * (num_key_exchanges + 1));
-
-    /* ESNIKeys */
-    if ((ret = ptls_decode16(&esni->version, &src, end)) != 0)
-        goto Exit;
-    /* Skip checksum fields */
-    if (end - src < 4) {
-        ret = PTLS_ALERT_DECRYPT_ERROR;
-        goto Exit;
-    }
-    src += 4;
-    /* Published SNI field */
-    ptls_decode_open_block(src, end, 2, { src = end; });
-
-    /* Process the list of KeyShareEntries, verify for each of them that the ciphersuite is supported. */
-    ptls_decode_open_block(src, end, 2, {
-        do {
-            /* parse */
-            uint16_t id;
-            if ((ret = ptls_decode16(&id, &src, end)) != 0)
-                goto Exit;
-            ptls_decode_open_block(src, end, 2, { src = end; });
-            /* check that matching key-share exists */
-            ptls_key_exchange_context_t **found;
-            for (found = key_exchanges; *found != NULL; ++found)
-                if ((*found)->algo->id == id)
-                    break;
-            if (found == NULL) {
-                ret = PTLS_ERROR_INCOMPATIBLE_KEY;
-                goto Exit;
-            }
-        } while (src != end);
-    });
-    /* Process the list of cipher_suites. If they are supported, store in esni context  */
-    ptls_decode_open_block(src, end, 2, {
-        void *newp;
-        do {
-            uint16_t id;
-            if ((ret = ptls_decode16(&id, &src, end)) != 0)
-                goto Exit;
-            size_t i;
-            for (i = 0; ctx->cipher_suites[i] != NULL; ++i)
-                if (ctx->cipher_suites[i]->id == id)
-                    break;
-            if (ctx->cipher_suites[i] != NULL) {
-                if ((newp = realloc(esni->cipher_suites, sizeof(*esni->cipher_suites) * (num_cipher_suites + 1))) == NULL) {
-                    ret = PTLS_ERROR_NO_MEMORY;
-                    goto Exit;
-                }
-                esni->cipher_suites = newp;
-                esni->cipher_suites[num_cipher_suites++].cipher_suite = ctx->cipher_suites[i];
-            }
-        } while (src != end);
-        if ((newp = realloc(esni->cipher_suites, sizeof(*esni->cipher_suites) * (num_cipher_suites + 1))) == NULL) {
-            ret = PTLS_ERROR_NO_MEMORY;
-            goto Exit;
-        }
-        esni->cipher_suites = newp;
-        esni->cipher_suites[num_cipher_suites].cipher_suite = NULL;
-    });
-    /* Parse the padded length, not before, not after parameters */
-    if ((ret = ptls_decode16(&esni->padded_length, &src, end)) != 0)
-        goto Exit;
-    if ((ret = ptls_decode64(&esni->not_before, &src, end)) != 0)
-        goto Exit;
-    if ((ret = ptls_decode64(&esni->not_after, &src, end)) != 0)
-        goto Exit;
-    /* Skip the extension fields */
-    ptls_decode_block(src, end, 2, {
-        while (src != end) {
-            uint16_t ext_type;
-            if ((ret = ptls_decode16(&ext_type, &src, end)) != 0)
-                goto Exit;
-            ptls_decode_open_block(src, end, 2, { src = end; });
-        }
-    });
-
-    { /* calculate digests for every cipher-suite */
-        size_t i;
-        for (i = 0; esni->cipher_suites[i].cipher_suite != NULL; ++i) {
-            if ((ret = ptls_calc_hash(esni->cipher_suites[i].cipher_suite->hash, esni->cipher_suites[i].record_digest,
-                                      esni_keys.base, esni_keys.len)) != 0)
-                goto Exit;
-        }
-    }
-
-    ret = 0;
-Exit:
-    if (ret != 0)
-        ptls_esni_dispose_context(esni);
-    return ret;
-}
-
-void ptls_esni_dispose_context(ptls_esni_context_t *esni)
-{
-    size_t i;
-
-    if (esni->key_exchanges != NULL) {
-        for (i = 0; esni->key_exchanges[i] != NULL; ++i)
-            esni->key_exchanges[i]->on_exchange(esni->key_exchanges + i, 1, NULL, ptls_iovec_init(NULL, 0));
-        free(esni->key_exchanges);
-    }
-    free(esni->cipher_suites);
-}
-
-/**
- * Obtain the ESNI secrets negotiated during the handshake.
- */
-ptls_esni_secret_t *ptls_get_esni_secret(ptls_t *ctx)
-{
-    return ctx->esni;
-}
-
 /**
  * checks if given name looks like an IP address
  */
@@ -6032,6 +6324,31 @@
     return 0;
 }
 
+int ptls_ech_encode_config(ptls_buffer_t *buf, uint8_t config_id, ptls_hpke_kem_t *kem, ptls_iovec_t public_key,
+                           ptls_hpke_cipher_suite_t **ciphers, uint8_t max_name_length, const char *public_name)
+{
+    int ret;
+
+    ptls_buffer_push16(buf, PTLS_ECH_CONFIG_VERSION);
+    ptls_buffer_push_block(buf, 2, {
+        ptls_buffer_push(buf, config_id);
+        ptls_buffer_push16(buf, kem->id);
+        ptls_buffer_push_block(buf, 2, { ptls_buffer_pushv(buf, public_key.base, public_key.len); });
+        ptls_buffer_push_block(buf, 2, {
+            for (size_t i = 0; ciphers[i] != NULL; ++i) {
+                ptls_buffer_push16(buf, ciphers[i]->id.kdf);
+                ptls_buffer_push16(buf, ciphers[i]->id.aead);
+            }
+        });
+        ptls_buffer_push(buf, max_name_length);
+        ptls_buffer_push_block(buf, 1, { ptls_buffer_pushv(buf, public_name, strlen(public_name)); });
+        ptls_buffer_push_block(buf, 2, {/* extensions */});
+    });
+
+Exit:
+    return ret;
+}
+
 static char *byte_to_hex(char *dst, uint8_t v)
 {
     *dst++ = "0123456789abcdef"[v >> 4];
diff --git a/picotls.xcodeproj/project.pbxproj b/picotls.xcodeproj/project.pbxproj
index 92b5954..ef65447 100644
--- a/picotls.xcodeproj/project.pbxproj
+++ b/picotls.xcodeproj/project.pbxproj
@@ -105,9 +105,6 @@
 		E9925A162354C3DF00CA2082 /* chacha20.c in Sources */ = {isa = PBXBuildFile; fileRef = E9F20BE422E34B340018D260 /* chacha20.c */; };
 		E9925A172354C3E200CA2082 /* random.c in Sources */ = {isa = PBXBuildFile; fileRef = E9F20BF922E34C110018D260 /* random.c */; };
 		E9925A182354C3E500CA2082 /* x25519.c in Sources */ = {isa = PBXBuildFile; fileRef = E9F20BE122E34B340018D260 /* x25519.c */; };
-		E992F7A320E99A7C0008154D /* libpicotls-openssl.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1059008C1DC8E1A300FB4085 /* libpicotls-openssl.a */; };
-		E992F7A420E99A7C0008154D /* libpicotls-core.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 106530DA1D9B3E6F005B2C60 /* libpicotls-core.a */; };
-		E992F7AA20E99AA10008154D /* esni.c in Sources */ = {isa = PBXBuildFile; fileRef = E992F79A20E99A6B0008154D /* esni.c */; };
 		E99B75E01F5CDDB500CF503E /* asn1.c in Sources */ = {isa = PBXBuildFile; fileRef = E99B75DE1F5CDDB500CF503E /* asn1.c */; };
 		E99B75E11F5CDDB500CF503E /* pembase64.c in Sources */ = {isa = PBXBuildFile; fileRef = E99B75DF1F5CDDB500CF503E /* pembase64.c */; };
 		E99B75E21F5CE54D00CF503E /* asn1.c in Sources */ = {isa = PBXBuildFile; fileRef = E99B75DE1F5CDDB500CF503E /* asn1.c */; };
@@ -159,20 +156,6 @@
 			remoteGlobalIDString = 106530D91D9B3E6F005B2C60;
 			remoteInfo = "picotls-core";
 		};
-		E992F79D20E99A7C0008154D /* PBXContainerItemProxy */ = {
-			isa = PBXContainerItemProxy;
-			containerPortal = 106530AA1D9985E0005B2C60 /* Project object */;
-			proxyType = 1;
-			remoteGlobalIDString = 106530D91D9B3E6F005B2C60;
-			remoteInfo = "picotls-core";
-		};
-		E992F79F20E99A7C0008154D /* PBXContainerItemProxy */ = {
-			isa = PBXContainerItemProxy;
-			containerPortal = 106530AA1D9985E0005B2C60 /* Project object */;
-			proxyType = 1;
-			remoteGlobalIDString = 105900701DC8E1A300FB4085;
-			remoteInfo = "picotls-openssl";
-		};
 /* End PBXContainerItemProxy section */
 
 /* Begin PBXCopyFilesBuildPhase section */
@@ -203,15 +186,6 @@
 			);
 			runOnlyForDeploymentPostprocessing = 1;
 		};
-		E992F7A520E99A7C0008154D /* CopyFiles */ = {
-			isa = PBXCopyFilesBuildPhase;
-			buildActionMask = 2147483647;
-			dstPath = /usr/share/man/man1/;
-			dstSubfolderSpec = 0;
-			files = (
-			);
-			runOnlyForDeploymentPostprocessing = 1;
-		};
 		E9B43DDD24619D5100824E51 /* CopyFiles */ = {
 			isa = PBXCopyFilesBuildPhase;
 			buildActionMask = 2147483647;
@@ -287,8 +261,6 @@
 		E97577002212405300D1EF74 /* ffx.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ffx.h; sourceTree = "<group>"; };
 		E97577022212405D00D1EF74 /* ffx.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = ffx.c; sourceTree = "<group>"; };
 		E97577072213148800D1EF74 /* e2e.t */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.perl; path = e2e.t; sourceTree = "<group>"; };
-		E992F79A20E99A6B0008154D /* esni.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = esni.c; sourceTree = "<group>"; };
-		E992F7A920E99A7C0008154D /* picotls-esni */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "picotls-esni"; sourceTree = BUILT_PRODUCTS_DIR; };
 		E99B75DE1F5CDDB500CF503E /* asn1.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = asn1.c; sourceTree = "<group>"; };
 		E99B75DF1F5CDDB500CF503E /* pembase64.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = pembase64.c; sourceTree = "<group>"; };
 		E9B43DBF24619D1700824E51 /* fusion.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = fusion.c; sourceTree = "<group>"; };
@@ -357,15 +329,6 @@
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
-		E992F7A220E99A7C0008154D /* Frameworks */ = {
-			isa = PBXFrameworksBuildPhase;
-			buildActionMask = 2147483647;
-			files = (
-				E992F7A320E99A7C0008154D /* libpicotls-openssl.a in Frameworks */,
-				E992F7A420E99A7C0008154D /* libpicotls-core.a in Frameworks */,
-			);
-			runOnlyForDeploymentPostprocessing = 0;
-		};
 		E9B43DDC24619D5100824E51 /* Frameworks */ = {
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
@@ -450,7 +413,6 @@
 				1059004B1DC8D57000FB4085 /* test-minicrypto */,
 				1059008C1DC8E1A300FB4085 /* libpicotls-openssl.a */,
 				10EACB171DCEAF0F00CA0341 /* libpicotls-minicrypto.a */,
-				E992F7A920E99A7C0008154D /* picotls-esni */,
 				E9B43DE124619D5100824E51 /* test-fusion */,
 			);
 			name = Products;
@@ -571,7 +533,6 @@
 		E992F79920E99A080008154D /* src */ = {
 			isa = PBXGroup;
 			children = (
-				E992F79A20E99A6B0008154D /* esni.c */,
 			);
 			path = src;
 			sourceTree = "<group>";
@@ -759,25 +720,6 @@
 			productReference = 10EACB171DCEAF0F00CA0341 /* libpicotls-minicrypto.a */;
 			productType = "com.apple.product-type.library.static";
 		};
-		E992F79B20E99A7C0008154D /* picotls-esni */ = {
-			isa = PBXNativeTarget;
-			buildConfigurationList = E992F7A620E99A7C0008154D /* Build configuration list for PBXNativeTarget "picotls-esni" */;
-			buildPhases = (
-				E992F7A020E99A7C0008154D /* Sources */,
-				E992F7A220E99A7C0008154D /* Frameworks */,
-				E992F7A520E99A7C0008154D /* CopyFiles */,
-			);
-			buildRules = (
-			);
-			dependencies = (
-				E992F79C20E99A7C0008154D /* PBXTargetDependency */,
-				E992F79E20E99A7C0008154D /* PBXTargetDependency */,
-			);
-			name = "picotls-esni";
-			productName = "test-crypto-openssl";
-			productReference = E992F7A920E99A7C0008154D /* picotls-esni */;
-			productType = "com.apple.product-type.tool";
-		};
 		E9B43DC024619D5100824E51 /* test-fusion */ = {
 			isa = PBXNativeTarget;
 			buildConfigurationList = E9B43DDE24619D5100824E51 /* Build configuration list for PBXNativeTarget "test-fusion" */;
@@ -831,7 +773,6 @@
 				106530F11DAD8985005B2C60 /* cli */,
 				106530CB1D9B3D45005B2C60 /* test-openssl */,
 				105900411DC8D57000FB4085 /* test-minicrypto */,
-				E992F79B20E99A7C0008154D /* picotls-esni */,
 				E9B43DC024619D5100824E51 /* test-fusion */,
 			);
 		};
@@ -966,14 +907,6 @@
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
-		E992F7A020E99A7C0008154D /* Sources */ = {
-			isa = PBXSourcesBuildPhase;
-			buildActionMask = 2147483647;
-			files = (
-				E992F7AA20E99AA10008154D /* esni.c in Sources */,
-			);
-			runOnlyForDeploymentPostprocessing = 0;
-		};
 		E9B43DC124619D5100824E51 /* Sources */ = {
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
@@ -998,16 +931,6 @@
 			target = 106530D91D9B3E6F005B2C60 /* picotls-core */;
 			targetProxy = 10EACB181DCEAF4A00CA0341 /* PBXContainerItemProxy */;
 		};
-		E992F79C20E99A7C0008154D /* PBXTargetDependency */ = {
-			isa = PBXTargetDependency;
-			target = 106530D91D9B3E6F005B2C60 /* picotls-core */;
-			targetProxy = E992F79D20E99A7C0008154D /* PBXContainerItemProxy */;
-		};
-		E992F79E20E99A7C0008154D /* PBXTargetDependency */ = {
-			isa = PBXTargetDependency;
-			target = 105900701DC8E1A300FB4085 /* picotls-openssl */;
-			targetProxy = E992F79F20E99A7C0008154D /* PBXContainerItemProxy */;
-		};
 /* End PBXTargetDependency section */
 
 /* Begin XCBuildConfiguration section */
@@ -1270,34 +1193,6 @@
 			};
 			name = Release;
 		};
-		E992F7A720E99A7C0008154D /* Debug */ = {
-			isa = XCBuildConfiguration;
-			buildSettings = {
-				GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)";
-				HEADER_SEARCH_PATHS = (
-					/usr/local/opt/openssl/include,
-					include,
-				);
-				LIBRARY_SEARCH_PATHS = /usr/local/opt/openssl/lib;
-				OTHER_LDFLAGS = "-lcrypto";
-				PRODUCT_NAME = "$(TARGET_NAME)";
-			};
-			name = Debug;
-		};
-		E992F7A820E99A7C0008154D /* Release */ = {
-			isa = XCBuildConfiguration;
-			buildSettings = {
-				GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)";
-				HEADER_SEARCH_PATHS = (
-					/usr/local/opt/openssl/include,
-					include,
-				);
-				LIBRARY_SEARCH_PATHS = /usr/local/opt/openssl/lib;
-				OTHER_LDFLAGS = "-lcrypto";
-				PRODUCT_NAME = "$(TARGET_NAME)";
-			};
-			name = Release;
-		};
 		E9B43DDF24619D5100824E51 /* Debug */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
@@ -1392,15 +1287,6 @@
 			defaultConfigurationIsVisible = 0;
 			defaultConfigurationName = Release;
 		};
-		E992F7A620E99A7C0008154D /* Build configuration list for PBXNativeTarget "picotls-esni" */ = {
-			isa = XCConfigurationList;
-			buildConfigurations = (
-				E992F7A720E99A7C0008154D /* Debug */,
-				E992F7A820E99A7C0008154D /* Release */,
-			);
-			defaultConfigurationIsVisible = 0;
-			defaultConfigurationName = Release;
-		};
 		E9B43DDE24619D5100824E51 /* Build configuration list for PBXNativeTarget "test-fusion" */ = {
 			isa = XCConfigurationList;
 			buildConfigurations = (
diff --git a/picotlsvs/picotlsvs.sln b/picotlsvs/picotlsvs.sln
index e6140c4..0322568 100644
--- a/picotlsvs/picotlsvs.sln
+++ b/picotlsvs/picotlsvs.sln
@@ -30,14 +30,6 @@
 		{497433FE-B252-4985-A504-54EB791F57F4} = {497433FE-B252-4985-A504-54EB791F57F4}
 	EndProjectSection
 EndProject
-Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "picotls-esni", "picotls-esni\picotls-esni.vcxproj", "{592127C5-DD8C-47ED-8EBA-026B5848C374}"
-	ProjectSection(ProjectDependencies) = postProject
-		{559AC085-1BEF-450A-A62D-0D370561D596} = {559AC085-1BEF-450A-A62D-0D370561D596}
-		{499B82B3-F5A5-4C2E-91EF-A2F77CBC33F5} = {499B82B3-F5A5-4C2E-91EF-A2F77CBC33F5}
-		{56C264BF-822B-4F29-B512-5B26157CA2EC} = {56C264BF-822B-4F29-B512-5B26157CA2EC}
-		{497433FE-B252-4985-A504-54EB791F57F4} = {497433FE-B252-4985-A504-54EB791F57F4}
-	EndProjectSection
-EndProject
 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "picotls-core", "picotls-core\picotls-core.vcxproj", "{497433FE-B252-4985-A504-54EB791F57F4}"
 EndProject
 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "picotls-minicrypto", "picotls-minicrypto\picotls-minicrypto.vcxproj", "{559AC085-1BEF-450A-A62D-0D370561D596}"
@@ -98,14 +90,6 @@
 		{15D7D32F-3B62-4B10-A06A-BA1ADD591D7F}.Release|x64.Build.0 = Release|x64
 		{15D7D32F-3B62-4B10-A06A-BA1ADD591D7F}.Release|x86.ActiveCfg = Release|Win32
 		{15D7D32F-3B62-4B10-A06A-BA1ADD591D7F}.Release|x86.Build.0 = Release|Win32
-		{592127C5-DD8C-47ED-8EBA-026B5848C374}.Debug|x64.ActiveCfg = Debug|x64
-		{592127C5-DD8C-47ED-8EBA-026B5848C374}.Debug|x64.Build.0 = Debug|x64
-		{592127C5-DD8C-47ED-8EBA-026B5848C374}.Debug|x86.ActiveCfg = Debug|Win32
-		{592127C5-DD8C-47ED-8EBA-026B5848C374}.Debug|x86.Build.0 = Debug|Win32
-		{592127C5-DD8C-47ED-8EBA-026B5848C374}.Release|x64.ActiveCfg = Release|x64
-		{592127C5-DD8C-47ED-8EBA-026B5848C374}.Release|x64.Build.0 = Release|x64
-		{592127C5-DD8C-47ED-8EBA-026B5848C374}.Release|x86.ActiveCfg = Release|Win32
-		{592127C5-DD8C-47ED-8EBA-026B5848C374}.Release|x86.Build.0 = Release|Win32
 		{497433FE-B252-4985-A504-54EB791F57F4}.Debug|x64.ActiveCfg = Debug|x64
 		{497433FE-B252-4985-A504-54EB791F57F4}.Debug|x64.Build.0 = Debug|x64
 		{497433FE-B252-4985-A504-54EB791F57F4}.Debug|x86.ActiveCfg = Debug|Win32
diff --git a/src/esni.c b/src/esni.c
deleted file mode 100644
index 3bd0bf0..0000000
--- a/src/esni.c
+++ /dev/null
@@ -1,246 +0,0 @@
-/*
- * Copyright (c) 2018 Fastly, Kazuho Oku
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to
- * deal in the Software without restriction, including without limitation the
- * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
- * sell copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
- * IN THE SOFTWARE.
- */
-#include <assert.h>
-#include <getopt.h>
-#include <inttypes.h>
-#include <stdio.h>
-#include <string.h>
-#ifdef _WINDOWS
-#include "..\picotls\wincompat.h"
-#ifndef _CRT_SECURE_NO_WARNINGS
-#define _CRT_SECURE_NO_WARNINGS
-#endif
-#pragma warning(disable : 4996)
-#else
-#include <strings.h>
-#endif
-#include <time.h>
-#define OPENSSL_API_COMPAT 0x00908000L
-#include <openssl/err.h>
-#if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L
-#include <openssl/provider.h>
-#endif
-#include <openssl/engine.h>
-#include <openssl/pem.h>
-#include "picotls.h"
-#include "picotls/pembase64.h"
-#include "picotls/openssl.h"
-
-static int emit_esni(ptls_key_exchange_context_t **key_exchanges, ptls_cipher_suite_t **cipher_suites, uint16_t padded_length,
-                     uint64_t not_before, uint64_t lifetime, char const *published_sni, char const *file_output)
-{
-    ptls_buffer_t buf;
-    ptls_key_exchange_context_t *ctx[256] = {NULL};
-    int ret;
-
-    ptls_buffer_init(&buf, "", 0);
-
-    ptls_buffer_push16(&buf, PTLS_ESNI_VERSION_DRAFT03);
-    ptls_buffer_push(&buf, 0, 0, 0, 0); /* checksum, filled later */
-    if (published_sni != NULL) {
-        ptls_buffer_push_block(&buf, 2, { ptls_buffer_pushv(&buf, published_sni, strlen(published_sni)); });
-    } else {
-        ptls_buffer_push16(&buf, 0);
-    }
-    ptls_buffer_push_block(&buf, 2, {
-        size_t i;
-        for (i = 0; key_exchanges[i] != NULL; ++i) {
-            ptls_buffer_push16(&buf, key_exchanges[i]->algo->id);
-            ptls_buffer_push_block(&buf, 2,
-                                   { ptls_buffer_pushv(&buf, key_exchanges[i]->pubkey.base, key_exchanges[i]->pubkey.len); });
-        }
-    });
-    ptls_buffer_push_block(&buf, 2, {
-        size_t i;
-        for (i = 0; cipher_suites[i] != NULL; ++i)
-            ptls_buffer_push16(&buf, cipher_suites[i]->id);
-    });
-    ptls_buffer_push16(&buf, padded_length);
-    ptls_buffer_push64(&buf, not_before);
-    ptls_buffer_push64(&buf, not_before + lifetime - 1);
-    ptls_buffer_push_block(&buf, 2, {});
-    { /* fill checksum */
-        uint8_t d[PTLS_SHA256_DIGEST_SIZE];
-        ptls_calc_hash(&ptls_openssl_sha256, d, buf.base, buf.off);
-        memcpy(buf.base + 2, d, 4);
-    }
-
-    if (file_output != NULL) {
-        FILE *fo = fopen(file_output, "wb");
-        if (fo == NULL) {
-            fprintf(stderr, "failed to open file:%s:%s\n", optarg, strerror(errno));
-            goto Exit;
-        } else {
-            fwrite(buf.base, 1, buf.off, fo);
-            fclose(fo);
-        }
-    } else {
-        /* emit the structure to stdout */
-        fwrite(buf.base, 1, buf.off, stdout);
-        fflush(stdout);
-    }
-
-    ret = 0;
-Exit : {
-    size_t i;
-    for (i = 0; ctx[i] != NULL; ++i)
-        ctx[i]->on_exchange(ctx + i, 1, NULL, ptls_iovec_init(NULL, 0));
-}
-    ptls_buffer_dispose(&buf);
-    return ret;
-}
-
-static void usage(const char *cmd, int status)
-{
-    printf("picotls-esni - generates an ESNI Resource Record\n"
-           "\n"
-           "Usage: %s [options]\n"
-           "Options:\n"
-           "  -n <published-sni>  published sni value\n"
-           "  -K <key-file>       private key files (repeat the option to include multiple\n"
-           "                      keys)\n"
-           "  -c <cipher-suite>   aes128-gcm, chacha20-poly1305, ...\n"
-           "  -d <days>           number of days until expiration (default: 90)\n"
-           "  -p <padded-length>  padded length (default: 260)\n"
-           "  -o <output-file>    write output to specified file instead of stdout\n"
-           "                      (use on Windows as stdout is not binary there)\n"
-           "  -h                  prints this help\n"
-           "\n"
-           "-c and -x can be used multiple times.\n"
-           "\n",
-           cmd);
-    exit(status);
-}
-
-int main(int argc, char **argv)
-{
-    char const *published_sni = NULL;
-    char const *file_output = NULL;
-    ERR_load_crypto_strings();
-    OpenSSL_add_all_algorithms();
-#if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L
-    (void)OSSL_PROVIDER_load(NULL, "default");
-#elif !defined(OPENSSL_NO_ENGINE)
-    /* Load all compiled-in ENGINEs */
-    ENGINE_load_builtin_engines();
-    ENGINE_register_all_ciphers();
-    ENGINE_register_all_digests();
-#endif
-
-    struct {
-        ptls_key_exchange_context_t *elements[256];
-        size_t count;
-    } key_exchanges = {{NULL}, 0};
-    struct {
-        ptls_cipher_suite_t *elements[256];
-        size_t count;
-    } cipher_suites = {{NULL}, 0};
-    uint16_t padded_length = 260;
-    uint64_t lifetime = 90 * 86400;
-
-    int ch;
-
-    while ((ch = getopt(argc, argv, "n:K:c:d:p:o:h")) != -1) {
-        switch (ch) {
-        case 'n':
-            published_sni = optarg;
-            break;
-        case 'K': {
-            FILE *fp;
-            EVP_PKEY *pkey;
-
-            if ((fp = fopen(optarg, "rt")) == NULL) {
-                fprintf(stderr, "failed to open file:%s:%s\n", optarg, strerror(errno));
-                exit(1);
-            }
-
-            if ((pkey = PEM_read_PrivateKey(fp, NULL, NULL, NULL)) == NULL) {
-                fprintf(stderr, "failed to read private key from file:%s:%s\n", optarg, strerror(errno));
-                exit(1);
-            }
-            fclose(fp);
-            if (ptls_openssl_create_key_exchange(key_exchanges.elements + key_exchanges.count++, pkey) != 0) {
-                fprintf(stderr, "unknown type of private key found in file:%s\n", optarg);
-                exit(1);
-            }
-            EVP_PKEY_free(pkey);
-        } break;
-        case 'c': {
-            size_t i;
-            for (i = 0; ptls_openssl_cipher_suites[i] != NULL; ++i)
-                if (strcasecmp(ptls_openssl_cipher_suites[i]->aead->name, optarg) == 0)
-                    break;
-            if (ptls_openssl_cipher_suites[i] == NULL) {
-                fprintf(stderr, "unknown cipher-suite: %s\n", optarg);
-                exit(1);
-            }
-            cipher_suites.elements[cipher_suites.count++] = ptls_openssl_cipher_suites[i];
-        } break;
-        case 'd':
-            if (sscanf(optarg, "%" SCNu64, &lifetime) != 1 || lifetime == 0) {
-                fprintf(stderr, "lifetime must be a positive integer\n");
-                exit(1);
-            }
-            lifetime *= 86400; /* convert to seconds */
-            break;
-        case 'p':
-#ifdef _WINDOWS
-            if (sscanf_s(optarg, "%" SCNu16, &padded_length) != 1 || padded_length == 0) {
-                fprintf(stderr, "padded length must be a positive integer\n");
-                exit(1);
-            }
-#else
-            if (sscanf(optarg, "%" SCNu16, &padded_length) != 1 || padded_length == 0) {
-                fprintf(stderr, "padded length must be a positive integer\n");
-                exit(1);
-            }
-#endif
-            break;
-        case 'o':
-            file_output = optarg;
-            break;
-        case 'h':
-            usage(argv[0], 0);
-            break;
-        default:
-            usage(argv[0], 1);
-            break;
-        }
-    }
-    if (cipher_suites.count == 0)
-        cipher_suites.elements[cipher_suites.count++] = &ptls_openssl_aes128gcmsha256;
-    if (key_exchanges.count == 0) {
-        fprintf(stderr, "no private key specified\n");
-        exit(1);
-    }
-
-    argc -= optind;
-    argv += optind;
-
-    if (emit_esni(key_exchanges.elements, cipher_suites.elements, padded_length, time(NULL), lifetime, published_sni,
-                  file_output) != 0) {
-        fprintf(stderr, "failed to generate ESNI private structure.\n");
-        exit(1);
-    }
-
-    return 0;
-}
diff --git a/t/cli.c b/t/cli.c
index a9a65c7..18bba34 100644
--- a/t/cli.c
+++ b/t/cli.c
@@ -57,6 +57,90 @@
 /* sentinels indicating that the endpoint is in benchmark mode */
 static const char input_file_is_benchmark[] = "is:benchmark";
 
+static struct {
+    ptls_iovec_t config_list;
+    struct {
+        struct {
+            ptls_hpke_kem_t *kem;
+            ptls_key_exchange_context_t *ctx;
+        } list[16];
+        size_t count;
+    } keyex;
+    struct {
+        ptls_iovec_t configs;
+        char *fn;
+    } retry;
+} ech;
+
+static ptls_hpke_kem_t *find_kem(ptls_key_exchange_algorithm_t *algo)
+{
+    for (size_t i = 0; ptls_openssl_hpke_kems[i] != NULL; ++i)
+        if (ptls_openssl_hpke_kems[i]->keyex == algo)
+            return ptls_openssl_hpke_kems[i];
+
+    fprintf(stderr, "HPKE KEM not found for %s\n", algo->name);
+    return NULL;
+}
+
+static ptls_aead_context_t *create_ech_opener(ptls_ech_create_opener_t *self, ptls_hpke_kem_t **kem,
+                                              ptls_hpke_cipher_suite_t **cipher, ptls_t *tls, uint8_t config_id,
+                                              ptls_hpke_cipher_suite_id_t cipher_id, ptls_iovec_t enc, ptls_iovec_t info_prefix)
+{
+    const uint8_t *src = ech.config_list.base, *const end = src + ech.config_list.len;
+    size_t index = 0;
+    int ret = 0;
+
+    /* look for the cipher implementation; this should better be specific to each ECHConfig (as each of them may advertise different
+     * set of values) */
+    *cipher = NULL;
+    for (size_t i = 0; ptls_openssl_hpke_cipher_suites[i] != NULL; ++i) {
+        if (ptls_openssl_hpke_cipher_suites[i]->id.kdf == cipher_id.kdf &&
+            ptls_openssl_hpke_cipher_suites[i]->id.aead == cipher_id.aead) {
+            *cipher = ptls_openssl_hpke_cipher_suites[i];
+            break;
+        }
+    }
+    if (*cipher == NULL)
+        goto Exit;
+
+    ptls_decode_open_block(src, end, 2, {
+        uint16_t version;
+        if ((ret = ptls_decode16(&version, &src, end)) != 0)
+            goto Exit;
+        do {
+            ptls_decode_open_block(src, end, 2, {
+                if (src == end) {
+                    ret = PTLS_ALERT_DECODE_ERROR;
+                    goto Exit;
+                }
+                if (*src == config_id) {
+                    /* this is the ECHConfig that we have been looking for */
+                    if (index >= ech.keyex.count) {
+                        fprintf(stderr, "ECH key missing for config %zu\n", index);
+                        return NULL;
+                    }
+                    uint8_t *info = malloc(info_prefix.len + end - (src - 4));
+                    memcpy(info, info_prefix.base, info_prefix.len);
+                    memcpy(info + info_prefix.len, src - 4, end - (src - 4));
+                    ptls_aead_context_t *aead;
+                    ptls_hpke_setup_base_r(ech.keyex.list[index].kem, *cipher, ech.keyex.list[index].ctx, &aead, enc,
+                                           ptls_iovec_init(info, info_prefix.len + end - (src - 4)));
+                    free(info);
+                    *kem = ech.keyex.list[index].kem;
+                    return aead;
+                }
+                ++index;
+                src = end;
+            });
+        } while (src != end);
+    });
+
+Exit:
+    if (ret != 0)
+        fprintf(stderr, "ECH decode error:%d\n", ret);
+    return NULL;
+}
+
 static void shift_buffer(ptls_buffer_t *buf, size_t delta)
 {
     if (delta != 0) {
@@ -167,6 +251,18 @@
                     } else if (ret == PTLS_ERROR_IN_PROGRESS) {
                         /* ok */
                     } else {
+                        if (ret == PTLS_ALERT_ECH_REQUIRED) {
+                            assert(!ptls_is_server(tls));
+                            if (ech.retry.configs.base != NULL) {
+                                FILE *fp;
+                                if ((fp = fopen(ech.retry.fn, "wt")) == NULL) {
+                                    fprintf(stderr, "failed to write to ECH config file:%s:%s\n", ech.retry.fn, strerror(errno));
+                                    exit(1);
+                                }
+                                fwrite(ech.retry.configs.base, 1, ech.retry.configs.len, fp);
+                                fclose(fp);
+                            }
+                        }
                         if (encbuf.off != 0)
                             repeat_while_eintr(write(sockfd, encbuf.base, encbuf.off), { break; });
                         fprintf(stderr, "ptls_handshake:%d\n", ret);
@@ -332,8 +428,6 @@
 {
     int fd;
 
-    hsprop->client.esni_keys = resolve_esni_keys(server_name);
-
     if ((fd = socket(sa->sa_family, SOCK_STREAM, 0)) == 1) {
         perror("socket(2) failed");
         return 1;
@@ -344,7 +438,6 @@
     }
 
     int ret = handle_connection(fd, ctx, server_name, input_file, hsprop, request_key_update, keep_sender_open);
-    free(hsprop->client.esni_keys.base);
     return ret;
 }
 
@@ -366,12 +459,14 @@
            "  -I                   keep send side open after sending all data (client-only)\n"
            "  -j log-file          file to log probe events in JSON-Lines\n"
            "  -k key-file          specifies the credentials for signing the certificate\n"
+           "  -K key-file          ECH private key for each ECH config provided by -E\n"
            "  -l log-file          file to log events (incl. traffic secrets)\n"
            "  -n                   negotiates the key exchange method (i.e. wait for HRR)\n"
            "  -N named-group       named group to be used (default: secp256r1)\n"
            "  -s session-file      file to read/write the session ticket\n"
            "  -S                   require public key exchange when resuming a session\n"
-           "  -E esni-file         file that stores ESNI data generated by picotls-esni\n"
+           "  -E echconfiglist     file that contains ECHConfigList; overwritten when\n"
+           "                       receiving retry_configs from the server\n"
            "  -e                   when resuming a session, send first 8,192 bytes of input\n"
            "                       as early data\n"
            "  -r public-key-file   use raw public keys (RFC 7250). When set and running as a\n"
@@ -425,13 +520,16 @@
 
     ptls_key_exchange_algorithm_t *key_exchanges[128] = {NULL};
     ptls_cipher_suite_t *cipher_suites[128] = {NULL};
-    ptls_context_t ctx = {ptls_openssl_random_bytes, &ptls_get_time, key_exchanges, cipher_suites};
+    ptls_ech_create_opener_t ech_opener = {.cb = create_ech_opener};
+    ptls_context_t ctx = {
+        .random_bytes = ptls_openssl_random_bytes,
+        .get_time = &ptls_get_time,
+        .key_exchanges = key_exchanges,
+        .cipher_suites = cipher_suites,
+        .ech = {.client = {ptls_openssl_hpke_cipher_suites, ptls_openssl_hpke_kems}, .server = {NULL /* activated by -K option */}},
+    };
     ptls_handshake_properties_t hsprop = {{{{NULL}}}};
-    const char *host, *port, *input_file = NULL, *esni_file = NULL;
-    struct {
-        ptls_key_exchange_context_t *elements[16];
-        size_t count;
-    } esni_key_exchanges;
+    const char *host, *port, *input_file = NULL;
     int is_server = 0, use_early_data = 0, request_key_update = 0, keep_sender_open = 0, ch;
     struct sockaddr_storage sa;
     socklen_t salen;
@@ -496,27 +594,41 @@
         case 'S':
             ctx.require_dhe_on_psk = 1;
             break;
-        case 'E':
-            esni_file = optarg;
-            break;
+        case 'E': {
+            FILE *fp;
+            if ((fp = fopen(optarg, "rt")) == NULL) {
+                fprintf(stderr, "failed to open ECHConfigList file:%s:%s\n", optarg, strerror(errno));
+                return 1;
+            }
+            ech.config_list.base = malloc(65536);
+            if ((ech.config_list.len = fread(ech.config_list.base, 1, 65536, fp)) == 65536) {
+                fprintf(stderr, "ECHConfigList is too large:%s\n", optarg);
+                return 1;
+            }
+            fclose(fp);
+            ech.retry.fn = optarg;
+        } break;
         case 'K': {
             FILE *fp;
             EVP_PKEY *pkey;
             int ret;
             if ((fp = fopen(optarg, "rt")) == NULL) {
-                fprintf(stderr, "failed to open ESNI private key file:%s:%s\n", optarg, strerror(errno));
+                fprintf(stderr, "failed to open ECH private key file:%s:%s\n", optarg, strerror(errno));
                 return 1;
             }
             if ((pkey = PEM_read_PrivateKey(fp, NULL, NULL, NULL)) == NULL) {
                 fprintf(stderr, "failed to load private key from file:%s\n", optarg);
                 return 1;
             }
-            if ((ret = ptls_openssl_create_key_exchange(esni_key_exchanges.elements + esni_key_exchanges.count++, pkey)) != 0) {
+            if ((ret = ptls_openssl_create_key_exchange(&ech.keyex.list[ech.keyex.count].ctx, pkey)) != 0) {
                 fprintf(stderr, "failed to load private key from file:%s:picotls-error:%d", optarg, ret);
                 return 1;
             }
+            ech.keyex.list[ech.keyex.count].kem = find_kem(ech.keyex.list[ech.keyex.count].ctx->algo);
+            ++ech.keyex.count;
             EVP_PKEY_free(pkey);
             fclose(fp);
+            ctx.ech.server.create_opener = &ech_opener;
         } break;
         case 'l':
             setup_log_event(&ctx, optarg);
@@ -636,6 +748,8 @@
             hsprop.client.max_early_data_size = &max_early_data_size;
         }
         ctx.send_change_cipher_spec = 1;
+        hsprop.client.ech.configs = ech.config_list;
+        hsprop.client.ech.retry_configs = &ech.retry.configs;
     }
     if (key_exchanges[0] == NULL)
         key_exchanges[0] = &ptls_openssl_secp256r1;
@@ -644,13 +758,6 @@
         for (i = 0; ptls_openssl_cipher_suites[i] != NULL; ++i)
             cipher_suites[i] = ptls_openssl_cipher_suites[i];
     }
-    if (esni_file != NULL) {
-        if (esni_key_exchanges.count == 0) {
-            fprintf(stderr, "-E must be used together with -K\n");
-            return 1;
-        }
-        setup_esni(&ctx, esni_file, esni_key_exchanges.elements);
-    }
     if (argc != 2) {
         fprintf(stderr, "missing host and port\n");
         return 1;
diff --git a/t/hpke.c b/t/hpke.c
index 0330064..e2cb010 100644
--- a/t/hpke.c
+++ b/t/hpke.c
@@ -169,7 +169,8 @@
     for (ptls_hpke_kem_t **kem = all_kems; *kem != NULL; ++kem) {
         for (ptls_hpke_cipher_suite_t **cipher = all_ciphers; *cipher != NULL; ++cipher) {
             char namebuf[64];
-            sprintf(namebuf, "%s-%s/%s-%s", (*kem)->keyex->name, (*kem)->hash->name, (*cipher)->hash->name, (*cipher)->aead->name);
+            snprintf(namebuf, sizeof(namebuf), "%s-%s/%s-%s", (*kem)->keyex->name, (*kem)->hash->name, (*cipher)->hash->name,
+                     (*cipher)->aead->name);
             test_kem = *kem;
             test_cipher = *cipher;
             subtest(namebuf, test_one_hpke);
diff --git a/t/minicrypto.c b/t/minicrypto.c
index bf09040..30395cf 100644
--- a/t/minicrypto.c
+++ b/t/minicrypto.c
@@ -155,7 +155,7 @@
                              ptls_minicrypto_key_exchanges,
                              ptls_minicrypto_cipher_suites,
                              {&cert, 1},
-                             NULL,
+                             {NULL},
                              NULL,
                              NULL,
                              &sign_certificate.super};
diff --git a/t/openssl.c b/t/openssl.c
index 32984ac..8e79b15 100644
--- a/t/openssl.c
+++ b/t/openssl.c
@@ -335,6 +335,43 @@
     test_hpke(ptls_openssl_hpke_kems, ptls_openssl_hpke_cipher_suites);
 }
 
+static ptls_aead_context_t *create_ech_opener(ptls_ech_create_opener_t *self, ptls_hpke_kem_t **kem,
+                                              ptls_hpke_cipher_suite_t **cipher, ptls_t *tls, uint8_t config_id,
+                                              ptls_hpke_cipher_suite_id_t cipher_id, ptls_iovec_t enc, ptls_iovec_t info_prefix)
+{
+    static ptls_key_exchange_context_t *pem = NULL;
+    if (pem == NULL) {
+        pem = key_from_pem(ECH_PRIVATE_KEY);
+        assert(pem != NULL);
+    }
+
+    *cipher = NULL;
+    for (size_t i = 0; ptls_openssl_hpke_cipher_suites[i] != NULL; ++i) {
+        if (ptls_openssl_hpke_cipher_suites[i]->id.kdf == cipher_id.kdf &&
+            ptls_openssl_hpke_cipher_suites[i]->id.aead == cipher_id.aead) {
+            *cipher = ptls_openssl_hpke_cipher_suites[i];
+            break;
+        }
+    }
+    if (*cipher == NULL)
+        return NULL;
+
+    ptls_aead_context_t *aead = NULL;
+    ptls_buffer_t infobuf;
+    int ret;
+
+    ptls_buffer_init(&infobuf, "", 0);
+    ptls_buffer_pushv(&infobuf, info_prefix.base, info_prefix.len);
+    ptls_buffer_pushv(&infobuf, (const uint8_t *)ECH_CONFIG_LIST + 2,
+                      sizeof(ECH_CONFIG_LIST) - 3); /* choose the only ECHConfig from the list */
+    ret = ptls_hpke_setup_base_r(&ptls_openssl_hpke_kem_p256sha256, *cipher, pem, &aead, enc,
+                                 ptls_iovec_init(infobuf.base, infobuf.off));
+
+Exit:
+    ptls_buffer_dispose(&infobuf);
+    return aead;
+}
+
 #if ASYNC_TESTS
 
 static ENGINE *load_engine(const char *name)
@@ -486,6 +523,7 @@
 {
     ptls_openssl_sign_certificate_t openssl_sign_certificate;
     ptls_openssl_verify_certificate_t openssl_verify_certificate;
+    ptls_ech_create_opener_t ech_create_opener = {.cb = create_ech_opener};
 
     ERR_load_crypto_strings();
     OpenSSL_add_all_algorithms();
@@ -518,14 +556,15 @@
                                   .cipher_suites = ptls_openssl_cipher_suites,
                                   .tls12_cipher_suites = ptls_openssl_tls12_cipher_suites,
                                   .certificates = {&cert, 1},
+                                  .ech = {.client = {.ciphers = ptls_openssl_hpke_cipher_suites, .kems = ptls_openssl_hpke_kems},
+                                          .server = {.create_opener = &ech_create_opener,
+                                                     .retry_configs = {(uint8_t *)ECH_CONFIG_LIST, sizeof(ECH_CONFIG_LIST) - 1}}},
                                   .sign_certificate = &openssl_sign_certificate.super};
     assert(openssl_ctx.cipher_suites[0]->hash->digest_size == 48); /* sha384 */
     ptls_context_t openssl_ctx_sha256only = openssl_ctx;
     ++openssl_ctx_sha256only.cipher_suites;
     assert(openssl_ctx_sha256only.cipher_suites[0]->hash->digest_size == 32); /* sha256 */
 
-    ptls_key_exchange_context_t *esni_private_keys[2] = {key_from_pem(ESNI_SECP256R1KEY), NULL};
-
     ctx = ctx_peer = &openssl_ctx;
     verify_certificate = &openssl_verify_certificate.super;
     ADD_FFX_AES128_ALGORITHMS(openssl);
@@ -539,7 +578,6 @@
     subtest("ed25519-sign", test_ed25519_sign);
     subtest("cert-verify", test_cert_verify);
     subtest("picotls", test_picotls);
-    test_picotls_esni(esni_private_keys);
 
     ctx = ctx_peer = &openssl_ctx_sha256only;
     subtest("picotls", test_picotls);
@@ -561,7 +599,7 @@
                                      ptls_minicrypto_key_exchanges,
                                      ptls_minicrypto_cipher_suites,
                                      {&minicrypto_certificate, 1},
-                                     NULL,
+                                     {NULL},
                                      NULL,
                                      NULL,
                                      &minicrypto_sign_certificate.super};
@@ -573,6 +611,8 @@
     ctx_peer = &openssl_ctx;
     subtest("minicrypto vs.", test_picotls);
 
+    subtest("hpke", test_all_hpke);
+
 #if ASYNC_TESTS
     // switch to x25519 / aes128gcmsha256 as we run benchmarks
     static ptls_key_exchange_algorithm_t *fast_keyex[] = {&ptls_openssl_x25519, NULL}; // use x25519 for speed
@@ -599,10 +639,6 @@
     }
 #endif
 
-    esni_private_keys[0]->on_exchange(esni_private_keys, 1, NULL, ptls_iovec_init(NULL, 0));
-
-    subtest("hpke", test_all_hpke);
-
     int ret = done_testing();
 #if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L
     OSSL_PROVIDER_unload(dflt);
diff --git a/t/picotls.c b/t/picotls.c
index a4a802b..bc68ab9 100644
--- a/t/picotls.c
+++ b/t/picotls.c
@@ -44,6 +44,18 @@
     ok(ptls_server_name_is_ipaddr("2001:db8::2:1"));
 }
 
+static void test_extension_bitmap(void)
+{
+    struct st_ptls_extension_bitmap_t bitmap = {0};
+
+    /* disallowed extension is rejected */
+    ok(!extension_bitmap_testandset(&bitmap, PTLS_HANDSHAKE_TYPE_SERVER_HELLO, PTLS_EXTENSION_TYPE_COOKIE));
+
+    /* allowed extension is accepted first, rejected upon repetition */
+    ok(extension_bitmap_testandset(&bitmap, PTLS_HANDSHAKE_TYPE_SERVER_HELLO, PTLS_EXTENSION_TYPE_KEY_SHARE));
+    ok(!extension_bitmap_testandset(&bitmap, PTLS_HANDSHAKE_TYPE_SERVER_HELLO, PTLS_EXTENSION_TYPE_KEY_SHARE));
+}
+
 static void test_select_cipher(void)
 {
 #define C(x) ((x) >> 8) & 0xff, (x)&0xff
@@ -498,6 +510,96 @@
     ptls_buffer_dispose(&buf);
 }
 
+static void test_ech_decode_config(void)
+{
+    static ptls_hpke_kem_t p256 = {PTLS_HPKE_KEM_P256_SHA256}, *kems[] = {&p256, NULL};
+    static ptls_hpke_cipher_suite_t aes128gcmsha256 = {{PTLS_HPKE_HKDF_SHA256, PTLS_HPKE_AEAD_AES_128_GCM}},
+                                    *ciphers[] = {&aes128gcmsha256, NULL};
+    struct st_decoded_ech_config_t decoded;
+
+    { /* broken list */
+        const uint8_t *src = (const uint8_t *)"a", *end = src + 1;
+        int ret = decode_one_ech_config(kems, ciphers, &decoded, &src, end);
+        ok(ret == PTLS_ALERT_DECODE_ERROR);
+    }
+
+    {
+        ptls_iovec_t input = ptls_iovec_init(ECH_CONFIG_LIST, sizeof(ECH_CONFIG_LIST) - 1);
+        const uint8_t *src = input.base + 6 /* dive into ECHConfigContents */, *const end = input.base + input.len;
+        int ret = decode_one_ech_config(kems, ciphers, &decoded, &src, end);
+        ok(ret == 0);
+        ok(decoded.id == 0x12);
+        ok(decoded.kem == &p256);
+        ok(decoded.public_key.len == 65);
+        ok(decoded.public_key.base == input.base + 11);
+        ok(decoded.cipher == &aes128gcmsha256);
+        ok(decoded.max_name_length == 64);
+        ok(decoded.public_name.len == sizeof("example.com") - 1);
+        ok(memcmp(decoded.public_name.base, "example.com", sizeof("example.com") - 1) == 0);
+    }
+}
+
+static void test_rebuild_ch_inner(void)
+{
+    ptls_buffer_t buf;
+    ptls_buffer_init(&buf, "", 0);
+
+#define TEST(_expected_err)                                                                                                        \
+    do {                                                                                                                           \
+        const uint8_t *src = encoded_inner;                                                                                        \
+        buf.off = 0;                                                                                                               \
+        ok(rebuild_ch_inner_extensions(&buf, &src, encoded_inner + sizeof(encoded_inner), outer, outer + sizeof(outer)) ==         \
+           _expected_err);                                                                                                         \
+        if (_expected_err == 0) {                                                                                                  \
+            ok(src == encoded_inner + sizeof(encoded_inner));                                                                      \
+            ok(buf.off == sizeof(expected));                                                                                       \
+            ok(memcmp(buf.base, expected, sizeof(expected)) == 0);                                                                 \
+        }                                                                                                                          \
+    } while (0)
+
+    { /* replace none */
+        static const uint8_t encoded_inner[] = {0x00, 0x09, 0x12, 0x34, 0x00, 0x05, 0x68, 0x65, 0x6c, 0x6c, 0x6f},
+                             outer[] = {0xde, 0xad},
+                             expected[] = {0x00, 0x09, 0x12, 0x34, 0x00, 0x05, 0x68, 0x65, 0x6c, 0x6c, 0x6f};
+        TEST(0);
+    }
+
+    { /* replace one */
+        static const uint8_t encoded_inner[] = {0x00, 0x07, 0xfd, 0x00, 0x00, 0x03, 0x02, 0x00, 0x01},
+                             outer[] = {0x00, 0x01, 0x00, 0x05, 0x68, 0x65, 0x6c, 0x6c, 0x6f},
+                             expected[] = {0x00, 0x09, 0x00, 0x01, 0x00, 0x05, 0x68, 0x65, 0x6c, 0x6c, 0x6f};
+        TEST(0);
+    }
+
+    { /* replace multi */
+        static const uint8_t encoded_inner[] = {0x00, 0x13, 0x00, 0x01, 0x00, 0x01, 0x31, 0xfd, 0x00, 0x00, 0x05,
+                                                0x04, 0x00, 0x02, 0x00, 0x04, 0x00, 0x05, 0x00, 0x01, 0x35},
+                             outer[] = {0x00, 0x01, 0x00, 0x01, 0x41, 0x00, 0x02, 0x00, 0x01, 0x42, 0x00, 0x03, 0x00,
+                                        0x01, 0x43, 0x00, 0x04, 0x00, 0x01, 0x44, 0x00, 0x05, 0x00, 0x01, 0x45},
+                             expected[] = {0x00, 0x14, 0x00, 0x01, 0x00, 0x01, 0x31, 0x00, 0x02, 0x00, 0x01,
+                                           0x42, 0x00, 0x04, 0x00, 0x01, 0x44, 0x00, 0x05, 0x00, 0x01, 0x35};
+        TEST(0);
+    }
+
+    { /* outer extension not found */
+        static const uint8_t encoded_inner[] = {0x00, 0x13, 0x00, 0x01, 0x00, 0x01, 0x31, 0xfd, 0x00, 0x00, 0x05,
+                                                0x04, 0x00, 0x02, 0x00, 0x04, 0x00, 0x05, 0x00, 0x01, 0x35},
+                             outer[] = {0x00, 0x01, 0x00, 0x01, 0x41, 0x00, 0x02, 0x00, 0x01, 0x42, 0x00, 0x03, 0x00, 0x01, 0x43},
+                             expected[] = {0x00, 0x14, 0x00, 0x01, 0x00, 0x01, 0x31, 0x00, 0x02, 0x00, 0x01,
+                                           0x42, 0x00, 0x04, 0x00, 0x01, 0x44, 0x00, 0x05, 0x00, 0x01, 0x35};
+        TEST(PTLS_ALERT_ILLEGAL_PARAMETER);
+    }
+
+#undef TEST
+    ptls_buffer_dispose(&buf);
+}
+
+static void test_ech(void)
+{
+    subtest("decode-config", test_ech_decode_config);
+    subtest("rebuild_ch_inner", test_rebuild_ch_inner);
+}
+
 static struct {
     struct {
         uint8_t buf[32];
@@ -605,16 +707,12 @@
 #undef SET_RECORD
 }
 
-static int was_esni;
-
 static int save_client_hello(ptls_on_client_hello_t *self, ptls_t *tls, ptls_on_client_hello_parameters_t *params)
 {
     ptls_set_server_name(tls, (const char *)params->server_name.base, params->server_name.len);
     if (params->negotiated_protocols.count != 0)
         ptls_set_negotiated_protocol(tls, (const char *)params->negotiated_protocols.list[0].base,
                                      params->negotiated_protocols.list[0].len);
-    if (params->esni)
-        ++was_esni;
     return 0;
 }
 
@@ -633,6 +731,15 @@
     return 0;
 }
 
+static int can_ech(ptls_context_t *ctx, int is_server)
+{
+    if (is_server) {
+        return ctx->ech.server.create_opener != NULL;
+    } else {
+        return ctx->ech.client.ciphers != NULL;
+    }
+}
+
 static void test_handshake(ptls_iovec_t ticket, int mode, int expect_ticket, int check_ch, int require_client_authentication)
 {
     ptls_t *client, *server;
@@ -666,17 +773,17 @@
         ptls_set_server_name(client, "test.example.com", 0);
     }
 
+    if (can_ech(ctx, 0)) {
+        ptls_set_server_name(client, "test.example.com", 0);
+        client_hs_prop.client.ech.configs = ptls_iovec_init(ECH_CONFIG_LIST, sizeof(ECH_CONFIG_LIST) - 1);
+    }
+
     static ptls_on_extension_t cb = {on_extension_cb};
     ctx_peer->on_extension = &cb;
 
     if (require_client_authentication)
         ctx_peer->require_client_authentication = 1;
 
-    if (ctx_peer->esni != NULL) {
-        was_esni = 0;
-        client_hs_prop.client.esni_keys = ptls_iovec_init(ESNIKEYS, sizeof(ESNIKEYS) - 1);
-    }
-
     switch (mode) {
     case TEST_HANDSHAKE_HRR:
         client_hs_prop.client.negotiate_before_key_exchange = 1;
@@ -742,10 +849,14 @@
     ok(sbuf.off != 0);
     if (check_ch) {
         ok(ptls_get_server_name(server) != NULL);
-        ok(strcmp(ptls_get_server_name(server), "test.example.com") == 0);
+        if (can_ech(ctx, 0) && !can_ech(ctx_peer, 1)) {
+            /* server should be using CHouter.sni that includes the public name of the ECH extension */
+            ok(strcmp(ptls_get_server_name(server), "example.com") == 0);
+        } else {
+            ok(strcmp(ptls_get_server_name(server), "test.example.com") == 0);
+        }
         ok(ptls_get_negotiated_protocol(server) != NULL);
         ok(strcmp(ptls_get_negotiated_protocol(server), "h2") == 0);
-        ok(was_esni == (ctx_peer->esni != NULL));
     } else {
         ok(ptls_get_server_name(server) == NULL);
         ok(ptls_get_negotiated_protocol(server) == NULL);
@@ -890,6 +1001,14 @@
         decbuf.off = 0;
     }
 
+    if (can_ech(ctx_peer, 1) && can_ech(ctx, 0)) {
+        ok(ptls_is_ech_handshake(client, NULL, NULL));
+        ok(ptls_is_ech_handshake(server, NULL, NULL));
+    } else {
+        ok(!ptls_is_ech_handshake(client, NULL, NULL));
+        ok(!ptls_is_ech_handshake(server, NULL, NULL));
+    }
+
     ptls_buffer_dispose(&cbuf);
     ptls_buffer_dispose(&sbuf);
     ptls_buffer_dispose(&decbuf);
@@ -1260,6 +1379,63 @@
     ptls_buffer_dispose(&sbuf);
 }
 
+static void test_ech_config_mismatch(void)
+{
+    ptls_t *client, *server;
+    ptls_buffer_t cbuf, sbuf, decryptbuf;
+    size_t consumed;
+    int ret;
+    ptls_iovec_t retry_configs = {NULL};
+    ptls_handshake_properties_t client_hs_prop = {
+        .client.ech = {
+            .configs = ptls_iovec_init((void *)ECH_ALTERNATIVE_CONFIG_LIST, sizeof(ECH_ALTERNATIVE_CONFIG_LIST) - 1),
+            .retry_configs = &retry_configs,
+        }};
+
+    client = ptls_new(ctx, 0);
+    ptls_set_server_name(client, "test.example.com", 0);
+    server = ptls_new(ctx_peer, 1);
+    ptls_buffer_init(&cbuf, "", 0);
+    ptls_buffer_init(&sbuf, "", 0);
+    ptls_buffer_init(&decryptbuf, "", 0);
+
+    ret = ptls_handshake(client, &cbuf, NULL, NULL, &client_hs_prop);
+    ok(ret == PTLS_ERROR_IN_PROGRESS);
+
+    consumed = cbuf.off;
+    ret = ptls_handshake(server, &sbuf, cbuf.base, &consumed, NULL);
+    ok(ret == 0);
+    ok(cbuf.off == consumed);
+    cbuf.off = 0;
+
+    consumed = sbuf.off;
+    ret = ptls_handshake(client, &cbuf, sbuf.base, &consumed, &client_hs_prop);
+    ok(ret == PTLS_ALERT_ECH_REQUIRED);
+    ok(sbuf.off == consumed);
+    ok(retry_configs.len == sizeof(ECH_CONFIG_LIST) - 1);
+    ok(memcmp(retry_configs.base, ECH_CONFIG_LIST, retry_configs.len) == 0);
+    sbuf.off = 0;
+
+    consumed = cbuf.off;
+    ret = ptls_handshake(server, &sbuf, cbuf.base, &consumed, NULL);
+    ok(ret == 0);
+    ok(consumed < cbuf.off);
+    memmove(cbuf.base, cbuf.base + consumed, cbuf.off - consumed);
+    cbuf.off -= consumed;
+
+    consumed = cbuf.off;
+    ret = ptls_receive(server, &decryptbuf, cbuf.base, &consumed);
+    ok(ret == PTLS_ALERT_TO_PEER_ERROR(PTLS_ALERT_ECH_REQUIRED));
+    ok(cbuf.off == consumed);
+
+    ptls_free(client);
+    ptls_free(server);
+    ptls_buffer_dispose(&cbuf);
+    ptls_buffer_dispose(&sbuf);
+    ptls_buffer_dispose(&decryptbuf);
+    free(retry_configs.base);
+}
+
 typedef uint8_t traffic_secrets_t[2 /* is_enc */][4 /* epoch */][PTLS_MAX_DIGEST_SIZE /* octets */];
 
 static int on_update_traffic_key(ptls_update_traffic_key_t *self, ptls_t *tls, int is_enc, size_t epoch, const void *secret)
@@ -1545,6 +1721,28 @@
     ctx_peer->max_early_data_size = 0;
 }
 
+static void test_all_handshakes_core(void)
+{
+    subtest("full-handshake", test_full_handshake);
+    subtest("full-handshake+client-auth", test_full_handshake_with_client_authentication);
+    subtest("hrr-handshake", test_hrr_handshake);
+    /* resumption does not work when the client offers ECH but the server does not recognize that */
+    if (!(can_ech(ctx, 0) && !can_ech(ctx_peer, 1))) {
+        subtest("resumption", test_resumption);
+        subtest("resumption-different-preferred-key-share", test_resumption_different_preferred_key_share);
+        subtest("resumption-with-client-authentication", test_resumption_with_client_authentication);
+    }
+    subtest("async-sign-certificate", test_async_sign_certificate);
+    subtest("enforce-retry-stateful", test_enforce_retry_stateful);
+    if (!(can_ech(ctx_peer, 1) && can_ech(ctx, 0))) {
+        subtest("hrr-stateless-handshake", test_hrr_stateless_handshake);
+        subtest("enforce-retry-stateless", test_enforce_retry_stateless);
+        subtest("stateless-hrr-aad-change", test_stateless_hrr_aad_change);
+    }
+    subtest("key-update", test_key_update);
+    subtest("handshake-api", test_handshake_api);
+}
+
 static void test_all_handshakes(void)
 {
     ptls_sign_certificate_t server_sc = {sign_certificate};
@@ -1557,24 +1755,27 @@
         ctx->sign_certificate = &client_sc;
     }
 
-    subtest("full-handshake", test_full_handshake);
-    subtest("full-handshake-with-client-authentication", test_full_handshake_with_client_authentication);
-    subtest("hrr-handshake", test_hrr_handshake);
-    subtest("hrr-stateless-handshake", test_hrr_stateless_handshake);
-    subtest("resumption", test_resumption);
-    subtest("resumption-different-preferred-key-share", test_resumption_different_preferred_key_share);
-    subtest("resumption-with-client-authentication", test_resumption_with_client_authentication);
+    struct {
+        ptls_ech_create_opener_t *create_opener;
+        ptls_hpke_cipher_suite_t **client_ciphers;
+    } orig_ech = {ctx_peer->ech.server.create_opener, ctx->ech.client.ciphers};
 
-    subtest("async-sign-certificate", test_async_sign_certificate);
+    /* first run tests wo. ECH */
+    ctx_peer->ech.server.create_opener = NULL;
+    ctx->ech.client.ciphers = NULL;
+    subtest("no-ech", test_all_handshakes_core);
+    ctx_peer->ech.server.create_opener = orig_ech.create_opener;
+    ctx->ech.client.ciphers = orig_ech.client_ciphers;
 
-    subtest("enforce-retry-stateful", test_enforce_retry_stateful);
-    subtest("enforce-retry-stateless", test_enforce_retry_stateless);
-
-    subtest("stateless-hrr-aad-change", test_stateless_hrr_aad_change);
-
-    subtest("key-update", test_key_update);
-
-    subtest("handshake-api", test_handshake_api);
+    if (can_ech(ctx_peer, 1) && can_ech(ctx, 0)) {
+        subtest("ech", test_all_handshakes_core);
+        if (ctx != ctx_peer) {
+            ctx->ech.client.ciphers = NULL;
+            subtest("ech (server-only)", test_all_handshakes_core);
+            ctx->ech.client.ciphers = orig_ech.client_ciphers;
+        }
+        subtest("ech-config-mismatch", test_ech_config_mismatch);
+    }
 
     ctx_peer->sign_certificate = sc_orig;
 
@@ -1760,7 +1961,8 @@
 void test_picotls(void)
 {
     subtest("is_ipaddr", test_is_ipaddr);
-    subtest("select_cypher", test_select_cipher);
+    subtest("extension_bitmap", test_extension_bitmap);
+    subtest("select_cipher", test_select_cipher);
     subtest("sha256", test_sha256);
     subtest("sha384", test_sha384);
     subtest("hmac-sha256", test_hmac_sha256);
@@ -1774,6 +1976,7 @@
     subtest("chacha20", test_chacha20);
     subtest("ffx", test_ffx);
     subtest("base64-decode", test_base64_decode);
+    subtest("ech", test_ech);
     subtest("fragmented-message", test_fragmented_message);
     subtest("handshake", test_all_handshakes);
     subtest("quic", test_quic);
@@ -1781,17 +1984,6 @@
     subtest("ptls_escape_json_unsafe_string", test_escape_json_unsafe_string);
 }
 
-void test_picotls_esni(ptls_key_exchange_context_t **keys)
-{
-    ptls_esni_context_t esni, *esni_list[] = {&esni, NULL};
-    ptls_esni_init_context(ctx_peer, &esni, ptls_iovec_init(ESNIKEYS, sizeof(ESNIKEYS) - 1), keys);
-    ctx_peer->esni = esni_list;
-
-    subtest("esni-handshake", test_picotls);
-
-    ctx_peer->esni = NULL;
-}
-
 void test_key_exchange(ptls_key_exchange_algorithm_t *client, ptls_key_exchange_algorithm_t *server)
 {
     ptls_key_exchange_context_t *ctx;
diff --git a/t/test.h b/t/test.h
index 0d2ecbb..e19f629 100644
--- a/t/test.h
+++ b/t/test.h
@@ -51,18 +51,24 @@
     "\x1d\x99\x42\xe0\xa2\xb7\x75\xbb\x14\x03\x79\x9a\xf6\x07\xd8\xa5\xab\x2b\x3a\x70\x8b\x77\x85\x70\x8a\x98\x38\x9b\x35\x09\xf6" \
     "\x62\x6b\x29\x4a\xa7\xa7\xf9\x3b\xde\xd8\xc8\x90\x57\xf2\x76\x2a\x23\x0b\x01\x68\xc6\x9a\xf2"
 
-/* secp256r1 key that lasts until 2028 */
-#define ESNIKEYS                                                                                                                   \
-    "\xff\x02\xcf\x27\xde\x17\x00\x0b\x65\x78\x61\x6d\x70\x6c\x65\x2e\x63\x6f\x6d\x00\x45"                                         \
-    "\x00\x17\x00\x41\x04\x3e\xee\xf7\x10\xe3\x75\x07\xa8\xfb\x3e\xfc\x62\x50\x24\x95\xa0"                                         \
-    "\x61\x6e\xff\x6b\x63\x0f\xa3\xfd\xcc\x33\x36\xd0\xb1\x2d\x55\xba\xb0\x06\xbd\xb4\x29"                                         \
-    "\x82\xc6\xd9\xee\x66\x84\xa9\x63\x94\x44\xbe\x04\xe7\xee\xcf\xab\xc2\xc9\xdd\x40\xe6"                                         \
-    "\xc8\x89\x88\xed\x94\x86\x00\x02\x13\x01\x01\x04\x00\x00\x00\x00\x5d\x1c\xc0\x63\x00"                                         \
-    "\x00\x4e\x94\xee\x6b\xc0\x62\x00\x00"
-#define ESNI_SECP256R1KEY                                                                                                          \
-    "-----BEGIN EC PARAMETERS-----\nBggqhkjOPQMBBw==\n-----END EC PARAMETERS-----\n-----BEGIN EC PRIVATE "                         \
-    "KEY-----\nMHcCAQEEIGrRVTfTXuOVewLt/g+Ugvg9XW/g4lGXrkZ8fdYaYuJCoAoGCCqGSM49\nAwEHoUQDQgAEPu73EON1B6j7PvxiUCSVoGFu/"            \
-    "2tjD6P9zDM20LEtVbqwBr20KYLG\n2e5mhKljlES+BOfuz6vCyd1A5siJiO2Uhg==\n-----END EC PRIVATE KEY-----\n"
+/* test vector using RFC 9180 A.3 */
+#define ECH_CONFIG_LIST                                                                                                            \
+    "\x00\x63\xfe\x0d\x00\x5f\x12\x00\x10\x00\x41\x04\xfe\x8c\x19\xce\x09\x05\x19\x1e\xbc\x29\x8a\x92\x45\x79\x25\x31\xf2\x6f\x0c" \
+    "\xec\xe2\x46\x06\x39\xe8\xbc\x39\xcb\x7f\x70\x6a\x82\x6a\x77\x9b\x4c\xf9\x69\xb8\xa0\xe5\x39\xc7\xf6\x2f\xb3\xd3\x0a\xd6\xaa" \
+    "\x8f\x80\xe3\x0f\x1d\x12\x8a\xaf\xd6\x8a\x2c\xe7\x2e\xa0\x00\x08\x00\x02\x00\x02\x00\x01\x00\x01\x40\x0b\x65\x78\x61\x6d\x70" \
+    "\x6c\x65\x2e\x63\x6f\x6d\x00\x00"
+/* another config using different ID and public key */
+#define ECH_ALTERNATIVE_CONFIG_LIST                                                                                                \
+    "\x00\x63\xfe\x0d\x00\x5f\x13\x00\x10\x00\x41\x04\x39\xd2\xc8\xfb\x6f\xcc\x79\x72\xb2\x28\x20\x33\xad\xc4\x97\x01\xff\xd6\x91" \
+    "\x76\xaa\x1a\x11\xd9\x36\x51\xb1\xb1\x29\xd9\x0e\xe0\x96\x1f\x75\xfa\x19\xff\xec\xe2\xd7\x91\xab\xf5\x29\x39\x35\x66\x90\xbf" \
+    "\xf3\x56\x73\xcf\xc1\x42\xc1\x6e\x99\x25\xd2\xab\xdb\xb6\x00\x08\x00\x02\x00\x02\x00\x01\x00\x01\x40\x0b\x65\x78\x61\x6d\x70" \
+    "\x6c\x65\x2e\x63\x6f\x6d\x00\x00"
+#define ECH_PRIVATE_KEY                                                                                                            \
+    "-----BEGIN PRIVATE KEY-----\n"                                                                                                \
+    "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg885/2uV+GjENh/Hr\n"                                                           \
+    "vebzKL4Kmc28rfTWWJzyneS4/9KhRANCAAT+jBnOCQUZHrwpipJFeSUx8m8M7OJG\n"                                                           \
+    "BjnovDnLf3Bqgmp3m0z5abig5TnH9i+z0wrWqo+A4w8dEoqv1oos5y6g\n"                                                                   \
+    "-----END PRIVATE KEY-----\n"
 
 extern ptls_context_t *ctx, *ctx_peer;
 extern ptls_verify_certificate_t *verify_certificate;
@@ -102,7 +108,6 @@
 
 void test_key_exchange(ptls_key_exchange_algorithm_t *client, ptls_key_exchange_algorithm_t *server);
 void test_picotls(void);
-void test_picotls_esni(ptls_key_exchange_context_t **keys);
 
 void test_hpke(ptls_hpke_kem_t **all_kems, ptls_hpke_cipher_suite_t **all_ciphers);
 
diff --git a/t/util.h b/t/util.h
index 6c1a9bc..3c59243 100644
--- a/t/util.h
+++ b/t/util.h
@@ -159,38 +159,6 @@
     ctx->verify_certificate = &vc.super;
 }
 
-static inline void setup_esni(ptls_context_t *ctx, const char *esni_fn, ptls_key_exchange_context_t **key_exchanges)
-{
-    uint8_t esnikeys[65536];
-    size_t esnikeys_len;
-    int ret = 0;
-
-    { /* read esnikeys */
-        FILE *fp;
-        if ((fp = fopen(esni_fn, "rb")) == NULL) {
-            fprintf(stderr, "failed to open file:%s:%s\n", esni_fn, strerror(errno));
-            exit(1);
-        }
-        esnikeys_len = fread(esnikeys, 1, sizeof(esnikeys), fp);
-        if (esnikeys_len == 0 || !feof(fp)) {
-            fprintf(stderr, "failed to load ESNI data from file:%s\n", esni_fn);
-            exit(1);
-        }
-        fclose(fp);
-    }
-
-    if ((ctx->esni = (ptls_esni_context_t **)malloc(sizeof(*ctx->esni) * 2)) == NULL ||
-        (*ctx->esni = (ptls_esni_context_t *)malloc(sizeof(**ctx->esni))) == NULL) {
-        fprintf(stderr, "no memory\n");
-        exit(1);
-    }
-
-    if ((ret = ptls_esni_init_context(ctx, ctx->esni[0], ptls_iovec_init(esnikeys, esnikeys_len), key_exchanges)) != 0) {
-        fprintf(stderr, "failed to parse ESNI data of file:%s:error=%d\n", esni_fn, ret);
-        exit(1);
-    }
-}
-
 struct st_util_log_event_t {
     ptls_log_event_t super;
     FILE *fp;
@@ -327,44 +295,6 @@
     return 1;
 }
 
-static inline ptls_iovec_t resolve_esni_keys(const char *server_name)
-{
-    char esni_name[256], *base64;
-    uint8_t answer[1024];
-    ns_msg msg;
-    ns_rr rr;
-    ptls_buffer_t decode_buf;
-    ptls_base64_decode_state_t ds;
-    int answer_len;
-
-    char *buf = "";
-    ptls_buffer_init(&decode_buf, buf, 0);
-
-    if (snprintf(esni_name, sizeof(esni_name), "_esni.%s", server_name) > sizeof(esni_name) - 1)
-        goto Error;
-    if ((answer_len = res_query(esni_name, ns_c_in, ns_t_txt, answer, sizeof(answer))) <= 0)
-        goto Error;
-    if (ns_initparse(answer, answer_len, &msg) != 0)
-        goto Error;
-    if (ns_msg_count(msg, ns_s_an) < 1)
-        goto Error;
-    if (ns_parserr(&msg, ns_s_an, 0, &rr) != 0)
-        goto Error;
-    base64 = (char *)ns_rr_rdata(rr);
-    if (!normalize_txt((uint8_t *)base64, ns_rr_rdlen(rr)))
-        goto Error;
-
-    ptls_base64_decode_init(&ds);
-    if (ptls_base64_decode(base64, &ds, &decode_buf) != 0)
-        goto Error;
-    assert(decode_buf.is_allocated);
-
-    return ptls_iovec_init(decode_buf.base, decode_buf.off);
-Error:
-    ptls_buffer_dispose(&decode_buf);
-    return ptls_iovec_init(NULL, 0);
-}
-
 /* The ptls_repeat_while_eintr macro will repeat a function call (block) if it is interrupted (EINTR) before completion. If failing
  * for other reason, the macro executes the exit block, such as either { break; } or { goto Fail; }.
  */