it works (for the most straight-forward case)
diff --git a/include/picotls.h b/include/picotls.h
index 80f9e05..9abdc6d 100644
--- a/include/picotls.h
+++ b/include/picotls.h
@@ -717,6 +717,13 @@
     int (*cb)(struct st_ptls_decompress_certificate_t *self, ptls_t *tls, uint16_t algorithm, ptls_iovec_t output,
               ptls_iovec_t input);
 } ptls_decompress_certificate_t;
+/**
+ * ECH: creates the AEAD context to be used for "Open"-ing inner CH. Given `condig_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(ptls_aead_context_t *, ech_create_opener, ptls_t *tls, uint8_t config_id, ptls_hpke_cipher_suite_t *cipher,
+                   ptls_iovec_t enc, ptls_iovec_t info_prefix);
 
 /**
  * the configuration
@@ -751,6 +758,7 @@
     struct {
         ptls_hpke_kem_t **kems;
         ptls_hpke_cipher_suite_t **ciphers;
+        ptls_ech_create_opener_t *create_opener;
     } ech;
     /**
      *
diff --git a/lib/picotls.c b/lib/picotls.c
index 75749d5..81bc646 100644
--- a/lib/picotls.c
+++ b/lib/picotls.c
@@ -74,6 +74,10 @@
 #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
+
+static const ptls_iovec_t ech_info_prefix = {(uint8_t *)"tls ech", 8};
 
 #define PTLS_SERVER_CERTIFICATE_VERIFY_CONTEXT_STRING "TLS 1.3, server CertificateVerify"
 #define PTLS_CLIENT_CERTIFICATE_VERIFY_CONTEXT_STRING "TLS 1.3, client CertificateVerify"
@@ -331,6 +335,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_t *cipher;
+        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];
@@ -340,12 +359,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 {
@@ -932,7 +947,6 @@
 
 static void client_free_ech(struct st_ptls_ech_client_t *ech)
 {
-    free(ech->public_name);
     free(ech->enc.base);
     if (ech->aead != NULL)
         ptls_aead_free(ech->aead);
@@ -1037,8 +1051,7 @@
     memcpy(ech->public_name, public_name.base, public_name.len);
     ech->public_name[public_name.len] = '\0';
 
-    ptls_buffer_pushv(&infobuf, "tls ech", 7);
-    ptls_buffer_push(&infobuf, 0);
+    ptls_buffer_pushv(&infobuf, ech_info_prefix.base, ech_info_prefix.len);
     ptls_buffer_pushv(&infobuf, ech_config.base, ech_config.len);
 
     ret = ptls_hpke_setup_base_s(kem, cipher, &ech->enc, &ech->aead, public_key, ptls_iovec_init(infobuf.base, infobuf.off));
@@ -1065,13 +1078,13 @@
 
     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) {
-                    const uint8_t *config_start = src;
                     uint8_t config_id;
                     ptls_hpke_kem_t *kem;
                     ptls_iovec_t public_key;
@@ -1966,23 +1979,27 @@
             if (ech != NULL) {
                 if (mode == ENCODE_CH_MODE_OUTER) {
                     buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_ENCRYPTED_CLIENT_HELLO, {
-                        ptls_buffer_push(sendbuf, 0);
+                        ptls_buffer_push(sendbuf, PTLS_ECH_CLIENT_HELLO_TYPE_OUTER, ech->config_id);
                         ptls_buffer_push16(sendbuf, ech->cipher->id.kdf);
                         ptls_buffer_push16(sendbuf, ech->cipher->id.aead);
                         ptls_buffer_push_block(sendbuf, 2, { ptls_buffer_pushv(sendbuf, ech->enc.base, ech->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;
+                        ptls_buffer_push_block(sendbuf, 2, {
+                            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 {
-                    buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_ENCRYPTED_CLIENT_HELLO, { ptls_buffer_push(sendbuf, 1); });
+                    buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_ENCRYPTED_CLIENT_HELLO,
+                                          { ptls_buffer_push(sendbuf, PTLS_ECH_CLIENT_HELLO_TYPE_INNER); });
                 }
             }
             if (mode == ENCODE_CH_MODE_ENCODED_INNER) {
-                buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_ECH_OUTER_EXTENSIONS,
-                                      { ptls_buffer_push16(sendbuf, PTLS_EXTENSION_TYPE_KEY_SHARE); });
+                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, {
@@ -3153,6 +3170,7 @@
 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;
 
@@ -3209,6 +3227,8 @@
         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;
@@ -3386,6 +3406,51 @@
         case PTLS_EXTENSION_TYPE_STATUS_REQUEST:
             ch->status_request = 1;
             break;
+        case PTLS_EXTENSION_TYPE_ENCRYPTED_CLIENT_HELLO: {
+            if (src == end) {
+                ret = PTLS_ALERT_DECODE_ERROR;
+                goto Exit;
+            }
+            ch->ech.type = *src++;
+            switch (ch->ech.type) {
+            case PTLS_ECH_CLIENT_HELLO_TYPE_OUTER:
+                if (src == end) {
+                    ret = PTLS_ALERT_DECODE_ERROR;
+                    goto Exit;
+                }
+                ch->ech.config_id = *src++;
+                ptls_hpke_cipher_suite_id_t cipher_id;
+                if ((ret = ptls_decode16(&cipher_id.kdf, &src, end)) != 0 || (ret = ptls_decode16(&cipher_id.aead, &src, end)) != 0)
+                    goto Exit;
+                /* find corresponding cipher-suite; if not found, the field is left NULL */
+                for (size_t i = 0; ctx->ech.ciphers[i] != NULL; ++i) {
+                    if (ctx->ech.ciphers[i]->id.kdf == cipher_id.kdf && ctx->ech.ciphers[i]->id.aead == cipher_id.aead) {
+                        ch->ech.cipher = ctx->ech.ciphers[i];
+                        break;
+                    }
+                }
+                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, {
+                    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 (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)
@@ -3401,6 +3466,132 @@
     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)
+
+    uint16_t exttype;
+    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 */
+        ptls_buffer_push_block(buf, 2, {
+            decode_extensions(src, end, PTLS_HANDSHAKE_TYPE_CLIENT_HELLO, &exttype, {
+                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;
+                            while (1) {
+                                if ((ret = ptls_decode16(&outertype, &outer_ext, outer_ext_end)) != 0 ||
+                                    (ret = ptls_decode16(&outersize, &outer_ext, outer_ext_end)) != 0)
+                                    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;
+
+#undef COPY_BLOCK
+}
+
+static int check_client_hello_constraints(ptls_context_t *ctx, struct st_ptls_client_hello_t *ch, int is_second_flight,
+                                          ptls_iovec_t raw_message, ptls_t *tls_cbarg)
+{
+    /* 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 && ctx->on_client_hello != NULL) {
+            ptls_on_client_hello_parameters_t params = {
+                .server_name = ch->server_name,
+                .raw_message = raw_message,
+                .negotiated_protocols = {ch->alpn.list, ch->alpn.count},
+                .incompatible_version = 1,
+            };
+            int ret;
+            if ((ret = ctx->on_client_hello->cb(ctx->on_client_hello, tls_cbarg, &params)) != 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;
+    }
+
+    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';
@@ -3610,11 +3801,19 @@
         ptls_key_exchange_algorithm_t *algorithm;
         ptls_iovec_t peer_key;
     } key_share = {NULL};
+    struct {
+        ptls_aead_context_t *aead;
+        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;
@@ -3626,46 +3825,43 @@
     if ((ret = decode_client_hello(tls->ctx, ch, message.base + PTLS_HANDSHAKE_HEADER_SIZE, message.base + message.len, properties,
                                    tls)) != 0)
         goto Exit;
+    if ((ret = check_client_hello_constraints(tls->ctx, ch, is_second_flight, message, 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)
+    /* ECH */
+    if (ch->ech.payload.base != NULL) {
+        if (ch->ech.type != PTLS_ECH_CLIENT_HELLO_TYPE_OUTER) {
+            ret = PTLS_ALERT_HANDSHAKE_FAILURE;
+            goto Exit;
+        }
+        /* obtain AEAD context for opening inner CH (FIXME second flight?) */
+        if (ch->ech.cipher != NULL && ch->ech.payload.len > ch->ech.cipher->aead->tag_size && tls->ctx->ech.create_opener != NULL &&
+            (ech.aead = tls->ctx->ech.create_opener->cb(tls->ctx->ech.create_opener, tls, ch->ech.config_id, ch->ech.cipher,
+                                                        ch->ech.enc, ech_info_prefix)) != NULL) {
+            /* now that AEAD context is available, create AAD and decrypt inner CH */
+            if ((ech.encoded_ch_inner = malloc(ch->ech.payload.len - 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;
-        }
-        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)) {
-        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;
+            }
+            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(ech.aead, ech.encoded_ch_inner, ch->ech.payload.base, ch->ech.payload.len, 0, ech.ch_outer_aad,
+                                  message.len - PTLS_HANDSHAKE_HEADER_SIZE) != SIZE_MAX) {
+                /* 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 - 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, message, tls)) != 0)
+                    goto Exit;
+            }
         }
     }
 
@@ -4047,6 +4243,11 @@
         ptls_clear_memory(ecdh_secret.base, ecdh_secret.len);
         free(ecdh_secret.base);
     }
+    if (ech.aead != NULL)
+        ptls_aead_free(ech.aead);
+    free(ech.encoded_ch_inner);
+    free(ech.ch_outer_aad);
+    ptls_buffer_dispose(&ech.ch_inner);
     free(ch);
     return ret;
 
diff --git a/t/openssl.c b/t/openssl.c
index b9f7667..ffbc778 100644
--- a/t/openssl.c
+++ b/t/openssl.c
@@ -328,10 +328,36 @@
     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_t *tls, uint8_t config_id,
+                                              ptls_hpke_cipher_suite_t *cipher, 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);
+    }
+
+    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;
+}
+
 int main(int argc, char **argv)
 {
     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();
@@ -365,15 +391,13 @@
                                   .cipher_suites = ptls_openssl_cipher_suites,
                                   .tls12_cipher_suites = ptls_openssl_tls12_cipher_suites,
                                   .certificates = {&cert, 1},
-                                  .ech = {ptls_openssl_hpke_kems, ptls_openssl_hpke_cipher_suites},
+                                  .ech = {ptls_openssl_hpke_kems, ptls_openssl_hpke_cipher_suites, &ech_create_opener},
                                   .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);
@@ -420,8 +444,6 @@
     ctx_peer = &openssl_ctx;
     subtest("minicrypto vs.", test_picotls);
 
-    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();
diff --git a/t/picotls.c b/t/picotls.c
index 768b623..edb79b9 100644
--- a/t/picotls.c
+++ b/t/picotls.c
@@ -499,7 +499,7 @@
 
 static void test_ech_decode_config(void)
 {
-    static ptls_hpke_kem_t x25519 = {PTLS_HPKE_KEM_X25519_SHA256}, *kems[] = {&x25519, NULL};
+    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};
     uint8_t config_id, max_name_length;
@@ -515,18 +515,15 @@
     }
 
     {
-        uint8_t input[] = {0x12, 0x00, 0x20, 0x00, 0x20, 0x0,  0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
-                           0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a,
-                           0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x00, 0x08, 0x00, 0x02, 0x00, 0x02, 0x00, 0x01, 0x00, 0x01, 0x40,
-                           0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00};
-        const uint8_t *src = input, *const end = input + sizeof(input);
+        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, &config_id, &kem, &public_key, &cipher, &max_name_length, &public_name, &src, end);
         ok(ret == 0);
         ok(config_id == 0x12);
-        ok(kem == &x25519);
-        ok(public_key.len == 32);
-        ok(public_key.base == input + 5);
+        ok(kem == &p256);
+        ok(public_key.len == 65);
+        ok(public_key.base == input.base + 11);
         ok(cipher == &aes128gcmsha256);
         ok(max_name_length == 64);
         ok(public_name.len == sizeof("example.com") - 1);
@@ -700,14 +697,8 @@
         ptls_set_server_name(client, "test.example.com", 0);
     }
 
-    if (ech) {
-        static const uint8_t ech_config_list[] = {
-            0x00, 0x42, 0xfe, 0x0d, 0x00, 0x3e, 0x12, 0x00, 0x20, 0x00, 0x20, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
-            0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
-            0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x00, 0x08, 0x00, 0x02, 0x00, 0x02, 0x00, 0x01,
-            0x00, 0x01, 0x40, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00};
-        client_hs_prop.client.ech_config_list = ptls_iovec_init(ech_config_list, sizeof(ech_config_list));
-    }
+    if (ech)
+        client_hs_prop.client.ech_config_list = 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;
diff --git a/t/test.h b/t/test.h
index 34a14c4..66667ef 100644
--- a/t/test.h
+++ b/t/test.h
@@ -51,18 +51,18 @@
     "\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 from 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"
+#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;