diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,3 +1,12 @@
+## 0.1.1
+
+- Fix recvStream hanging
+  [#54](https://github.com/kazu-yamamoto/quic/pull/54)
+- Don't use the fusion crypto on Intel if the CPU does not
+  provides enough features.
+- Add cabal flag for fusion support
+  [#53](https://github.com/kazu-yamamoto/quic/pull/53)
+
 ## 0.1.0
 
 - Supporting QUICv2 and version negotiation.
diff --git a/Network/QUIC/Connection/Crypto.hs b/Network/QUIC/Connection/Crypto.hs
--- a/Network/QUIC/Connection/Crypto.hs
+++ b/Network/QUIC/Connection/Crypto.hs
@@ -105,8 +105,9 @@
            else
              getVersion conn
     cipher <- getCipher conn lvl
+    avail <- isFusionAvailable
     (coder, protector) <-
-        if useFusion then
+        if useFusion && avail then
             genFusionCoder (isClient conn) ver cipher sec
           else
             genNiteCoder (isClient conn) ver cipher sec
@@ -117,8 +118,9 @@
 initializeCoder1RTT conn sec = do
     ver <- getVersion conn
     cipher <- getCipher conn RTT1Level
+    avail <- isFusionAvailable
     (coder, protector) <-
-        if useFusion then
+        if useFusion && avail then
             genFusionCoder (isClient conn) ver cipher sec
           else
             genNiteCoder (isClient conn) ver cipher sec
@@ -133,7 +135,8 @@
     cipher <- getCipher conn RTT1Level
     Coder1RTT coder secN <- readArray (coders1RTT conn) (not nextPhase)
     let secN1 = updateSecret ver cipher secN
-    coderN1 <- if useFusion then
+    avail <- isFusionAvailable
+    coderN1 <- if useFusion && avail then
                    genFusionCoder1RTT (isClient conn) ver cipher secN1 coder
                  else
                    genNiteCoder1RTT (isClient conn) ver cipher secN1 coder
diff --git a/Network/QUIC/Crypto/Fusion.hs b/Network/QUIC/Crypto/Fusion.hs
--- a/Network/QUIC/Crypto/Fusion.hs
+++ b/Network/QUIC/Crypto/Fusion.hs
@@ -10,6 +10,7 @@
   , fusionSetupSupplement
   , fusionSetSample
   , fusionGetMask
+  , isFusionAvailable
   ) where
 
 #ifdef USE_FUSION
@@ -97,6 +98,11 @@
 fusionGetMask :: Supplement -> IO Buffer
 fusionGetMask (SP fsupp) = withForeignPtr fsupp c_supplement_get_mask
 
+isFusionAvailable :: IO Bool
+isFusionAvailable = do
+    n <- c_ptls_fusion_is_supported_by_cpu
+    return $ not (n == 0)
+
 ----------------------------------------------------------------
 
 foreign import ccall unsafe "aead_context_new"
@@ -155,6 +161,10 @@
 
 foreign import ccall unsafe "supplement_get_mask"
     c_supplement_get_mask :: Ptr SupplementOpaque -> IO (Ptr Word8)
+
+foreign import ccall unsafe "ptls_fusion_is_supported_by_cpu"
+    c_ptls_fusion_is_supported_by_cpu :: IO Int
+
 #else
 import Network.QUIC.Crypto.Types
 import Network.QUIC.Imports
@@ -186,4 +196,7 @@
 
 fusionGetMask :: Supplement -> IO Buffer
 fusionGetMask = undefined
+
+isFusionAvailable :: IO Bool
+isFusionAvailable = return False
 #endif
diff --git a/Network/QUIC/Stream/Misc.hs b/Network/QUIC/Stream/Misc.hs
--- a/Network/QUIC/Stream/Misc.hs
+++ b/Network/QUIC/Stream/Misc.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE RecordWildCards #-}
 
 module Network.QUIC.Stream.Misc (
@@ -22,6 +23,7 @@
 import UnliftIO.STM
 
 import Network.QUIC.Imports
+import Network.QUIC.Stream.Queue
 import Network.QUIC.Stream.Types
 
 ----------------------------------------------------------------
@@ -54,7 +56,9 @@
     return fin
 
 setRxStreamClosed :: Stream -> IO ()
-setRxStreamClosed Stream{..} = atomicModifyIORef'' streamStateRx set
+setRxStreamClosed strm@Stream{..} = do
+    atomicModifyIORef'' streamStateRx set
+    putRecvStreamQ strm ""
   where
     set (StreamState off _) = StreamState off True
 
diff --git a/cbits/fusion.c b/cbits/fusion.c
--- a/cbits/fusion.c
+++ b/cbits/fusion.c
@@ -17,1059 +17,2247 @@
  * All other work, including modifications to the `transformH` function is
  * covered by the following MIT license:
  *
- * Copyright (c) 2020 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 <stdint.h>
-#ifdef _WINDOWS
-#include <intrin.h>
-#endif
-#include <stdlib.h>
-#include <string.h>
-#include <immintrin.h>
-#include <tmmintrin.h>
-#include <nmmintrin.h>
-#include <wmmintrin.h>
-#include "picotls.h"
-#include "picotls/fusion.h"
-
-struct ptls_fusion_aesgcm_context {
-    ptls_fusion_aesecb_context_t ecb;
-    size_t capacity;
-    size_t ghash_cnt;
-    struct ptls_fusion_aesgcm_ghash_precompute {
-        __m128i H;
-        __m128i r;
-    } ghash[0];
-};
-
-struct ctr_context {
-    ptls_cipher_context_t super;
-    ptls_fusion_aesecb_context_t fusion;
-    __m128i bits;
-    uint8_t is_ready;
-};
-
-struct aesgcm_context {
-    ptls_aead_context_t super;
-    ptls_fusion_aesgcm_context_t *aesgcm;
-    /**
-     * retains the static IV in the upper 96 bits (in little endian)
-     */
-    __m128i static_iv;
-};
-
-static const uint64_t poly_[2] __attribute__((aligned(16))) = {1, 0xc200000000000000};
-#define poly (*(__m128i *)poly_)
-static const uint8_t bswap8_[16] __attribute__((aligned(16))) = {15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
-#define bswap8 (*(__m128i *)bswap8_)
-static const uint8_t one8_[16] __attribute__((aligned(16))) = {1};
-#define one8 (*(__m128i *)one8_)
-
-/* This function is covered by the Apache License and the MIT License. The origin is crypto/modes/asm/ghash-x86_64.pl of openssl
- * at commit 33388b4. */
-static __m128i transformH(__m128i H)
-{
-    //  # <<1 twist
-    //  pshufd          \$0b11111111,$Hkey,$T2  # broadcast uppermost dword
-    __m128i t2 = _mm_shuffle_epi32(H, 0xff);
-    // movdqa          $Hkey,$T1
-    __m128i t1 = H;
-    // psllq           \$1,$Hkey
-    H = _mm_slli_epi64(H, 1);
-    // pxor            $T3,$T3                 #
-    __m128i t3 = _mm_setzero_si128();
-    // psrlq           \$63,$T1
-    t1 = _mm_srli_epi64(t1, 63);
-    // pcmpgtd         $T2,$T3                 # broadcast carry bit
-    t3 = _mm_cmplt_epi32(t2, t3);
-    //     pslldq          \$8,$T1
-    t1 = _mm_slli_si128(t1, 8);
-    // por             $T1,$Hkey               # H<<=1
-    H = _mm_or_si128(t1, H);
-
-    // # magic reduction
-    // pand            .L0x1c2_polynomial(%rip),$T3
-    t3 = _mm_and_si128(t3, poly);
-    // pxor            $T3,$Hkey               # if(carry) H^=0x1c2_polynomial
-    H = _mm_xor_si128(t3, H);
-
-    return H;
-}
-// end of Apache License code
-
-static __m128i gfmul(__m128i x, __m128i y)
-{
-    __m128i lo = _mm_clmulepi64_si128(x, y, 0x00);
-    __m128i hi = _mm_clmulepi64_si128(x, y, 0x11);
-
-    __m128i a = _mm_shuffle_epi32(x, 78);
-    __m128i b = _mm_shuffle_epi32(y, 78);
-    a = _mm_xor_si128(a, x);
-    b = _mm_xor_si128(b, y);
-
-    a = _mm_clmulepi64_si128(a, b, 0x00);
-    a = _mm_xor_si128(a, lo);
-    a = _mm_xor_si128(a, hi);
-
-    b = _mm_slli_si128(a, 8);
-    a = _mm_srli_si128(a, 8);
-
-    lo = _mm_xor_si128(lo, b);
-    hi = _mm_xor_si128(hi, a);
-
-    // from https://crypto.stanford.edu/RealWorldCrypto/slides/gueron.pdf
-    __m128i t = _mm_clmulepi64_si128(lo, poly, 0x10);
-    lo = _mm_shuffle_epi32(lo, 78);
-    lo = _mm_xor_si128(lo, t);
-    t = _mm_clmulepi64_si128(lo, poly, 0x10);
-    lo = _mm_shuffle_epi32(lo, 78);
-    lo = _mm_xor_si128(lo, t);
-
-    return _mm_xor_si128(hi, lo);
-}
-
-struct ptls_fusion_gfmul_state {
-    __m128i hi, lo, mid;
-};
-
-static inline void gfmul_onestep(struct ptls_fusion_gfmul_state *gstate, __m128i X,
-                                 struct ptls_fusion_aesgcm_ghash_precompute *precompute)
-{
-    X = _mm_shuffle_epi8(X, bswap8);
-    __m128i t = _mm_clmulepi64_si128(precompute->H, X, 0x00);
-    gstate->lo = _mm_xor_si128(gstate->lo, t);
-    t = _mm_clmulepi64_si128(precompute->H, X, 0x11);
-    gstate->hi = _mm_xor_si128(gstate->hi, t);
-    t = _mm_shuffle_epi32(X, 78);
-    t = _mm_xor_si128(t, X);
-    t = _mm_clmulepi64_si128(precompute->r, t, 0x00);
-    gstate->mid = _mm_xor_si128(gstate->mid, t);
-}
-
-static inline __m128i gfmul_final(struct ptls_fusion_gfmul_state *gstate, __m128i ek0)
-{
-    /* finish multiplication */
-    gstate->mid = _mm_xor_si128(gstate->mid, gstate->hi);
-    gstate->mid = _mm_xor_si128(gstate->mid, gstate->lo);
-    gstate->lo = _mm_xor_si128(gstate->lo, _mm_slli_si128(gstate->mid, 8));
-    gstate->hi = _mm_xor_si128(gstate->hi, _mm_srli_si128(gstate->mid, 8));
-
-    /* fast reduction, using https://crypto.stanford.edu/RealWorldCrypto/slides/gueron.pdf */
-    __m128i r = _mm_clmulepi64_si128(gstate->lo, poly, 0x10);
-    gstate->lo = _mm_shuffle_epi32(gstate->lo, 78);
-    gstate->lo = _mm_xor_si128(gstate->lo, r);
-    r = _mm_clmulepi64_si128(gstate->lo, poly, 0x10);
-    gstate->lo = _mm_shuffle_epi32(gstate->lo, 78);
-    gstate->lo = _mm_xor_si128(gstate->lo, r);
-    __m128i tag = _mm_xor_si128(gstate->hi, gstate->lo);
-    tag = _mm_shuffle_epi8(tag, bswap8);
-    tag = _mm_xor_si128(tag, ek0);
-
-    return tag;
-}
-
-static inline __m128i aesecb_encrypt(ptls_fusion_aesecb_context_t *ctx, __m128i v)
-{
-    size_t i;
-
-    v = _mm_xor_si128(v, ctx->keys[0]);
-    for (i = 1; i < ctx->rounds; ++i)
-        v = _mm_aesenc_si128(v, ctx->keys[i]);
-    v = _mm_aesenclast_si128(v, ctx->keys[i]);
-
-    return v;
-}
-
-static const uint8_t loadn_mask[31] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
-                                       0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
-static const uint8_t loadn_shuffle[31] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
-                                          0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, // first 16 bytes map to byte offsets
-                                          0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
-                                          0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80}; // latter 15 bytes map to zero
-
-static inline __m128i loadn(const void *p, size_t l)
-{
-    __m128i v, mask = _mm_loadu_si128((__m128i *)(loadn_mask + 16 - l));
-    uintptr_t mod4k = (uintptr_t)p % 4096;
-
-    if (PTLS_LIKELY(mod4k <= 4080) || mod4k + l > 4096) {
-        v = _mm_loadu_si128(p);
-    } else {
-        uintptr_t shift = (uintptr_t)p & 15;
-        __m128i pattern = _mm_loadu_si128((const __m128i *)(loadn_shuffle + shift));
-        v = _mm_shuffle_epi8(_mm_load_si128((const __m128i *)((uintptr_t)p - shift)), pattern);
-    }
-    v = _mm_and_si128(v, mask);
-    return v;
-}
-
-static inline void storen(void *_p, size_t l, __m128i v)
-{
-    uint8_t buf[16], *p = _p;
-
-    *(__m128i *)buf = v;
-
-    for (size_t i = 0; i != l; ++i)
-        p[i] = buf[i];
-}
-
-void ptls_fusion_aesgcm_encrypt(ptls_fusion_aesgcm_context_t *ctx, void *output, const void *input, size_t inlen, __m128i ctr,
-                                const void *_aad, size_t aadlen, ptls_aead_supplementary_encryption_t *supp)
-{
-/* init the bits (we can always run in full), but use the last slot for calculating ek0, if possible */
-#define AESECB6_INIT()                                                                                                             \
-    do {                                                                                                                           \
-        ctr = _mm_add_epi64(ctr, one8);                                                                                            \
-        bits0 = _mm_shuffle_epi8(ctr, bswap8);                                                                                     \
-        ctr = _mm_add_epi64(ctr, one8);                                                                                            \
-        bits1 = _mm_shuffle_epi8(ctr, bswap8);                                                                                     \
-        ctr = _mm_add_epi64(ctr, one8);                                                                                            \
-        bits2 = _mm_shuffle_epi8(ctr, bswap8);                                                                                     \
-        ctr = _mm_add_epi64(ctr, one8);                                                                                            \
-        bits3 = _mm_shuffle_epi8(ctr, bswap8);                                                                                     \
-        ctr = _mm_add_epi64(ctr, one8);                                                                                            \
-        bits4 = _mm_shuffle_epi8(ctr, bswap8);                                                                                     \
-        if (PTLS_LIKELY(srclen > 16 * 5)) {                                                                                        \
-            ctr = _mm_add_epi64(ctr, one8);                                                                                        \
-            bits5 = _mm_shuffle_epi8(ctr, bswap8);                                                                                 \
-        } else {                                                                                                                   \
-            if ((state & STATE_EK0_BEEN_FED) == 0) {                                                                               \
-                bits5 = ek0;                                                                                                       \
-                state |= STATE_EK0_BEEN_FED;                                                                                       \
-            }                                                                                                                      \
-            if ((state & STATE_SUPP_USED) != 0 && srclen <= 16 * 4 && (const __m128i *)supp->input + 1 <= dst_ghash) {             \
-                bits4 = _mm_loadu_si128(supp->input);                                                                              \
-                bits4keys = ((struct ctr_context *)supp->ctx)->fusion.keys;                                                        \
-                state |= STATE_SUPP_IN_PROCESS;                                                                                    \
-            }                                                                                                                      \
-        }                                                                                                                          \
-        __m128i k = ctx->ecb.keys[0];                                                                                              \
-        bits0 = _mm_xor_si128(bits0, k);                                                                                           \
-        bits1 = _mm_xor_si128(bits1, k);                                                                                           \
-        bits2 = _mm_xor_si128(bits2, k);                                                                                           \
-        bits3 = _mm_xor_si128(bits3, k);                                                                                           \
-        bits4 = _mm_xor_si128(bits4, bits4keys[0]);                                                                                \
-        bits5 = _mm_xor_si128(bits5, k);                                                                                           \
-    } while (0)
-
-/* aes block update */
-#define AESECB6_UPDATE(i)                                                                                                          \
-    do {                                                                                                                           \
-        __m128i k = ctx->ecb.keys[i];                                                                                              \
-        bits0 = _mm_aesenc_si128(bits0, k);                                                                                        \
-        bits1 = _mm_aesenc_si128(bits1, k);                                                                                        \
-        bits2 = _mm_aesenc_si128(bits2, k);                                                                                        \
-        bits3 = _mm_aesenc_si128(bits3, k);                                                                                        \
-        bits4 = _mm_aesenc_si128(bits4, bits4keys[i]);                                                                             \
-        bits5 = _mm_aesenc_si128(bits5, k);                                                                                        \
-    } while (0)
-
-/* aesenclast */
-#define AESECB6_FINAL(i)                                                                                                           \
-    do {                                                                                                                           \
-        __m128i k = ctx->ecb.keys[i];                                                                                              \
-        bits0 = _mm_aesenclast_si128(bits0, k);                                                                                    \
-        bits1 = _mm_aesenclast_si128(bits1, k);                                                                                    \
-        bits2 = _mm_aesenclast_si128(bits2, k);                                                                                    \
-        bits3 = _mm_aesenclast_si128(bits3, k);                                                                                    \
-        bits4 = _mm_aesenclast_si128(bits4, bits4keys[i]);                                                                         \
-        bits5 = _mm_aesenclast_si128(bits5, k);                                                                                    \
-    } while (0)
-
-    __m128i ek0, bits0, bits1, bits2, bits3, bits4, bits5 = _mm_setzero_si128();
-    const __m128i *bits4keys = ctx->ecb.keys; /* is changed to supp->ctx.keys when calcurating suppout */
-    struct ptls_fusion_gfmul_state gstate = {0};
-    __m128i gdatabuf[6];
-    __m128i ac = _mm_shuffle_epi8(_mm_set_epi32(0, (int)aadlen * 8, 0, (int)inlen * 8), bswap8);
-
-    // src and dst are updated after the chunk is processed
-    const __m128i *src = input;
-    __m128i *dst = output;
-    size_t srclen = inlen;
-    // aad and src_ghash are updated before the chunk is processed (i.e., when the pointers are fed indo the processor)
-    const __m128i *aad = _aad, *dst_ghash = dst;
-    size_t dst_ghashlen = srclen;
-
-    struct ptls_fusion_aesgcm_ghash_precompute *ghash_precompute = ctx->ghash + (aadlen + 15) / 16 + (srclen + 15) / 16 + 1;
-
-#define STATE_EK0_BEEN_FED 0x3
-#define STATE_EK0_INCOMPLETE 0x2
-#define STATE_EK0_READY() ((state & STATE_EK0_BEEN_FED) == 0x1)
-#define STATE_SUPP_USED 0x4
-#define STATE_SUPP_IN_PROCESS 0x8
-    int32_t state = supp != NULL ? STATE_SUPP_USED : 0;
-
-    /* build counter */
-    ctr = _mm_insert_epi32(ctr, 1, 0);
-    ek0 = _mm_shuffle_epi8(ctr, bswap8);
-
-    /* start preparing AES */
-    AESECB6_INIT();
-    AESECB6_UPDATE(1);
-
-    /* build first ghash data (only AAD can be fed at this point, as this would be calculated alongside the first AES block) */
-    const __m128i *gdata = gdatabuf; // points to the elements fed into GHASH
-    size_t gdata_cnt = 0;
-    if (PTLS_LIKELY(aadlen != 0)) {
-        while (gdata_cnt < 6) {
-            if (PTLS_LIKELY(aadlen < 16)) {
-                if (aadlen != 0) {
-                    gdatabuf[gdata_cnt++] = loadn(aad, aadlen);
-                    aadlen = 0;
-                }
-                goto MainLoop;
-            }
-            gdatabuf[gdata_cnt++] = _mm_loadu_si128(aad++);
-            aadlen -= 16;
-        }
-    }
-
-    /* the main loop */
-MainLoop:
-    while (1) {
-        /* run AES and multiplication in parallel */
-        size_t i;
-        for (i = 2; i < gdata_cnt + 2; ++i) {
-            AESECB6_UPDATE(i);
-            gfmul_onestep(&gstate, _mm_loadu_si128(gdata++), --ghash_precompute);
-        }
-        for (; i < ctx->ecb.rounds; ++i)
-            AESECB6_UPDATE(i);
-        AESECB6_FINAL(i);
-
-        /* apply the bit stream to src and write to dest */
-        if (PTLS_LIKELY(srclen >= 6 * 16)) {
-#define APPLY(i) _mm_storeu_si128(dst + i, _mm_xor_si128(_mm_loadu_si128(src + i), bits##i))
-            APPLY(0);
-            APPLY(1);
-            APPLY(2);
-            APPLY(3);
-            APPLY(4);
-            APPLY(5);
-#undef APPLY
-            dst += 6;
-            src += 6;
-            srclen -= 6 * 16;
-        } else {
-            if ((state & STATE_EK0_BEEN_FED) == STATE_EK0_BEEN_FED) {
-                ek0 = bits5;
-                state &= ~STATE_EK0_INCOMPLETE;
-            }
-            if ((state & STATE_SUPP_IN_PROCESS) != 0) {
-                _mm_storeu_si128((__m128i *)supp->output, bits4);
-                state &= ~(STATE_SUPP_USED | STATE_SUPP_IN_PROCESS);
-            }
-            if (srclen != 0) {
-#define APPLY(i)                                                                                                                   \
-    do {                                                                                                                           \
-        if (PTLS_LIKELY(srclen >= 16)) {                                                                                           \
-            _mm_storeu_si128(dst++, _mm_xor_si128(_mm_loadu_si128(src++), bits##i));                                               \
-            srclen -= 16;                                                                                                          \
-        } else if (PTLS_LIKELY(srclen != 0)) {                                                                                     \
-            bits0 = bits##i;                                                                                                       \
-            goto ApplyRemainder;                                                                                                   \
-        } else {                                                                                                                   \
-            goto ApplyEnd;                                                                                                         \
-        }                                                                                                                          \
-    } while (0)
-                APPLY(0);
-                APPLY(1);
-                APPLY(2);
-                APPLY(3);
-                APPLY(4);
-                APPLY(5);
-#undef APPLY
-                goto ApplyEnd;
-            ApplyRemainder:
-                storen(dst, srclen, _mm_xor_si128(loadn(src, srclen), bits0));
-                dst = (__m128i *)((uint8_t *)dst + srclen);
-                srclen = 0;
-            ApplyEnd:;
-            }
-        }
-
-        /* next block AES starts here */
-        AESECB6_INIT();
-
-        AESECB6_UPDATE(1);
-
-        /* setup gdata */
-        if (PTLS_UNLIKELY(aadlen != 0)) {
-            gdata_cnt = 0;
-            while (gdata_cnt < 6) {
-                if (aadlen < 16) {
-                    if (aadlen != 0) {
-                        gdatabuf[gdata_cnt++] = loadn(aad, aadlen);
-                        aadlen = 0;
-                    }
-                    goto GdataFillDST;
-                }
-                gdatabuf[gdata_cnt++] = _mm_loadu_si128(aad++);
-                aadlen -= 16;
-            }
-            gdata = gdatabuf;
-        } else if (PTLS_LIKELY(dst_ghashlen >= 6 * 16)) {
-            gdata = dst_ghash;
-            gdata_cnt = 6;
-            dst_ghash += 6;
-            dst_ghashlen -= 96;
-        } else {
-            gdata_cnt = 0;
-        GdataFillDST:
-            while (gdata_cnt < 6) {
-                if (dst_ghashlen < 16) {
-                    if (dst_ghashlen != 0) {
-                        gdatabuf[gdata_cnt++] = loadn(dst_ghash, dst_ghashlen);
-                        dst_ghashlen = 0;
-                    }
-                    if (gdata_cnt < 6)
-                        goto Finish;
-                    break;
-                }
-                gdatabuf[gdata_cnt++] = _mm_loadu_si128(dst_ghash++);
-                dst_ghashlen -= 16;
-            }
-            gdata = gdatabuf;
-        }
-    }
-
-Finish:
-    gdatabuf[gdata_cnt++] = ac;
-
-    /* We have complete set of data to be fed into GHASH. Let's finish the remaining calculation.
-     * Note that by now, all AES operations for payload encryption and ek0 are complete. This is is because it is necessary for GCM
-     * to process at least the same amount of data (i.e. payload-blocks + AC), and because AES is at least one 96-byte block ahead.
-     */
-    assert(STATE_EK0_READY());
-    for (size_t i = 0; i < gdata_cnt; ++i)
-        gfmul_onestep(&gstate, gdatabuf[i], --ghash_precompute);
-
-    _mm_storeu_si128(dst, gfmul_final(&gstate, ek0));
-
-    /* Finish the calculation of supplemental vector. Done at the very last, because the sample might cover the GCM tag. */
-    if ((state & STATE_SUPP_USED) != 0) {
-        size_t i;
-        if ((state & STATE_SUPP_IN_PROCESS) == 0) {
-            bits4keys = ((struct ctr_context *)supp->ctx)->fusion.keys;
-            bits4 = _mm_xor_si128(_mm_loadu_si128(supp->input), bits4keys[0]);
-            i = 1;
-        } else {
-            i = 2;
-        }
-        do {
-            bits4 = _mm_aesenc_si128(bits4, bits4keys[i++]);
-        } while (i != ctx->ecb.rounds);
-        bits4 = _mm_aesenclast_si128(bits4, bits4keys[i]);
-        _mm_storeu_si128((__m128i *)supp->output, bits4);
-    }
-
-#undef AESECB6_INIT
-#undef AESECB6_UPDATE
-#undef AESECB6_FINAL
-#undef STATE_EK0_BEEN_FOUND
-#undef STATE_EK0_READY
-#undef STATE_SUPP_IN_PROCESS
-}
-
-int ptls_fusion_aesgcm_decrypt(ptls_fusion_aesgcm_context_t *ctx, void *output, const void *input, size_t inlen, __m128i ctr,
-                               const void *_aad, size_t aadlen, const void *tag)
-{
-    __m128i ek0 = _mm_setzero_si128(), bits0, bits1 = _mm_setzero_si128(), bits2 = _mm_setzero_si128(), bits3 = _mm_setzero_si128(),
-            bits4 = _mm_setzero_si128(), bits5 = _mm_setzero_si128();
-    struct ptls_fusion_gfmul_state gstate = {0};
-    __m128i gdatabuf[6];
-    __m128i ac = _mm_shuffle_epi8(_mm_set_epi32(0, (int)aadlen * 8, 0, (int)inlen * 8), bswap8);
-    struct ptls_fusion_aesgcm_ghash_precompute *ghash_precompute = ctx->ghash + (aadlen + 15) / 16 + (inlen + 15) / 16 + 1;
-
-    const __m128i *gdata; // points to the elements fed into GHASH
-    size_t gdata_cnt;
-
-    const __m128i *src_ghash = input, *src_aes = input, *aad = _aad;
-    __m128i *dst = output;
-    size_t nondata_aes_cnt = 0, src_ghashlen = inlen, src_aeslen = inlen;
-
-    /* schedule ek0 and suppkey */
-    ctr = _mm_add_epi64(ctr, one8);
-    bits0 = _mm_xor_si128(_mm_shuffle_epi8(ctr, bswap8), ctx->ecb.keys[0]);
-    ++nondata_aes_cnt;
-
-#define STATE_IS_FIRST_RUN 0x1
-#define STATE_GHASH_HAS_MORE 0x2
-    int state = STATE_IS_FIRST_RUN | STATE_GHASH_HAS_MORE;
-
-    /* the main loop */
-    while (1) {
-
-        /* setup gdata */
-        if (PTLS_UNLIKELY(aadlen != 0)) {
-            gdata = gdatabuf;
-            gdata_cnt = 0;
-            while (gdata_cnt < 6) {
-                if (aadlen < 16) {
-                    if (aadlen != 0) {
-                        gdatabuf[gdata_cnt++] = loadn(aad, aadlen);
-                        aadlen = 0;
-                        ++nondata_aes_cnt;
-                    }
-                    goto GdataFillSrc;
-                }
-                gdatabuf[gdata_cnt++] = _mm_loadu_si128(aad++);
-                aadlen -= 16;
-                ++nondata_aes_cnt;
-            }
-        } else if (PTLS_LIKELY(src_ghashlen >= 6 * 16)) {
-            gdata = src_ghash;
-            gdata_cnt = 6;
-            src_ghash += 6;
-            src_ghashlen -= 6 * 16;
-        } else {
-            gdata = gdatabuf;
-            gdata_cnt = 0;
-        GdataFillSrc:
-            while (gdata_cnt < 6) {
-                if (src_ghashlen < 16) {
-                    if (src_ghashlen != 0) {
-                        gdatabuf[gdata_cnt++] = loadn(src_ghash, src_ghashlen);
-                        src_ghash = (__m128i *)((uint8_t *)src_ghash + src_ghashlen);
-                        src_ghashlen = 0;
-                    }
-                    if (gdata_cnt < 6 && (state & STATE_GHASH_HAS_MORE) != 0) {
-                        gdatabuf[gdata_cnt++] = ac;
-                        state &= ~STATE_GHASH_HAS_MORE;
-                    }
-                    break;
-                }
-                gdatabuf[gdata_cnt++] = _mm_loadu_si128(src_ghash++);
-                src_ghashlen -= 16;
-            }
-        }
-
-        /* setup aes bits */
-        if (PTLS_LIKELY(nondata_aes_cnt == 0))
-            goto InitAllBits;
-        switch (nondata_aes_cnt) {
-#define INIT_BITS(n, keys)                                                                                                         \
-    case n:                                                                                                                        \
-        ctr = _mm_add_epi64(ctr, one8);                                                                                            \
-        bits##n = _mm_xor_si128(_mm_shuffle_epi8(ctr, bswap8), keys[0]);
-        InitAllBits:
-            INIT_BITS(0, ctx->ecb.keys);
-            INIT_BITS(1, ctx->ecb.keys);
-            INIT_BITS(2, ctx->ecb.keys);
-            INIT_BITS(3, ctx->ecb.keys);
-            INIT_BITS(4, ctx->ecb.keys);
-            INIT_BITS(5, ctx->ecb.keys);
-#undef INIT_BITS
-        }
-
-        { /* run aes and ghash */
-#define AESECB6_UPDATE(i)                                                                                                          \
-    do {                                                                                                                           \
-        __m128i k = ctx->ecb.keys[i];                                                                                              \
-        bits0 = _mm_aesenc_si128(bits0, k);                                                                                        \
-        bits1 = _mm_aesenc_si128(bits1, k);                                                                                        \
-        bits2 = _mm_aesenc_si128(bits2, k);                                                                                        \
-        bits3 = _mm_aesenc_si128(bits3, k);                                                                                        \
-        bits4 = _mm_aesenc_si128(bits4, k);                                                                                        \
-        bits5 = _mm_aesenc_si128(bits5, k);                                                                                        \
-    } while (0)
-
-            size_t aesi;
-            for (aesi = 1; aesi <= gdata_cnt; ++aesi) {
-                AESECB6_UPDATE(aesi);
-                gfmul_onestep(&gstate, _mm_loadu_si128(gdata++), --ghash_precompute);
-            }
-            for (; aesi < ctx->ecb.rounds; ++aesi)
-                AESECB6_UPDATE(aesi);
-            __m128i k = ctx->ecb.keys[aesi];
-            bits0 = _mm_aesenclast_si128(bits0, k);
-            bits1 = _mm_aesenclast_si128(bits1, k);
-            bits2 = _mm_aesenclast_si128(bits2, k);
-            bits3 = _mm_aesenclast_si128(bits3, k);
-            bits4 = _mm_aesenclast_si128(bits4, k);
-            bits5 = _mm_aesenclast_si128(bits5, k);
-
-#undef AESECB6_UPDATE
-        }
-
-        /* apply aes bits */
-        if (PTLS_LIKELY(nondata_aes_cnt == 0 && src_aeslen >= 6 * 16)) {
-#define APPLY(i) _mm_storeu_si128(dst + i, _mm_xor_si128(_mm_loadu_si128(src_aes + i), bits##i))
-            APPLY(0);
-            APPLY(1);
-            APPLY(2);
-            APPLY(3);
-            APPLY(4);
-            APPLY(5);
-#undef APPLY
-            dst += 6;
-            src_aes += 6;
-            src_aeslen -= 6 * 16;
-        } else {
-            if ((state & STATE_IS_FIRST_RUN) != 0) {
-                ek0 = bits0;
-                state &= ~STATE_IS_FIRST_RUN;
-            }
-            switch (nondata_aes_cnt) {
-#define APPLY(i)                                                                                                                   \
-    case i:                                                                                                                        \
-        if (PTLS_LIKELY(src_aeslen > 16)) {                                                                                        \
-            _mm_storeu_si128(dst++, _mm_xor_si128(_mm_loadu_si128(src_aes++), bits##i));                                           \
-            src_aeslen -= 16;                                                                                                      \
-        } else {                                                                                                                   \
-            bits0 = bits##i;                                                                                                       \
-            goto Finish;                                                                                                           \
-        }
-                APPLY(0);
-                APPLY(1);
-                APPLY(2);
-                APPLY(3);
-                APPLY(4);
-                APPLY(5);
-#undef APPLY
-            }
-            nondata_aes_cnt = 0;
-        }
-    }
-
-Finish:
-    if (src_aeslen == 16) {
-        _mm_storeu_si128(dst, _mm_xor_si128(_mm_loadu_si128(src_aes), bits0));
-    } else if (src_aeslen != 0) {
-        storen(dst, src_aeslen, _mm_xor_si128(loadn(src_aes, src_aeslen), bits0));
-    }
-
-    assert((state & STATE_IS_FIRST_RUN) == 0);
-
-    /* the only case where AES operation is complete and GHASH is not is when the application of AC is remaining */
-    if ((state & STATE_GHASH_HAS_MORE) != 0) {
-        assert(ghash_precompute - 1 == ctx->ghash);
-        gfmul_onestep(&gstate, ac, --ghash_precompute);
-    }
-
-    __m128i calctag = gfmul_final(&gstate, ek0);
-
-    return _mm_movemask_epi8(_mm_cmpeq_epi8(calctag, _mm_loadu_si128(tag))) == 0xffff;
-
-#undef STATE_IS_FIRST_RUN
-#undef STATE_GHASH_HAS_MORE
-}
-
-static __m128i expand_key(__m128i key, __m128i temp)
-{
-    key = _mm_xor_si128(key, _mm_slli_si128(key, 4));
-    key = _mm_xor_si128(key, _mm_slli_si128(key, 4));
-    key = _mm_xor_si128(key, _mm_slli_si128(key, 4));
-
-    key = _mm_xor_si128(key, temp);
-
-    return key;
-}
-
-void ptls_fusion_aesecb_init(ptls_fusion_aesecb_context_t *ctx, int is_enc, const void *key, size_t key_size)
-{
-    assert(is_enc && "decryption is not supported (yet)");
-
-    size_t i = 0;
-
-    switch (key_size) {
-    case 16: /* AES128 */
-        ctx->rounds = 10;
-        break;
-    case 32: /* AES256 */
-        ctx->rounds = 14;
-        break;
-    default:
-        assert(!"invalid key size; AES128 / AES256 are supported");
-        break;
-    }
-
-    ctx->keys[i++] = _mm_loadu_si128((__m128i *)key);
-    if (key_size == 32)
-        ctx->keys[i++] = _mm_loadu_si128((__m128i *)key + 1);
-
-#define EXPAND(R)                                                                                                                  \
-    do {                                                                                                                           \
-        ctx->keys[i] = expand_key(ctx->keys[i - key_size / 16],                                                                    \
-                                  _mm_shuffle_epi32(_mm_aeskeygenassist_si128(ctx->keys[i - 1], R), _MM_SHUFFLE(3, 3, 3, 3)));     \
-        if (i == ctx->rounds)                                                                                                      \
-            goto Done;                                                                                                             \
-        ++i;                                                                                                                       \
-        if (key_size > 24) {                                                                                                       \
-            ctx->keys[i] = expand_key(ctx->keys[i - key_size / 16],                                                                \
-                                      _mm_shuffle_epi32(_mm_aeskeygenassist_si128(ctx->keys[i - 1], R), _MM_SHUFFLE(2, 2, 2, 2))); \
-            ++i;                                                                                                                   \
-        }                                                                                                                          \
-    } while (0)
-    EXPAND(0x1);
-    EXPAND(0x2);
-    EXPAND(0x4);
-    EXPAND(0x8);
-    EXPAND(0x10);
-    EXPAND(0x20);
-    EXPAND(0x40);
-    EXPAND(0x80);
-    EXPAND(0x1b);
-    EXPAND(0x36);
-#undef EXPAND
-Done:
-    assert(i == ctx->rounds);
-}
-
-void ptls_fusion_aesecb_dispose(ptls_fusion_aesecb_context_t *ctx)
-{
-    ptls_clear_memory(ctx, sizeof(*ctx));
-}
-
-void ptls_fusion_aesecb_encrypt(ptls_fusion_aesecb_context_t *ctx, void *dst, const void *src)
-{
-    __m128i v = _mm_loadu_si128(src);
-    v = aesecb_encrypt(ctx, v);
-    _mm_storeu_si128(dst, v);
-}
-
-/**
- * returns the number of ghash entries that is required to handle an AEAD block of given size
- */
-static size_t aesgcm_calc_ghash_cnt(size_t capacity)
-{
-    // round-up by block size, add to handle worst split of the size between AAD and payload, plus context to hash AC
-    return (capacity + 15) / 16 + 2;
-}
-
-static void setup_one_ghash_entry(ptls_fusion_aesgcm_context_t *ctx)
-{
-    if (ctx->ghash_cnt != 0)
-        ctx->ghash[ctx->ghash_cnt].H = gfmul(ctx->ghash[ctx->ghash_cnt - 1].H, ctx->ghash[0].H);
-
-    __m128i r = _mm_shuffle_epi32(ctx->ghash[ctx->ghash_cnt].H, 78);
-    r = _mm_xor_si128(r, ctx->ghash[ctx->ghash_cnt].H);
-    ctx->ghash[ctx->ghash_cnt].r = r;
-
-    ++ctx->ghash_cnt;
-}
-
-ptls_fusion_aesgcm_context_t *ptls_fusion_aesgcm_new(const void *key, size_t key_size, size_t capacity)
-{
-    ptls_fusion_aesgcm_context_t *ctx;
-    size_t ghash_cnt = aesgcm_calc_ghash_cnt(capacity);
-
-    if ((ctx = malloc(sizeof(*ctx) + sizeof(ctx->ghash[0]) * ghash_cnt)) == NULL)
-        return NULL;
-
-    ptls_fusion_aesecb_init(&ctx->ecb, 1, key, key_size);
-
-    ctx->capacity = capacity;
-
-    ctx->ghash[0].H = aesecb_encrypt(&ctx->ecb, _mm_setzero_si128());
-    ctx->ghash[0].H = _mm_shuffle_epi8(ctx->ghash[0].H, bswap8);
-    ctx->ghash[0].H = transformH(ctx->ghash[0].H);
-    ctx->ghash_cnt = 0;
-    while (ctx->ghash_cnt < ghash_cnt)
-        setup_one_ghash_entry(ctx);
-
-    return ctx;
-}
-
-ptls_fusion_aesgcm_context_t *ptls_fusion_aesgcm_set_capacity(ptls_fusion_aesgcm_context_t *ctx, size_t capacity)
-{
-    size_t ghash_cnt = aesgcm_calc_ghash_cnt(capacity);
-
-    if (ghash_cnt <= ctx->ghash_cnt)
-        return ctx;
-
-    if ((ctx = realloc(ctx, sizeof(*ctx) + sizeof(ctx->ghash[0]) * ghash_cnt)) == NULL)
-        return NULL;
-
-    ctx->capacity = capacity;
-    while (ghash_cnt < ctx->ghash_cnt)
-        setup_one_ghash_entry(ctx);
-
-    return ctx;
-}
-
-void ptls_fusion_aesgcm_free(ptls_fusion_aesgcm_context_t *ctx)
-{
-    ptls_clear_memory(ctx->ghash, sizeof(ctx->ghash[0]) * ctx->ghash_cnt);
-    ctx->ghash_cnt = 0;
-    ptls_fusion_aesecb_dispose(&ctx->ecb);
-    free(ctx);
-}
-
-static void ctr_dispose(ptls_cipher_context_t *_ctx)
-{
-    struct ctr_context *ctx = (struct ctr_context *)_ctx;
-    ptls_fusion_aesecb_dispose(&ctx->fusion);
-    _mm_storeu_si128(&ctx->bits, _mm_setzero_si128());
-}
-
-static void ctr_init(ptls_cipher_context_t *_ctx, const void *iv)
-{
-    struct ctr_context *ctx = (struct ctr_context *)_ctx;
-    _mm_storeu_si128(&ctx->bits, aesecb_encrypt(&ctx->fusion, _mm_loadu_si128(iv)));
-    ctx->is_ready = 1;
-}
-
-static void ctr_transform(ptls_cipher_context_t *_ctx, void *output, const void *input, size_t len)
-{
-    struct ctr_context *ctx = (struct ctr_context *)_ctx;
-
-    assert((ctx->is_ready && len <= 16) ||
-           !"CTR transfomation is supported only once per call to `init` and the maximum size is limited  to 16 bytes");
-    ctx->is_ready = 0;
-
-    if (len < 16) {
-        storen(output, len, _mm_xor_si128(_mm_loadu_si128(&ctx->bits), loadn(input, len)));
-    } else {
-        _mm_storeu_si128(output, _mm_xor_si128(_mm_loadu_si128(&ctx->bits), _mm_loadu_si128(input)));
-    }
-}
-
-static int aesctr_setup(ptls_cipher_context_t *_ctx, int is_enc, const void *key, size_t key_size)
-{
-    struct ctr_context *ctx = (struct ctr_context *)_ctx;
-
-    ctx->super.do_dispose = ctr_dispose;
-    ctx->super.do_init = ctr_init;
-    ctx->super.do_transform = ctr_transform;
-    ptls_fusion_aesecb_init(&ctx->fusion, 1, key, key_size);
-    ctx->is_ready = 0;
-
-    return 0;
-}
-
-static int aes128ctr_setup(ptls_cipher_context_t *ctx, int is_enc, const void *key)
-{
-    return aesctr_setup(ctx, is_enc, key, PTLS_AES128_KEY_SIZE);
-}
-
-static int aes256ctr_setup(ptls_cipher_context_t *ctx, int is_enc, const void *key)
-{
-    return aesctr_setup(ctx, is_enc, key, PTLS_AES256_KEY_SIZE);
-}
-
-static void aesgcm_dispose_crypto(ptls_aead_context_t *_ctx)
-{
-    struct aesgcm_context *ctx = (struct aesgcm_context *)_ctx;
-
-    ptls_fusion_aesgcm_free(ctx->aesgcm);
-}
-
-static void aead_do_encrypt_init(ptls_aead_context_t *_ctx, uint64_t seq, const void *aad, size_t aadlen)
-{
-    assert(!"FIXME");
-}
-
-static size_t aead_do_encrypt_update(ptls_aead_context_t *_ctx, void *output, const void *input, size_t inlen)
-{
-    assert(!"FIXME");
-    return SIZE_MAX;
-}
-
-static size_t aead_do_encrypt_final(ptls_aead_context_t *_ctx, void *_output)
-{
-    assert(!"FIXME");
-    return SIZE_MAX;
-}
-
-static inline __m128i calc_counter(struct aesgcm_context *ctx, uint64_t seq)
-{
-    __m128i ctr = _mm_setzero_si128();
-    ctr = _mm_insert_epi64(ctr, seq, 0);
-    ctr = _mm_slli_si128(ctr, 4);
-    ctr = _mm_xor_si128(ctx->static_iv, ctr);
-    return ctr;
-}
-
-void aead_do_encrypt(struct st_ptls_aead_context_t *_ctx, void *output, const void *input, size_t inlen, uint64_t seq,
-                     const void *aad, size_t aadlen, ptls_aead_supplementary_encryption_t *supp)
-{
-    struct aesgcm_context *ctx = (void *)_ctx;
-
-    if (inlen + aadlen > ctx->aesgcm->capacity)
-        ctx->aesgcm = ptls_fusion_aesgcm_set_capacity(ctx->aesgcm, inlen + aadlen);
-    ptls_fusion_aesgcm_encrypt(ctx->aesgcm, output, input, inlen, calc_counter(ctx, seq), aad, aadlen, supp);
-}
-
-size_t aead_do_decrypt(ptls_aead_context_t *_ctx, void *output, const void *input, size_t inlen, uint64_t seq,
-                              const void *aad, size_t aadlen)
-{
-    struct aesgcm_context *ctx = (void *)_ctx;
-
-    if (inlen < 16)
-        return SIZE_MAX;
-
-    size_t enclen = inlen - 16;
-    if (enclen + aadlen > ctx->aesgcm->capacity)
-        ctx->aesgcm = ptls_fusion_aesgcm_set_capacity(ctx->aesgcm, enclen + aadlen);
-    if (!ptls_fusion_aesgcm_decrypt(ctx->aesgcm, output, input, enclen, calc_counter(ctx, seq), aad, aadlen,
-                                    (const uint8_t *)input + enclen))
-        return SIZE_MAX;
-    return enclen;
-}
-
-static inline void aesgcm_xor_iv(ptls_aead_context_t *_ctx, const void *_bytes, size_t len)
-{
-    struct aesgcm_context *ctx = (struct aesgcm_context *)_ctx;
-    __m128i xor_mask = loadn(_bytes, len);
-    xor_mask = _mm_shuffle_epi8(xor_mask, bswap8);
-    ctx->static_iv = _mm_xor_si128(ctx->static_iv, xor_mask);
-}
-
-static int aesgcm_setup(ptls_aead_context_t *_ctx, int is_enc, const void *key, const void *iv, size_t key_size)
-{
-    struct aesgcm_context *ctx = (struct aesgcm_context *)_ctx;
-
-    ctx->static_iv = loadn(iv, PTLS_AESGCM_IV_SIZE);
-    ctx->static_iv = _mm_shuffle_epi8(ctx->static_iv, bswap8);
-    if (key == NULL)
-        return 0;
-
-    ctx->super.dispose_crypto = aesgcm_dispose_crypto;
-    ctx->super.do_xor_iv = aesgcm_xor_iv;
-    ctx->super.do_encrypt_init = aead_do_encrypt_init;
-    ctx->super.do_encrypt_update = aead_do_encrypt_update;
-    ctx->super.do_encrypt_final = aead_do_encrypt_final;
-    ctx->super.do_encrypt = aead_do_encrypt;
-    ctx->super.do_decrypt = aead_do_decrypt;
-
-    ctx->aesgcm = ptls_fusion_aesgcm_new(key, key_size, 1500 /* assume ordinary packet size */);
-
-    return 0;
-}
-
-int aes128gcm_setup(ptls_aead_context_t *ctx, int is_enc, const void *key, const void *iv)
-{
-    return aesgcm_setup(ctx, is_enc, key, iv, PTLS_AES128_KEY_SIZE);
-}
-
-int aes256gcm_setup(ptls_aead_context_t *ctx, int is_enc, const void *key, const void *iv)
-{
-    return aesgcm_setup(ctx, is_enc, key, iv, PTLS_AES256_KEY_SIZE);
-}
-
-ptls_cipher_algorithm_t ptls_fusion_aes128ctr = {"AES128-CTR",
-                                                 PTLS_AES128_KEY_SIZE,
-                                                 1, // block size
-                                                 PTLS_AES_IV_SIZE,
-                                                 sizeof(struct ctr_context),
-                                                 aes128ctr_setup};
-ptls_cipher_algorithm_t ptls_fusion_aes256ctr = {"AES256-CTR",
-                                                 PTLS_AES256_KEY_SIZE,
-                                                 1, // block size
-                                                 PTLS_AES_IV_SIZE,
-                                                 sizeof(struct ctr_context),
-                                                 aes256ctr_setup};
-ptls_aead_algorithm_t ptls_fusion_aes128gcm = {"AES128-GCM",
-                                               PTLS_AESGCM_CONFIDENTIALITY_LIMIT,
-                                               PTLS_AESGCM_INTEGRITY_LIMIT,
-                                               &ptls_fusion_aes128ctr,
-                                               NULL, // &ptls_fusion_aes128ecb,
-                                               PTLS_AES128_KEY_SIZE,
-                                               PTLS_AESGCM_IV_SIZE,
-                                               PTLS_AESGCM_TAG_SIZE,
-                                               sizeof(struct aesgcm_context),
-                                               aes128gcm_setup};
-ptls_aead_algorithm_t ptls_fusion_aes256gcm = {"AES256-GCM",
-                                               PTLS_AESGCM_CONFIDENTIALITY_LIMIT,
-                                               PTLS_AESGCM_INTEGRITY_LIMIT,
-                                               &ptls_fusion_aes256ctr,
-                                               NULL, // &ptls_fusion_aes256ecb,
-                                               PTLS_AES256_KEY_SIZE,
-                                               PTLS_AESGCM_IV_SIZE,
-                                               PTLS_AESGCM_TAG_SIZE,
-                                               sizeof(struct aesgcm_context),
-                                               aes256gcm_setup};
-
-#ifdef _WINDOWS
-/**
- * ptls_fusion_is_supported_by_cpu:
- * Check that the CPU has extended instructions for PCMUL, AES and AVX2.
- * This test assumes that the CPU is following the x86/x64 architecture.
- * A slightly more refined test could check that the cpu_info spells out
- * "genuineIntel" or "authenticAMD", but would fail in presence of
- * little known CPU brands or some VM */
-int ptls_fusion_is_supported_by_cpu(void)
-{
-    uint32_t cpu_info[4];
-    uint32_t nb_ids;
-    int is_supported = 0;
-
-    __cpuid(cpu_info, 0);
-    nb_ids = cpu_info[0];
-
-    if (nb_ids >= 7) {
-        uint32_t leaf1_ecx;
-        __cpuid(cpu_info, 1);
-        leaf1_ecx = cpu_info[2];
-
-        if (/* PCLMUL */ (leaf1_ecx & (1 << 5)) != 0 && /* AES */ (leaf1_ecx & (1 << 25)) != 0) {
-            uint32_t leaf7_ebx;
-            __cpuid(cpu_info, 7);
-            leaf7_ebx = cpu_info[1];
-
-            is_supported = /* AVX2 */ (leaf7_ebx & (1 << 5)) != 0;
-        }
-    }
-
-    return is_supported;
-}
-#else
-int ptls_fusion_is_supported_by_cpu(void)
-{
-    unsigned leaf1_ecx, leaf7_ebx;
-
-    { /* GCC-specific code to obtain CPU features */
-        unsigned leaf_cnt;
-        __asm__("cpuid" : "=a"(leaf_cnt) : "a"(0) : "ebx", "ecx", "edx");
-        if (leaf_cnt < 7)
-            return 0;
-        __asm__("cpuid" : "=c"(leaf1_ecx) : "a"(1) : "ebx", "edx");
-        __asm__("cpuid" : "=b"(leaf7_ebx) : "a"(7), "c"(0) : "edx");
-    }
-
-    /* AVX2 */
-    if ((leaf7_ebx & (1 << 5)) == 0)
-        return 0;
-    /* AES */
-    if ((leaf1_ecx & (1 << 25)) == 0)
-        return 0;
-    /* PCLMUL */
-    if ((leaf1_ecx & (1 << 1)) == 0)
-        return 0;
-
-    return 1;
-}
-#endif
-
-/* ---------------------------------------------------------------- */
-
-// struct aesgcm_context {
-//    ptls_aead_context_t super;
-//    ptls_fusion_aesgcm_context_t *aesgcm; <- ptls_fusion_aesgcm_free
-
-ptls_aead_context_t *aead_context_new() {
-   ptls_aead_context_t *p = malloc(sizeof(struct aesgcm_context));
-   return p;
-}
+ * Copyright (c) 2020-2022 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 <stdint.h>
+
+#include <stdlib.h>
+#include <string.h>
+#include <immintrin.h>
+#include <tmmintrin.h>
+#include <nmmintrin.h>
+#include <wmmintrin.h>
+#include "picotls.h"
+#include "picotls/fusion.h"
+
+#if defined(__clang__)
+#if __has_feature(address_sanitizer)
+#define NO_SANITIZE_ADDRESS __attribute__((no_sanitize("address")))
+#endif
+#elif __SANITIZE_ADDRESS__ /* gcc */
+#define NO_SANITIZE_ADDRESS __attribute__((no_sanitize_address))
+#endif
+#ifndef NO_SANITIZE_ADDRESS
+#define NO_SANITIZE_ADDRESS
+#endif
+
+#ifdef _WINDOWS
+#define aligned_alloc(a, s) _aligned_malloc((s), (a))
+#define aligned_free(p) _aligned_free(p)
+#else
+#define aligned_free(p) free(p)
+#endif
+
+struct ptls_fusion_aesgcm_context {
+    ptls_fusion_aesecb_context_t ecb;
+    size_t capacity;
+    size_t ghash_cnt;
+};
+
+struct ptls_fusion_aesgcm_context128 {
+    struct ptls_fusion_aesgcm_context super;
+    struct ptls_fusion_aesgcm_ghash_precompute128 {
+        __m128i H;
+        __m128i r;
+    } ghash[0];
+};
+
+struct ptls_fusion_aesgcm_context256 {
+    struct ptls_fusion_aesgcm_context super;
+    union ptls_fusion_aesgcm_ghash_precompute256 {
+        struct {
+            __m128i H[2];
+            __m128i r[2];
+        };
+        struct {
+            __m256i Hx2;
+            __m256i rx2;
+        };
+    } ghash[0];
+};
+
+struct ctr_context {
+    ptls_cipher_context_t super;
+    ptls_fusion_aesecb_context_t fusion;
+    __m128i bits;
+    uint8_t is_ready;
+};
+
+struct aesgcm_context {
+    ptls_aead_context_t super;
+    ptls_fusion_aesgcm_context_t *aesgcm;
+    /**
+     * retains the static IV in the upper 96 bits (in little endian)
+     */
+    __m128i static_iv;
+};
+
+static const uint64_t poly_[2] __attribute__((aligned(16))) = {1, 0xc200000000000000};
+#define poly (*(__m128i *)poly_)
+static const uint8_t byteswap_[32] __attribute__((aligned(32))) = {15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0,
+                                                                   15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0};
+#define byteswap128 (*(__m128i *)byteswap_)
+#define byteswap256 (*(__m256i *)byteswap_)
+static const uint8_t one_[16] __attribute__((aligned(16))) = {1};
+#define one8 (*(__m128i *)one_)
+static const uint8_t incr128x2_[32] __attribute__((aligned(32))) = {2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2};
+#define incr128x2 (*(__m256i *)incr128x2_)
+
+/* This function is covered by the Apache License and the MIT License. The origin is crypto/modes/asm/ghash-x86_64.pl of openssl
+ * at commit 33388b4. */
+static __m128i transformH(__m128i H)
+{
+    //  # <<1 twist
+    //  pshufd          \$0b11111111,$Hkey,$T2  # broadcast uppermost dword
+    __m128i t2 = _mm_shuffle_epi32(H, 0xff);
+    // movdqa          $Hkey,$T1
+    __m128i t1 = H;
+    // psllq           \$1,$Hkey
+    H = _mm_slli_epi64(H, 1);
+    // pxor            $T3,$T3                 #
+    __m128i t3 = _mm_setzero_si128();
+    // psrlq           \$63,$T1
+    t1 = _mm_srli_epi64(t1, 63);
+    // pcmpgtd         $T2,$T3                 # broadcast carry bit
+    t3 = _mm_cmplt_epi32(t2, t3);
+    //     pslldq          \$8,$T1
+    t1 = _mm_slli_si128(t1, 8);
+    // por             $T1,$Hkey               # H<<=1
+    H = _mm_or_si128(t1, H);
+
+    // # magic reduction
+    // pand            .L0x1c2_polynomial(%rip),$T3
+    t3 = _mm_and_si128(t3, poly);
+    // pxor            $T3,$Hkey               # if(carry) H^=0x1c2_polynomial
+    H = _mm_xor_si128(t3, H);
+
+    return H;
+}
+// end of Apache License code
+
+static __m128i gfmul(__m128i x, __m128i y)
+{
+    __m128i lo = _mm_clmulepi64_si128(x, y, 0x00);
+    __m128i hi = _mm_clmulepi64_si128(x, y, 0x11);
+
+    __m128i a = _mm_shuffle_epi32(x, 78);
+    __m128i b = _mm_shuffle_epi32(y, 78);
+    a = _mm_xor_si128(a, x);
+    b = _mm_xor_si128(b, y);
+
+    a = _mm_clmulepi64_si128(a, b, 0x00);
+    a = _mm_xor_si128(a, lo);
+    a = _mm_xor_si128(a, hi);
+
+    b = _mm_slli_si128(a, 8);
+    a = _mm_srli_si128(a, 8);
+
+    lo = _mm_xor_si128(lo, b);
+    hi = _mm_xor_si128(hi, a);
+
+    // from https://crypto.stanford.edu/RealWorldCrypto/slides/gueron.pdf
+    __m128i t = _mm_clmulepi64_si128(lo, poly, 0x10);
+    lo = _mm_shuffle_epi32(lo, 78);
+    lo = _mm_xor_si128(lo, t);
+    t = _mm_clmulepi64_si128(lo, poly, 0x10);
+    lo = _mm_shuffle_epi32(lo, 78);
+    lo = _mm_xor_si128(lo, t);
+
+    return _mm_xor_si128(hi, lo);
+}
+
+static inline __m128i gfmul_do_reduce(__m128i hi, __m128i lo, __m128i mid)
+{
+    mid = _mm_xor_si128(mid, hi);
+    mid = _mm_xor_si128(mid, lo);
+    lo = _mm_xor_si128(lo, _mm_slli_si128(mid, 8));
+    hi = _mm_xor_si128(hi, _mm_srli_si128(mid, 8));
+
+    /* fast reduction, using https://crypto.stanford.edu/RealWorldCrypto/slides/gueron.pdf */
+    __m128i r = _mm_clmulepi64_si128(lo, poly, 0x10);
+    lo = _mm_shuffle_epi32(lo, 78);
+    lo = _mm_xor_si128(lo, r);
+    r = _mm_clmulepi64_si128(lo, poly, 0x10);
+    lo = _mm_shuffle_epi32(lo, 78);
+    lo = _mm_xor_si128(lo, r);
+    lo = _mm_xor_si128(hi, lo);
+
+    return lo;
+}
+
+struct ptls_fusion_gfmul_state128 {
+    __m128i hi, lo, mid;
+};
+
+#if defined(__GNUC__) && !defined(__clang__)
+static inline __m128i xor128(__m128i x, __m128i y)
+{
+    __m128i ret;
+    __asm__("vpxor %2, %1, %0" : "=x"(ret) : "x"(x), "xm"(y));
+    return ret;
+}
+#else
+#define xor128 _mm_xor_si128
+#endif
+
+static inline void gfmul_do_step128(struct ptls_fusion_gfmul_state128 *gstate, __m128i X,
+                                    struct ptls_fusion_aesgcm_ghash_precompute128 *precompute)
+{
+    __m128i t1 = _mm_clmulepi64_si128(precompute->H, X, 0x00);
+    __m128i t2 = _mm_clmulepi64_si128(precompute->H, X, 0x11);
+    __m128i t3 = _mm_shuffle_epi32(X, 78);
+    t3 = _mm_xor_si128(t3, X);
+    t3 = _mm_clmulepi64_si128(precompute->r, t3, 0x00);
+    gstate->lo = xor128(gstate->lo, t1);
+    gstate->hi = xor128(gstate->hi, t2);
+    gstate->mid = xor128(gstate->mid, t3);
+}
+
+#undef xor128
+
+static inline void gfmul_firststep128(struct ptls_fusion_gfmul_state128 *gstate, __m128i X,
+                                      struct ptls_fusion_aesgcm_ghash_precompute128 *precompute)
+{
+    X = _mm_shuffle_epi8(X, byteswap128);
+    X = _mm_xor_si128(gstate->lo, X);
+    gstate->lo = _mm_setzero_si128();
+    gstate->hi = _mm_setzero_si128();
+    gstate->mid = _mm_setzero_si128();
+    gfmul_do_step128(gstate, X, precompute);
+}
+
+static inline void gfmul_nextstep128(struct ptls_fusion_gfmul_state128 *gstate, __m128i X,
+                                     struct ptls_fusion_aesgcm_ghash_precompute128 *precompute)
+{
+    X = _mm_shuffle_epi8(X, byteswap128);
+    gfmul_do_step128(gstate, X, precompute);
+}
+
+static inline void gfmul_reduce128(struct ptls_fusion_gfmul_state128 *gstate)
+{
+    gstate->lo = gfmul_do_reduce(gstate->hi, gstate->lo, gstate->mid);
+}
+
+static inline __m128i gfmul_get_tag128(struct ptls_fusion_gfmul_state128 *gstate, __m128i ek0)
+{
+    __m128i tag = _mm_shuffle_epi8(gstate->lo, byteswap128);
+    tag = _mm_xor_si128(tag, ek0);
+    return tag;
+}
+
+struct ptls_fusion_gfmul_state256 {
+    __m256i hi, lo, mid;
+};
+
+static inline void gfmul_do_step256(struct ptls_fusion_gfmul_state256 *gstate, __m256i X,
+                                    union ptls_fusion_aesgcm_ghash_precompute256 *precompute)
+{
+    __m256i t = _mm256_clmulepi64_epi128(precompute->Hx2, X, 0x00);
+    gstate->lo = _mm256_xor_si256(gstate->lo, t);
+    t = _mm256_clmulepi64_epi128(precompute->Hx2, X, 0x11);
+    gstate->hi = _mm256_xor_si256(gstate->hi, t);
+    t = _mm256_shuffle_epi32(X, 78);
+    t = _mm256_xor_si256(t, X);
+    t = _mm256_clmulepi64_epi128(precompute->rx2, t, 0x00);
+    gstate->mid = _mm256_xor_si256(gstate->mid, t);
+}
+
+static inline void gfmul_firststep256(struct ptls_fusion_gfmul_state256 *gstate, __m256i X, int half,
+                                      union ptls_fusion_aesgcm_ghash_precompute256 *precompute)
+{
+    X = _mm256_shuffle_epi8(X, byteswap256);
+    X = _mm256_xor_si256(gstate->lo, X);
+    if (half)
+        X = _mm256_permute2f128_si256(X, X, 0x08);
+    gstate->lo = _mm256_setzero_si256();
+    gstate->hi = _mm256_setzero_si256();
+    gstate->mid = _mm256_setzero_si256();
+    gfmul_do_step256(gstate, X, precompute);
+}
+
+static inline void gfmul_nextstep256(struct ptls_fusion_gfmul_state256 *gstate, __m256i X,
+                                     union ptls_fusion_aesgcm_ghash_precompute256 *precompute)
+{
+    X = _mm256_shuffle_epi8(X, byteswap256);
+    gfmul_do_step256(gstate, X, precompute);
+}
+
+static inline void gfmul_reduce256(struct ptls_fusion_gfmul_state256 *gstate)
+{
+#define XOR_256TO128(y) _mm_xor_si128(_mm256_castsi256_si128(y), _mm256_extractf128_si256((y), 1))
+    __m128i hi = XOR_256TO128(gstate->hi);
+    __m128i lo = XOR_256TO128(gstate->lo);
+    __m128i mid = XOR_256TO128(gstate->mid);
+#undef XOR_256TO128
+
+    lo = gfmul_do_reduce(hi, lo, mid);
+    gstate->lo = _mm256_castsi128_si256(lo);
+}
+
+static inline __m128i gfmul_get_tag256(struct ptls_fusion_gfmul_state256 *gstate, __m128i ek0)
+{
+    __m128i tag = _mm_shuffle_epi8(_mm256_castsi256_si128(gstate->lo), byteswap128);
+    tag = _mm_xor_si128(tag, ek0);
+    return tag;
+}
+
+static inline __m128i aesecb_encrypt(ptls_fusion_aesecb_context_t *ctx, __m128i v)
+{
+#define ROUNDKEY(i) (ctx->aesni256 ? _mm256_castsi256_si128(ctx->keys.m256[i]) : ctx->keys.m128[i])
+
+    v = _mm_xor_si128(v, ROUNDKEY(0));
+    for (size_t i = 1; i < ctx->rounds; ++i)
+        v = _mm_aesenc_si128(v, ROUNDKEY(i));
+    v = _mm_aesenclast_si128(v, ROUNDKEY(ctx->rounds));
+
+    return v;
+
+#undef ROUNDKEY
+}
+
+// 32-bytes of 0xff followed by 31-bytes of 0x00
+static const uint8_t loadn_mask[63] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+                                       0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+                                       0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
+static const uint8_t loadn_shuffle[31] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+                                          0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, // first 16 bytes map to byte offsets
+                                          0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
+                                          0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80}; // latter 15 bytes map to zero
+
+NO_SANITIZE_ADDRESS
+static inline __m128i loadn_end_of_page(const void *p, size_t l)
+{
+    uintptr_t shift = (uintptr_t)p & 15;
+    __m128i pattern = _mm_loadu_si128((const __m128i *)(loadn_shuffle + shift));
+    return _mm_shuffle_epi8(_mm_load_si128((const __m128i *)((uintptr_t)p - shift)), pattern);
+}
+
+NO_SANITIZE_ADDRESS
+static inline __m128i loadn128(const void *p, size_t l)
+{
+    __m128i v, mask = _mm_loadu_si128((__m128i *)(loadn_mask + 32 - l));
+    uintptr_t mod4k = (uintptr_t)p % 4096;
+
+    if (PTLS_LIKELY(mod4k <= 4096 - 16) || mod4k + l > 4096) {
+        v = _mm_loadu_si128(p);
+    } else {
+        v = loadn_end_of_page(p, l);
+    }
+    v = _mm_and_si128(v, mask);
+
+    return v;
+}
+
+NO_SANITIZE_ADDRESS
+static inline __m256i loadn256(const void *p, size_t l)
+{
+    __m256i v, mask = _mm256_loadu_si256((__m256i *)(loadn_mask + 32 - l));
+    uintptr_t mod4k = (uintptr_t)p % 4096;
+
+    if (PTLS_LIKELY(mod4k < 4096 - 32) || mod4k + l > 4096) {
+        v = _mm256_loadu_si256(p);
+    } else if (l > 16) {
+        __m128i first16 = _mm_loadu_si128(p), second16 = loadn128((uint8_t *)p + 16, l - 16);
+        v = _mm256_permute2f128_si256(_mm256_castsi128_si256(first16), _mm256_castsi128_si256(second16), 0x20);
+    } else if (l == 16) {
+        v = _mm256_castsi128_si256(_mm_loadu_si128(p));
+    } else {
+        v = _mm256_castsi128_si256(loadn_end_of_page(p, l));
+    }
+    v = _mm256_and_si256(v, mask);
+
+    return v;
+}
+
+static inline void storen128(void *_p, size_t l, __m128i v)
+{
+    uint8_t buf[16], *p = _p;
+
+    *(__m128i *)buf = v;
+
+    for (size_t i = 0; i != l; ++i)
+        p[i] = buf[i];
+}
+
+void ptls_fusion_aesgcm_encrypt(ptls_fusion_aesgcm_context_t *_ctx, void *output, const void *input, size_t inlen, __m128i ctr,
+                                const void *_aad, size_t aadlen, ptls_aead_supplementary_encryption_t *supp)
+{
+/* init the bits (we can always run in full), but use the last slot for calculating ek0, if possible */
+#define AESECB6_INIT()                                                                                                             \
+    do {                                                                                                                           \
+        ctr = _mm_add_epi64(ctr, one8);                                                                                            \
+        bits0 = _mm_shuffle_epi8(ctr, byteswap128);                                                                                \
+        ctr = _mm_add_epi64(ctr, one8);                                                                                            \
+        bits1 = _mm_shuffle_epi8(ctr, byteswap128);                                                                                \
+        ctr = _mm_add_epi64(ctr, one8);                                                                                            \
+        bits2 = _mm_shuffle_epi8(ctr, byteswap128);                                                                                \
+        ctr = _mm_add_epi64(ctr, one8);                                                                                            \
+        bits3 = _mm_shuffle_epi8(ctr, byteswap128);                                                                                \
+        ctr = _mm_add_epi64(ctr, one8);                                                                                            \
+        bits4 = _mm_shuffle_epi8(ctr, byteswap128);                                                                                \
+        if (PTLS_LIKELY(srclen > 16 * 5)) {                                                                                        \
+            ctr = _mm_add_epi64(ctr, one8);                                                                                        \
+            bits5 = _mm_shuffle_epi8(ctr, byteswap128);                                                                            \
+        } else {                                                                                                                   \
+            if ((state & STATE_EK0_BEEN_FED) == 0) {                                                                               \
+                bits5 = ek0;                                                                                                       \
+                state |= STATE_EK0_BEEN_FED;                                                                                       \
+            }                                                                                                                      \
+            if ((state & STATE_SUPP_USED) != 0 && srclen <= 16 * 4 && (const __m128i *)supp->input + 1 <= dst_ghash) {             \
+                bits4 = _mm_loadu_si128(supp->input);                                                                              \
+                bits4keys = ((struct ctr_context *)supp->ctx)->fusion.keys.m128;                                                   \
+                state |= STATE_SUPP_IN_PROCESS;                                                                                    \
+            }                                                                                                                      \
+        }                                                                                                                          \
+        __m128i k = ctx->super.ecb.keys.m128[0];                                                                                   \
+        bits0 = _mm_xor_si128(bits0, k);                                                                                           \
+        bits1 = _mm_xor_si128(bits1, k);                                                                                           \
+        bits2 = _mm_xor_si128(bits2, k);                                                                                           \
+        bits3 = _mm_xor_si128(bits3, k);                                                                                           \
+        bits4 = _mm_xor_si128(bits4, bits4keys[0]);                                                                                \
+        bits5 = _mm_xor_si128(bits5, k);                                                                                           \
+    } while (0)
+
+/* aes block update */
+#define AESECB6_UPDATE(i)                                                                                                          \
+    do {                                                                                                                           \
+        __m128i k = ctx->super.ecb.keys.m128[i];                                                                                   \
+        bits0 = _mm_aesenc_si128(bits0, k);                                                                                        \
+        bits1 = _mm_aesenc_si128(bits1, k);                                                                                        \
+        bits2 = _mm_aesenc_si128(bits2, k);                                                                                        \
+        bits3 = _mm_aesenc_si128(bits3, k);                                                                                        \
+        bits4 = _mm_aesenc_si128(bits4, bits4keys[i]);                                                                             \
+        bits5 = _mm_aesenc_si128(bits5, k);                                                                                        \
+    } while (0)
+
+/* aesenclast */
+#define AESECB6_FINAL(i)                                                                                                           \
+    do {                                                                                                                           \
+        __m128i k = ctx->super.ecb.keys.m128[i];                                                                                   \
+        bits0 = _mm_aesenclast_si128(bits0, k);                                                                                    \
+        bits1 = _mm_aesenclast_si128(bits1, k);                                                                                    \
+        bits2 = _mm_aesenclast_si128(bits2, k);                                                                                    \
+        bits3 = _mm_aesenclast_si128(bits3, k);                                                                                    \
+        bits4 = _mm_aesenclast_si128(bits4, bits4keys[i]);                                                                         \
+        bits5 = _mm_aesenclast_si128(bits5, k);                                                                                    \
+    } while (0)
+
+    struct ptls_fusion_aesgcm_context128 *ctx = (void *)_ctx;
+    __m128i ek0, bits0, bits1, bits2, bits3, bits4, bits5 = _mm_setzero_si128();
+    const __m128i *bits4keys = ctx->super.ecb.keys.m128; /* is changed to supp->ctx.keys when calcurating suppout */
+    struct ptls_fusion_gfmul_state128 gstate = {0};
+    __m128i gdatabuf[6];
+    __m128i ac = _mm_shuffle_epi8(_mm_set_epi32(0, (int)aadlen * 8, 0, (int)inlen * 8), byteswap128);
+
+    // src and dst are updated after the chunk is processed
+    const __m128i *src = input;
+    __m128i *dst = output;
+    size_t srclen = inlen;
+    // aad and src_ghash are updated before the chunk is processed (i.e., when the pointers are fed indo the processor)
+    const __m128i *aad = _aad, *dst_ghash = dst;
+    size_t dst_ghashlen = srclen;
+
+    struct ptls_fusion_aesgcm_ghash_precompute128 *ghash_precompute = ctx->ghash + (aadlen + 15) / 16 + (srclen + 15) / 16 + 1;
+
+#define STATE_EK0_BEEN_FED 0x3
+#define STATE_EK0_INCOMPLETE 0x2
+#define STATE_EK0_READY() ((state & STATE_EK0_BEEN_FED) == 0x1)
+#define STATE_SUPP_USED 0x4
+#define STATE_SUPP_IN_PROCESS 0x8
+    int32_t state = supp != NULL ? STATE_SUPP_USED : 0;
+
+    /* build counter */
+    ctr = _mm_insert_epi32(ctr, 1, 0);
+    ek0 = _mm_shuffle_epi8(ctr, byteswap128);
+
+    /* start preparing AES */
+    AESECB6_INIT();
+    AESECB6_UPDATE(1);
+
+    /* build first ghash data (only AAD can be fed at this point, as this would be calculated alongside the first AES block) */
+    const __m128i *gdata = gdatabuf; // points to the elements fed into GHASH
+    size_t gdata_cnt = 0;
+    if (PTLS_LIKELY(aadlen != 0)) {
+        while (gdata_cnt < 6) {
+            if (PTLS_LIKELY(aadlen < 16)) {
+                if (aadlen != 0) {
+                    gdatabuf[gdata_cnt++] = loadn128(aad, aadlen);
+                    aadlen = 0;
+                }
+                goto MainLoop;
+            }
+            gdatabuf[gdata_cnt++] = _mm_loadu_si128(aad++);
+            aadlen -= 16;
+        }
+    }
+
+    /* the main loop */
+MainLoop:
+    while (1) {
+        /* run AES and multiplication in parallel */
+        size_t i;
+        for (i = 2; i < gdata_cnt + 2; ++i) {
+            AESECB6_UPDATE(i);
+            gfmul_nextstep128(&gstate, _mm_loadu_si128(gdata++), --ghash_precompute);
+        }
+        for (; i < ctx->super.ecb.rounds; ++i)
+            AESECB6_UPDATE(i);
+        AESECB6_FINAL(i);
+
+        /* apply the bit stream to src and write to dest */
+        if (PTLS_LIKELY(srclen >= 6 * 16)) {
+#define APPLY(i) _mm_storeu_si128(dst + i, _mm_xor_si128(_mm_loadu_si128(src + i), bits##i))
+            APPLY(0);
+            APPLY(1);
+            APPLY(2);
+            APPLY(3);
+            APPLY(4);
+            APPLY(5);
+#undef APPLY
+            dst += 6;
+            src += 6;
+            srclen -= 6 * 16;
+        } else {
+            if ((state & STATE_EK0_BEEN_FED) == STATE_EK0_BEEN_FED) {
+                ek0 = bits5;
+                state &= ~STATE_EK0_INCOMPLETE;
+            }
+            if ((state & STATE_SUPP_IN_PROCESS) != 0) {
+                _mm_storeu_si128((__m128i *)supp->output, bits4);
+                state &= ~(STATE_SUPP_USED | STATE_SUPP_IN_PROCESS);
+            }
+            if (srclen != 0) {
+#define APPLY(i)                                                                                                                   \
+    do {                                                                                                                           \
+        if (PTLS_LIKELY(srclen >= 16)) {                                                                                           \
+            _mm_storeu_si128(dst++, _mm_xor_si128(_mm_loadu_si128(src++), bits##i));                                               \
+            srclen -= 16;                                                                                                          \
+        } else if (PTLS_LIKELY(srclen != 0)) {                                                                                     \
+            bits0 = bits##i;                                                                                                       \
+            goto ApplyRemainder;                                                                                                   \
+        } else {                                                                                                                   \
+            goto ApplyEnd;                                                                                                         \
+        }                                                                                                                          \
+    } while (0)
+                APPLY(0);
+                APPLY(1);
+                APPLY(2);
+                APPLY(3);
+                APPLY(4);
+                APPLY(5);
+#undef APPLY
+                goto ApplyEnd;
+            ApplyRemainder:
+                storen128(dst, srclen, _mm_xor_si128(loadn128(src, srclen), bits0));
+                dst = (__m128i *)((uint8_t *)dst + srclen);
+                srclen = 0;
+            ApplyEnd:;
+            }
+        }
+
+        /* next block AES starts here */
+        AESECB6_INIT();
+
+        AESECB6_UPDATE(1);
+
+        /* setup gdata */
+        if (PTLS_UNLIKELY(aadlen != 0)) {
+            gdata_cnt = 0;
+            while (gdata_cnt < 6) {
+                if (aadlen < 16) {
+                    if (aadlen != 0) {
+                        gdatabuf[gdata_cnt++] = loadn128(aad, aadlen);
+                        aadlen = 0;
+                    }
+                    goto GdataFillDST;
+                }
+                gdatabuf[gdata_cnt++] = _mm_loadu_si128(aad++);
+                aadlen -= 16;
+            }
+            gdata = gdatabuf;
+        } else if (PTLS_LIKELY(dst_ghashlen >= 6 * 16)) {
+            gdata = dst_ghash;
+            gdata_cnt = 6;
+            dst_ghash += 6;
+            dst_ghashlen -= 96;
+        } else {
+            gdata_cnt = 0;
+        GdataFillDST:
+            while (gdata_cnt < 6) {
+                if (dst_ghashlen < 16) {
+                    if (dst_ghashlen != 0) {
+                        gdatabuf[gdata_cnt++] = loadn128(dst_ghash, dst_ghashlen);
+                        dst_ghashlen = 0;
+                    }
+                    if (gdata_cnt < 6)
+                        goto Finish;
+                    break;
+                }
+                gdatabuf[gdata_cnt++] = _mm_loadu_si128(dst_ghash++);
+                dst_ghashlen -= 16;
+            }
+            gdata = gdatabuf;
+        }
+    }
+
+Finish:
+    gdatabuf[gdata_cnt++] = ac;
+
+    /* We have complete set of data to be fed into GHASH. Let's finish the remaining calculation.
+     * Note that by now, all AES operations for payload encryption and ek0 are complete. This is is because it is necessary for GCM
+     * to process at least the same amount of data (i.e. payload-blocks + AC), and because AES is at least one 96-byte block ahead.
+     */
+    assert(STATE_EK0_READY());
+    for (size_t i = 0; i < gdata_cnt; ++i)
+        gfmul_nextstep128(&gstate, gdatabuf[i], --ghash_precompute);
+
+    gfmul_reduce128(&gstate);
+    _mm_storeu_si128(dst, gfmul_get_tag128(&gstate, ek0));
+
+    /* Finish the calculation of supplemental vector. Done at the very last, because the sample might cover the GCM tag. */
+    if ((state & STATE_SUPP_USED) != 0) {
+        size_t i;
+        if ((state & STATE_SUPP_IN_PROCESS) == 0) {
+            bits4keys = ((struct ctr_context *)supp->ctx)->fusion.keys.m128;
+            bits4 = _mm_xor_si128(_mm_loadu_si128(supp->input), bits4keys[0]);
+            i = 1;
+        } else {
+            i = 2;
+        }
+        do {
+            bits4 = _mm_aesenc_si128(bits4, bits4keys[i++]);
+        } while (i != ctx->super.ecb.rounds);
+        bits4 = _mm_aesenclast_si128(bits4, bits4keys[i]);
+        _mm_storeu_si128((__m128i *)supp->output, bits4);
+    }
+
+#undef AESECB6_INIT
+#undef AESECB6_UPDATE
+#undef AESECB6_FINAL
+#undef STATE_EK0_BEEN_FOUND
+#undef STATE_EK0_READY
+#undef STATE_SUPP_IN_PROCESS
+}
+
+int ptls_fusion_aesgcm_decrypt(ptls_fusion_aesgcm_context_t *_ctx, void *output, const void *input, size_t inlen, __m128i ctr,
+                               const void *_aad, size_t aadlen, const void *tag)
+{
+    struct ptls_fusion_aesgcm_context128 *ctx = (void *)_ctx;
+    __m128i ek0 = _mm_setzero_si128(), bits0, bits1 = _mm_setzero_si128(), bits2 = _mm_setzero_si128(), bits3 = _mm_setzero_si128(),
+            bits4 = _mm_setzero_si128(), bits5 = _mm_setzero_si128();
+    struct ptls_fusion_gfmul_state128 gstate = {0};
+    __m128i gdatabuf[6];
+    __m128i ac = _mm_shuffle_epi8(_mm_set_epi32(0, (int)aadlen * 8, 0, (int)inlen * 8), byteswap128);
+    struct ptls_fusion_aesgcm_ghash_precompute128 *ghash_precompute = ctx->ghash + (aadlen + 15) / 16 + (inlen + 15) / 16 + 1;
+
+    const __m128i *gdata; // points to the elements fed into GHASH
+    size_t gdata_cnt;
+
+    const __m128i *src_ghash = input, *src_aes = input, *aad = _aad;
+    __m128i *dst = output;
+    size_t nondata_aes_cnt = 0, src_ghashlen = inlen, src_aeslen = inlen;
+
+    /* schedule ek0 and suppkey */
+    ctr = _mm_add_epi64(ctr, one8);
+    bits0 = _mm_xor_si128(_mm_shuffle_epi8(ctr, byteswap128), ctx->super.ecb.keys.m128[0]);
+    ++nondata_aes_cnt;
+
+#define STATE_IS_FIRST_RUN 0x1
+#define STATE_GHASH_HAS_MORE 0x2
+    int state = STATE_IS_FIRST_RUN | STATE_GHASH_HAS_MORE;
+
+    /* the main loop */
+    while (1) {
+
+        /* setup gdata */
+        if (PTLS_UNLIKELY(aadlen != 0)) {
+            gdata = gdatabuf;
+            gdata_cnt = 0;
+            while (gdata_cnt < 6) {
+                if (aadlen < 16) {
+                    if (aadlen != 0) {
+                        gdatabuf[gdata_cnt++] = loadn128(aad, aadlen);
+                        aadlen = 0;
+                        ++nondata_aes_cnt;
+                    }
+                    goto GdataFillSrc;
+                }
+                gdatabuf[gdata_cnt++] = _mm_loadu_si128(aad++);
+                aadlen -= 16;
+                ++nondata_aes_cnt;
+            }
+        } else if (PTLS_LIKELY(src_ghashlen >= 6 * 16)) {
+            gdata = src_ghash;
+            gdata_cnt = 6;
+            src_ghash += 6;
+            src_ghashlen -= 6 * 16;
+        } else {
+            gdata = gdatabuf;
+            gdata_cnt = 0;
+        GdataFillSrc:
+            while (gdata_cnt < 6) {
+                if (src_ghashlen < 16) {
+                    if (src_ghashlen != 0) {
+                        gdatabuf[gdata_cnt++] = loadn128(src_ghash, src_ghashlen);
+                        src_ghash = (__m128i *)((uint8_t *)src_ghash + src_ghashlen);
+                        src_ghashlen = 0;
+                    }
+                    if (gdata_cnt < 6 && (state & STATE_GHASH_HAS_MORE) != 0) {
+                        gdatabuf[gdata_cnt++] = ac;
+                        state &= ~STATE_GHASH_HAS_MORE;
+                    }
+                    break;
+                }
+                gdatabuf[gdata_cnt++] = _mm_loadu_si128(src_ghash++);
+                src_ghashlen -= 16;
+            }
+        }
+
+        /* setup aes bits */
+        if (PTLS_LIKELY(nondata_aes_cnt == 0))
+            goto InitAllBits;
+        switch (nondata_aes_cnt) {
+#define INIT_BITS(n, keys)                                                                                                         \
+    case n:                                                                                                                        \
+        ctr = _mm_add_epi64(ctr, one8);                                                                                            \
+        bits##n = _mm_xor_si128(_mm_shuffle_epi8(ctr, byteswap128), keys.m128[0]);
+        InitAllBits:
+            INIT_BITS(0, ctx->super.ecb.keys);
+            INIT_BITS(1, ctx->super.ecb.keys);
+            INIT_BITS(2, ctx->super.ecb.keys);
+            INIT_BITS(3, ctx->super.ecb.keys);
+            INIT_BITS(4, ctx->super.ecb.keys);
+            INIT_BITS(5, ctx->super.ecb.keys);
+#undef INIT_BITS
+        }
+
+        { /* run aes and ghash */
+#define AESECB6_UPDATE(i)                                                                                                          \
+    do {                                                                                                                           \
+        __m128i k = ctx->super.ecb.keys.m128[i];                                                                                   \
+        bits0 = _mm_aesenc_si128(bits0, k);                                                                                        \
+        bits1 = _mm_aesenc_si128(bits1, k);                                                                                        \
+        bits2 = _mm_aesenc_si128(bits2, k);                                                                                        \
+        bits3 = _mm_aesenc_si128(bits3, k);                                                                                        \
+        bits4 = _mm_aesenc_si128(bits4, k);                                                                                        \
+        bits5 = _mm_aesenc_si128(bits5, k);                                                                                        \
+    } while (0)
+
+            size_t aesi;
+            for (aesi = 1; aesi <= gdata_cnt; ++aesi) {
+                AESECB6_UPDATE(aesi);
+                gfmul_nextstep128(&gstate, _mm_loadu_si128(gdata++), --ghash_precompute);
+            }
+            for (; aesi < ctx->super.ecb.rounds; ++aesi)
+                AESECB6_UPDATE(aesi);
+            __m128i k = ctx->super.ecb.keys.m128[aesi];
+            bits0 = _mm_aesenclast_si128(bits0, k);
+            bits1 = _mm_aesenclast_si128(bits1, k);
+            bits2 = _mm_aesenclast_si128(bits2, k);
+            bits3 = _mm_aesenclast_si128(bits3, k);
+            bits4 = _mm_aesenclast_si128(bits4, k);
+            bits5 = _mm_aesenclast_si128(bits5, k);
+
+#undef AESECB6_UPDATE
+        }
+
+        /* apply aes bits */
+        if (PTLS_LIKELY(nondata_aes_cnt == 0 && src_aeslen >= 6 * 16)) {
+#define APPLY(i) _mm_storeu_si128(dst + i, _mm_xor_si128(_mm_loadu_si128(src_aes + i), bits##i))
+            APPLY(0);
+            APPLY(1);
+            APPLY(2);
+            APPLY(3);
+            APPLY(4);
+            APPLY(5);
+#undef APPLY
+            dst += 6;
+            src_aes += 6;
+            src_aeslen -= 6 * 16;
+        } else {
+            if ((state & STATE_IS_FIRST_RUN) != 0) {
+                ek0 = bits0;
+                state &= ~STATE_IS_FIRST_RUN;
+            }
+            switch (nondata_aes_cnt) {
+#define APPLY(i)                                                                                                                   \
+    case i:                                                                                                                        \
+        if (PTLS_LIKELY(src_aeslen > 16)) {                                                                                        \
+            _mm_storeu_si128(dst++, _mm_xor_si128(_mm_loadu_si128(src_aes++), bits##i));                                           \
+            src_aeslen -= 16;                                                                                                      \
+        } else {                                                                                                                   \
+            bits0 = bits##i;                                                                                                       \
+            goto Finish;                                                                                                           \
+        }
+                APPLY(0);
+                APPLY(1);
+                APPLY(2);
+                APPLY(3);
+                APPLY(4);
+                APPLY(5);
+#undef APPLY
+            }
+            nondata_aes_cnt = 0;
+        }
+    }
+
+Finish:
+    if (src_aeslen == 16) {
+        _mm_storeu_si128(dst, _mm_xor_si128(_mm_loadu_si128(src_aes), bits0));
+    } else if (src_aeslen != 0) {
+        storen128(dst, src_aeslen, _mm_xor_si128(loadn128(src_aes, src_aeslen), bits0));
+    }
+
+    assert((state & STATE_IS_FIRST_RUN) == 0);
+
+    /* the only case where AES operation is complete and GHASH is not is when the application of AC is remaining */
+    if ((state & STATE_GHASH_HAS_MORE) != 0) {
+        assert(ghash_precompute - 1 == ctx->ghash);
+        gfmul_nextstep128(&gstate, ac, --ghash_precompute);
+    }
+
+    gfmul_reduce128(&gstate);
+    __m128i calctag = gfmul_get_tag128(&gstate, ek0);
+
+    return _mm_movemask_epi8(_mm_cmpeq_epi8(calctag, _mm_loadu_si128(tag))) == 0xffff;
+
+#undef STATE_IS_FIRST_RUN
+#undef STATE_GHASH_HAS_MORE
+}
+
+static __m128i expand_key(__m128i key, __m128i temp)
+{
+    key = _mm_xor_si128(key, _mm_slli_si128(key, 4));
+    key = _mm_xor_si128(key, _mm_slli_si128(key, 4));
+    key = _mm_xor_si128(key, _mm_slli_si128(key, 4));
+
+    key = _mm_xor_si128(key, temp);
+
+    return key;
+}
+
+void ptls_fusion_aesecb_init(ptls_fusion_aesecb_context_t *ctx, int is_enc, const void *key, size_t key_size, int aesni256)
+{
+    assert(is_enc && "decryption is not supported (yet)");
+
+    size_t i = 0;
+
+    switch (key_size) {
+    case 16: /* AES128 */
+        ctx->rounds = 10;
+        break;
+    case 32: /* AES256 */
+        ctx->rounds = 14;
+        break;
+    default:
+        assert(!"invalid key size; AES128 / AES256 are supported");
+        break;
+    }
+    ctx->aesni256 = aesni256;
+
+    /* load and expand keys using keys.m128 */
+    ctx->keys.m128[i++] = _mm_loadu_si128((__m128i *)key);
+    if (key_size == 32)
+        ctx->keys.m128[i++] = _mm_loadu_si128((__m128i *)key + 1);
+    while (1) {
+#define EXPAND(R)                                                                                                                  \
+    {                                                                                                                              \
+        ctx->keys.m128[i] =                                                                                                        \
+            expand_key(ctx->keys.m128[i - key_size / 16],                                                                          \
+                       _mm_shuffle_epi32(_mm_aeskeygenassist_si128(ctx->keys.m128[i - 1], R), _MM_SHUFFLE(3, 3, 3, 3)));           \
+        if (i == ctx->rounds)                                                                                                      \
+            break;                                                                                                                 \
+        ++i;                                                                                                                       \
+        if (key_size > 24) {                                                                                                       \
+            ctx->keys.m128[i] =                                                                                                    \
+                expand_key(ctx->keys.m128[i - key_size / 16],                                                                      \
+                           _mm_shuffle_epi32(_mm_aeskeygenassist_si128(ctx->keys.m128[i - 1], R), _MM_SHUFFLE(2, 2, 2, 2)));       \
+            ++i;                                                                                                                   \
+        }                                                                                                                          \
+    }
+        EXPAND(0x1);
+        EXPAND(0x2);
+        EXPAND(0x4);
+        EXPAND(0x8);
+        EXPAND(0x10);
+        EXPAND(0x20);
+        EXPAND(0x40);
+        EXPAND(0x80);
+        EXPAND(0x1b);
+        EXPAND(0x36);
+#undef EXPAND
+    }
+
+    /* convert to keys.m256 if aesni256 is used */
+    if (ctx->aesni256) {
+        size_t i = ctx->rounds;
+        do {
+            ctx->keys.m256[i] = _mm256_broadcastsi128_si256(ctx->keys.m128[i]);
+        } while (i-- != 0);
+    }
+}
+
+void ptls_fusion_aesecb_dispose(ptls_fusion_aesecb_context_t *ctx)
+{
+    ptls_clear_memory(ctx, sizeof(*ctx));
+}
+
+void ptls_fusion_aesecb_encrypt(ptls_fusion_aesecb_context_t *ctx, void *dst, const void *src)
+{
+    __m128i v = _mm_loadu_si128(src);
+    v = aesecb_encrypt(ctx, v);
+    _mm_storeu_si128(dst, v);
+}
+
+/**
+ * returns the number of ghash entries that is required to handle an AEAD block of given size
+ */
+static size_t aesgcm_calc_ghash_cnt(size_t capacity)
+{
+    // round-up by block size, add to handle worst split of the size between AAD and payload, plus context to hash AC
+    return (capacity + 15) / 16 + 2;
+}
+
+static void setup_one_ghash_entry(ptls_fusion_aesgcm_context_t *ctx)
+{
+    __m128i *H, *r, *Hprev, H0;
+
+    if (ctx->ecb.aesni256) {
+        struct ptls_fusion_aesgcm_context256 *ctx256 = (void *)ctx;
+#define GET_SLOT(i, mem) (&ctx256->ghash[(i) / 2].mem[(i) % 2 == 0])
+        H = GET_SLOT(ctx->ghash_cnt, H);
+        r = GET_SLOT(ctx->ghash_cnt, r);
+        Hprev = ctx->ghash_cnt == 0 ? NULL : GET_SLOT(ctx->ghash_cnt - 1, H);
+#undef GET_SLOT
+        H0 = ctx256->ghash[0].H[1];
+    } else {
+        struct ptls_fusion_aesgcm_context128 *ctx128 = (void *)ctx;
+        H = &ctx128->ghash[ctx->ghash_cnt].H;
+        r = &ctx128->ghash[ctx->ghash_cnt].r;
+        Hprev = ctx->ghash_cnt == 0 ? NULL : &ctx128->ghash[ctx->ghash_cnt - 1].H;
+        H0 = ctx128->ghash[0].H;
+    }
+
+    if (Hprev != NULL)
+        *H = gfmul(*Hprev, H0);
+
+    *r = _mm_shuffle_epi32(*H, 78);
+    *r = _mm_xor_si128(*r, *H);
+
+    ++ctx->ghash_cnt;
+}
+
+static size_t calc_aesgcm_context_size(size_t *ghash_cnt, int aesni256)
+{
+    size_t sz;
+
+    if (aesni256) {
+        if (*ghash_cnt % 2 != 0)
+            ++*ghash_cnt;
+        sz = offsetof(struct ptls_fusion_aesgcm_context256, ghash) +
+             sizeof(union ptls_fusion_aesgcm_ghash_precompute256) * *ghash_cnt / 2;
+    } else {
+        sz = offsetof(struct ptls_fusion_aesgcm_context128, ghash) +
+             sizeof(struct ptls_fusion_aesgcm_ghash_precompute128) * *ghash_cnt;
+    }
+    return sz;
+}
+
+static ptls_fusion_aesgcm_context_t *new_aesgcm(const void *key, size_t key_size, size_t capacity, int aesni256)
+{
+    ptls_fusion_aesgcm_context_t *ctx;
+    size_t ghash_cnt = aesgcm_calc_ghash_cnt(capacity), ctx_size = calc_aesgcm_context_size(&ghash_cnt, aesni256);
+
+    if ((ctx = aligned_alloc(32, ctx_size)) == NULL)
+        return NULL;
+
+    ptls_fusion_aesecb_init(&ctx->ecb, 1, key, key_size, aesni256);
+
+    ctx->capacity = capacity;
+
+    __m128i H0 = aesecb_encrypt(&ctx->ecb, _mm_setzero_si128());
+    H0 = _mm_shuffle_epi8(H0, byteswap128);
+    H0 = transformH(H0);
+    if (ctx->ecb.aesni256) {
+        ((struct ptls_fusion_aesgcm_context256 *)ctx)->ghash[0].H[1] = H0;
+    } else {
+        ((struct ptls_fusion_aesgcm_context128 *)ctx)->ghash[0].H = H0;
+    }
+
+    ctx->ghash_cnt = 0;
+    while (ctx->ghash_cnt < ghash_cnt)
+        setup_one_ghash_entry(ctx);
+
+    return ctx;
+}
+
+ptls_fusion_aesgcm_context_t *ptls_fusion_aesgcm_new(const void *key, size_t key_size, size_t capacity)
+{
+    return new_aesgcm(key, key_size, capacity, 0);
+}
+
+ptls_fusion_aesgcm_context_t *ptls_fusion_aesgcm_set_capacity(ptls_fusion_aesgcm_context_t *ctx, size_t capacity)
+{
+    size_t new_ghash_cnt = aesgcm_calc_ghash_cnt(capacity);
+
+    if (new_ghash_cnt <= ctx->ghash_cnt)
+        return ctx;
+
+    size_t new_ctx_size = calc_aesgcm_context_size(&new_ghash_cnt, ctx->ecb.aesni256),
+           old_ctx_size = calc_aesgcm_context_size(&ctx->ghash_cnt, ctx->ecb.aesni256);
+
+    ptls_fusion_aesgcm_context_t *newp;
+    if ((newp = aligned_alloc(32, new_ctx_size)) == NULL)
+        return NULL;
+    memcpy(newp, ctx, old_ctx_size);
+    ptls_clear_memory(ctx, old_ctx_size);
+    aligned_free(ctx);
+    ctx = newp;
+
+    ctx->capacity = capacity;
+    while (ctx->ghash_cnt < new_ghash_cnt)
+        setup_one_ghash_entry(ctx);
+
+    return ctx;
+}
+
+void ptls_fusion_aesgcm_free(ptls_fusion_aesgcm_context_t *ctx)
+{
+    ptls_clear_memory(ctx, calc_aesgcm_context_size(&ctx->ghash_cnt, ctx->ecb.aesni256));
+    /* skip ptls_fusion_aesecb_dispose, based on the knowledge that it does not allocate memory elsewhere */
+
+    aligned_free(ctx);
+}
+
+static void ctr_dispose(ptls_cipher_context_t *_ctx)
+{
+    struct ctr_context *ctx = (struct ctr_context *)_ctx;
+    ptls_fusion_aesecb_dispose(&ctx->fusion);
+    _mm_storeu_si128(&ctx->bits, _mm_setzero_si128());
+}
+
+static void ctr_init(ptls_cipher_context_t *_ctx, const void *iv)
+{
+    struct ctr_context *ctx = (struct ctr_context *)_ctx;
+    _mm_storeu_si128(&ctx->bits, aesecb_encrypt(&ctx->fusion, _mm_loadu_si128(iv)));
+    ctx->is_ready = 1;
+}
+
+static void ctr_transform(ptls_cipher_context_t *_ctx, void *output, const void *input, size_t len)
+{
+    struct ctr_context *ctx = (struct ctr_context *)_ctx;
+
+    assert((ctx->is_ready && len <= 16) ||
+           !"CTR transfomation is supported only once per call to `init` and the maximum size is limited  to 16 bytes");
+    ctx->is_ready = 0;
+
+    if (len < 16) {
+        storen128(output, len, _mm_xor_si128(_mm_loadu_si128(&ctx->bits), loadn128(input, len)));
+    } else {
+        _mm_storeu_si128(output, _mm_xor_si128(_mm_loadu_si128(&ctx->bits), _mm_loadu_si128(input)));
+    }
+}
+
+static int aesctr_setup(ptls_cipher_context_t *_ctx, int is_enc, const void *key, size_t key_size)
+{
+    struct ctr_context *ctx = (struct ctr_context *)_ctx;
+
+    ctx->super.do_dispose = ctr_dispose;
+    ctx->super.do_init = ctr_init;
+    ctx->super.do_transform = ctr_transform;
+    ptls_fusion_aesecb_init(&ctx->fusion, 1, key, key_size, 0 /* probably we do not need aesni256 for CTR? */);
+    ctx->is_ready = 0;
+
+    return 0;
+}
+
+static int aes128ctr_setup(ptls_cipher_context_t *ctx, int is_enc, const void *key)
+{
+    return aesctr_setup(ctx, is_enc, key, PTLS_AES128_KEY_SIZE);
+}
+
+static int aes256ctr_setup(ptls_cipher_context_t *ctx, int is_enc, const void *key)
+{
+    return aesctr_setup(ctx, is_enc, key, PTLS_AES256_KEY_SIZE);
+}
+
+static void aesgcm_dispose_crypto(ptls_aead_context_t *_ctx)
+{
+    struct aesgcm_context *ctx = (struct aesgcm_context *)_ctx;
+
+    ptls_fusion_aesgcm_free(ctx->aesgcm);
+}
+
+static void aead_do_encrypt_init(ptls_aead_context_t *_ctx, uint64_t seq, const void *aad, size_t aadlen)
+{
+    assert(!"FIXME");
+}
+
+static size_t aead_do_encrypt_update(ptls_aead_context_t *_ctx, void *output, const void *input, size_t inlen)
+{
+    assert(!"FIXME");
+    return SIZE_MAX;
+}
+
+static size_t aead_do_encrypt_final(ptls_aead_context_t *_ctx, void *_output)
+{
+    assert(!"FIXME");
+    return SIZE_MAX;
+}
+
+static inline __m128i calc_counter(struct aesgcm_context *ctx, uint64_t seq)
+{
+    __m128i ctr = _mm_setzero_si128();
+    ctr = _mm_insert_epi64(ctr, seq, 0);
+    ctr = _mm_slli_si128(ctr, 4);
+    ctr = _mm_xor_si128(ctx->static_iv, ctr);
+    return ctr;
+}
+
+void aead_do_encrypt(struct st_ptls_aead_context_t *_ctx, void *output, const void *input, size_t inlen, uint64_t seq,
+                     const void *aad, size_t aadlen, ptls_aead_supplementary_encryption_t *supp)
+{
+    struct aesgcm_context *ctx = (void *)_ctx;
+
+    if (inlen + aadlen > ctx->aesgcm->capacity)
+        ctx->aesgcm = ptls_fusion_aesgcm_set_capacity(ctx->aesgcm, inlen + aadlen);
+    ptls_fusion_aesgcm_encrypt(ctx->aesgcm, output, input, inlen, calc_counter(ctx, seq), aad, aadlen, supp);
+}
+
+static void aead_do_encrypt_v(struct st_ptls_aead_context_t *ctx, void *output, ptls_iovec_t *input, size_t incnt, uint64_t seq,
+                              const void *aad, size_t aadlen)
+{
+    assert(!"FIXME");
+}
+
+size_t aead_do_decrypt(ptls_aead_context_t *_ctx, void *output, const void *input, size_t inlen, uint64_t seq,
+                              const void *aad, size_t aadlen)
+{
+    struct aesgcm_context *ctx = (void *)_ctx;
+
+    if (inlen < 16)
+        return SIZE_MAX;
+
+    size_t enclen = inlen - 16;
+    if (enclen + aadlen > ctx->aesgcm->capacity)
+        ctx->aesgcm = ptls_fusion_aesgcm_set_capacity(ctx->aesgcm, enclen + aadlen);
+    if (!ptls_fusion_aesgcm_decrypt(ctx->aesgcm, output, input, enclen, calc_counter(ctx, seq), aad, aadlen,
+                                    (const uint8_t *)input + enclen))
+        return SIZE_MAX;
+    return enclen;
+}
+
+static inline void aesgcm_get_iv(ptls_aead_context_t *_ctx, void *iv)
+{
+    struct aesgcm_context *ctx = (struct aesgcm_context *)_ctx;
+
+    __m128i m128 = _mm_shuffle_epi8(ctx->static_iv, byteswap128);
+    storen128(iv, PTLS_AESGCM_IV_SIZE, m128);
+}
+
+static inline void aesgcm_set_iv(ptls_aead_context_t *_ctx, const void *iv)
+{
+    struct aesgcm_context *ctx = (struct aesgcm_context *)_ctx;
+
+    ctx->static_iv = loadn128(iv, PTLS_AESGCM_IV_SIZE);
+    ctx->static_iv = _mm_shuffle_epi8(ctx->static_iv, byteswap128);
+}
+
+static int aesgcm_setup(ptls_aead_context_t *_ctx, int is_enc, const void *key, const void *iv, size_t key_size)
+{
+    struct aesgcm_context *ctx = (struct aesgcm_context *)_ctx;
+
+    ctx->static_iv = loadn128(iv, PTLS_AESGCM_IV_SIZE);
+    ctx->static_iv = _mm_shuffle_epi8(ctx->static_iv, byteswap128);
+    if (key == NULL)
+        return 0;
+
+    ctx->super.dispose_crypto = aesgcm_dispose_crypto;
+    ctx->super.do_get_iv = aesgcm_get_iv;
+    ctx->super.do_set_iv = aesgcm_set_iv;
+    ctx->super.do_encrypt_init = aead_do_encrypt_init;
+    ctx->super.do_encrypt_update = aead_do_encrypt_update;
+    ctx->super.do_encrypt_final = aead_do_encrypt_final;
+    ctx->super.do_encrypt = aead_do_encrypt;
+    ctx->super.do_encrypt_v = aead_do_encrypt_v;
+    ctx->super.do_decrypt = aead_do_decrypt;
+
+    ctx->aesgcm = new_aesgcm(key, key_size, 1500 /* assume ordinary packet size */, 0 /* no support for aesni256 yet */);
+
+    return 0;
+}
+
+int aes128gcm_setup(ptls_aead_context_t *ctx, int is_enc, const void *key, const void *iv)
+{
+    return aesgcm_setup(ctx, is_enc, key, iv, PTLS_AES128_KEY_SIZE);
+}
+
+int aes256gcm_setup(ptls_aead_context_t *ctx, int is_enc, const void *key, const void *iv)
+{
+    return aesgcm_setup(ctx, is_enc, key, iv, PTLS_AES256_KEY_SIZE);
+}
+
+int ptls_fusion_can_aesni256 = 0;
+ptls_cipher_algorithm_t ptls_fusion_aes128ctr = {"AES128-CTR",
+                                                 PTLS_AES128_KEY_SIZE,
+                                                 1, // block size
+                                                 PTLS_AES_IV_SIZE,
+                                                 sizeof(struct ctr_context),
+                                                 aes128ctr_setup};
+ptls_cipher_algorithm_t ptls_fusion_aes256ctr = {"AES256-CTR",
+                                                 PTLS_AES256_KEY_SIZE,
+                                                 1, // block size
+                                                 PTLS_AES_IV_SIZE,
+                                                 sizeof(struct ctr_context),
+                                                 aes256ctr_setup};
+ptls_aead_algorithm_t ptls_fusion_aes128gcm = {"AES128-GCM",
+                                               PTLS_AESGCM_CONFIDENTIALITY_LIMIT,
+                                               PTLS_AESGCM_INTEGRITY_LIMIT,
+                                               &ptls_fusion_aes128ctr,
+                                               NULL, // &ptls_fusion_aes128ecb,
+                                               PTLS_AES128_KEY_SIZE,
+                                               PTLS_AESGCM_IV_SIZE,
+                                               PTLS_AESGCM_TAG_SIZE,
+                                               {0}, // while it may work, no reason to support TLS/1.2
+                                               0,
+                                               0,
+                                               sizeof(struct aesgcm_context),
+                                               aes128gcm_setup};
+ptls_aead_algorithm_t ptls_fusion_aes256gcm = {"AES256-GCM",
+                                               PTLS_AESGCM_CONFIDENTIALITY_LIMIT,
+                                               PTLS_AESGCM_INTEGRITY_LIMIT,
+                                               &ptls_fusion_aes256ctr,
+                                               NULL, // &ptls_fusion_aes256ecb,
+                                               PTLS_AES256_KEY_SIZE,
+                                               PTLS_AESGCM_IV_SIZE,
+                                               PTLS_AESGCM_TAG_SIZE,
+                                               {0}, // while it may work, no reason to support TLS/1.2
+                                               0,
+                                               0,
+                                               sizeof(struct aesgcm_context),
+                                               aes256gcm_setup};
+
+static inline size_t calc_total_length(ptls_iovec_t *input, size_t incnt)
+{
+    size_t totlen = 0;
+    for (size_t i = 0; i < incnt; ++i)
+        totlen += input[i].len;
+    return totlen;
+}
+
+static inline void reduce_aad128(struct ptls_fusion_gfmul_state128 *gstate, struct ptls_fusion_aesgcm_ghash_precompute128 *ghash,
+                                 const void *_aad, size_t aadlen)
+{
+    struct ptls_fusion_aesgcm_ghash_precompute128 *ghash_precompute;
+    const uint8_t *aad = _aad;
+
+    while (PTLS_UNLIKELY(aadlen >= 6 * 16)) {
+        ghash_precompute = ghash + 6;
+        gfmul_firststep128(gstate, _mm_loadu_si128((void *)aad), --ghash_precompute);
+        aad += 16;
+        aadlen -= 16;
+        for (int i = 1; i < 6; ++i) {
+            gfmul_nextstep128(gstate, _mm_loadu_si128((void *)aad), --ghash_precompute);
+            aad += 16;
+            aadlen -= 16;
+        }
+        gfmul_reduce128(gstate);
+    }
+
+    if (PTLS_LIKELY(aadlen != 0)) {
+        ghash_precompute = ghash + (aadlen + 15) / 16;
+        if (PTLS_UNLIKELY(aadlen >= 16)) {
+            gfmul_firststep128(gstate, _mm_loadu_si128((void *)aad), --ghash_precompute);
+            aad += 16;
+            aadlen -= 16;
+            while (aadlen >= 16) {
+                gfmul_nextstep128(gstate, _mm_loadu_si128((void *)aad), --ghash_precompute);
+                aad += 16;
+                aadlen -= 16;
+            }
+            if (PTLS_LIKELY(aadlen != 0))
+                gfmul_nextstep128(gstate, loadn128(aad, aadlen), --ghash_precompute);
+        } else {
+            gfmul_firststep128(gstate, loadn128(aad, aadlen), --ghash_precompute);
+        }
+        assert(ghash == ghash_precompute);
+        gfmul_reduce128(gstate);
+    }
+}
+
+NO_SANITIZE_ADDRESS
+static inline uint8_t *load_preceding_unaligned(uint8_t *encbuf, uint8_t **output)
+{
+    uint8_t *encp;
+
+    if ((encp = encbuf + ((uintptr_t)*output & 63)) != encbuf) {
+        _mm256_store_si256((void *)encbuf, _mm256_load_si256((void *)(*output - (encp - encbuf))));
+        _mm256_store_si256((void *)(encbuf + 32), _mm256_load_si256((void *)(*output - (encp - encbuf) + 32)));
+        *output -= encp - encbuf;
+    }
+
+    return encp;
+}
+
+NO_SANITIZE_ADDRESS
+static inline void write_remaining_bytes(uint8_t *dst, const uint8_t *src, const uint8_t *end)
+{
+    /* Write in 64-byte chunks, using NT store instructions. Last partial block, if any, is written to cache, as that cache line
+     * would likely be read when the next TLS record is being built. */
+
+    for (; end - src >= 64; dst += 64, src += 64) {
+        _mm256_stream_si256((void *)dst, _mm256_load_si256((void *)src));
+        _mm256_stream_si256((void *)(dst + 32), _mm256_load_si256((void *)(src + 32)));
+    }
+    _mm_sfence(); /* weakly ordered writes have to be synced before being passed to NIC */
+    if (src != end) {
+        for (; end - src >= 16; dst += 16, src += 16)
+            _mm_store_si128((void *)dst, _mm_load_si128((void *)src));
+        if (src != end)
+            storen128((void *)dst, end - src, loadn128((void *)src, end - src));
+    }
+}
+
+NO_SANITIZE_ADDRESS
+static void non_temporal_encrypt_v128(struct st_ptls_aead_context_t *_ctx, void *_output, ptls_iovec_t *input, size_t incnt,
+                                      uint64_t seq, const void *aad, size_t aadlen)
+{
+/* init the bits (we can always run in full), but use the last slot for calculating ek0, if possible */
+#define AESECB6_INIT()                                                                                                             \
+    do {                                                                                                                           \
+        ctr = _mm_add_epi64(ctr, one8);                                                                                            \
+        bits0 = _mm_shuffle_epi8(ctr, byteswap128);                                                                                \
+        ctr = _mm_add_epi64(ctr, one8);                                                                                            \
+        bits1 = _mm_shuffle_epi8(ctr, byteswap128);                                                                                \
+        ctr = _mm_add_epi64(ctr, one8);                                                                                            \
+        bits2 = _mm_shuffle_epi8(ctr, byteswap128);                                                                                \
+        ctr = _mm_add_epi64(ctr, one8);                                                                                            \
+        bits3 = _mm_shuffle_epi8(ctr, byteswap128);                                                                                \
+        ctr = _mm_add_epi64(ctr, one8);                                                                                            \
+        bits4 = _mm_shuffle_epi8(ctr, byteswap128);                                                                                \
+        if (PTLS_LIKELY(srclen > 16 * 5) || src_vecleft != 0) {                                                                    \
+            ctr = _mm_add_epi64(ctr, one8);                                                                                        \
+            bits5 = _mm_shuffle_epi8(ctr, byteswap128);                                                                            \
+        } else {                                                                                                                   \
+            bits5 = ek0;                                                                                                           \
+            state |= STATE_EK0_READY;                                                                                              \
+        }                                                                                                                          \
+        __m128i k = ctx->super.ecb.keys.m128[0];                                                                                   \
+        bits0 = _mm_xor_si128(bits0, k);                                                                                           \
+        bits1 = _mm_xor_si128(bits1, k);                                                                                           \
+        bits2 = _mm_xor_si128(bits2, k);                                                                                           \
+        bits3 = _mm_xor_si128(bits3, k);                                                                                           \
+        bits4 = _mm_xor_si128(bits4, k);                                                                                           \
+        bits5 = _mm_xor_si128(bits5, k);                                                                                           \
+    } while (0)
+
+/* aes block update */
+#define AESECB6_UPDATE(i)                                                                                                          \
+    do {                                                                                                                           \
+        __m128i k = ctx->super.ecb.keys.m128[i];                                                                                   \
+        bits0 = _mm_aesenc_si128(bits0, k);                                                                                        \
+        bits1 = _mm_aesenc_si128(bits1, k);                                                                                        \
+        bits2 = _mm_aesenc_si128(bits2, k);                                                                                        \
+        bits3 = _mm_aesenc_si128(bits3, k);                                                                                        \
+        bits4 = _mm_aesenc_si128(bits4, k);                                                                                        \
+        bits5 = _mm_aesenc_si128(bits5, k);                                                                                        \
+    } while (0)
+
+/* aesenclast */
+#define AESECB6_FINAL(i)                                                                                                           \
+    do {                                                                                                                           \
+        __m128i k = ctx->super.ecb.keys.m128[i];                                                                                   \
+        bits0 = _mm_aesenclast_si128(bits0, k);                                                                                    \
+        bits1 = _mm_aesenclast_si128(bits1, k);                                                                                    \
+        bits2 = _mm_aesenclast_si128(bits2, k);                                                                                    \
+        bits3 = _mm_aesenclast_si128(bits3, k);                                                                                    \
+        bits4 = _mm_aesenclast_si128(bits4, k);                                                                                    \
+        bits5 = _mm_aesenclast_si128(bits5, k);                                                                                    \
+    } while (0)
+
+    struct aesgcm_context *agctx = (void *)_ctx;
+    uint8_t *output = _output;
+
+#define STATE_EK0_READY 0x1
+#define STATE_COPY_128B 0x2
+    int32_t state = 0;
+
+    /* Bytes are written here first then written using NT store instructions, 64 bytes at a time. */
+    uint8_t encbuf[32 * 6] __attribute__((aligned(32))), *encp;
+
+    /* `encbuf` should be large enough to store up to 63-bytes of unaligned bytes, 6 16-byte AES blocks, plus AEAD tag that is
+     * append to the ciphertext before writing the bytes to main memory using NT store instructions. */
+    PTLS_BUILD_ASSERT(sizeof(encbuf) >= 64 + 6 * 16 + 16);
+
+    /* load unaligned data within same cache line preceding `output`, adjusting pointers accordingly */
+    encp = load_preceding_unaligned(encbuf, &output);
+
+    /* First write would be 128 bytes (32+6*16), if encbuf contains no less than 32 bytes already. */
+    if (encp - encbuf >= 32)
+        state |= STATE_COPY_128B;
+
+    /* setup ctr, retain Ek(0), len(A) | len(C) to be fed into GCM */
+    __m128i ctr = calc_counter(agctx, seq);
+    ctr = _mm_insert_epi32(ctr, 1, 0);
+    __m128i ek0 = _mm_shuffle_epi8(ctr, byteswap128);
+    __m128i ac = _mm_shuffle_epi8(_mm_set_epi32(0, (int)aadlen * 8, 0, (int)calc_total_length(input, incnt) * 8), byteswap128);
+
+    struct ptls_fusion_aesgcm_context128 *ctx = (void *)agctx->aesgcm;
+    __m128i bits0, bits1, bits2, bits3, bits4, bits5 = _mm_setzero_si128();
+    struct ptls_fusion_gfmul_state128 gstate = {0};
+
+    /* find the first non-empty vec */
+    const uint8_t *src = NULL;
+    size_t srclen = 0, src_vecleft = incnt;
+    while (srclen == 0 && src_vecleft != 0) {
+        src = (void *)input[0].base;
+        srclen = input[0].len;
+        ++input;
+        --src_vecleft;
+    }
+
+    /* Prepare first 6 blocks of bit stream, at the same time calculating ghash of AAD. */
+    AESECB6_INIT();
+    AESECB6_UPDATE(1);
+    AESECB6_UPDATE(2);
+    reduce_aad128(&gstate, ctx->ghash, aad, aadlen);
+    for (size_t i = 3; i < ctx->super.ecb.rounds; ++i)
+        AESECB6_UPDATE(i);
+    AESECB6_FINAL(ctx->super.ecb.rounds);
+
+    /* Main loop. This loop:
+     *  1. using current keystream (bits0..bits5), xors a up to 6 * 16 bytes and writes to encbuf,
+     *  2. then if there is no more data to be encrypted, exit the loop, otherwise,
+     *  3. calculate ghash of the blocks being written to encbuf,
+     *  4. calculate next 6 * 16 bytes of keystream,
+     *  5. writes encbuf in 64-byte blocks
+     * When exitting the loop, `remaining_ghash_from` represents the offset within `encbuf` from where ghash remains to be
+     * calculated. */
+    size_t remaining_ghash_from = encp - encbuf;
+    if (srclen != 0) {
+        while (1) {
+            /* apply the bit stream to input, writing to encbuf */
+            if (PTLS_LIKELY(srclen >= 6 * 16)) {
+#define APPLY(i) _mm_storeu_si128((void *)(encp + i * 16), _mm_xor_si128(_mm_loadu_si128((void *)(src + i * 16)), bits##i))
+                APPLY(0);
+                APPLY(1);
+                APPLY(2);
+                APPLY(3);
+                APPLY(4);
+                APPLY(5);
+#undef APPLY
+                encp += 6 * 16;
+                src += 6 * 16;
+                srclen -= 6 * 16;
+                if (PTLS_UNLIKELY(srclen == 0)) {
+                    if (src_vecleft == 0) {
+                        remaining_ghash_from = (encp - encbuf) - 96;
+                        break;
+                    }
+                    src = (void *)input[0].base;
+                    srclen = input[0].len;
+                    ++input;
+                    --src_vecleft;
+                }
+            } else {
+                /* slow path, load at most 6 * 16 bytes to encbuf then encrypt in-place */
+                size_t bytes_copied = 0;
+                do {
+                    if (srclen >= 16 && bytes_copied < 5 * 16) {
+                        _mm_storeu_si128((void *)(encp + bytes_copied), _mm_loadu_si128((void *)src));
+                        bytes_copied += 16;
+                        src += 16;
+                        srclen -= 16;
+                    } else {
+                        encp[bytes_copied++] = *src++;
+                        --srclen;
+                    }
+                    if (PTLS_UNLIKELY(srclen == 0)) {
+                        do {
+                            if (src_vecleft == 0)
+                                break;
+                            src = (void *)input[0].base;
+                            srclen = input[0].len;
+                            ++input;
+                            --src_vecleft;
+                        } while (srclen == 0);
+                        if (srclen == 0)
+                            break;
+                    }
+                } while (bytes_copied < 6 * 16);
+#define APPLY(i) _mm_storeu_si128((void *)(encp + i * 16), _mm_xor_si128(_mm_loadu_si128((void *)(encp + i * 16)), bits##i))
+                APPLY(0);
+                APPLY(1);
+                APPLY(2);
+                APPLY(3);
+                APPLY(4);
+                APPLY(5);
+#undef APPLY
+                encp += bytes_copied;
+                if (PTLS_UNLIKELY(srclen == 0)) {
+                    /* Calculate amonut of data left to be ghashed, as well as zero-clearing the remainedr of partial block, as it
+                     * will be fed into ghash. */
+                    remaining_ghash_from = (encp - encbuf) - bytes_copied;
+                    if ((bytes_copied & 15) != 0)
+                        _mm_storeu_si128((void *)encp, _mm_setzero_si128());
+                    break;
+                }
+            }
+
+            /* Next 96-byte block starts here. Run AES and ghash in while writing output using non-temporal stores in 64-byte
+             * blocks. */
+            AESECB6_INIT();
+            struct ptls_fusion_aesgcm_ghash_precompute128 *ghash_precompute = ctx->ghash + 6;
+            gfmul_firststep128(&gstate, _mm_loadu_si128((void *)(encp - 6 * 16)), --ghash_precompute);
+            AESECB6_UPDATE(1);
+            gfmul_nextstep128(&gstate, _mm_loadu_si128((void *)(encp - 5 * 16)), --ghash_precompute);
+            AESECB6_UPDATE(2);
+            gfmul_nextstep128(&gstate, _mm_loadu_si128((void *)(encp - 4 * 16)), --ghash_precompute);
+            AESECB6_UPDATE(3);
+            _mm256_stream_si256((void *)output, _mm256_load_si256((void *)encbuf));
+            _mm256_stream_si256((void *)(output + 32), _mm256_load_si256((void *)(encbuf + 32)));
+            AESECB6_UPDATE(4);
+            gfmul_nextstep128(&gstate, _mm_loadu_si128((void *)(encp - 3 * 16)), --ghash_precompute);
+            AESECB6_UPDATE(5);
+            gfmul_nextstep128(&gstate, _mm_loadu_si128((void *)(encp - 2 * 16)), --ghash_precompute);
+            AESECB6_UPDATE(6);
+            gfmul_nextstep128(&gstate, _mm_loadu_si128((void *)(encp - 1 * 16)), --ghash_precompute);
+            AESECB6_UPDATE(7);
+            if ((state & STATE_COPY_128B) != 0) {
+                _mm256_stream_si256((void *)(output + 64), _mm256_load_si256((void *)(encbuf + 64)));
+                _mm256_stream_si256((void *)(output + 96), _mm256_load_si256((void *)(encbuf + 96)));
+                output += 128;
+                encp -= 128;
+                AESECB6_UPDATE(8);
+                _mm256_store_si256((void *)encbuf, _mm256_load_si256((void *)(encbuf + 128)));
+                _mm256_store_si256((void *)(encbuf + 32), _mm256_load_si256((void *)(encbuf + 160)));
+            } else {
+                output += 64;
+                encp -= 64;
+                _mm256_store_si256((void *)encbuf, _mm256_load_si256((void *)(encbuf + 64)));
+                _mm256_store_si256((void *)(encbuf + 32), _mm256_load_si256((void *)(encbuf + 96)));
+                AESECB6_UPDATE(8);
+            }
+            state ^= STATE_COPY_128B;
+            AESECB6_UPDATE(9);
+            if (PTLS_UNLIKELY(ctx->super.ecb.rounds != 10)) {
+                for (size_t i = 10; PTLS_LIKELY(i < ctx->super.ecb.rounds); ++i)
+                    AESECB6_UPDATE(i);
+            }
+            assert(ctx->ghash == ghash_precompute);
+            gfmul_reduce128(&gstate);
+            AESECB6_FINAL(ctx->super.ecb.rounds);
+        }
+    }
+
+    /* Now, All the encrypted bits are built in encbuf. Calculate AEAD tag and append to encbuf. */
+
+    { /* Run ghash against the remaining bytes, after appending `ac` (i.e., len(A) | len(C)). At this point, we might be ghashing 7
+       * blocks at once. */
+        size_t ac_off = remaining_ghash_from + ((encp - encbuf) - remaining_ghash_from + 15) / 16 * 16;
+        _mm_storeu_si128((void *)(encbuf + ac_off), ac);
+        size_t blocks = ((encp - encbuf) - remaining_ghash_from + 15) / 16 + 1; /* round up, +1 for AC */
+        assert(blocks <= 7);
+        struct ptls_fusion_aesgcm_ghash_precompute128 *ghash_precompute = ctx->ghash + blocks;
+        gfmul_firststep128(&gstate, _mm_loadu_si128((void *)(encbuf + remaining_ghash_from)), --ghash_precompute);
+        remaining_ghash_from += 16;
+        while (ghash_precompute != ctx->ghash) {
+            gfmul_nextstep128(&gstate, _mm_loadu_si128((void *)(encbuf + remaining_ghash_from)), --ghash_precompute);
+            remaining_ghash_from += 16;
+        }
+        gfmul_reduce128(&gstate);
+    }
+
+    /* Calculate EK0, if in the unlikely case on not been done yet. When encoding in full size (16K), EK0 will be ready. */
+    if (PTLS_UNLIKELY((state & STATE_EK0_READY) == 0)) {
+        bits5 = _mm_xor_si128(ek0, ctx->super.ecb.keys.m128[0]);
+        for (size_t i = 1; i < ctx->super.ecb.rounds; ++i)
+            bits5 = _mm_aesenc_si128(bits5, ctx->super.ecb.keys.m128[i]);
+        bits5 = _mm_aesenclast_si128(bits5, ctx->super.ecb.keys.m128[ctx->super.ecb.rounds]);
+    }
+
+    /* append tag to encbuf */
+    _mm_storeu_si128((void *)encp, gfmul_get_tag128(&gstate, bits5));
+    encp += 16;
+
+    /* write remaining bytes */
+    write_remaining_bytes(output, encbuf, encp);
+
+#undef AESECB6_INIT
+#undef AESECB6_UPDATE
+#undef AESECB6_FINAL
+#undef STATE_EK0_READY
+#undef STATE_COPY_128B
+}
+
+static size_t non_temporal_decrypt128(ptls_aead_context_t *_ctx, void *_output, const void *_input, size_t inlen, uint64_t seq,
+                                      const void *aad, size_t aadlen)
+{
+    /* Bail out if the input is too short, or remove tag from range. */
+    if (inlen < 16)
+        return SIZE_MAX;
+    inlen -= 16;
+    size_t textlen = inlen;
+
+/* init the bits (we can always run in full), but use the last slot for calculating ek0, if possible */
+#define AESECB6_INIT()                                                                                                             \
+    do {                                                                                                                           \
+        ctr = _mm_add_epi64(ctr, one8);                                                                                            \
+        bits0 = _mm_shuffle_epi8(ctr, byteswap128);                                                                                \
+        ctr = _mm_add_epi64(ctr, one8);                                                                                            \
+        bits1 = _mm_shuffle_epi8(ctr, byteswap128);                                                                                \
+        ctr = _mm_add_epi64(ctr, one8);                                                                                            \
+        bits2 = _mm_shuffle_epi8(ctr, byteswap128);                                                                                \
+        ctr = _mm_add_epi64(ctr, one8);                                                                                            \
+        bits3 = _mm_shuffle_epi8(ctr, byteswap128);                                                                                \
+        ctr = _mm_add_epi64(ctr, one8);                                                                                            \
+        bits4 = _mm_shuffle_epi8(ctr, byteswap128);                                                                                \
+        if (PTLS_LIKELY(inlen > 16 * 5)) {                                                                                         \
+            ctr = _mm_add_epi64(ctr, one8);                                                                                        \
+            bits5 = _mm_shuffle_epi8(ctr, byteswap128);                                                                            \
+        } else {                                                                                                                   \
+            bits5 = ek0;                                                                                                           \
+            state |= STATE_EK0_READY;                                                                                              \
+        }                                                                                                                          \
+        __m128i k = ctx->super.ecb.keys.m128[0];                                                                                   \
+        bits0 = _mm_xor_si128(bits0, k);                                                                                           \
+        bits1 = _mm_xor_si128(bits1, k);                                                                                           \
+        bits2 = _mm_xor_si128(bits2, k);                                                                                           \
+        bits3 = _mm_xor_si128(bits3, k);                                                                                           \
+        bits4 = _mm_xor_si128(bits4, k);                                                                                           \
+        bits5 = _mm_xor_si128(bits5, k);                                                                                           \
+    } while (0)
+
+/* aes block update */
+#define AESECB6_UPDATE(i)                                                                                                          \
+    do {                                                                                                                           \
+        __m128i k = ctx->super.ecb.keys.m128[i];                                                                                   \
+        bits0 = _mm_aesenc_si128(bits0, k);                                                                                        \
+        bits1 = _mm_aesenc_si128(bits1, k);                                                                                        \
+        bits2 = _mm_aesenc_si128(bits2, k);                                                                                        \
+        bits3 = _mm_aesenc_si128(bits3, k);                                                                                        \
+        bits4 = _mm_aesenc_si128(bits4, k);                                                                                        \
+        bits5 = _mm_aesenc_si128(bits5, k);                                                                                        \
+    } while (0)
+
+/* aesenclast */
+#define AESECB6_FINAL(i)                                                                                                           \
+    do {                                                                                                                           \
+        __m128i k = ctx->super.ecb.keys.m128[i];                                                                                   \
+        bits0 = _mm_aesenclast_si128(bits0, k);                                                                                    \
+        bits1 = _mm_aesenclast_si128(bits1, k);                                                                                    \
+        bits2 = _mm_aesenclast_si128(bits2, k);                                                                                    \
+        bits3 = _mm_aesenclast_si128(bits3, k);                                                                                    \
+        bits4 = _mm_aesenclast_si128(bits4, k);                                                                                    \
+        bits5 = _mm_aesenclast_si128(bits5, k);                                                                                    \
+    } while (0)
+
+    struct aesgcm_context *agctx = (void *)_ctx;
+    uint8_t *output = _output;
+    const uint8_t *input = _input;
+
+#define STATE_EK0_READY 0x1
+    int32_t state = 0;
+
+    /* setup ctr, retain Ek(0), len(A) | len(C) to be fed into GCM */
+    __m128i ctr = calc_counter(agctx, seq);
+    ctr = _mm_insert_epi32(ctr, 1, 0);
+    __m128i ek0 = _mm_shuffle_epi8(ctr, byteswap128);
+    __m128i ac = _mm_shuffle_epi8(_mm_set_epi32(0, (int)aadlen * 8, 0, (int)inlen * 8), byteswap128);
+
+    struct ptls_fusion_aesgcm_context128 *ctx = (void *)agctx->aesgcm;
+    __m128i bits0, bits1, bits2, bits3, bits4, bits5 = _mm_setzero_si128();
+    struct ptls_fusion_gfmul_state128 gstate = {0};
+
+    /* Prepare first 6 blocks of bit stream, at the same time calculating ghash of AAD. */
+    AESECB6_INIT();
+    AESECB6_UPDATE(1);
+    AESECB6_UPDATE(2);
+    reduce_aad128(&gstate, ctx->ghash, aad, aadlen);
+    for (size_t i = 3; i < ctx->super.ecb.rounds; ++i)
+        AESECB6_UPDATE(i);
+    AESECB6_FINAL(ctx->super.ecb.rounds);
+
+    /* Main loop. Operate in full blocks (6 * 16 bytes). */
+    while (PTLS_LIKELY(inlen >= 6 * 16)) {
+#define DECRYPT(i) _mm_storeu_si128((void *)(output + i * 16), _mm_xor_si128(bits##i, _mm_loadu_si128((void *)(input + i * 16))))
+        DECRYPT(0);
+        DECRYPT(1);
+        DECRYPT(2);
+        DECRYPT(3);
+        DECRYPT(4);
+        DECRYPT(5);
+#undef DECRYPT
+#define GFMUL_NEXT(i) gfmul_nextstep128(&gstate, _mm_loadu_si128((void *)(input + i * 16)), ctx->ghash + 5 - i)
+        AESECB6_INIT();
+        AESECB6_UPDATE(1);
+        AESECB6_UPDATE(2);
+        AESECB6_UPDATE(3);
+        gfmul_firststep128(&gstate, _mm_loadu_si128((void *)input), ctx->ghash + 5);
+        AESECB6_UPDATE(4);
+        GFMUL_NEXT(1);
+        AESECB6_UPDATE(5);
+        GFMUL_NEXT(2);
+        AESECB6_UPDATE(6);
+        GFMUL_NEXT(3);
+        AESECB6_UPDATE(7);
+        GFMUL_NEXT(4);
+        AESECB6_UPDATE(8);
+        GFMUL_NEXT(5);
+        AESECB6_UPDATE(9);
+        gfmul_reduce128(&gstate);
+        if (PTLS_UNLIKELY(ctx->super.ecb.rounds != 10)) {
+            size_t i = 10;
+            do {
+                AESECB6_UPDATE(i);
+            } while (++i < ctx->super.ecb.rounds);
+        }
+        AESECB6_FINAL(ctx->super.ecb.rounds);
+        output += 6 * 16;
+        input += 6 * 16;
+        inlen -= 6 * 16;
+#undef GFMUL_NEXT
+    }
+
+    /* Decrypt the remainder as well as finishing GHASH calculation. */
+    if (inlen != 0) {
+        struct ptls_fusion_aesgcm_ghash_precompute128 *ghash_precompute = ctx->ghash + (inlen + 15) / 16 + 1;
+#define ONEBLOCK(i)                                                                                                                \
+    do {                                                                                                                           \
+        if (inlen != 0) {                                                                                                          \
+            __m128i b = inlen >= 16 ? _mm_loadu_si128((void *)input) : loadn128(input, inlen);                                     \
+            if (i == 0) {                                                                                                          \
+                gfmul_firststep128(&gstate, b, --ghash_precompute);                                                                \
+            } else {                                                                                                               \
+                gfmul_nextstep128(&gstate, b, --ghash_precompute);                                                                 \
+            }                                                                                                                      \
+            b = _mm_xor_si128(b, bits##i);                                                                                         \
+            if (inlen >= 16) {                                                                                                     \
+                _mm_storeu_si128((void *)output, b);                                                                               \
+                output += 16;                                                                                                      \
+                input += 16;                                                                                                       \
+                inlen -= 16;                                                                                                       \
+            } else {                                                                                                               \
+                storen128(output, inlen, b);                                                                                       \
+                output += inlen;                                                                                                   \
+                input += inlen;                                                                                                    \
+                inlen = 0;                                                                                                         \
+            }                                                                                                                      \
+        }                                                                                                                          \
+    } while (0)
+        ONEBLOCK(0);
+        ONEBLOCK(1);
+        ONEBLOCK(2);
+        ONEBLOCK(3);
+        ONEBLOCK(4);
+        ONEBLOCK(5);
+#undef ONEBLOCK
+        gfmul_nextstep128(&gstate, ac, --ghash_precompute);
+        assert(ghash_precompute == ctx->ghash);
+    } else {
+        gfmul_firststep128(&gstate, ac, ctx->ghash);
+    }
+    gfmul_reduce128(&gstate);
+
+    /* Calculate EK0 if not yet available in bits5. */
+    if ((state & STATE_EK0_READY) == 0) {
+        bits5 = _mm_xor_si128(ek0, ctx->super.ecb.keys.m128[0]);
+        for (size_t i = 1; i < ctx->super.ecb.rounds; ++i)
+            bits5 = _mm_aesenc_si128(bits5, ctx->super.ecb.keys.m128[i]);
+        bits5 = _mm_aesenclast_si128(bits5, ctx->super.ecb.keys.m128[ctx->super.ecb.rounds]);
+    }
+
+    /* Calculate GCM tag and compare. */
+    __m128i calctag = gfmul_get_tag128(&gstate, bits5);
+    __m128i recvtag = _mm_loadu_si128((void *)input);
+    if (_mm_movemask_epi8(_mm_cmpeq_epi8(calctag, recvtag)) != 0xffff)
+        return SIZE_MAX;
+
+    return textlen;
+
+#undef AESECB6_INIT
+#undef AESECB6_UPDATE
+#undef AESECB6_FINAL
+#undef STATE_EK0_READY
+}
+
+NO_SANITIZE_ADDRESS
+static void non_temporal_encrypt_v256(struct st_ptls_aead_context_t *_ctx, void *_output, ptls_iovec_t *input, size_t incnt,
+                                      uint64_t seq, const void *_aad, size_t aadlen)
+{
+/* init the bits (we can always run in full), but use the last slot for calculating ek0, if possible */
+#define AESECB6_INIT()                                                                                                             \
+    do {                                                                                                                           \
+        ctr = _mm256_add_epi64(ctr, incr128x2);                                                                                    \
+        bits0 = _mm256_shuffle_epi8(ctr, byteswap256);                                                                             \
+        ctr = _mm256_add_epi64(ctr, incr128x2);                                                                                    \
+        bits1 = _mm256_shuffle_epi8(ctr, byteswap256);                                                                             \
+        ctr = _mm256_add_epi64(ctr, incr128x2);                                                                                    \
+        bits2 = _mm256_shuffle_epi8(ctr, byteswap256);                                                                             \
+        ctr = _mm256_add_epi64(ctr, incr128x2);                                                                                    \
+        bits3 = _mm256_shuffle_epi8(ctr, byteswap256);                                                                             \
+        ctr = _mm256_add_epi64(ctr, incr128x2);                                                                                    \
+        bits4 = _mm256_shuffle_epi8(ctr, byteswap256);                                                                             \
+        ctr = _mm256_add_epi64(ctr, incr128x2);                                                                                    \
+        bits5 = _mm256_shuffle_epi8(ctr, byteswap256);                                                                             \
+        if (PTLS_UNLIKELY(srclen <= 32 * 6 - 16) && src_vecleft == 0) {                                                            \
+            bits5 = _mm256_permute2f128_si256(bits5, ac_ek0, 0x30);                                                                \
+            state |= STATE_EK0_READY;                                                                                              \
+        }                                                                                                                          \
+        __m256i k = ctx->super.ecb.keys.m256[0];                                                                                   \
+        bits0 = _mm256_xor_si256(bits0, k);                                                                                        \
+        bits1 = _mm256_xor_si256(bits1, k);                                                                                        \
+        bits2 = _mm256_xor_si256(bits2, k);                                                                                        \
+        bits3 = _mm256_xor_si256(bits3, k);                                                                                        \
+        bits4 = _mm256_xor_si256(bits4, k);                                                                                        \
+        bits5 = _mm256_xor_si256(bits5, k);                                                                                        \
+    } while (0)
+
+/* aes block update */
+#define AESECB6_UPDATE(i)                                                                                                          \
+    do {                                                                                                                           \
+        __m256i k = ctx->super.ecb.keys.m256[i];                                                                                   \
+        bits0 = _mm256_aesenc_epi128(bits0, k);                                                                                    \
+        bits1 = _mm256_aesenc_epi128(bits1, k);                                                                                    \
+        bits2 = _mm256_aesenc_epi128(bits2, k);                                                                                    \
+        bits3 = _mm256_aesenc_epi128(bits3, k);                                                                                    \
+        bits4 = _mm256_aesenc_epi128(bits4, k);                                                                                    \
+        bits5 = _mm256_aesenc_epi128(bits5, k);                                                                                    \
+    } while (0)
+
+/* aesenclast */
+#define AESECB6_FINAL(i)                                                                                                           \
+    do {                                                                                                                           \
+        __m256i k = ctx->super.ecb.keys.m256[i];                                                                                   \
+        bits0 = _mm256_aesenclast_epi128(bits0, k);                                                                                \
+        bits1 = _mm256_aesenclast_epi128(bits1, k);                                                                                \
+        bits2 = _mm256_aesenclast_epi128(bits2, k);                                                                                \
+        bits3 = _mm256_aesenclast_epi128(bits3, k);                                                                                \
+        bits4 = _mm256_aesenclast_epi128(bits4, k);                                                                                \
+        bits5 = _mm256_aesenclast_epi128(bits5, k);                                                                                \
+    } while (0)
+
+    struct aesgcm_context *agctx = (void *)_ctx;
+    uint8_t *output = _output;
+    const uint8_t *aad = _aad;
+
+#define STATE_EK0_READY 0x1
+    int32_t state = 0;
+
+    /* Bytes are written here first then written using NT store instructions, 64 bytes at a time. */
+    uint8_t encbuf[32 * 9] __attribute__((aligned(32))), *encp;
+
+    /* `encbuf` should be large enough to store up to 63-bytes of unaligned bytes, 6 16-byte AES blocks, plus AEAD tag that is
+     * append to the ciphertext before writing the bytes to main memory using NT store instructions. */
+    PTLS_BUILD_ASSERT(sizeof(encbuf) >= 64 + 6 * 32 + 16);
+
+    /* load unaligned data within same cache line preceding `output`, adjusting pointers accordingly */
+    encp = load_preceding_unaligned(encbuf, &output);
+
+    /* setup ctr, retaining Ek(0), len(A) | len(C) to be fed into GCM */
+    __m256i ctr = _mm256_broadcastsi128_si256(calc_counter(agctx, seq));
+    ctr = _mm256_insert_epi32(ctr, 1, 4);
+    __m256i ac_ek0 = _mm256_permute2f128_si256(
+        /* first half: ac */
+        _mm256_castsi128_si256(
+            _mm_shuffle_epi8(_mm_set_epi32(0, (int)aadlen * 8, 0, (int)calc_total_length(input, incnt) * 8), byteswap128)),
+        /* second half: ek0 */
+        _mm256_shuffle_epi8(ctr, byteswap256), 0x30);
+
+    struct ptls_fusion_aesgcm_context256 *ctx = (void *)agctx->aesgcm;
+    __m256i bits0, bits1, bits2, bits3, bits4, bits5 = _mm256_setzero_si256();
+    struct ptls_fusion_gfmul_state256 gstate = {0};
+
+    /* find the first non-empty vec */
+    const uint8_t *src = NULL;
+    size_t srclen = 0, src_vecleft = incnt;
+    while (srclen == 0 && src_vecleft != 0) {
+        src = (void *)input[0].base;
+        srclen = input[0].len;
+        ++input;
+        --src_vecleft;
+    }
+
+    /* Prepare first 6 blocks of bit stream, at the same time calculating ghash of AAD. */
+    AESECB6_INIT();
+    AESECB6_UPDATE(1);
+    AESECB6_UPDATE(2);
+    if (PTLS_LIKELY(aadlen != 0)) {
+        union ptls_fusion_aesgcm_ghash_precompute256 *ghash_precompute;
+        while (PTLS_UNLIKELY(aadlen >= 6 * 32)) {
+            ghash_precompute = ctx->ghash + 6;
+            gfmul_firststep256(&gstate, _mm256_loadu_si256((void *)aad), 0, --ghash_precompute);
+            aad += 32;
+            aadlen -= 32;
+            for (int i = 1; i < 6; ++i) {
+                gfmul_nextstep256(&gstate, _mm256_loadu_si256((void *)aad), --ghash_precompute);
+                aad += 32;
+                aadlen -= 32;
+            }
+            gfmul_reduce256(&gstate);
+        }
+        if (PTLS_LIKELY(aadlen != 0)) {
+            ghash_precompute = ctx->ghash + (aadlen + 31) / 32;
+            if (PTLS_UNLIKELY(aadlen >= 32)) {
+                if (aadlen % 32 == 0 || aadlen % 32 > 16) {
+                    gfmul_firststep256(&gstate, _mm256_loadu_si256((void *)aad), 0, --ghash_precompute);
+                    aad += 32;
+                    aadlen -= 32;
+                } else {
+                    gfmul_firststep256(&gstate, _mm256_loadu_si256((void *)aad), 1, --ghash_precompute);
+                    aad += 16;
+                    aadlen -= 16;
+                }
+                while (aadlen >= 32) {
+                    gfmul_nextstep256(&gstate, _mm256_loadu_si256((void *)aad), --ghash_precompute);
+                    aad += 32;
+                    aadlen -= 32;
+                }
+                if (PTLS_LIKELY(aadlen != 0)) {
+                    assert(aadlen > 16);
+                    gfmul_nextstep256(&gstate, loadn256(aad, aadlen), --ghash_precompute);
+                }
+            } else {
+                gfmul_firststep256(&gstate, loadn256(aad, aadlen), aadlen <= 16, --ghash_precompute);
+            }
+            assert(ctx->ghash == ghash_precompute);
+            gfmul_reduce256(&gstate);
+        }
+    }
+    for (size_t i = 3; i < ctx->super.ecb.rounds; ++i)
+        AESECB6_UPDATE(i);
+    AESECB6_FINAL(ctx->super.ecb.rounds);
+
+    /* Main loop. This loop:
+     *  1. using current keystream (bits0..bits5), xors a up to 6 * 16 bytes and writes to encbuf,
+     *  2. then if there is no more data to be encrypted, exit the loop, otherwise,
+     *  3. calculate ghash of the blocks being written to encbuf,
+     *  4. calculate next 6 * 16 bytes of keystream,
+     *  5. writes encbuf in 64-byte blocks
+     * When exitting the loop, `remaining_ghash_from` represents the offset within `encbuf` from where ghash remains to be
+     * calculated. */
+    size_t remaining_ghash_from = encp - encbuf;
+    if (srclen != 0) {
+        while (1) {
+            /* apply the bit stream to input, writing to encbuf */
+            if (PTLS_LIKELY(srclen >= 6 * 32)) {
+#define APPLY(i) _mm256_storeu_si256((void *)(encp + i * 32), _mm256_xor_si256(_mm256_loadu_si256((void *)(src + i * 32)), bits##i))
+                APPLY(0);
+                APPLY(1);
+                APPLY(2);
+                APPLY(3);
+                APPLY(4);
+                APPLY(5);
+#undef APPLY
+                encp += 6 * 32;
+                src += 6 * 32;
+                srclen -= 6 * 32;
+                if (PTLS_UNLIKELY(srclen == 0)) {
+                    if (src_vecleft == 0) {
+                        remaining_ghash_from = (encp - encbuf) - 6 * 32;
+                        break;
+                    }
+                    src = (void *)input[0].base;
+                    srclen = input[0].len;
+                    ++input;
+                    --src_vecleft;
+                }
+            } else {
+                /* slow path, load at most 6 * 32 bytes to encbuf then encrypt in-place */
+                size_t bytes_copied = 0;
+                do {
+                    if (srclen >= 32 && bytes_copied < 5 * 32) {
+                        _mm256_storeu_si256((void *)(encp + bytes_copied), _mm256_loadu_si256((void *)src));
+                        bytes_copied += 32;
+                        src += 32;
+                        srclen -= 32;
+                    } else {
+                        encp[bytes_copied++] = *src++;
+                        --srclen;
+                    }
+                    if (PTLS_UNLIKELY(srclen == 0)) {
+                        do {
+                            if (src_vecleft == 0)
+                                break;
+                            src = (void *)input[0].base;
+                            srclen = input[0].len;
+                            ++input;
+                            --src_vecleft;
+                        } while (srclen == 0);
+                        if (srclen == 0)
+                            break;
+                    }
+                } while (bytes_copied < 6 * 32);
+#define APPLY(i)                                                                                                                   \
+    _mm256_storeu_si256((void *)(encp + i * 32), _mm256_xor_si256(_mm256_loadu_si256((void *)(encp + i * 32)), bits##i))
+                APPLY(0);
+                APPLY(1);
+                APPLY(2);
+                APPLY(3);
+                APPLY(4);
+                APPLY(5);
+#undef APPLY
+                encp += bytes_copied;
+                if (PTLS_UNLIKELY(srclen == 0)) {
+                    /* Calculate amonut of data left to be ghashed, as well as zero-clearing the remainedr of partial block, as it
+                     * will be fed into ghash. */
+                    remaining_ghash_from = (encp - encbuf) - bytes_copied;
+                    if ((bytes_copied & 15) != 0)
+                        _mm_storeu_si128((void *)encp, _mm_setzero_si128());
+                    break;
+                }
+            }
+
+            /* Next 96-byte block starts here. Run AES and ghash in parallel while writing output using non-temporal store
+             * instructions. */
+            AESECB6_INIT();
+            union ptls_fusion_aesgcm_ghash_precompute256 *ghash_precompute = ctx->ghash + 6;
+            gfmul_firststep256(&gstate, _mm256_loadu_si256((void *)(encp - 6 * 32)), 0, --ghash_precompute);
+            AESECB6_UPDATE(1);
+            gfmul_nextstep256(&gstate, _mm256_loadu_si256((void *)(encp - 5 * 32)), --ghash_precompute);
+            AESECB6_UPDATE(2);
+            gfmul_nextstep256(&gstate, _mm256_loadu_si256((void *)(encp - 4 * 32)), --ghash_precompute);
+            AESECB6_UPDATE(3);
+            gfmul_nextstep256(&gstate, _mm256_loadu_si256((void *)(encp - 3 * 32)), --ghash_precompute);
+            AESECB6_UPDATE(4);
+            _mm256_stream_si256((void *)output, _mm256_load_si256((void *)encbuf));
+            _mm256_stream_si256((void *)(output + 32), _mm256_load_si256((void *)(encbuf + 32)));
+            _mm256_stream_si256((void *)(output + 64), _mm256_load_si256((void *)(encbuf + 64)));
+            _mm256_stream_si256((void *)(output + 96), _mm256_load_si256((void *)(encbuf + 96)));
+            _mm256_stream_si256((void *)(output + 128), _mm256_load_si256((void *)(encbuf + 128)));
+            _mm256_stream_si256((void *)(output + 160), _mm256_load_si256((void *)(encbuf + 160)));
+            AESECB6_UPDATE(5);
+            gfmul_nextstep256(&gstate, _mm256_loadu_si256((void *)(encp - 2 * 32)), --ghash_precompute);
+            AESECB6_UPDATE(6);
+            gfmul_nextstep256(&gstate, _mm256_loadu_si256((void *)(encp - 1 * 32)), --ghash_precompute);
+            output += 192;
+            encp -= 192;
+            AESECB6_UPDATE(7);
+            _mm256_store_si256((void *)encbuf, _mm256_load_si256((void *)(encbuf + 192)));
+            AESECB6_UPDATE(8);
+            _mm256_store_si256((void *)(encbuf + 32), _mm256_load_si256((void *)(encbuf + 224)));
+            AESECB6_UPDATE(9);
+            if (PTLS_UNLIKELY(ctx->super.ecb.rounds != 10)) {
+                for (size_t i = 10; PTLS_LIKELY(i < ctx->super.ecb.rounds); ++i)
+                    AESECB6_UPDATE(i);
+            }
+            assert(ctx->ghash == ghash_precompute);
+            gfmul_reduce256(&gstate);
+            AESECB6_FINAL(ctx->super.ecb.rounds);
+        }
+    }
+
+    /* Now, All the encrypted bits are built in encbuf. Calculate AEAD tag and append to encbuf. */
+
+    { /* Run ghash against the remaining bytes, after appending `ac` (i.e., len(A) | len(C)). At this point, we might be ghashing 7
+       * blocks at once. */
+        size_t ac_off = remaining_ghash_from + ((encp - encbuf) - remaining_ghash_from + 15) / 16 * 16;
+        _mm_storeu_si128((void *)(encbuf + ac_off), _mm256_castsi256_si128(ac_ek0));
+        size_t blocks = ((encp - encbuf) - remaining_ghash_from + 15) / 16 + 1; /* round up, +1 for AC */
+        assert(blocks <= 13);
+        union ptls_fusion_aesgcm_ghash_precompute256 *ghash_precompute = ctx->ghash + blocks / 2;
+        if (blocks % 2 != 0) {
+            gfmul_firststep256(&gstate, _mm256_loadu_si256((void *)(encbuf + remaining_ghash_from)), 1, ghash_precompute);
+            remaining_ghash_from += 16;
+        } else {
+            gfmul_firststep256(&gstate, _mm256_loadu_si256((void *)(encbuf + remaining_ghash_from)), 0, --ghash_precompute);
+            remaining_ghash_from += 32;
+        }
+        while (ghash_precompute != ctx->ghash) {
+            gfmul_nextstep256(&gstate, _mm256_loadu_si256((void *)(encbuf + remaining_ghash_from)), --ghash_precompute);
+            remaining_ghash_from += 32;
+        }
+        gfmul_reduce256(&gstate);
+    }
+
+    /* Calculate EK0, if in the unlikely case on not been done yet. When encoding in full size (16K), EK0 will be ready. */
+    if (PTLS_UNLIKELY((state & STATE_EK0_READY) == 0)) {
+        bits5 = ac_ek0;
+        bits5 = _mm256_xor_si256(bits5, ctx->super.ecb.keys.m256[0]);
+        for (size_t i = 1; i < ctx->super.ecb.rounds; ++i)
+            bits5 = _mm256_aesenc_epi128(bits5, ctx->super.ecb.keys.m256[i]);
+        bits5 = _mm256_aesenclast_epi128(bits5, ctx->super.ecb.keys.m256[ctx->super.ecb.rounds]);
+    }
+
+    /* append tag to encbuf */
+    _mm_storeu_si128((void *)encp,
+                     gfmul_get_tag256(&gstate, _mm256_castsi256_si128(_mm256_permute2f128_si256(bits5, bits5, 0x11))));
+    encp += 16;
+
+    /* write remaining bytes */
+    write_remaining_bytes(output, encbuf, encp);
+}
+
+static int non_temporal_setup(ptls_aead_context_t *_ctx, int is_enc, const void *key, const void *iv, size_t key_size)
+{
+    struct aesgcm_context *ctx = (struct aesgcm_context *)_ctx;
+    int aesni256 = is_enc && ptls_fusion_can_aesni256;
+
+    ctx->static_iv = loadn128(iv, PTLS_AESGCM_IV_SIZE);
+    ctx->static_iv = _mm_shuffle_epi8(ctx->static_iv, byteswap128);
+    if (key == NULL)
+        return 0;
+
+    ctx->super.dispose_crypto = aesgcm_dispose_crypto;
+    ctx->super.do_get_iv = aesgcm_get_iv;
+    ctx->super.do_set_iv = aesgcm_set_iv;
+    ctx->super.do_encrypt_init = NULL;
+    ctx->super.do_encrypt_update = NULL;
+    ctx->super.do_encrypt_final = NULL;
+    if (is_enc) {
+        ctx->super.do_encrypt = ptls_aead__do_encrypt;
+        ctx->super.do_encrypt_v = aesni256 ? non_temporal_encrypt_v256 : non_temporal_encrypt_v128;
+        ctx->super.do_decrypt = NULL;
+    } else {
+        assert(!aesni256);
+        ctx->super.do_encrypt = NULL;
+        ctx->super.do_encrypt_v = NULL;
+        ctx->super.do_decrypt = non_temporal_decrypt128;
+    }
+
+    ctx->aesgcm =
+        new_aesgcm(key, key_size,
+                   7 * (ptls_fusion_can_aesni256 ? 32 : 16), // 6 blocks at once, plus len(A) | len(C) that we might append
+                   aesni256);
+
+    return 0;
+}
+
+static int non_temporal_aes128gcm_setup(ptls_aead_context_t *ctx, int is_enc, const void *key, const void *iv)
+{
+    return non_temporal_setup(ctx, is_enc, key, iv, PTLS_AES128_KEY_SIZE);
+}
+
+static int non_temporal_aes256gcm_setup(ptls_aead_context_t *ctx, int is_enc, const void *key, const void *iv)
+{
+    return non_temporal_setup(ctx, is_enc, key, iv, PTLS_AES256_KEY_SIZE);
+}
+
+ptls_aead_algorithm_t ptls_non_temporal_aes128gcm = {"AES128-GCM",
+                                                     PTLS_AESGCM_CONFIDENTIALITY_LIMIT,
+                                                     PTLS_AESGCM_INTEGRITY_LIMIT,
+                                                     &ptls_fusion_aes128ctr,
+                                                     NULL, // &ptls_fusion_aes128ecb,
+                                                     PTLS_AES128_KEY_SIZE,
+                                                     PTLS_AESGCM_IV_SIZE,
+                                                     PTLS_AESGCM_TAG_SIZE,
+                                                     {PTLS_TLS12_AESGCM_FIXED_IV_SIZE, PTLS_TLS12_AESGCM_RECORD_IV_SIZE},
+                                                     1,
+                                                     PTLS_X86_CACHE_LINE_ALIGN_BITS,
+                                                     sizeof(struct aesgcm_context),
+                                                     non_temporal_aes128gcm_setup};
+ptls_aead_algorithm_t ptls_non_temporal_aes256gcm = {"AES256-GCM",
+                                                     PTLS_AESGCM_CONFIDENTIALITY_LIMIT,
+                                                     PTLS_AESGCM_INTEGRITY_LIMIT,
+                                                     &ptls_fusion_aes256ctr,
+                                                     NULL, // &ptls_fusion_aes128ecb,
+                                                     PTLS_AES256_KEY_SIZE,
+                                                     PTLS_AESGCM_IV_SIZE,
+                                                     PTLS_AESGCM_TAG_SIZE,
+                                                     {PTLS_TLS12_AESGCM_FIXED_IV_SIZE, PTLS_TLS12_AESGCM_RECORD_IV_SIZE},
+                                                     1,
+                                                     PTLS_X86_CACHE_LINE_ALIGN_BITS,
+                                                     sizeof(struct aesgcm_context),
+                                                     non_temporal_aes256gcm_setup};
+
+#ifdef _WINDOWS
+/**
+ * ptls_fusion_is_supported_by_cpu:
+ * Check that the CPU has extended instructions for PCMUL, AES and AVX2.
+ * This test assumes that the CPU is following the x86/x64 architecture.
+ * A slightly more refined test could check that the cpu_info spells out
+ * "genuineIntel" or "authenticAMD", but would fail in presence of
+ * little known CPU brands or some VM */
+int ptls_fusion_is_supported_by_cpu(void)
+{
+    uint32_t cpu_info[4];
+    uint32_t nb_ids;
+    int is_supported = 0;
+
+    __cpuid(cpu_info, 0);
+    nb_ids = cpu_info[0];
+
+    if (nb_ids >= 7) {
+        uint32_t leaf1_ecx;
+        __cpuid(cpu_info, 1);
+        leaf1_ecx = cpu_info[2];
+
+        if (/* PCLMUL */ (leaf1_ecx & (1 << 5)) != 0 && /* AES */ (leaf1_ecx & (1 << 25)) != 0) {
+            uint32_t leaf7_ebx, leaf7_ecx;
+            __cpuid(cpu_info, 7);
+            leaf7_ebx = cpu_info[1];
+            leaf7_ecx = cpu_info[2];
+
+            is_supported = /* AVX2 */ (leaf7_ebx & (1 << 5)) != 0;
+
+            /* enable 256-bit mode if possible */
+            if (is_supported && (leaf7_ecx & 0x600) != 0 && !ptls_fusion_can_aesni256)
+                ptls_fusion_can_aesni256 = 1;
+        }
+    }
+
+    return is_supported;
+}
+#else
+int ptls_fusion_is_supported_by_cpu(void)
+{
+    unsigned leaf1_ecx, leaf7_ebx, leaf7_ecx;
+
+    { /* GCC-specific code to obtain CPU features */
+        unsigned leaf_cnt;
+        __asm__("cpuid" : "=a"(leaf_cnt) : "a"(0) : "ebx", "ecx", "edx");
+        if (leaf_cnt < 7)
+            return 0;
+        __asm__("cpuid" : "=c"(leaf1_ecx) : "a"(1) : "ebx", "edx");
+        __asm__("cpuid" : "=b"(leaf7_ebx), "=c"(leaf7_ecx) : "a"(7), "c"(0) : "edx");
+    }
+
+    /* AVX2 */
+    if ((leaf7_ebx & (1 << 5)) == 0)
+        return 0;
+    /* AES */
+    if ((leaf1_ecx & (1 << 25)) == 0)
+        return 0;
+    /* PCLMUL */
+    if ((leaf1_ecx & (1 << 1)) == 0)
+        return 0;
+
+    /* enable 256-bit mode if possible */
+    if ((leaf7_ecx & 0x600) != 0 && !ptls_fusion_can_aesni256)
+        ptls_fusion_can_aesni256 = 1;
+
+    return 1;
+}
+#endif
+
+/* ---------------------------------------------------------------- */
+
+// struct aesgcm_context {
+//    ptls_aead_context_t super;
+//    ptls_fusion_aesgcm_context_t *aesgcm; <- ptls_fusion_aesgcm_free
+
+ ptls_aead_context_t *aead_context_new() {
+    ptls_aead_context_t *p = malloc(sizeof(struct aesgcm_context));
+    return p;
+ }
 
 void aead_context_free(ptls_aead_context_t *p) {
   aesgcm_dispose_crypto(p);
diff --git a/cbits/picotls.c b/cbits/picotls.c
--- a/cbits/picotls.c
+++ b/cbits/picotls.c
@@ -19,5568 +19,6629 @@
  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
  * IN THE SOFTWARE.
  */
-#include <assert.h>
-#include <stddef.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#ifdef _WINDOWS
-#include "winsock.h"
-#else
-#include <arpa/inet.h>
-#include <sys/time.h>
-#endif
-#include "picotls.h"
-#if PICOTLS_USE_DTRACE
-#include "picotls-probes.h"
-#endif
-
-#define PTLS_MAX_PLAINTEXT_RECORD_SIZE 16384
-#define PTLS_MAX_ENCRYPTED_RECORD_SIZE (16384 + 256)
-
-#define PTLS_RECORD_VERSION_MAJOR 3
-#define PTLS_RECORD_VERSION_MINOR 3
-
-#define PTLS_CONTENT_TYPE_CHANGE_CIPHER_SPEC 20
-#define PTLS_CONTENT_TYPE_ALERT 21
-#define PTLS_CONTENT_TYPE_HANDSHAKE 22
-#define PTLS_CONTENT_TYPE_APPDATA 23
-
-#define PTLS_PSK_KE_MODE_PSK 0
-#define PTLS_PSK_KE_MODE_PSK_DHE 1
-
-#define PTLS_HANDSHAKE_HEADER_SIZE 4
-
-#define PTLS_EXTENSION_TYPE_SERVER_NAME 0
-#define PTLS_EXTENSION_TYPE_STATUS_REQUEST 5
-#define PTLS_EXTENSION_TYPE_SUPPORTED_GROUPS 10
-#define PTLS_EXTENSION_TYPE_SIGNATURE_ALGORITHMS 13
-#define PTLS_EXTENSION_TYPE_ALPN 16
-#define PTLS_EXTENSION_TYPE_SERVER_CERTIFICATE_TYPE 20
-#define PTLS_EXTENSION_TYPE_COMPRESS_CERTIFICATE 27
-#define PTLS_EXTENSION_TYPE_PRE_SHARED_KEY 41
-#define PTLS_EXTENSION_TYPE_EARLY_DATA 42
-#define PTLS_EXTENSION_TYPE_SUPPORTED_VERSIONS 43
-#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_PROTOCOL_VERSION_TLS13_FINAL 0x0304
-#define PTLS_PROTOCOL_VERSION_TLS13_DRAFT26 0x7f1a
-#define PTLS_PROTOCOL_VERSION_TLS13_DRAFT27 0x7f1b
-#define PTLS_PROTOCOL_VERSION_TLS13_DRAFT28 0x7f1c
-
-#define PTLS_SERVER_NAME_TYPE_HOSTNAME 0
-
-#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                                                                                  \
-    (64 + sizeof(PTLS_SERVER_CERTIFICATE_VERIFY_CONTEXT_STRING) + PTLS_MAX_DIGEST_SIZE * 2)
-
-#define PTLS_EARLY_DATA_MAX_DELAY 10000 /* max. RTT (in msec) to permit early data */
-
-#ifndef PTLS_MAX_EARLY_DATA_SKIP_SIZE
-#define PTLS_MAX_EARLY_DATA_SKIP_SIZE 65536
-#endif
-#if defined(PTLS_DEBUG) && PTLS_DEBUG
-#define PTLS_DEBUGF(...) fprintf(stderr, __VA_ARGS__)
-#else
-#define PTLS_DEBUGF(...)
-#endif
-
-#ifndef PTLS_MEMORY_DEBUG
-#define PTLS_MEMORY_DEBUG 0
-#endif
-
-#if PICOTLS_USE_DTRACE
-#define PTLS_SHOULD_PROBE(LABEL, tls) (PTLS_UNLIKELY(PICOTLS_##LABEL##_ENABLED()) && !(tls)->skip_tracing)
-#define PTLS_PROBE0(LABEL, tls)                                                                                                    \
-    do {                                                                                                                           \
-        ptls_t *_tls = (tls);                                                                                                      \
-        if (PTLS_SHOULD_PROBE(LABEL, _tls))                                                                                        \
-            PICOTLS_##LABEL(_tls);                                                                                                 \
-    } while (0)
-#define PTLS_PROBE(LABEL, tls, ...)                                                                                                \
-    do {                                                                                                                           \
-        ptls_t *_tls = (tls);                                                                                                      \
-        if (PTLS_SHOULD_PROBE(LABEL, _tls))                                                                                        \
-            PICOTLS_##LABEL(_tls, __VA_ARGS__);                                                                                    \
-    } while (0)
-#else
-#define PTLS_PROBE0(LABEL, tls)
-#define PTLS_PROBE(LABEL, tls, ...)
-#endif
-
-/**
- * list of supported versions in the preferred order
- */
-static const uint16_t supported_versions[] = {PTLS_PROTOCOL_VERSION_TLS13_FINAL, PTLS_PROTOCOL_VERSION_TLS13_DRAFT28,
-                                              PTLS_PROTOCOL_VERSION_TLS13_DRAFT27, PTLS_PROTOCOL_VERSION_TLS13_DRAFT26};
-
-static const uint8_t hello_retry_random[PTLS_HELLO_RANDOM_SIZE] = {0xCF, 0x21, 0xAD, 0x74, 0xE5, 0x9A, 0x61, 0x11, 0xBE, 0x1D, 0x8C,
-                                                                   0x02, 0x1E, 0x65, 0xB8, 0x91, 0xC2, 0xA2, 0x11, 0x16, 0x7A, 0xBB,
-                                                                   0x8C, 0x5E, 0x07, 0x9E, 0x09, 0xE2, 0xC8, 0xA8, 0x33, 0x9C};
-
-struct st_ptls_traffic_protection_t {
-    uint8_t secret[PTLS_MAX_DIGEST_SIZE];
-    size_t epoch;
-    /* the following fields are not used if the key_change callback is set */
-    ptls_aead_context_t *aead;
-    uint64_t seq;
-};
-
-struct st_ptls_record_message_emitter_t {
-    ptls_message_emitter_t super;
-    size_t rec_start;
-};
-
-struct st_ptls_signature_algorithms_t {
-    uint16_t list[16]; /* expand? */
-    size_t count;
-};
-
-struct st_ptls_certificate_request_t {
-    /**
-     * context.base becomes non-NULL when a CertificateRequest is pending for processing
-     */
-    ptls_iovec_t context;
-    struct st_ptls_signature_algorithms_t signature_algorithms;
-};
-
-struct st_ptls_t {
-    /**
-     * the context
-     */
-    ptls_context_t *ctx;
-    /**
-     * the state
-     */
-    enum en_ptls_state_t {
-        PTLS_STATE_CLIENT_HANDSHAKE_START,
-        PTLS_STATE_CLIENT_EXPECT_SERVER_HELLO,
-        PTLS_STATE_CLIENT_EXPECT_SECOND_SERVER_HELLO,
-        PTLS_STATE_CLIENT_EXPECT_ENCRYPTED_EXTENSIONS,
-        PTLS_STATE_CLIENT_EXPECT_CERTIFICATE_REQUEST_OR_CERTIFICATE,
-        PTLS_STATE_CLIENT_EXPECT_CERTIFICATE,
-        PTLS_STATE_CLIENT_EXPECT_CERTIFICATE_VERIFY,
-        PTLS_STATE_CLIENT_EXPECT_FINISHED,
-        PTLS_STATE_SERVER_EXPECT_CLIENT_HELLO,
-        PTLS_STATE_SERVER_EXPECT_SECOND_CLIENT_HELLO,
-        PTLS_STATE_SERVER_EXPECT_CERTIFICATE,
-        PTLS_STATE_SERVER_EXPECT_CERTIFICATE_VERIFY,
-        /* ptls_send can be called if the state is below here */
-        PTLS_STATE_SERVER_EXPECT_END_OF_EARLY_DATA,
-        PTLS_STATE_SERVER_EXPECT_FINISHED,
-        PTLS_STATE_POST_HANDSHAKE_MIN,
-        PTLS_STATE_CLIENT_POST_HANDSHAKE = PTLS_STATE_POST_HANDSHAKE_MIN,
-        PTLS_STATE_SERVER_POST_HANDSHAKE
-    } state;
-    /**
-     * receive buffers
-     */
-    struct {
-        ptls_buffer_t rec;
-        ptls_buffer_t mess;
-    } recvbuf;
-    /**
-     * key schedule
-     */
-    ptls_key_schedule_t *key_schedule;
-    /**
-     * values used for record protection
-     */
-    struct {
-        struct st_ptls_traffic_protection_t dec;
-        struct st_ptls_traffic_protection_t enc;
-    } traffic_protection;
-    /**
-     * server-name passed using SNI
-     */
-    char *server_name;
-    /**
-     * result of ALPN
-     */
-    char *negotiated_protocol;
-    /**
-     * selected key-exchange
-     */
-    ptls_key_exchange_algorithm_t *key_share;
-    /**
-     * selected cipher-suite
-     */
-    ptls_cipher_suite_t *cipher_suite;
-    /**
-     * clienthello.random
-     */
-    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;
-    /* flags */
-    unsigned is_server : 1;
-    unsigned is_psk_handshake : 1;
-    unsigned send_change_cipher_spec : 1;
-    unsigned needs_key_update : 1;
-    unsigned key_update_send_request : 1;
-    unsigned skip_tracing : 1;
-    /**
-     * misc.
-     */
-    union {
-        struct {
-            ptls_iovec_t legacy_session_id;
-            uint8_t legacy_session_id_buf[32];
-            ptls_key_exchange_context_t *key_share_ctx;
-            unsigned offered_psk : 1;
-            /**
-             * if 1-RTT write key is active
-             */
-            unsigned using_early_data : 1;
-            struct st_ptls_certificate_request_t certificate_request;
-        } client;
-        struct {
-            uint8_t pending_traffic_secret[PTLS_MAX_DIGEST_SIZE];
-            uint32_t early_data_skipped_bytes; /* if not UINT32_MAX, the server is skipping early data */
-        } server;
-    };
-    /**
-     * certificate verify
-     * will be used by the client and the server (if require_client_authentication is set).
-     */
-    struct {
-        int (*cb)(void *verify_ctx, uint16_t algo, ptls_iovec_t data, ptls_iovec_t signature);
-        void *verify_ctx;
-    } certificate_verify;
-    /**
-     * handshake traffic secret to be commisioned (an array of `uint8_t [PTLS_MAX_DIGEST_SIZE]` or NULL)
-     */
-    uint8_t *pending_handshake_secret;
-    /**
-     * user data
-     */
-    void *data_ptr;
-};
-
-struct st_ptls_record_t {
-    uint8_t type;
-    uint16_t version;
-    size_t length;
-    const uint8_t *fragment;
-};
-
-struct st_ptls_client_hello_psk_t {
-    ptls_iovec_t identity;
-    uint32_t obfuscated_ticket_age;
-    ptls_iovec_t binder;
-};
-
-#define MAX_UNKNOWN_EXTENSIONS 16
-#define MAX_CLIENT_CIPHERS 32
-#define MAX_CERTIFICATE_TYPES 8
-
-struct st_ptls_client_hello_t {
-    uint16_t legacy_version;
-    const uint8_t *random_bytes;
-    ptls_iovec_t legacy_session_id;
-    struct {
-        const uint8_t *ids;
-        size_t count;
-    } compression_methods;
-    uint16_t selected_version;
-    ptls_iovec_t cipher_suites;
-    ptls_iovec_t negotiated_groups;
-    ptls_iovec_t key_shares;
-    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;
-    struct {
-        uint16_t list[16];
-        size_t count;
-    } cert_compression_algos;
-    struct {
-        uint16_t list[MAX_CLIENT_CIPHERS];
-        size_t count;
-    } client_ciphers;
-    struct {
-        ptls_iovec_t all;
-        ptls_iovec_t tbs;
-        ptls_iovec_t ch1_hash;
-        ptls_iovec_t signature;
-        unsigned sent_key_share : 1;
-    } cookie;
-    struct {
-        const uint8_t *hash_end;
-        struct {
-            struct st_ptls_client_hello_psk_t list[4];
-            size_t count;
-        } identities;
-        unsigned ke_modes;
-        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;
-};
-
-struct st_ptls_server_hello_t {
-    uint8_t random_[PTLS_HELLO_RANDOM_SIZE];
-    ptls_iovec_t legacy_session_id;
-    int is_retry_request;
-    union {
-        ptls_iovec_t peerkey;
-        struct {
-            uint16_t selected_group;
-            ptls_iovec_t cookie;
-        } retry_request;
-    };
-};
-
-struct st_ptls_key_schedule_t {
-    unsigned generation; /* early secret (1), hanshake secret (2), master secret (3) */
-    const char *hkdf_label_prefix;
-    uint8_t secret[PTLS_MAX_DIGEST_SIZE];
-    size_t num_hashes;
-    struct {
-        ptls_hash_algorithm_t *algo;
-        ptls_hash_context_t *ctx;
-    } hashes[1];
-};
-
-struct st_ptls_extension_decoder_t {
-    uint16_t type;
-    int (*cb)(ptls_t *tls, void *arg, const uint8_t *src, const uint8_t *const end);
-};
-
-struct st_ptls_extension_bitmap_t {
-    uint8_t bits[8]; /* only ids below 64 is tracked */
-};
-
-static const uint8_t zeroes_of_max_digest_size[PTLS_MAX_DIGEST_SIZE] = {0};
-
-static int hkdf_expand_label(ptls_hash_algorithm_t *algo, void *output, size_t outlen, ptls_iovec_t secret, const char *label,
-                             ptls_iovec_t hash_value, const char *label_prefix);
-static ptls_aead_context_t *new_aead(ptls_aead_algorithm_t *aead, ptls_hash_algorithm_t *hash, int is_enc, const void *secret,
-                                     ptls_iovec_t hash_value, const char *label_prefix);
-
-static int is_supported_version(uint16_t v)
-{
-    size_t i;
-    for (i = 0; i != PTLS_ELEMENTSOF(supported_versions); ++i)
-        if (supported_versions[i] == v)
-            return 1;
-    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, uint8_t hstype)
-{
-    *bitmap = (struct st_ptls_extension_bitmap_t){{0}};
-
-#define EXT(extid, proc)                                                                                                           \
-    do {                                                                                                                           \
-        int _found = 0;                                                                                                            \
-        do {                                                                                                                       \
-            proc                                                                                                                   \
-        } while (0);                                                                                                               \
-        if (!_found)                                                                                                               \
-            extension_bitmap_set(bitmap, PTLS_EXTENSION_TYPE_##extid);                                                             \
-    } while (0)
-#define ALLOW(allowed_hstype) _found = _found || hstype == PTLS_HANDSHAKE_TYPE_##allowed_hstype
-
-    /* Implements the table found in section 4.2 of draft-19; "If an implementation receives an extension which it recognizes and
-     * which is not specified for the message in which it appears it MUST abort the handshake with an “illegal_parameter” alert."
-     */
-    EXT(SERVER_NAME, {
-        ALLOW(CLIENT_HELLO);
-        ALLOW(ENCRYPTED_EXTENSIONS);
-    });
-    EXT(STATUS_REQUEST, {
-        ALLOW(CLIENT_HELLO);
-        ALLOW(CERTIFICATE);
-        ALLOW(CERTIFICATE_REQUEST);
-    });
-    EXT(SUPPORTED_GROUPS, {
-        ALLOW(CLIENT_HELLO);
-        ALLOW(ENCRYPTED_EXTENSIONS);
-    });
-    EXT(SIGNATURE_ALGORITHMS, {
-        ALLOW(CLIENT_HELLO);
-        ALLOW(CERTIFICATE_REQUEST);
-    });
-    EXT(ALPN, {
-        ALLOW(CLIENT_HELLO);
-        ALLOW(ENCRYPTED_EXTENSIONS);
-    });
-    EXT(KEY_SHARE, {
-        ALLOW(CLIENT_HELLO);
-        ALLOW(SERVER_HELLO);
-    });
-    EXT(PRE_SHARED_KEY, {
-        ALLOW(CLIENT_HELLO);
-        ALLOW(SERVER_HELLO);
-    });
-    EXT(PSK_KEY_EXCHANGE_MODES, { ALLOW(CLIENT_HELLO); });
-    EXT(EARLY_DATA, {
-        ALLOW(CLIENT_HELLO);
-        ALLOW(ENCRYPTED_EXTENSIONS);
-        ALLOW(NEW_SESSION_TICKET);
-    });
-    EXT(COOKIE, {
-        ALLOW(CLIENT_HELLO);
-        ALLOW(SERVER_HELLO);
-    });
-    EXT(SUPPORTED_VERSIONS, {
-        ALLOW(CLIENT_HELLO);
-        ALLOW(SERVER_HELLO);
-    });
-    EXT(SERVER_CERTIFICATE_TYPE, {
-        ALLOW(CLIENT_HELLO);
-        ALLOW(ENCRYPTED_EXTENSIONS);
-    });
-
-#undef ALLOW
-#undef EXT
-}
-
-#ifndef ntoh16
-static uint16_t ntoh16(const uint8_t *src)
-{
-    return (uint16_t)src[0] << 8 | src[1];
-}
-#endif
-
-#ifndef ntoh24
-static uint32_t ntoh24(const uint8_t *src)
-{
-    return (uint32_t)src[0] << 16 | (uint32_t)src[1] << 8 | src[2];
-}
-#endif
-
-#ifndef ntoh32
-static uint32_t ntoh32(const uint8_t *src)
-{
-    return (uint32_t)src[0] << 24 | (uint32_t)src[1] << 16 | (uint32_t)src[2] << 8 | src[3];
-}
-#endif
-
-#ifndef ntoh64
-static uint64_t ntoh64(const uint8_t *src)
-{
-    return (uint64_t)src[0] << 56 | (uint64_t)src[1] << 48 | (uint64_t)src[2] << 40 | (uint64_t)src[3] << 32 |
-           (uint64_t)src[4] << 24 | (uint64_t)src[5] << 16 | (uint64_t)src[6] << 8 | src[7];
-}
-#endif
-
-void ptls_buffer__release_memory(ptls_buffer_t *buf)
-{
-    ptls_clear_memory(buf->base, buf->off);
-    if (buf->is_allocated)
-        free(buf->base);
-}
-
-int ptls_buffer_reserve(ptls_buffer_t *buf, size_t delta)
-{
-    if (buf->base == NULL)
-        return PTLS_ERROR_NO_MEMORY;
-
-    if (PTLS_MEMORY_DEBUG || buf->capacity < buf->off + delta) {
-        uint8_t *newp;
-        size_t new_capacity = buf->capacity;
-        if (new_capacity < 1024)
-            new_capacity = 1024;
-        while (new_capacity < buf->off + delta) {
-            new_capacity *= 2;
-        }
-        if ((newp = malloc(new_capacity)) == NULL)
-            return PTLS_ERROR_NO_MEMORY;
-        memcpy(newp, buf->base, buf->off);
-        ptls_buffer__release_memory(buf);
-        buf->base = newp;
-        buf->capacity = new_capacity;
-        buf->is_allocated = 1;
-    }
-
-    return 0;
-}
-
-int ptls_buffer__do_pushv(ptls_buffer_t *buf, const void *src, size_t len)
-{
-    int ret;
-
-    if (len == 0)
-        return 0;
-    if ((ret = ptls_buffer_reserve(buf, len)) != 0)
-        return ret;
-    memcpy(buf->base + buf->off, src, len);
-    buf->off += len;
-    return 0;
-}
-
-int ptls_buffer__adjust_quic_blocksize(ptls_buffer_t *buf, size_t body_size)
-{
-    uint8_t sizebuf[PTLS_ENCODE_QUICINT_CAPACITY];
-    size_t sizelen = ptls_encode_quicint(sizebuf, body_size) - sizebuf;
-
-    /* adjust amount of space before body_size to `sizelen` bytes */
-    if (sizelen != 1) {
-        int ret;
-        if ((ret = ptls_buffer_reserve(buf, sizelen - 1)) != 0)
-            return ret;
-        memmove(buf->base + buf->off - body_size - 1 + sizelen, buf->base + buf->off - body_size, body_size);
-        buf->off += sizelen - 1;
-    }
-
-    /* write the size */
-    memcpy(buf->base + buf->off - body_size - sizelen, sizebuf, sizelen);
-
-    return 0;
-}
-
-int ptls_buffer__adjust_asn1_blocksize(ptls_buffer_t *buf, size_t body_size)
-{
-    fprintf(stderr, "unimplemented\n");
-    abort();
-}
-
-int ptls_buffer_push_asn1_ubigint(ptls_buffer_t *buf, const void *bignum, size_t size)
-{
-    const uint8_t *p = bignum, *const end = p + size;
-    int ret;
-
-    /* skip zeroes */
-    for (; end - p >= 1; ++p)
-        if (*p != 0)
-            break;
-
-    /* emit */
-    ptls_buffer_push(buf, 2);
-    ptls_buffer_push_asn1_block(buf, {
-        if (*p >= 0x80)
-            ptls_buffer_push(buf, 0);
-        if (p != end) {
-            ptls_buffer_pushv(buf, p, end - p);
-        } else {
-            ptls_buffer_pushv(buf, "", 1);
-        }
-    });
-    ret = 0;
-
-Exit:
-    return ret;
-}
-
-#if PTLS_FUZZ_HANDSHAKE
-
-static size_t aead_encrypt(struct st_ptls_traffic_protection_t *ctx, void *output, const void *input, size_t inlen,
-                           uint8_t content_type)
-{
-    memcpy(output, input, inlen);
-    memcpy(output + inlen, &content_type, 1);
-    return inlen + 1 + 16;
-}
-
-static int aead_decrypt(struct st_ptls_traffic_protection_t *ctx, void *output, size_t *outlen, const void *input, size_t inlen)
-{
-    if (inlen < 16) {
-        return PTLS_ALERT_BAD_RECORD_MAC;
-    }
-    memcpy(output, input, inlen - 16);
-    *outlen = inlen - 16; /* removing the 16 bytes of tag */
-    return 0;
-}
-
-#else
-
-static void build_aad(uint8_t aad[5], size_t reclen)
-{
-    aad[0] = PTLS_CONTENT_TYPE_APPDATA;
-    aad[1] = PTLS_RECORD_VERSION_MAJOR;
-    aad[2] = PTLS_RECORD_VERSION_MINOR;
-    aad[3] = (uint8_t)(reclen >> 8);
-    aad[4] = (uint8_t)reclen;
-}
-
-static size_t aead_encrypt(struct st_ptls_traffic_protection_t *ctx, void *output, const void *input, size_t inlen,
-                           uint8_t content_type)
-{
-    uint8_t aad[5];
-    size_t off = 0;
-
-    build_aad(aad, inlen + 1 + ctx->aead->algo->tag_size);
-    ptls_aead_encrypt_init(ctx->aead, ctx->seq++, aad, sizeof(aad));
-    off += ptls_aead_encrypt_update(ctx->aead, ((uint8_t *)output) + off, input, inlen);
-    off += ptls_aead_encrypt_update(ctx->aead, ((uint8_t *)output) + off, &content_type, 1);
-    off += ptls_aead_encrypt_final(ctx->aead, ((uint8_t *)output) + off);
-
-    return off;
-}
-
-static int aead_decrypt(struct st_ptls_traffic_protection_t *ctx, void *output, size_t *outlen, const void *input, size_t inlen)
-{
-    uint8_t aad[5];
-
-    build_aad(aad, inlen);
-    if ((*outlen = ptls_aead_decrypt(ctx->aead, output, input, inlen, ctx->seq, aad, sizeof(aad))) == SIZE_MAX)
-        return PTLS_ALERT_BAD_RECORD_MAC;
-    ++ctx->seq;
-    return 0;
-}
-
-#endif /* #if PTLS_FUZZ_HANDSHAKE */
-
-#define buffer_push_record(buf, type, block)                                                                                       \
-    do {                                                                                                                           \
-        ptls_buffer_push((buf), (type), PTLS_RECORD_VERSION_MAJOR, PTLS_RECORD_VERSION_MINOR);                                     \
-        ptls_buffer_push_block((buf), 2, block);                                                                                   \
-    } while (0)
-
-static int buffer_push_encrypted_records(ptls_buffer_t *buf, uint8_t type, const uint8_t *src, size_t len,
-                                         struct st_ptls_traffic_protection_t *enc)
-{
-    int ret = 0;
-
-    while (len != 0) {
-        size_t chunk_size = len;
-        if (chunk_size > PTLS_MAX_PLAINTEXT_RECORD_SIZE)
-            chunk_size = PTLS_MAX_PLAINTEXT_RECORD_SIZE;
-        buffer_push_record(buf, PTLS_CONTENT_TYPE_APPDATA, {
-            if ((ret = ptls_buffer_reserve(buf, chunk_size + enc->aead->algo->tag_size + 1)) != 0)
-                goto Exit;
-            buf->off += aead_encrypt(enc, buf->base + buf->off, src, chunk_size, type);
-        });
-        src += chunk_size;
-        len -= chunk_size;
-    }
-
-Exit:
-    return ret;
-}
-
-static int buffer_encrypt_record(ptls_buffer_t *buf, size_t rec_start, struct st_ptls_traffic_protection_t *enc)
-{
-    size_t bodylen = buf->off - rec_start - 5;
-    uint8_t *tmpbuf, type = buf->base[rec_start];
-    int ret;
-
-    /* fast path: do in-place encryption if only one record needs to be emitted */
-    if (bodylen <= PTLS_MAX_PLAINTEXT_RECORD_SIZE) {
-        size_t overhead = 1 + enc->aead->algo->tag_size;
-        if ((ret = ptls_buffer_reserve(buf, overhead)) != 0)
-            return ret;
-        size_t encrypted_len = aead_encrypt(enc, buf->base + rec_start + 5, buf->base + rec_start + 5, bodylen, type);
-        assert(encrypted_len == bodylen + overhead);
-        buf->off += overhead;
-        buf->base[rec_start] = PTLS_CONTENT_TYPE_APPDATA;
-        buf->base[rec_start + 3] = (encrypted_len >> 8) & 0xff;
-        buf->base[rec_start + 4] = encrypted_len & 0xff;
-        return 0;
-    }
-
-    /* move plaintext to temporary buffer */
-    if ((tmpbuf = malloc(bodylen)) == NULL) {
-        ret = PTLS_ERROR_NO_MEMORY;
-        goto Exit;
-    }
-    memcpy(tmpbuf, buf->base + rec_start + 5, bodylen);
-    ptls_clear_memory(buf->base + rec_start, bodylen + 5);
-    buf->off = rec_start;
-
-    /* push encrypted records */
-    ret = buffer_push_encrypted_records(buf, type, tmpbuf, bodylen, enc);
-
-Exit:
-    if (tmpbuf != NULL) {
-        ptls_clear_memory(tmpbuf, bodylen);
-        free(tmpbuf);
-    }
-    return ret;
-}
-
-static int begin_record_message(ptls_message_emitter_t *_self)
-{
-    struct st_ptls_record_message_emitter_t *self = (void *)_self;
-    int ret;
-
-    self->rec_start = self->super.buf->off;
-    ptls_buffer_push(self->super.buf, PTLS_CONTENT_TYPE_HANDSHAKE, PTLS_RECORD_VERSION_MAJOR, PTLS_RECORD_VERSION_MINOR, 0, 0);
-    ret = 0;
-Exit:
-    return ret;
-}
-
-static int commit_record_message(ptls_message_emitter_t *_self)
-{
-    struct st_ptls_record_message_emitter_t *self = (void *)_self;
-    int ret;
-
-    if (self->super.enc->aead != NULL) {
-        ret = buffer_encrypt_record(self->super.buf, self->rec_start, self->super.enc);
-    } else {
-        /* TODO allow CH,SH,HRR above 16KB */
-        size_t sz = self->super.buf->off - self->rec_start - 5;
-        assert(sz <= PTLS_MAX_PLAINTEXT_RECORD_SIZE);
-        self->super.buf->base[self->rec_start + 3] = (uint8_t)(sz >> 8);
-        self->super.buf->base[self->rec_start + 4] = (uint8_t)(sz);
-        ret = 0;
-    }
-
-    return ret;
-}
-
-#define buffer_push_extension(buf, type, block)                                                                                    \
-    do {                                                                                                                           \
-        ptls_buffer_push16((buf), (type));                                                                                         \
-        ptls_buffer_push_block((buf), 2, block);                                                                                   \
-    } while (0);
-
-#define decode_open_extensions(src, end, hstype, exttype, block)                                                                   \
-    do {                                                                                                                           \
-        struct st_ptls_extension_bitmap_t bitmap;                                                                                  \
-        init_extension_bitmap(&bitmap, (hstype));                                                                                  \
-        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) {                                                           \
-                    ret = PTLS_ALERT_ILLEGAL_PARAMETER;                                                                            \
-                    goto Exit;                                                                                                     \
-                }                                                                                                                  \
-                extension_bitmap_set(&bitmap, *(exttype));                                                                         \
-                ptls_decode_open_block((src), end, 2, block);                                                                      \
-            }                                                                                                                      \
-        });                                                                                                                        \
-    } while (0)
-
-#define decode_extensions(src, end, hstype, exttype, block)                                                                        \
-    do {                                                                                                                           \
-        decode_open_extensions((src), end, hstype, exttype, block);                                                                \
-        ptls_decode_assert_block_close((src), end);                                                                                \
-    } while (0)
-
-int ptls_decode16(uint16_t *value, const uint8_t **src, const uint8_t *end)
-{
-    if (end - *src < 2)
-        return PTLS_ALERT_DECODE_ERROR;
-    *value = ntoh16(*src);
-    *src += 2;
-    return 0;
-}
-
-int ptls_decode24(uint32_t *value, const uint8_t **src, const uint8_t *end)
-{
-    if (end - *src < 3)
-        return PTLS_ALERT_DECODE_ERROR;
-    *value = ((uint32_t)(*src)[0] << 16) | ((uint32_t)(*src)[1] << 8) | (*src)[2];
-    *src += 3;
-    return 0;
-}
-
-int ptls_decode32(uint32_t *value, const uint8_t **src, const uint8_t *end)
-{
-    if (end - *src < 4)
-        return PTLS_ALERT_DECODE_ERROR;
-    *value = ntoh32(*src);
-    *src += 4;
-    return 0;
-}
-
-int ptls_decode64(uint64_t *value, const uint8_t **src, const uint8_t *end)
-{
-    if (end - *src < 8)
-        return PTLS_ALERT_DECODE_ERROR;
-    *value = ntoh64(*src);
-    *src += 8;
-    return 0;
-}
-
-uint64_t ptls_decode_quicint(const uint8_t **src, const uint8_t *end)
-{
-    if (PTLS_UNLIKELY(*src == end))
-        return UINT64_MAX;
-
-    uint8_t b = *(*src)++;
-
-    if (PTLS_LIKELY(b <= 0x3f))
-        return b;
-
-    uint64_t v = b & 0x3f;
-    unsigned bytes_left = (1 << (b >> 6)) - 1;
-    if (PTLS_UNLIKELY((size_t)(end - *src) < bytes_left))
-        return UINT64_MAX;
-    do {
-        v = (v << 8) | *(*src)++;
-    } while (--bytes_left != 0);
-    return v;
-}
-
-static void log_secret(ptls_t *tls, const char *type, ptls_iovec_t secret)
-{
-    char hexbuf[PTLS_MAX_DIGEST_SIZE * 2 + 1];
-
-    PTLS_PROBE(NEW_SECRET, tls, type, ptls_hexdump(hexbuf, secret.base, secret.len));
-
-    if (tls->ctx->log_event != NULL)
-        tls->ctx->log_event->cb(tls->ctx->log_event, tls, type, "%s", ptls_hexdump(hexbuf, secret.base, secret.len));
-}
-
-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)
-        sched->hashes[i].ctx->final(sched->hashes[i].ctx, 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,
-                                             const char *hkdf_label_prefix)
-{
-#define FOREACH_HASH(block)                                                                                                        \
-    do {                                                                                                                           \
-        ptls_cipher_suite_t *cs;                                                                                                   \
-        if ((cs = preferred) != NULL) {                                                                                            \
-            block                                                                                                                  \
-        }                                                                                                                          \
-        if (offered != NULL) {                                                                                                     \
-            size_t i, j;                                                                                                           \
-            for (i = 0; (cs = offered[i]) != NULL; ++i) {                                                                          \
-                if (preferred == NULL || cs->hash != preferred->hash) {                                                            \
-                    for (j = 0; j != i; ++j)                                                                                       \
-                        if (cs->hash == offered[j]->hash)                                                                          \
-                            break;                                                                                                 \
-                    if (j == i) {                                                                                                  \
-                        block                                                                                                      \
-                    }                                                                                                              \
-                }                                                                                                                  \
-            }                                                                                                                      \
-        }                                                                                                                          \
-    } while (0)
-
-    ptls_key_schedule_t *sched;
-
-    if (hkdf_label_prefix == NULL)
-        hkdf_label_prefix = PTLS_HKDF_EXPAND_LABEL_PREFIX;
-
-    { /* allocate */
-        size_t num_hashes = 0;
-        FOREACH_HASH({ ++num_hashes; });
-        if ((sched = malloc(offsetof(ptls_key_schedule_t, hashes) + sizeof(sched->hashes[0]) * num_hashes)) == NULL)
-            return NULL;
-        *sched = (ptls_key_schedule_t){0, hkdf_label_prefix};
-    }
-
-    /* setup the hash algos and contexts */
-    FOREACH_HASH({
-        sched->hashes[sched->num_hashes].algo = cs->hash;
-        if ((sched->hashes[sched->num_hashes].ctx = cs->hash->create()) == NULL)
-            goto Fail;
-        ++sched->num_hashes;
-    });
-
-    return sched;
-Fail:
-    key_schedule_free(sched);
-    return NULL;
-
-#undef FOREACH_HASH
-}
-
-static int key_schedule_extract(ptls_key_schedule_t *sched, ptls_iovec_t ikm)
-{
-    int ret;
-
-    if (ikm.base == NULL)
-        ikm = ptls_iovec_init(zeroes_of_max_digest_size, sched->hashes[0].algo->digest_size);
-
-    if (sched->generation != 0 &&
-        (ret = hkdf_expand_label(sched->hashes[0].algo, sched->secret, sched->hashes[0].algo->digest_size,
-                                 ptls_iovec_init(sched->secret, sched->hashes[0].algo->digest_size), "derived",
-                                 ptls_iovec_init(sched->hashes[0].algo->empty_digest, sched->hashes[0].algo->digest_size),
-                                 sched->hkdf_label_prefix)) != 0)
-        return ret;
-
-    ++sched->generation;
-    ret = ptls_hkdf_extract(sched->hashes[0].algo, sched->secret,
-                            ptls_iovec_init(sched->secret, sched->hashes[0].algo->digest_size), ikm);
-    PTLS_DEBUGF("%s: %u, %02x%02x\n", __FUNCTION__, sched->generation, (int)sched->secret[0], (int)sched->secret[1]);
-    return ret;
-}
-
-static int key_schedule_select_one(ptls_key_schedule_t *sched, ptls_cipher_suite_t *cs, int reset)
-{
-    size_t found_slot = SIZE_MAX, i;
-    int ret;
-
-    assert(sched->generation == 1);
-
-    /* find the one, while freeing others */
-    for (i = 0; i != sched->num_hashes; ++i) {
-        if (sched->hashes[i].algo == cs->hash) {
-            assert(found_slot == SIZE_MAX);
-            found_slot = i;
-        } else {
-            sched->hashes[i].ctx->final(sched->hashes[i].ctx, NULL, PTLS_HASH_FINAL_MODE_FREE);
-        }
-    }
-    if (found_slot != 0) {
-        sched->hashes[0] = sched->hashes[found_slot];
-        reset = 1;
-    }
-    sched->num_hashes = 1;
-
-    /* recalculate the hash if a different hash as been selected than the one we used for calculating the early secrets */
-    if (reset) {
-        --sched->generation;
-        memset(sched->secret, 0, sizeof(sched->secret));
-        if ((ret = key_schedule_extract(sched, ptls_iovec_init(NULL, 0))) != 0)
-            goto Exit;
-    }
-
-    ret = 0;
-Exit:
-    return ret;
-}
-
-void ptls__key_schedule_update_hash(ptls_key_schedule_t *sched, const uint8_t *msg, size_t msglen)
-{
-    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);
-}
-
-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));
-}
-
-static void key_schedule_extract_ch1hash(ptls_key_schedule_t *sched, uint8_t *hash)
-{
-    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)
-{
-    uint8_t ch1hash[PTLS_MAX_DIGEST_SIZE];
-
-    key_schedule_extract_ch1hash(sched, ch1hash);
-
-    key_schedule_update_ch1hash_prefix(sched);
-    ptls__key_schedule_update_hash(sched, ch1hash, sched->hashes[0].algo->digest_size);
-}
-
-static int derive_secret_with_hash(ptls_key_schedule_t *sched, void *secret, const char *label, const uint8_t *hash)
-{
-    int ret = hkdf_expand_label(sched->hashes[0].algo, secret, sched->hashes[0].algo->digest_size,
-                                ptls_iovec_init(sched->secret, sched->hashes[0].algo->digest_size), label,
-                                ptls_iovec_init(hash, sched->hashes[0].algo->digest_size), sched->hkdf_label_prefix);
-    PTLS_DEBUGF("%s: (label=%s, hash=%02x%02x) => %02x%02x\n", __FUNCTION__, label, hash[0], hash[1], ((uint8_t *)secret)[0],
-                ((uint8_t *)secret)[1]);
-    return ret;
-}
-
-static int derive_secret(ptls_key_schedule_t *sched, void *secret, const char *label)
-{
-    uint8_t hash_value[PTLS_MAX_DIGEST_SIZE];
-
-    sched->hashes[0].ctx->final(sched->hashes[0].ctx, hash_value, PTLS_HASH_FINAL_MODE_SNAPSHOT);
-    int ret = derive_secret_with_hash(sched, secret, label, hash_value);
-    ptls_clear_memory(hash_value, sizeof(hash_value));
-    return ret;
-}
-
-static int derive_secret_with_empty_digest(ptls_key_schedule_t *sched, void *secret, const char *label)
-{
-    return derive_secret_with_hash(sched, secret, label, sched->hashes[0].algo->empty_digest);
-}
-
-static int derive_exporter_secret(ptls_t *tls, int is_early)
-{
-    int ret;
-
-    if (!tls->ctx->use_exporter)
-        return 0;
-
-    uint8_t **slot = is_early ? &tls->exporter_master_secret.early : &tls->exporter_master_secret.one_rtt;
-    assert(*slot == NULL);
-    if ((*slot = malloc(tls->key_schedule->hashes[0].algo->digest_size)) == NULL)
-        return PTLS_ERROR_NO_MEMORY;
-
-    if ((ret = derive_secret(tls->key_schedule, *slot, is_early ? "e exp master" : "exp master")) != 0)
-        return ret;
-
-    log_secret(tls, is_early ? "EARLY_EXPORTER_SECRET" : "EXPORTER_SECRET",
-               ptls_iovec_init(*slot, tls->key_schedule->hashes[0].algo->digest_size));
-
-    return 0;
-}
-
-static void free_exporter_master_secret(ptls_t *tls, int is_early)
-{
-    uint8_t *slot = is_early ? tls->exporter_master_secret.early : tls->exporter_master_secret.one_rtt;
-    if (slot == NULL)
-        return;
-    assert(tls->key_schedule != NULL);
-    ptls_clear_memory(slot, tls->key_schedule->hashes[0].algo->digest_size);
-    free(slot);
-}
-
-static int derive_resumption_secret(ptls_key_schedule_t *sched, uint8_t *secret, ptls_iovec_t nonce)
-{
-    int ret;
-
-    if ((ret = derive_secret(sched, secret, "res master")) != 0)
-        goto Exit;
-    if ((ret = hkdf_expand_label(sched->hashes[0].algo, secret, sched->hashes[0].algo->digest_size,
-                                 ptls_iovec_init(secret, sched->hashes[0].algo->digest_size), "resumption", nonce,
-                                 sched->hkdf_label_prefix)) != 0)
-        goto Exit;
-
-Exit:
-    if (ret != 0)
-        ptls_clear_memory(secret, sched->hashes[0].algo->digest_size);
-    return ret;
-}
-
-static int decode_new_session_ticket(ptls_t *tls, uint32_t *lifetime, uint32_t *age_add, ptls_iovec_t *nonce, ptls_iovec_t *ticket,
-                                     uint32_t *max_early_data_size, const uint8_t *src, const uint8_t *const end)
-{
-    uint16_t exttype;
-    int ret;
-
-    if ((ret = ptls_decode32(lifetime, &src, end)) != 0)
-        goto Exit;
-    if ((ret = ptls_decode32(age_add, &src, end)) != 0)
-        goto Exit;
-    ptls_decode_open_block(src, end, 1, {
-        *nonce = 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;
-        }
-        *ticket = ptls_iovec_init(src, end - src);
-        src = end;
-    });
-
-    *max_early_data_size = 0;
-    decode_extensions(src, end, PTLS_HANDSHAKE_TYPE_NEW_SESSION_TICKET, &exttype, {
-        if (tls->ctx->on_extension != NULL &&
-            (ret = tls->ctx->on_extension->cb(tls->ctx->on_extension, tls, PTLS_HANDSHAKE_TYPE_NEW_SESSION_TICKET, exttype,
-                                              ptls_iovec_init(src, end - src)) != 0))
-            goto Exit;
-        switch (exttype) {
-        case PTLS_EXTENSION_TYPE_EARLY_DATA:
-            if ((ret = ptls_decode32(max_early_data_size, &src, end)) != 0)
-                goto Exit;
-            break;
-        default:
-            src = end;
-            break;
-        }
-    });
-
-    ret = 0;
-Exit:
-    return ret;
-}
-
-static int decode_stored_session_ticket(ptls_t *tls, ptls_key_exchange_algorithm_t **key_share, ptls_cipher_suite_t **cs,
-                                        ptls_iovec_t *secret, uint32_t *obfuscated_ticket_age, ptls_iovec_t *ticket,
-                                        uint32_t *max_early_data_size, const uint8_t *src, const uint8_t *const end)
-{
-    uint16_t kxid, csid;
-    uint32_t lifetime, age_add;
-    uint64_t obtained_at, now;
-    ptls_iovec_t nonce;
-    int ret;
-
-    /* decode */
-    if ((ret = ptls_decode64(&obtained_at, &src, end)) != 0)
-        goto Exit;
-    if ((ret = ptls_decode16(&kxid, &src, end)) != 0)
-        goto Exit;
-    if ((ret = ptls_decode16(&csid, &src, end)) != 0)
-        goto Exit;
-    ptls_decode_open_block(src, end, 3, {
-        if ((ret = decode_new_session_ticket(tls, &lifetime, &age_add, &nonce, ticket, max_early_data_size, src, end)) != 0)
-            goto Exit;
-        src = end;
-    });
-    ptls_decode_block(src, end, 2, {
-        *secret = ptls_iovec_init(src, end - src);
-        src = end;
-    });
-
-    { /* determine the key-exchange */
-        ptls_key_exchange_algorithm_t **cand;
-        for (cand = tls->ctx->key_exchanges; *cand != NULL; ++cand)
-            if ((*cand)->id == kxid)
-                break;
-        if (*cand == NULL) {
-            ret = PTLS_ERROR_LIBRARY;
-            goto Exit;
-        }
-        *key_share = *cand;
-    }
-
-    { /* determine the cipher-suite */
-        ptls_cipher_suite_t **cand;
-        for (cand = tls->ctx->cipher_suites; *cand != NULL; ++cand)
-            if ((*cand)->id == csid)
-                break;
-        if (*cand == NULL) {
-            ret = PTLS_ERROR_LIBRARY;
-            goto Exit;
-        }
-        *cs = *cand;
-    }
-
-    /* calculate obfuscated_ticket_age */
-    now = tls->ctx->get_time->cb(tls->ctx->get_time);
-    if (!(obtained_at <= now && now - obtained_at < 7 * 86400 * 1000)) {
-        ret = PTLS_ERROR_LIBRARY;
-        goto Exit;
-    }
-    *obfuscated_ticket_age = (uint32_t)(now - obtained_at) + age_add;
-
-    ret = 0;
-Exit:
-    return ret;
-}
-
-static int get_traffic_key(ptls_hash_algorithm_t *algo, void *key, size_t key_size, int is_iv, const void *secret,
-                           ptls_iovec_t hash_value, const char *label_prefix)
-{
-    return ptls_hkdf_expand_label(algo, key, key_size, ptls_iovec_init(secret, algo->digest_size), is_iv ? "iv" : "key", hash_value,
-                                  label_prefix);
-}
-
-static int setup_traffic_protection(ptls_t *tls, int is_enc, const char *secret_label, size_t epoch, int skip_notify)
-{
-    static const char *log_labels[2][4] = {
-        {NULL, "CLIENT_EARLY_TRAFFIC_SECRET", "CLIENT_HANDSHAKE_TRAFFIC_SECRET", "CLIENT_TRAFFIC_SECRET_0"},
-        {NULL, NULL, "SERVER_HANDSHAKE_TRAFFIC_SECRET", "SERVER_TRAFFIC_SECRET_0"}};
-    struct st_ptls_traffic_protection_t *ctx = is_enc ? &tls->traffic_protection.enc : &tls->traffic_protection.dec;
-
-    if (secret_label != NULL) {
-        int ret;
-        if ((ret = derive_secret(tls->key_schedule, ctx->secret, secret_label)) != 0)
-            return ret;
-    }
-
-    ctx->epoch = epoch;
-
-    /* special path for applications having their own record layer */
-    if (tls->ctx->update_traffic_key != NULL) {
-        if (skip_notify)
-            return 0;
-        return tls->ctx->update_traffic_key->cb(tls->ctx->update_traffic_key, tls, is_enc, epoch, ctx->secret);
-    }
-
-    if (ctx->aead != NULL)
-        ptls_aead_free(ctx->aead);
-    if ((ctx->aead = ptls_aead_new(tls->cipher_suite->aead, tls->cipher_suite->hash, is_enc, ctx->secret,
-                                   tls->ctx->hkdf_label_prefix__obsolete)) == NULL)
-        return PTLS_ERROR_NO_MEMORY; /* TODO obtain error from ptls_aead_new */
-    ctx->seq = 0;
-
-    log_secret(tls, log_labels[ptls_is_server(tls) == is_enc][epoch],
-               ptls_iovec_init(ctx->secret, tls->key_schedule->hashes[0].algo->digest_size));
-    PTLS_DEBUGF("[%s] %02x%02x,%02x%02x\n", log_labels[ptls_is_server(tls)][epoch], (unsigned)ctx->secret[0],
-                (unsigned)ctx->secret[1], (unsigned)ctx->aead->static_iv[0], (unsigned)ctx->aead->static_iv[1]);
-
-    return 0;
-}
-
-static int commission_handshake_secret(ptls_t *tls)
-{
-    int is_enc = !ptls_is_server(tls);
-
-    assert(tls->pending_handshake_secret != NULL);
-    memcpy((is_enc ? &tls->traffic_protection.enc : &tls->traffic_protection.dec)->secret, tls->pending_handshake_secret,
-           PTLS_MAX_DIGEST_SIZE);
-    ptls_clear_memory(tls->pending_handshake_secret, PTLS_MAX_DIGEST_SIZE);
-    free(tls->pending_handshake_secret);
-    tls->pending_handshake_secret = NULL;
-
-    return setup_traffic_protection(tls, is_enc, NULL, 2, 1);
-}
-
-static void log_client_random(ptls_t *tls)
-{
-    PTLS_PROBE(CLIENT_RANDOM, tls,
-               ptls_hexdump(alloca(sizeof(tls->client_random) * 2 + 1), tls->client_random, sizeof(tls->client_random)));
-}
-
-#define SESSION_IDENTIFIER_MAGIC "ptls0001" /* the number should be changed upon incompatible format change */
-#define SESSION_IDENTIFIER_MAGIC_SIZE (sizeof(SESSION_IDENTIFIER_MAGIC) - 1)
-
-static int encode_session_identifier(ptls_context_t *ctx, ptls_buffer_t *buf, uint32_t ticket_age_add, ptls_iovec_t ticket_nonce,
-                                     ptls_key_schedule_t *sched, const char *server_name, uint16_t key_exchange_id, uint16_t csid,
-                                     const char *negotiated_protocol)
-{
-    int ret = 0;
-
-    ptls_buffer_push_block(buf, 2, {
-        /* format id */
-        ptls_buffer_pushv(buf, SESSION_IDENTIFIER_MAGIC, SESSION_IDENTIFIER_MAGIC_SIZE);
-        /* date */
-        ptls_buffer_push64(buf, ctx->get_time->cb(ctx->get_time));
-        /* resumption master secret */
-        ptls_buffer_push_block(buf, 2, {
-            if ((ret = ptls_buffer_reserve(buf, sched->hashes[0].algo->digest_size)) != 0)
-                goto Exit;
-            if ((ret = derive_resumption_secret(sched, buf->base + buf->off, ticket_nonce)) != 0)
-                goto Exit;
-            buf->off += sched->hashes[0].algo->digest_size;
-        });
-        /* key-exchange */
-        ptls_buffer_push16(buf, key_exchange_id);
-        /* cipher-suite */
-        ptls_buffer_push16(buf, csid);
-        /* ticket_age_add */
-        ptls_buffer_push32(buf, ticket_age_add);
-        /* server-name */
-        ptls_buffer_push_block(buf, 2, {
-            if (server_name != NULL)
-                ptls_buffer_pushv(buf, server_name, strlen(server_name));
-        });
-        /* alpn */
-        ptls_buffer_push_block(buf, 1, {
-            if (negotiated_protocol != NULL)
-                ptls_buffer_pushv(buf, negotiated_protocol, strlen(negotiated_protocol));
-        });
-    });
-
-Exit:
-    return ret;
-}
-
-int decode_session_identifier(uint64_t *issued_at, ptls_iovec_t *psk, uint32_t *ticket_age_add, ptls_iovec_t *server_name,
-                              uint16_t *key_exchange_id, uint16_t *csid, ptls_iovec_t *negotiated_protocol, const uint8_t *src,
-                              const uint8_t *const end)
-{
-    int ret = 0;
-
-    ptls_decode_block(src, end, 2, {
-        if (end - src < SESSION_IDENTIFIER_MAGIC_SIZE ||
-            memcmp(src, SESSION_IDENTIFIER_MAGIC, SESSION_IDENTIFIER_MAGIC_SIZE) != 0) {
-            ret = PTLS_ALERT_DECODE_ERROR;
-            goto Exit;
-        }
-        src += SESSION_IDENTIFIER_MAGIC_SIZE;
-        if ((ret = ptls_decode64(issued_at, &src, end)) != 0)
-            goto Exit;
-        ptls_decode_open_block(src, end, 2, {
-            *psk = ptls_iovec_init(src, end - src);
-            src = end;
-        });
-        if ((ret = ptls_decode16(key_exchange_id, &src, end)) != 0)
-            goto Exit;
-        if ((ret = ptls_decode16(csid, &src, end)) != 0)
-            goto Exit;
-        if ((ret = ptls_decode32(ticket_age_add, &src, end)) != 0)
-            goto Exit;
-        ptls_decode_open_block(src, end, 2, {
-            *server_name = ptls_iovec_init(src, end - src);
-            src = end;
-        });
-        ptls_decode_open_block(src, end, 1, {
-            *negotiated_protocol = ptls_iovec_init(src, end - src);
-            src = end;
-        });
-    });
-
-Exit:
-    return ret;
-}
-
-static size_t build_certificate_verify_signdata(uint8_t *data, ptls_key_schedule_t *sched, const char *context_string)
-{
-    size_t datalen = 0;
-
-    memset(data + datalen, 32, 64);
-    datalen += 64;
-    memcpy(data + datalen, context_string, strlen(context_string) + 1);
-    datalen += strlen(context_string) + 1;
-    sched->hashes[0].ctx->final(sched->hashes[0].ctx, data + datalen, PTLS_HASH_FINAL_MODE_SNAPSHOT);
-    datalen += sched->hashes[0].algo->digest_size;
-    assert(datalen <= PTLS_MAX_CERTIFICATE_VERIFY_SIGNDATA_SIZE);
-
-    return datalen;
-}
-
-static int calc_verify_data(void *output, ptls_key_schedule_t *sched, const void *secret)
-{
-    ptls_hash_context_t *hmac;
-    uint8_t digest[PTLS_MAX_DIGEST_SIZE];
-    int ret;
-
-    if ((ret = hkdf_expand_label(sched->hashes[0].algo, digest, sched->hashes[0].algo->digest_size,
-                                 ptls_iovec_init(secret, sched->hashes[0].algo->digest_size), "finished", ptls_iovec_init(NULL, 0),
-                                 sched->hkdf_label_prefix)) != 0)
-        return ret;
-    if ((hmac = ptls_hmac_create(sched->hashes[0].algo, digest, sched->hashes[0].algo->digest_size)) == NULL) {
-        ptls_clear_memory(digest, sizeof(digest));
-        return PTLS_ERROR_NO_MEMORY;
-    }
-
-    sched->hashes[0].ctx->final(sched->hashes[0].ctx, digest, PTLS_HASH_FINAL_MODE_SNAPSHOT);
-    PTLS_DEBUGF("%s: %02x%02x,%02x%02x\n", __FUNCTION__, ((uint8_t *)secret)[0], ((uint8_t *)secret)[1], digest[0], digest[1]);
-    hmac->update(hmac, digest, sched->hashes[0].algo->digest_size);
-    ptls_clear_memory(digest, sizeof(digest));
-    hmac->final(hmac, output, PTLS_HASH_FINAL_MODE_FREE);
-
-    return 0;
-}
-
-static int verify_finished(ptls_t *tls, ptls_iovec_t message)
-{
-    uint8_t verify_data[PTLS_MAX_DIGEST_SIZE];
-    int ret;
-
-    if (PTLS_HANDSHAKE_HEADER_SIZE + tls->key_schedule->hashes[0].algo->digest_size != message.len) {
-        ret = PTLS_ALERT_DECODE_ERROR;
-        goto Exit;
-    }
-
-    if ((ret = calc_verify_data(verify_data, tls->key_schedule, tls->traffic_protection.dec.secret)) != 0)
-        goto Exit;
-    if (!ptls_mem_equal(message.base + PTLS_HANDSHAKE_HEADER_SIZE, verify_data, tls->key_schedule->hashes[0].algo->digest_size)) {
-        ret = PTLS_ALERT_HANDSHAKE_FAILURE;
-        goto Exit;
-    }
-
-Exit:
-    ptls_clear_memory(verify_data, sizeof(verify_data));
-    return ret;
-}
-
-static int send_finished(ptls_t *tls, ptls_message_emitter_t *emitter)
-{
-    int ret;
-
-    ptls_push_message(emitter, tls->key_schedule, PTLS_HANDSHAKE_TYPE_FINISHED, {
-        if ((ret = ptls_buffer_reserve(emitter->buf, tls->key_schedule->hashes[0].algo->digest_size)) != 0)
-            goto Exit;
-        if ((ret = calc_verify_data(emitter->buf->base + emitter->buf->off, tls->key_schedule,
-                                    tls->traffic_protection.enc.secret)) != 0)
-            goto Exit;
-        emitter->buf->off += tls->key_schedule->hashes[0].algo->digest_size;
-    });
-
-Exit:
-    return ret;
-}
-
-static int send_session_ticket(ptls_t *tls, ptls_message_emitter_t *emitter)
-{
-    ptls_hash_context_t *msghash_backup = tls->key_schedule->hashes[0].ctx->clone_(tls->key_schedule->hashes[0].ctx);
-    ptls_buffer_t session_id;
-    char session_id_smallbuf[128];
-    uint32_t ticket_age_add;
-    int ret = 0;
-
-    assert(tls->ctx->ticket_lifetime != 0);
-    assert(tls->ctx->encrypt_ticket != NULL);
-
-    { /* calculate verify-data that will be sent by the client */
-        size_t orig_off = emitter->buf->off;
-        if (tls->pending_handshake_secret != NULL && !tls->ctx->omit_end_of_early_data) {
-            assert(tls->state == PTLS_STATE_SERVER_EXPECT_END_OF_EARLY_DATA);
-            ptls_buffer_push_message_body(emitter->buf, tls->key_schedule, PTLS_HANDSHAKE_TYPE_END_OF_EARLY_DATA, {});
-            emitter->buf->off = orig_off;
-        }
-        ptls_buffer_push_message_body(emitter->buf, tls->key_schedule, PTLS_HANDSHAKE_TYPE_FINISHED, {
-            if ((ret = ptls_buffer_reserve(emitter->buf, tls->key_schedule->hashes[0].algo->digest_size)) != 0)
-                goto Exit;
-            if ((ret = calc_verify_data(emitter->buf->base + emitter->buf->off, tls->key_schedule,
-                                        tls->pending_handshake_secret != NULL ? tls->pending_handshake_secret
-                                                                              : tls->traffic_protection.dec.secret)) != 0)
-                goto Exit;
-            emitter->buf->off += tls->key_schedule->hashes[0].algo->digest_size;
-        });
-        emitter->buf->off = orig_off;
-    }
-
-    tls->ctx->random_bytes(&ticket_age_add, sizeof(ticket_age_add));
-
-    /* build the raw nsk */
-    ptls_buffer_init(&session_id, session_id_smallbuf, sizeof(session_id_smallbuf));
-    ret = encode_session_identifier(tls->ctx, &session_id, ticket_age_add, ptls_iovec_init(NULL, 0), tls->key_schedule,
-                                    tls->server_name, tls->key_share->id, tls->cipher_suite->id, tls->negotiated_protocol);
-    if (ret != 0)
-        goto Exit;
-
-    /* encrypt and send */
-    ptls_push_message(emitter, tls->key_schedule, PTLS_HANDSHAKE_TYPE_NEW_SESSION_TICKET, {
-        ptls_buffer_push32(emitter->buf, tls->ctx->ticket_lifetime);
-        ptls_buffer_push32(emitter->buf, ticket_age_add);
-        ptls_buffer_push_block(emitter->buf, 1, {});
-        ptls_buffer_push_block(emitter->buf, 2, {
-            if ((ret = tls->ctx->encrypt_ticket->cb(tls->ctx->encrypt_ticket, tls, 1, emitter->buf,
-                                                    ptls_iovec_init(session_id.base, session_id.off))) != 0)
-                goto Exit;
-        });
-        ptls_buffer_push_block(emitter->buf, 2, {
-            if (tls->ctx->max_early_data_size != 0)
-                buffer_push_extension(emitter->buf, PTLS_EXTENSION_TYPE_EARLY_DATA,
-                                      { ptls_buffer_push32(emitter->buf, tls->ctx->max_early_data_size); });
-        });
-    });
-
-Exit:
-    ptls_buffer_dispose(&session_id);
-
-    /* restore handshake state */
-    tls->key_schedule->hashes[0].ctx->final(tls->key_schedule->hashes[0].ctx, NULL, PTLS_HASH_FINAL_MODE_FREE);
-    tls->key_schedule->hashes[0].ctx = msghash_backup;
-
-    return ret;
-}
-
-static int push_change_cipher_spec(ptls_t *tls, ptls_message_emitter_t *emitter)
-{
-    int ret;
-
-    /* check if we are requested to (or still need to) */
-    if (!tls->send_change_cipher_spec) {
-        ret = 0;
-        goto Exit;
-    }
-
-    /* CCS is a record, can only be sent when using a record-based protocol. */
-    if (emitter->begin_message != begin_record_message) {
-        ret = PTLS_ALERT_UNEXPECTED_MESSAGE;
-        goto Exit;
-    }
-
-    /* emit CCS */
-    buffer_push_record(emitter->buf, PTLS_CONTENT_TYPE_CHANGE_CIPHER_SPEC, { ptls_buffer_push(emitter->buf, 1); });
-
-    tls->send_change_cipher_spec = 0;
-    ret = 0;
-Exit:
-    return ret;
-}
-
-static int push_additional_extensions(ptls_handshake_properties_t *properties, ptls_buffer_t *sendbuf)
-{
-    int ret;
-
-    if (properties != NULL && properties->additional_extensions != NULL) {
-        ptls_raw_extension_t *ext;
-        for (ext = properties->additional_extensions; ext->type != UINT16_MAX; ++ext) {
-            buffer_push_extension(sendbuf, ext->type, { ptls_buffer_pushv(sendbuf, ext->data.base, ext->data.len); });
-        }
-    }
-    ret = 0;
-Exit:
-    return ret;
-}
-
-static int push_signature_algorithms(ptls_verify_certificate_t *vc, ptls_buffer_t *sendbuf)
-{
-    /* The list sent when verify callback is not registered */
-    static const uint16_t default_algos[] = {PTLS_SIGNATURE_RSA_PSS_RSAE_SHA256, PTLS_SIGNATURE_ECDSA_SECP256R1_SHA256,
-                                             PTLS_SIGNATURE_RSA_PKCS1_SHA256, PTLS_SIGNATURE_RSA_PKCS1_SHA1, UINT16_MAX};
-    int ret;
-
-    ptls_buffer_push_block(sendbuf, 2, {
-        for (const uint16_t *p = vc != NULL ? vc->algos : default_algos; *p != UINT16_MAX; ++p)
-            ptls_buffer_push16(sendbuf, *p);
-    });
-
-    ret = 0;
-Exit:
-    return ret;
-}
-
-static int decode_signature_algorithms(struct st_ptls_signature_algorithms_t *sa, const uint8_t **src, const uint8_t *end)
-{
-    int ret;
-
-    ptls_decode_block(*src, end, 2, {
-        do {
-            uint16_t id;
-            if ((ret = ptls_decode16(&id, src, end)) != 0)
-                goto Exit;
-            if (sa->count < PTLS_ELEMENTSOF(sa->list))
-                sa->list[sa->count++] = id;
-        } while (*src != end);
-    });
-
-    ret = 0;
-Exit:
-    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)
-{
-    size_t found_index = SIZE_MAX;
-    int ret;
-
-    while (src != end) {
-        uint16_t id;
-        if ((ret = ptls_decode16(&id, &src, end)) != 0)
-            goto Exit;
-        for (size_t i = 0; candidates[i] != NULL; ++i) {
-            if (candidates[i]->id == id) {
-                if (server_preference) {
-                    /* preserve smallest matching index, and proceed to the next input */
-                    if (i < found_index) {
-                        found_index = i;
-                        break;
-                    }
-                } else {
-                    /* return the pointer matching to the first input that can be used */
-                    *selected = candidates[i];
-                    goto Exit;
-                }
-            }
-        }
-    }
-    if (found_index != SIZE_MAX) {
-        *selected = candidates[found_index];
-        ret = 0;
-    } else {
-        ret = PTLS_ALERT_HANDSHAKE_FAILURE;
-    }
-
-Exit:
-    return ret;
-}
-
-static int push_key_share_entry(ptls_buffer_t *buf, uint16_t group, ptls_iovec_t pubkey)
-{
-    int ret;
-
-    ptls_buffer_push16(buf, group);
-    ptls_buffer_push_block(buf, 2, { ptls_buffer_pushv(buf, pubkey.base, pubkey.len); });
-    ret = 0;
-Exit:
-    return ret;
-}
-
-static int decode_key_share_entry(uint16_t *group, ptls_iovec_t *key_exchange, const uint8_t **src, const uint8_t *const end)
-{
-    int ret;
-
-    if ((ret = ptls_decode16(group, src, end)) != 0)
-        goto Exit;
-    ptls_decode_open_block(*src, end, 2, {
-        *key_exchange = ptls_iovec_init(*src, end - *src);
-        *src = end;
-    });
-
-Exit:
-    return ret;
-}
-
-static int select_key_share(ptls_key_exchange_algorithm_t **selected, ptls_iovec_t *peer_key,
-                            ptls_key_exchange_algorithm_t **candidates, const uint8_t **src, const uint8_t *const end,
-                            int expect_one)
-{
-    int ret;
-
-    *selected = NULL;
-
-    if (expect_one && *src == end) {
-        ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-        goto Exit;
-    }
-
-    while (*src != end) {
-        uint16_t group;
-        ptls_iovec_t key;
-        if ((ret = decode_key_share_entry(&group, &key, src, end)) != 0)
-            goto Exit;
-        ptls_key_exchange_algorithm_t **c = candidates;
-        for (; *c != NULL; ++c) {
-            if (*selected == NULL && (*c)->id == group) {
-                *selected = *c;
-                *peer_key = key;
-            }
-        }
-        if (expect_one) {
-            ret = *selected != NULL ? 0 : PTLS_ALERT_ILLEGAL_PARAMETER;
-            goto Exit;
-        }
-    }
-
-    ret = 0;
-
-Exit:
-    return ret;
-}
-
-static int emit_server_name_extension(ptls_buffer_t *buf, const char *server_name)
-{
-    int ret;
-
-    ptls_buffer_push_block(buf, 2, {
-        ptls_buffer_push(buf, PTLS_SERVER_NAME_TYPE_HOSTNAME);
-        ptls_buffer_push_block(buf, 2, { ptls_buffer_pushv(buf, server_name, strlen(server_name)); });
-    });
-
-    ret = 0;
-Exit:
-    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)
-{
-    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;
-
-    /* version */
-    if ((ret = ptls_decode16(&version, &src, end)) != 0)
-        goto Exit;
-    if (version != PTLS_ESNI_VERSION_DRAFT03) {
-        ret = PTLS_ALERT_DECODE_ERROR;
-        goto Exit;
-    }
-
-    { /* 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)
-                goto Exit;
-            ptls_decode_open_block(src, end, 2, { src = end; });
-        }
-    });
-
-    /* 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(buf, aead->algo->tag_size)) != 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;
-    uint32_t obfuscated_ticket_age = 0;
-    size_t 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);
-
-    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)
-                    goto Exit;
-            }
-        }
-        /* setup resumption-related data. If successful, resumption_secret becomes a non-zero value. */
-        if (properties->client.session_ticket.base != NULL) {
-            ptls_key_exchange_algorithm_t *key_share = NULL;
-            ptls_cipher_suite_t *cipher_suite = NULL;
-            uint32_t max_early_data_size;
-            if (decode_stored_session_ticket(tls, &key_share, &cipher_suite, &resumption_secret, &obfuscated_ticket_age,
-                                             &resumption_ticket, &max_early_data_size, properties->client.session_ticket.base,
-                                             properties->client.session_ticket.base + properties->client.session_ticket.len) == 0) {
-                tls->client.offered_psk = 1;
-                /* key-share selected by HRR should not be overridden */
-                if (tls->key_share == NULL)
-                    tls->key_share = key_share;
-                tls->cipher_suite = cipher_suite;
-                if (!is_second_flight && max_early_data_size != 0 && properties->client.max_early_data_size != NULL) {
-                    tls->client.using_early_data = 1;
-                    *properties->client.max_early_data_size = max_early_data_size;
-                }
-            } else {
-                resumption_secret = ptls_iovec_init(NULL, 0);
-            }
-        }
-        if (tls->client.using_early_data) {
-            properties->client.early_data_acceptance = PTLS_EARLY_DATA_ACCEPTANCE_UNKNOWN;
-        } else {
-            if (properties->client.max_early_data_size != NULL)
-                *properties->client.max_early_data_size = 0;
-            properties->client.early_data_acceptance = PTLS_EARLY_DATA_REJECTED;
-        }
-    }
-
-    /* use the default key share if still not undetermined */
-    if (tls->key_share == NULL && !(properties != NULL && properties->client.negotiate_before_key_exchange))
-        tls->key_share = tls->ctx->key_exchanges[0];
-
-    if (!is_second_flight) {
-        tls->key_schedule = key_schedule_new(tls->cipher_suite, tls->ctx->cipher_suites, tls->ctx->hkdf_label_prefix__obsolete);
-        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;
-                        });
-                    });
-                });
-            }
-        });
-    });
-
-    /* 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);
-        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);
-
-    if (tls->client.using_early_data) {
-        assert(!is_second_flight);
-        if ((ret = setup_traffic_protection(tls, 1, "c e traffic", 1, 0)) != 0)
-            goto Exit;
-        if ((ret = push_change_cipher_spec(tls, emitter)) != 0)
-            goto Exit;
-    }
-    if (resumption_secret.base != NULL && !is_second_flight) {
-        if ((ret = derive_exporter_secret(tls, 1)) != 0)
-            goto Exit;
-    }
-    tls->state = cookie == NULL ? PTLS_STATE_CLIENT_EXPECT_SERVER_HELLO : PTLS_STATE_CLIENT_EXPECT_SECOND_SERVER_HELLO;
-    ret = PTLS_ERROR_IN_PROGRESS;
-
-Exit:
-    if (published_sni != NULL) {
-        free(published_sni);
-    }
-    ptls_clear_memory(binder_key, sizeof(binder_key));
-    return ret;
-}
-
-static ptls_cipher_suite_t *find_cipher_suite(ptls_context_t *ctx, uint16_t id)
-{
-    ptls_cipher_suite_t **cs;
-
-    for (cs = ctx->cipher_suites; *cs != NULL && (*cs)->id != id; ++cs)
-        ;
-    return *cs;
-}
-
-static int decode_server_hello(ptls_t *tls, struct st_ptls_server_hello_t *sh, const uint8_t *src, const uint8_t *const end)
-{
-    int ret;
-
-    *sh = (struct st_ptls_server_hello_t){{0}};
-
-    /* ignore legacy-version */
-    if (end - src < 2) {
-        ret = PTLS_ALERT_DECODE_ERROR;
-        goto Exit;
-    }
-    src += 2;
-
-    /* random */
-    if (end - src < PTLS_HELLO_RANDOM_SIZE) {
-        ret = PTLS_ALERT_DECODE_ERROR;
-        goto Exit;
-    }
-    sh->is_retry_request = memcmp(src, hello_retry_random, PTLS_HELLO_RANDOM_SIZE) == 0;
-    src += PTLS_HELLO_RANDOM_SIZE;
-
-    /* legacy_session_id */
-    ptls_decode_open_block(src, end, 1, {
-        if (end - src > 32) {
-            ret = PTLS_ALERT_DECODE_ERROR;
-            goto Exit;
-        }
-        sh->legacy_session_id = ptls_iovec_init(src, end - src);
-        src = end;
-    });
-
-    { /* select cipher_suite */
-        uint16_t csid;
-        if ((ret = ptls_decode16(&csid, &src, end)) != 0)
-            goto Exit;
-        if ((tls->cipher_suite = find_cipher_suite(tls->ctx, csid)) == NULL) {
-            ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-            goto Exit;
-        }
-    }
-
-    /* legacy_compression_method */
-    if (src == end || *src++ != 0) {
-        ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-        goto Exit;
-    }
-
-    if (sh->is_retry_request)
-        sh->retry_request.selected_group = UINT16_MAX;
-
-    uint16_t exttype, found_version = UINT16_MAX, selected_psk_identity = UINT16_MAX;
-    decode_extensions(src, end, PTLS_HANDSHAKE_TYPE_SERVER_HELLO, &exttype, {
-        if (tls->ctx->on_extension != NULL &&
-            (ret = tls->ctx->on_extension->cb(tls->ctx->on_extension, tls, PTLS_HANDSHAKE_TYPE_SERVER_HELLO, exttype,
-                                              ptls_iovec_init(src, end - src)) != 0))
-            goto Exit;
-        switch (exttype) {
-        case PTLS_EXTENSION_TYPE_SUPPORTED_VERSIONS:
-            if ((ret = ptls_decode16(&found_version, &src, end)) != 0)
-                goto Exit;
-            break;
-        case PTLS_EXTENSION_TYPE_KEY_SHARE:
-            if (sh->is_retry_request) {
-                if ((ret = ptls_decode16(&sh->retry_request.selected_group, &src, end)) != 0)
-                    goto Exit;
-            } else {
-                uint16_t group;
-                if ((ret = decode_key_share_entry(&group, &sh->peerkey, &src, end)) != 0)
-                    goto Exit;
-                if (src != end) {
-                    ret = PTLS_ALERT_DECODE_ERROR;
-                    goto Exit;
-                }
-                if (tls->key_share == NULL || tls->key_share->id != group) {
-                    ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-                    goto Exit;
-                }
-            }
-            break;
-        case PTLS_EXTENSION_TYPE_COOKIE:
-            if (sh->is_retry_request) {
-                ptls_decode_block(src, end, 2, {
-                    if (src == end) {
-                        ret = PTLS_ALERT_DECODE_ERROR;
-                        goto Exit;
-                    }
-                    sh->retry_request.cookie = ptls_iovec_init(src, end - src);
-                    src = end;
-                });
-            } else {
-                ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-                goto Exit;
-            }
-            break;
-        case PTLS_EXTENSION_TYPE_PRE_SHARED_KEY:
-            if (sh->is_retry_request) {
-                ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-                goto Exit;
-            } else {
-                if ((ret = ptls_decode16(&selected_psk_identity, &src, end)) != 0)
-                    goto Exit;
-            }
-            break;
-        default:
-            src = end;
-            break;
-        }
-    });
-
-    if (!is_supported_version(found_version)) {
-        ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-        goto Exit;
-    }
-    if (!sh->is_retry_request) {
-        if (selected_psk_identity != UINT16_MAX) {
-            if (!tls->client.offered_psk) {
-                ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-                goto Exit;
-            }
-            if (selected_psk_identity != 0) {
-                ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-                goto Exit;
-            }
-            tls->is_psk_handshake = 1;
-        }
-        if (sh->peerkey.base == NULL && !tls->is_psk_handshake) {
-            ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-            goto Exit;
-        }
-    }
-
-    ret = 0;
-Exit:
-    return ret;
-}
-
-static int handle_hello_retry_request(ptls_t *tls, ptls_message_emitter_t *emitter, struct st_ptls_server_hello_t *sh,
-                                      ptls_iovec_t message, ptls_handshake_properties_t *properties)
-{
-    int ret;
-
-    if (tls->client.key_share_ctx != NULL) {
-        tls->client.key_share_ctx->on_exchange(&tls->client.key_share_ctx, 1, NULL, ptls_iovec_init(NULL, 0));
-        tls->client.key_share_ctx = NULL;
-    }
-    if (tls->client.using_early_data) {
-        /* release traffic encryption key so that 2nd CH goes out in cleartext, but keep the epoch at 1 since we've already
-         * called derive-secret */
-        if (tls->ctx->update_traffic_key == NULL) {
-            assert(tls->traffic_protection.enc.aead != NULL);
-            ptls_aead_free(tls->traffic_protection.enc.aead);
-            tls->traffic_protection.enc.aead = NULL;
-        }
-        tls->client.using_early_data = 0;
-    }
-
-    if (sh->retry_request.selected_group != UINT16_MAX) {
-        /* we offer the first key_exchanges[0] as KEY_SHARE unless client.negotiate_before_key_exchange is set */
-        ptls_key_exchange_algorithm_t **cand;
-        for (cand = tls->ctx->key_exchanges; *cand != NULL; ++cand)
-            if ((*cand)->id == sh->retry_request.selected_group)
-                break;
-        if (*cand == NULL) {
-            ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-            goto Exit;
-        }
-        tls->key_share = *cand;
-    } else if (tls->key_share != NULL) {
-        /* retain the key-share using in first CH, if server does not specify one */
-    } else {
-        ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-        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_handle_hello(ptls_t *tls, ptls_message_emitter_t *emitter, ptls_iovec_t message,
-                               ptls_handshake_properties_t *properties)
-{
-    struct st_ptls_server_hello_t sh;
-    ptls_iovec_t ecdh_secret = {NULL};
-    int ret;
-
-    if ((ret = decode_server_hello(tls, &sh, message.base + PTLS_HANDSHAKE_HEADER_SIZE, message.base + message.len)) != 0)
-        goto Exit;
-    if (!(sh.legacy_session_id.len == tls->client.legacy_session_id.len &&
-          ptls_mem_equal(sh.legacy_session_id.base, tls->client.legacy_session_id.base, tls->client.legacy_session_id.len))) {
-        ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-        goto Exit;
-    }
-
-    if (sh.is_retry_request) {
-        if ((ret = key_schedule_select_one(tls->key_schedule, tls->cipher_suite, 0)) != 0)
-            goto Exit;
-        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)
-        goto Exit;
-
-    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)
-        goto Exit;
-    if (tls->client.using_early_data) {
-        if ((tls->pending_handshake_secret = malloc(PTLS_MAX_DIGEST_SIZE)) == NULL) {
-            ret = PTLS_ERROR_NO_MEMORY;
-            goto Exit;
-        }
-        if ((ret = derive_secret(tls->key_schedule, tls->pending_handshake_secret, "c hs traffic")) != 0)
-            goto Exit;
-        if (tls->ctx->update_traffic_key != NULL &&
-            (ret = tls->ctx->update_traffic_key->cb(tls->ctx->update_traffic_key, tls, 1, 2, tls->pending_handshake_secret)) != 0)
-            goto Exit;
-    } else {
-        if ((ret = setup_traffic_protection(tls, 1, "c hs traffic", 2, 0)) != 0)
-            goto Exit;
-    }
-
-    tls->state = PTLS_STATE_CLIENT_EXPECT_ENCRYPTED_EXTENSIONS;
-    ret = PTLS_ERROR_IN_PROGRESS;
-
-Exit:
-    if (ecdh_secret.base != NULL) {
-        ptls_clear_memory(ecdh_secret.base, ecdh_secret.len);
-        free(ecdh_secret.base);
-    }
-    return ret;
-}
-
-static int should_collect_unknown_extension(ptls_t *tls, ptls_handshake_properties_t *properties, uint16_t type)
-{
-    return properties != NULL && properties->collect_extension != NULL && properties->collect_extension(tls, properties, type);
-}
-
-static int collect_unknown_extension(ptls_t *tls, uint16_t type, const uint8_t *src, const uint8_t *const end,
-                                     ptls_raw_extension_t *slots)
-{
-    size_t i;
-    for (i = 0; slots[i].type != UINT16_MAX; ++i) {
-        assert(i < MAX_UNKNOWN_EXTENSIONS);
-        if (slots[i].type == type)
-            return PTLS_ALERT_ILLEGAL_PARAMETER;
-    }
-    if (i < MAX_UNKNOWN_EXTENSIONS) {
-        slots[i].type = type;
-        slots[i].data = ptls_iovec_init(src, end - src);
-        slots[i + 1].type = UINT16_MAX;
-    }
-    return 0;
-}
-
-static int report_unknown_extensions(ptls_t *tls, ptls_handshake_properties_t *properties, ptls_raw_extension_t *slots)
-{
-    if (properties != NULL && properties->collect_extension != NULL) {
-        assert(properties->collected_extensions != NULL);
-        return properties->collected_extensions(tls, properties, slots);
-    } else {
-        return 0;
-    }
-}
-
-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;
-    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;
-    int ret, skip_early_data = 1;
-    uint8_t server_offered_cert_type = PTLS_CERTIFICATE_TYPE_X509;
-
-    decode_extensions(src, end, PTLS_HANDSHAKE_TYPE_ENCRYPTED_EXTENSIONS, &type, {
-        if (tls->ctx->on_extension != NULL &&
-            (ret = tls->ctx->on_extension->cb(tls->ctx->on_extension, tls, PTLS_HANDSHAKE_TYPE_ENCRYPTED_EXTENSIONS, type,
-                                              ptls_iovec_init(src, end - src)) != 0))
-            goto Exit;
-        switch (type) {
-        case PTLS_EXTENSION_TYPE_SERVER_NAME:
-            if (src != end) {
-                ret = PTLS_ALERT_DECODE_ERROR;
-                goto Exit;
-            }
-            if (!(tls->server_name != NULL && !ptls_server_name_is_ipaddr(tls->server_name))) {
-                ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-                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, {
-                    if (src == end) {
-                        ret = PTLS_ALERT_DECODE_ERROR;
-                        goto Exit;
-                    }
-                    if ((ret = ptls_set_negotiated_protocol(tls, (const char *)src, end - src)) != 0)
-                        goto Exit;
-                    src = end;
-                });
-                if (src != end) {
-                    ret = PTLS_ALERT_HANDSHAKE_FAILURE;
-                    goto Exit;
-                }
-            });
-            break;
-        case PTLS_EXTENSION_TYPE_EARLY_DATA:
-            if (!tls->client.using_early_data) {
-                ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-                goto Exit;
-            }
-            skip_early_data = 0;
-            break;
-        case PTLS_EXTENSION_TYPE_SERVER_CERTIFICATE_TYPE:
-            if (end - src != 1) {
-                ret = PTLS_ALERT_DECODE_ERROR;
-                goto Exit;
-            }
-            server_offered_cert_type = *src;
-            src = end;
-            break;
-        default:
-            if (should_collect_unknown_extension(tls, properties, type)) {
-                if (unknown_extensions == &no_unknown_extensions) {
-                    if ((unknown_extensions = malloc(sizeof(*unknown_extensions) * (MAX_UNKNOWN_EXTENSIONS + 1))) == NULL) {
-                        ret = PTLS_ERROR_NO_MEMORY;
-                        goto Exit;
-                    }
-                    unknown_extensions[0].type = UINT16_MAX;
-                }
-                if ((ret = collect_unknown_extension(tls, type, src, end, unknown_extensions)) != 0)
-                    goto Exit;
-            }
-            break;
-        }
-        src = end;
-    });
-
-    if (server_offered_cert_type !=
-        (tls->ctx->use_raw_public_keys ? PTLS_CERTIFICATE_TYPE_RAW_PUBLIC_KEY : PTLS_CERTIFICATE_TYPE_X509)) {
-        ret = PTLS_ALERT_UNSUPPORTED_CERTIFICATE;
-        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;
-        if (properties != NULL)
-            properties->client.early_data_acceptance = skip_early_data ? PTLS_EARLY_DATA_REJECTED : PTLS_EARLY_DATA_ACCEPTED;
-    }
-    if ((ret = report_unknown_extensions(tls, properties, unknown_extensions)) != 0)
-        goto Exit;
-
-    ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len);
-    tls->state =
-        tls->is_psk_handshake ? PTLS_STATE_CLIENT_EXPECT_FINISHED : PTLS_STATE_CLIENT_EXPECT_CERTIFICATE_REQUEST_OR_CERTIFICATE;
-    ret = PTLS_ERROR_IN_PROGRESS;
-
-Exit:
-    if (unknown_extensions != &no_unknown_extensions)
-        free(unknown_extensions);
-    return ret;
-}
-
-static int decode_certificate_request(ptls_t *tls, struct st_ptls_certificate_request_t *cr, const uint8_t *src,
-                                      const uint8_t *const end)
-{
-    int ret;
-    uint16_t exttype = 0;
-
-    /* certificate request context */
-    ptls_decode_open_block(src, end, 1, {
-        size_t len = end - src;
-        if (len > 255) {
-            ret = PTLS_ALERT_DECODE_ERROR;
-            goto Exit;
-        }
-        if ((cr->context.base = malloc(len != 0 ? len : 1)) == NULL) {
-            ret = PTLS_ERROR_NO_MEMORY;
-            goto Exit;
-        }
-        cr->context.len = len;
-        memcpy(cr->context.base, src, len);
-        src = end;
-    });
-
-    /* decode extensions */
-    decode_extensions(src, end, PTLS_HANDSHAKE_TYPE_CERTIFICATE_REQUEST, &exttype, {
-        if (tls->ctx->on_extension != NULL &&
-            (ret = tls->ctx->on_extension->cb(tls->ctx->on_extension, tls, PTLS_HANDSHAKE_TYPE_CERTIFICATE_REQUEST, exttype,
-                                              ptls_iovec_init(src, end - src)) != 0))
-            goto Exit;
-        switch (exttype) {
-        case PTLS_EXTENSION_TYPE_SIGNATURE_ALGORITHMS:
-            if ((ret = decode_signature_algorithms(&cr->signature_algorithms, &src, end)) != 0)
-                goto Exit;
-            break;
-        }
-        src = end;
-    });
-
-    if (cr->signature_algorithms.count == 0) {
-        ret = PTLS_ALERT_MISSING_EXTENSION;
-        goto Exit;
-    }
-
-    ret = 0;
-Exit:
-    return ret;
-}
-
-int ptls_build_certificate_message(ptls_buffer_t *buf, ptls_iovec_t context, ptls_iovec_t *certificates, size_t num_certificates,
-                                   ptls_iovec_t ocsp_status)
-{
-    int ret;
-
-    ptls_buffer_push_block(buf, 1, { ptls_buffer_pushv(buf, context.base, context.len); });
-    ptls_buffer_push_block(buf, 3, {
-        size_t i;
-        for (i = 0; i != num_certificates; ++i) {
-            ptls_buffer_push_block(buf, 3, { ptls_buffer_pushv(buf, certificates[i].base, certificates[i].len); });
-            ptls_buffer_push_block(buf, 2, {
-                if (i == 0 && ocsp_status.len != 0) {
-                    buffer_push_extension(buf, PTLS_EXTENSION_TYPE_STATUS_REQUEST, {
-                        ptls_buffer_push(buf, 1); /* status_type == ocsp */
-                        ptls_buffer_push_block(buf, 3, { ptls_buffer_pushv(buf, ocsp_status.base, ocsp_status.len); });
-                    });
-                }
-            });
-        }
-    });
-
-    ret = 0;
-Exit:
-    return ret;
-}
-
-static int default_emit_certificate_cb(ptls_emit_certificate_t *_self, ptls_t *tls, ptls_message_emitter_t *emitter,
-                                       ptls_key_schedule_t *key_sched, ptls_iovec_t context, int push_status_request,
-                                       const uint16_t *compress_algos, size_t num_compress_algos)
-{
-    int ret;
-
-    ptls_push_message(emitter, key_sched, PTLS_HANDSHAKE_TYPE_CERTIFICATE, {
-        if ((ret = ptls_build_certificate_message(emitter->buf, context, tls->ctx->certificates.list, tls->ctx->certificates.count,
-                                                  ptls_iovec_init(NULL, 0))) != 0)
-            goto Exit;
-    });
-
-    ret = 0;
-Exit:
-    return ret;
-}
-
-static int send_certificate_and_certificate_verify(ptls_t *tls, ptls_message_emitter_t *emitter,
-                                                   struct st_ptls_signature_algorithms_t *signature_algorithms,
-                                                   ptls_iovec_t context, const char *context_string, int push_status_request,
-                                                   const uint16_t *compress_algos, size_t num_compress_algos)
-{
-    int ret;
-
-    if (signature_algorithms->count == 0) {
-        ret = PTLS_ALERT_MISSING_EXTENSION;
-        goto Exit;
-    }
-
-    { /* send Certificate (or the equivalent) */
-        static ptls_emit_certificate_t default_emit_certificate = {default_emit_certificate_cb};
-        ptls_emit_certificate_t *emit_certificate =
-            tls->ctx->emit_certificate != NULL ? tls->ctx->emit_certificate : &default_emit_certificate;
-    Redo:
-        if ((ret = emit_certificate->cb(emit_certificate, tls, emitter, tls->key_schedule, context, push_status_request,
-                                        compress_algos, num_compress_algos)) != 0) {
-            if (ret == PTLS_ERROR_DELEGATE) {
-                assert(emit_certificate != &default_emit_certificate);
-                emit_certificate = &default_emit_certificate;
-                goto Redo;
-            }
-            goto Exit;
-        }
-    }
-
-    /* build and send CertificateVerify */
-    if (tls->ctx->sign_certificate != NULL) {
-        ptls_push_message(emitter, tls->key_schedule, PTLS_HANDSHAKE_TYPE_CERTIFICATE_VERIFY, {
-            ptls_buffer_t *sendbuf = emitter->buf;
-            size_t algo_off = sendbuf->off;
-            ptls_buffer_push16(sendbuf, 0); /* filled in later */
-            ptls_buffer_push_block(sendbuf, 2, {
-                uint16_t algo;
-                uint8_t data[PTLS_MAX_CERTIFICATE_VERIFY_SIGNDATA_SIZE];
-                size_t datalen = build_certificate_verify_signdata(data, tls->key_schedule, context_string);
-                if ((ret = tls->ctx->sign_certificate->cb(tls->ctx->sign_certificate, tls, &algo, sendbuf,
-                                                          ptls_iovec_init(data, datalen), signature_algorithms->list,
-                                                          signature_algorithms->count)) != 0) {
-                    goto Exit;
-                }
-                sendbuf->base[algo_off] = (uint8_t)(algo >> 8);
-                sendbuf->base[algo_off + 1] = (uint8_t)algo;
-            });
-        });
-    }
-
-Exit:
-    return ret;
-}
-
-static int client_handle_certificate_request(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;
-    int ret = 0;
-
-    if ((ret = decode_certificate_request(tls, &tls->client.certificate_request, src, end)) != 0)
-        return ret;
-
-    /* This field SHALL be zero length unless used for the post-handshake authentication exchanges (section 4.3.2) */
-    if (tls->client.certificate_request.context.len != 0)
-        return PTLS_ALERT_ILLEGAL_PARAMETER;
-
-    tls->state = PTLS_STATE_CLIENT_EXPECT_CERTIFICATE;
-    ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len);
-
-    return PTLS_ERROR_IN_PROGRESS;
-}
-
-static int handle_certificate(ptls_t *tls, const uint8_t *src, const uint8_t *end, int *got_certs)
-{
-    ptls_iovec_t certs[16];
-    size_t num_certs = 0;
-    int ret = 0;
-
-    /* certificate request context */
-    ptls_decode_open_block(src, end, 1, {
-        if (src != end) {
-            ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-            goto Exit;
-        }
-    });
-    /* certificate_list */
-    ptls_decode_block(src, end, 3, {
-        while (src != end) {
-            ptls_decode_open_block(src, end, 3, {
-                if (num_certs < PTLS_ELEMENTSOF(certs))
-                    certs[num_certs++] = ptls_iovec_init(src, end - src);
-                src = end;
-            });
-            uint16_t type;
-            decode_open_extensions(src, end, PTLS_HANDSHAKE_TYPE_CERTIFICATE, &type, {
-                if (tls->ctx->on_extension != NULL &&
-                    (ret = tls->ctx->on_extension->cb(tls->ctx->on_extension, tls, PTLS_HANDSHAKE_TYPE_CERTIFICATE, type,
-                                                      ptls_iovec_init(src, end - src)) != 0))
-                    goto Exit;
-                src = end;
-            });
-        }
-    });
-
-    if (num_certs != 0 && tls->ctx->verify_certificate != NULL) {
-        if ((ret = tls->ctx->verify_certificate->cb(tls->ctx->verify_certificate, tls, &tls->certificate_verify.cb,
-                                                    &tls->certificate_verify.verify_ctx, certs, num_certs)) != 0)
-            goto Exit;
-    }
-
-    *got_certs = num_certs != 0;
-
-Exit:
-    return ret;
-}
-
-static int client_do_handle_certificate(ptls_t *tls, const uint8_t *src, const uint8_t *end)
-{
-    int got_certs, ret;
-
-    if ((ret = handle_certificate(tls, src, end, &got_certs)) != 0)
-        return ret;
-    if (!got_certs)
-        return PTLS_ALERT_ILLEGAL_PARAMETER;
-
-    return 0;
-}
-
-static int client_handle_certificate(ptls_t *tls, ptls_iovec_t message)
-{
-    int ret;
-
-    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);
-
-    tls->state = PTLS_STATE_CLIENT_EXPECT_CERTIFICATE_VERIFY;
-    return PTLS_ERROR_IN_PROGRESS;
-}
-
-static int client_handle_compressed_certificate(ptls_t *tls, ptls_iovec_t message)
-{
-    const uint8_t *src = message.base + PTLS_HANDSHAKE_HEADER_SIZE, *const end = message.base + message.len;
-    uint16_t algo;
-    uint32_t uncompressed_size;
-    uint8_t *uncompressed = NULL;
-    int ret;
-
-    if (tls->ctx->decompress_certificate == NULL) {
-        ret = PTLS_ALERT_UNEXPECTED_MESSAGE;
-        goto Exit;
-    }
-
-    /* decode */
-    if ((ret = ptls_decode16(&algo, &src, end)) != 0)
-        goto Exit;
-    if ((ret = ptls_decode24(&uncompressed_size, &src, end)) != 0)
-        goto Exit;
-    if (uncompressed_size > 65536) { /* TODO find a sensible number */
-        ret = PTLS_ALERT_BAD_CERTIFICATE;
-        goto Exit;
-    }
-    if ((uncompressed = malloc(uncompressed_size)) == NULL) {
-        ret = PTLS_ERROR_NO_MEMORY;
-        goto Exit;
-    }
-    ptls_decode_block(src, end, 3, {
-        if ((ret = tls->ctx->decompress_certificate->cb(tls->ctx->decompress_certificate, tls, algo,
-                                                        ptls_iovec_init(uncompressed, uncompressed_size),
-                                                        ptls_iovec_init(src, end - src))) != 0)
-            goto Exit;
-        src = end;
-    });
-
-    /* handle */
-    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);
-    tls->state = PTLS_STATE_CLIENT_EXPECT_CERTIFICATE_VERIFY;
-    ret = PTLS_ERROR_IN_PROGRESS;
-
-Exit:
-    free(uncompressed);
-    return ret;
-}
-
-static int server_handle_certificate(ptls_t *tls, ptls_iovec_t message)
-{
-    int got_certs, ret;
-
-    if ((ret = handle_certificate(tls, message.base + PTLS_HANDSHAKE_HEADER_SIZE, message.base + message.len, &got_certs)) != 0)
-        return ret;
-    if (!got_certs)
-        return PTLS_ALERT_CERTIFICATE_REQUIRED;
-
-    ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len);
-
-    tls->state = PTLS_STATE_SERVER_EXPECT_CERTIFICATE_VERIFY;
-    return PTLS_ERROR_IN_PROGRESS;
-}
-
-static int handle_certificate_verify(ptls_t *tls, ptls_iovec_t message, const char *context_string)
-{
-    const uint8_t *src = message.base + PTLS_HANDSHAKE_HEADER_SIZE, *const end = message.base + message.len;
-    uint16_t algo;
-    ptls_iovec_t signature;
-    uint8_t signdata[PTLS_MAX_CERTIFICATE_VERIFY_SIGNDATA_SIZE];
-    size_t signdata_size;
-    int ret;
-
-    /* decode */
-    if ((ret = ptls_decode16(&algo, &src, end)) != 0)
-        goto Exit;
-    ptls_decode_block(src, end, 2, {
-        signature = ptls_iovec_init(src, end - src);
-        src = end;
-    });
-
-    signdata_size = build_certificate_verify_signdata(signdata, tls->key_schedule, context_string);
-    if (tls->certificate_verify.cb != NULL) {
-        ret = tls->certificate_verify.cb(tls->certificate_verify.verify_ctx, algo, ptls_iovec_init(signdata, signdata_size),
-                                         signature);
-    } else {
-        ret = 0;
-    }
-    ptls_clear_memory(signdata, signdata_size);
-    tls->certificate_verify.cb = NULL;
-    if (ret != 0) {
-        goto Exit;
-    }
-
-    ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len);
-
-Exit:
-    return ret;
-}
-
-static int client_handle_certificate_verify(ptls_t *tls, ptls_iovec_t message)
-{
-    int ret = handle_certificate_verify(tls, message, PTLS_SERVER_CERTIFICATE_VERIFY_CONTEXT_STRING);
-
-    if (ret == 0) {
-        tls->state = PTLS_STATE_CLIENT_EXPECT_FINISHED;
-        ret = PTLS_ERROR_IN_PROGRESS;
-    }
-
-    return ret;
-}
-
-static int server_handle_certificate_verify(ptls_t *tls, ptls_iovec_t message)
-{
-    int ret = handle_certificate_verify(tls, message, PTLS_CLIENT_CERTIFICATE_VERIFY_CONTEXT_STRING);
-
-    if (ret == 0) {
-        tls->state = PTLS_STATE_SERVER_EXPECT_FINISHED;
-        ret = PTLS_ERROR_IN_PROGRESS;
-    }
-
-    return ret;
-}
-
-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;
-
-    if ((ret = verify_finished(tls, message)) != 0)
-        goto Exit;
-    ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len);
-
-    /* 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)
-        goto Exit;
-    if ((ret = setup_traffic_protection(tls, 0, "s ap traffic", 3, 0)) != 0)
-        goto Exit;
-    if ((ret = derive_secret(tls->key_schedule, send_secret, "c ap traffic")) != 0)
-        goto Exit;
-    if ((ret = derive_exporter_secret(tls, 0)) != 0)
-        goto Exit;
-
-    /* if sending early data, emit EOED and commision the client handshake traffic secret */
-    if (tls->pending_handshake_secret != NULL) {
-        assert(tls->traffic_protection.enc.aead != NULL || tls->ctx->update_traffic_key != NULL);
-        if (tls->client.using_early_data && !tls->ctx->omit_end_of_early_data)
-            ptls_push_message(emitter, tls->key_schedule, PTLS_HANDSHAKE_TYPE_END_OF_EARLY_DATA, {});
-        tls->client.using_early_data = 0;
-        if ((ret = commission_handshake_secret(tls)) != 0)
-            goto Exit;
-    }
-
-    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;
-        }
-        ret = send_certificate_and_certificate_verify(tls, emitter, &tls->client.certificate_request.signature_algorithms,
-                                                      tls->client.certificate_request.context,
-                                                      PTLS_CLIENT_CERTIFICATE_VERIFY_CONTEXT_STRING, 0, NULL, 0);
-        free(tls->client.certificate_request.context.base);
-        tls->client.certificate_request.context = ptls_iovec_init(NULL, 0);
-        if (ret != 0)
-            goto Exit;
-    }
-
-    ret = send_finished(tls, emitter);
-
-    memcpy(tls->traffic_protection.enc.secret, send_secret, sizeof(send_secret));
-    if ((ret = setup_traffic_protection(tls, 1, NULL, 3, 0)) != 0)
-        goto Exit;
-
-    tls->state = PTLS_STATE_CLIENT_POST_HANDSHAKE;
-
-Exit:
-    ptls_clear_memory(send_secret, sizeof(send_secret));
-    return ret;
-}
-
-static int client_handle_new_session_ticket(ptls_t *tls, ptls_iovec_t message)
-{
-    const uint8_t *src = message.base + PTLS_HANDSHAKE_HEADER_SIZE, *const end = message.base + message.len;
-    ptls_iovec_t ticket_nonce;
-    int ret;
-
-    { /* verify the format */
-        uint32_t ticket_lifetime, ticket_age_add, max_early_data_size;
-        ptls_iovec_t ticket;
-        if ((ret = decode_new_session_ticket(tls, &ticket_lifetime, &ticket_age_add, &ticket_nonce, &ticket, &max_early_data_size,
-                                             src, end)) != 0)
-            return ret;
-    }
-
-    /* do nothing if use of session ticket is disabled */
-    if (tls->ctx->save_ticket == NULL)
-        return 0;
-
-    /* save the extension, along with the key of myself */
-    ptls_buffer_t ticket_buf;
-    ptls_buffer_init(&ticket_buf, "", 0);
-    ptls_buffer_push64(&ticket_buf, tls->ctx->get_time->cb(tls->ctx->get_time));
-    ptls_buffer_push16(&ticket_buf, tls->key_share->id);
-    ptls_buffer_push16(&ticket_buf, tls->cipher_suite->id);
-    ptls_buffer_push_block(&ticket_buf, 3, { ptls_buffer_pushv(&ticket_buf, src, end - src); });
-    ptls_buffer_push_block(&ticket_buf, 2, {
-        if ((ret = ptls_buffer_reserve(&ticket_buf, tls->key_schedule->hashes[0].algo->digest_size)) != 0)
-            goto Exit;
-        if ((ret = derive_resumption_secret(tls->key_schedule, ticket_buf.base + ticket_buf.off, ticket_nonce)) != 0)
-            goto Exit;
-        ticket_buf.off += tls->key_schedule->hashes[0].algo->digest_size;
-    });
-
-    if ((ret = tls->ctx->save_ticket->cb(tls->ctx->save_ticket, tls, ptls_iovec_init(ticket_buf.base, ticket_buf.off))) != 0)
-        goto Exit;
-
-    ret = 0;
-Exit:
-    ptls_buffer_dispose(&ticket_buf);
-    return ret;
-}
-
-static int client_hello_decode_server_name(ptls_iovec_t *name, const uint8_t **src, const uint8_t *const end)
-{
-    int ret = 0;
-
-    ptls_decode_open_block(*src, end, 2, {
-        if (*src == end) {
-            ret = PTLS_ALERT_DECODE_ERROR;
-            goto Exit;
-        }
-        do {
-            uint8_t type = *(*src)++;
-            ptls_decode_open_block(*src, end, 2, {
-                switch (type) {
-                case PTLS_SERVER_NAME_TYPE_HOSTNAME:
-                    if (memchr(*src, '\0', end - *src) != 0) {
-                        ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-                        goto Exit;
-                    }
-                    *name = ptls_iovec_init(*src, end - *src);
-                    break;
-                default:
-                    break;
-                }
-                *src = end;
-            });
-        } while (*src != end);
-    });
-
-Exit:
-    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)
-{
-    int ret;
-
-    ptls_decode_block(src, end, 2, {
-        while (src != end) {
-            uint16_t group;
-            if ((ret = ptls_decode16(&group, &src, end)) != 0)
-                goto Exit;
-            ptls_key_exchange_algorithm_t **c = candidates;
-            for (; *c != NULL; ++c) {
-                if ((*c)->id == group) {
-                    *selected = *c;
-                    return 0;
-                }
-            }
-        }
-    });
-
-    ret = PTLS_ALERT_HANDSHAKE_FAILURE;
-
-Exit:
-    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)
-{
-    uint16_t exttype = 0;
-    int ret;
-
-    /* decode protocol version (do not bare to decode something older than TLS 1.0) */
-    if ((ret = ptls_decode16(&ch->legacy_version, &src, end)) != 0)
-        goto Exit;
-    if (ch->legacy_version < 0x0301) {
-        ret = PTLS_ALERT_PROTOCOL_VERSION;
-        goto Exit;
-    }
-
-    /* skip random */
-    if (end - src < PTLS_HELLO_RANDOM_SIZE) {
-        ret = PTLS_ALERT_DECODE_ERROR;
-        goto Exit;
-    }
-    ch->random_bytes = src;
-    src += PTLS_HELLO_RANDOM_SIZE;
-
-    /* skip legacy_session_id */
-    ptls_decode_open_block(src, end, 1, {
-        if (end - src > 32) {
-            ret = PTLS_ALERT_DECODE_ERROR;
-            goto Exit;
-        }
-        ch->legacy_session_id = ptls_iovec_init(src, end - src);
-        src = end;
-    });
-
-    /* decode and select from ciphersuites */
-    ptls_decode_open_block(src, end, 2, {
-        ch->cipher_suites = ptls_iovec_init(src, end - src);
-        uint16_t *id = ch->client_ciphers.list;
-        do {
-            if ((ret = ptls_decode16(id, &src, end)) != 0)
-                goto Exit;
-            id++;
-            ch->client_ciphers.count++;
-            if (id >= ch->client_ciphers.list + MAX_CLIENT_CIPHERS) {
-                src = end;
-                break;
-            }
-        } while (src != end);
-    });
-
-    /* decode legacy_compression_methods */
-    ptls_decode_open_block(src, end, 1, {
-        if (src == end) {
-            ret = PTLS_ALERT_DECODE_ERROR;
-            goto Exit;
-        }
-        ch->compression_methods.ids = src;
-        ch->compression_methods.count = end - src;
-        src = end;
-    });
-
-    /* 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))
-            goto Exit;
-        switch (exttype) {
-        case PTLS_EXTENSION_TYPE_SERVER_NAME:
-            if ((ret = client_hello_decode_server_name(&ch->server_name, &src, end)) != 0)
-                goto Exit;
-            if (src != end) {
-                ret = PTLS_ALERT_DECODE_ERROR;
-                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 {
-                    ptls_decode_open_block(src, end, 1, {
-                        /* rfc7301 3.1: empty strings MUST NOT be included */
-                        if (src == end) {
-                            ret = PTLS_ALERT_DECODE_ERROR;
-                            goto Exit;
-                        }
-                        if (ch->alpn.count < PTLS_ELEMENTSOF(ch->alpn.list))
-                            ch->alpn.list[ch->alpn.count++] = ptls_iovec_init(src, end - src);
-                        src = end;
-                    });
-                } while (src != end);
-            });
-            break;
-        case PTLS_EXTENSION_TYPE_SERVER_CERTIFICATE_TYPE:
-            ptls_decode_block(src, end, 1, {
-                size_t list_size = end - src;
-
-                /* RFC7250 4.1: No empty list, no list with single x509 element */
-                if (list_size == 0 || (list_size == 1 && *src == PTLS_CERTIFICATE_TYPE_X509)) {
-                    ret = PTLS_ALERT_DECODE_ERROR;
-                    goto Exit;
-                }
-
-                do {
-                    if (ch->server_certificate_types.count < PTLS_ELEMENTSOF(ch->server_certificate_types.list))
-                        ch->server_certificate_types.list[ch->server_certificate_types.count++] = *src;
-                    src++;
-                } while (src != end);
-            });
-            break;
-        case PTLS_EXTENSION_TYPE_COMPRESS_CERTIFICATE:
-            ptls_decode_block(src, end, 1, {
-                do {
-                    uint16_t id;
-                    if ((ret = ptls_decode16(&id, &src, end)) != 0)
-                        goto Exit;
-                    if (ch->cert_compression_algos.count < PTLS_ELEMENTSOF(ch->cert_compression_algos.list))
-                        ch->cert_compression_algos.list[ch->cert_compression_algos.count++] = id;
-                } while (src != end);
-            });
-            break;
-        case PTLS_EXTENSION_TYPE_SUPPORTED_GROUPS:
-            ch->negotiated_groups = ptls_iovec_init(src, end - src);
-            break;
-        case PTLS_EXTENSION_TYPE_SIGNATURE_ALGORITHMS:
-            if ((ret = decode_signature_algorithms(&ch->signature_algorithms, &src, end)) != 0)
-                goto Exit;
-            break;
-        case PTLS_EXTENSION_TYPE_KEY_SHARE:
-            ch->key_shares = ptls_iovec_init(src, end - src);
-            break;
-        case PTLS_EXTENSION_TYPE_SUPPORTED_VERSIONS:
-            ptls_decode_block(src, end, 1, {
-                size_t selected_index = PTLS_ELEMENTSOF(supported_versions);
-                do {
-                    size_t i;
-                    uint16_t v;
-                    if ((ret = ptls_decode16(&v, &src, end)) != 0)
-                        goto Exit;
-                    for (i = 0; i != selected_index; ++i) {
-                        if (supported_versions[i] == v) {
-                            selected_index = i;
-                            break;
-                        }
-                    }
-                } while (src != end);
-                if (selected_index != PTLS_ELEMENTSOF(supported_versions))
-                    ch->selected_version = supported_versions[selected_index];
-            });
-            break;
-        case PTLS_EXTENSION_TYPE_COOKIE:
-            if (properties == NULL || properties->server.cookie.key == NULL) {
-                ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-                goto Exit;
-            }
-            ch->cookie.all = ptls_iovec_init(src, end - src);
-            ptls_decode_block(src, end, 2, {
-                ch->cookie.tbs.base = (void *)src;
-                ptls_decode_open_block(src, end, 2, {
-                    ptls_decode_open_block(src, end, 1, {
-                        ch->cookie.ch1_hash = ptls_iovec_init(src, end - src);
-                        src = end;
-                    });
-                    if (src == end) {
-                        ret = PTLS_ALERT_DECODE_ERROR;
-                        goto Exit;
-                    }
-                    switch (*src++) {
-                    case 0:
-                        assert(!ch->cookie.sent_key_share);
-                        break;
-                    case 1:
-                        ch->cookie.sent_key_share = 1;
-                        break;
-                    default:
-                        ret = PTLS_ALERT_DECODE_ERROR;
-                        goto Exit;
-                    }
-                });
-                ch->cookie.tbs.len = src - ch->cookie.tbs.base;
-                ptls_decode_block(src, end, 1, {
-                    ch->cookie.signature = ptls_iovec_init(src, end - src);
-                    src = end;
-                });
-            });
-            break;
-        case PTLS_EXTENSION_TYPE_PRE_SHARED_KEY: {
-            size_t num_identities = 0;
-            ptls_decode_open_block(src, end, 2, {
-                do {
-                    struct st_ptls_client_hello_psk_t psk = {{NULL}};
-                    ptls_decode_open_block(src, end, 2, {
-                        psk.identity = ptls_iovec_init(src, end - src);
-                        src = end;
-                    });
-                    if ((ret = ptls_decode32(&psk.obfuscated_ticket_age, &src, end)) != 0)
-                        goto Exit;
-                    if (ch->psk.identities.count < PTLS_ELEMENTSOF(ch->psk.identities.list))
-                        ch->psk.identities.list[ch->psk.identities.count++] = psk;
-                    ++num_identities;
-                } while (src != end);
-            });
-            ch->psk.hash_end = src;
-            ptls_decode_block(src, end, 2, {
-                size_t num_binders = 0;
-                do {
-                    ptls_decode_open_block(src, end, 1, {
-                        if (num_binders < ch->psk.identities.count)
-                            ch->psk.identities.list[num_binders].binder = ptls_iovec_init(src, end - src);
-                        src = end;
-                    });
-                    ++num_binders;
-                } while (src != end);
-                if (num_identities != num_binders) {
-                    ret = PTLS_ALERT_ILLEGAL_PARAMETER;
-                    goto Exit;
-                }
-            });
-            ch->psk.is_last_extension = 1;
-        } break;
-        case PTLS_EXTENSION_TYPE_PSK_KEY_EXCHANGE_MODES:
-            ptls_decode_block(src, end, 1, {
-                if (src == end) {
-                    ret = PTLS_ALERT_DECODE_ERROR;
-                    goto Exit;
-                }
-                for (; src != end; ++src) {
-                    if (*src < sizeof(ch->psk.ke_modes) * 8)
-                        ch->psk.ke_modes |= 1u << *src;
-                }
-            });
-            break;
-        case PTLS_EXTENSION_TYPE_EARLY_DATA:
-            ch->psk.early_data_indication = 1;
-            break;
-        case PTLS_EXTENSION_TYPE_STATUS_REQUEST:
-            ch->status_request = 1;
-            break;
-        default:
-            if (should_collect_unknown_extension(tls, properties, exttype)) {
-                if ((ret = collect_unknown_extension(tls, exttype, src, end, ch->unknown_extensions)) != 0)
-                    goto Exit;
-            }
-            break;
-        }
-        src = end;
-    });
-
-    ret = 0;
-Exit:
-    return ret;
-}
-
-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';
-}
-
-static int try_psk_handshake(ptls_t *tls, size_t *psk_index, int *accept_early_data, struct st_ptls_client_hello_t *ch,
-                             ptls_iovec_t ch_trunc)
-{
-    ptls_buffer_t decbuf;
-    ptls_iovec_t ticket_psk, ticket_server_name, ticket_negotiated_protocol;
-    uint64_t issue_at, now = tls->ctx->get_time->cb(tls->ctx->get_time);
-    uint32_t age_add;
-    uint16_t ticket_key_exchange_id, ticket_csid;
-    uint8_t binder_key[PTLS_MAX_DIGEST_SIZE];
-    int ret;
-
-    ptls_buffer_init(&decbuf, "", 0);
-
-    for (*psk_index = 0; *psk_index < ch->psk.identities.count; ++*psk_index) {
-        struct st_ptls_client_hello_psk_t *identity = ch->psk.identities.list + *psk_index;
-        /* decrypt and decode */
-        int can_accept_early_data = 1;
-        decbuf.off = 0;
-        switch (tls->ctx->encrypt_ticket->cb(tls->ctx->encrypt_ticket, tls, 0, &decbuf, identity->identity)) {
-        case 0: /* decrypted */
-            break;
-        case PTLS_ERROR_REJECT_EARLY_DATA: /* decrypted, but early data is rejected */
-            can_accept_early_data = 0;
-            break;
-        default: /* decryption failure */
-            continue;
-        }
-        if (decode_session_identifier(&issue_at, &ticket_psk, &age_add, &ticket_server_name, &ticket_key_exchange_id, &ticket_csid,
-                                      &ticket_negotiated_protocol, decbuf.base, decbuf.base + decbuf.off) != 0)
-            continue;
-        /* check age */
-        if (now < issue_at)
-            continue;
-        if (now - issue_at > (uint64_t)tls->ctx->ticket_lifetime * 1000)
-            continue;
-        *accept_early_data = 0;
-        if (ch->psk.early_data_indication && can_accept_early_data) {
-            /* accept early-data if abs(diff) between the reported age and the actual age is within += 10 seconds */
-            int64_t delta = (now - issue_at) - (identity->obfuscated_ticket_age - age_add);
-            if (delta < 0)
-                delta = -delta;
-            if (tls->ctx->max_early_data_size != 0 && delta <= PTLS_EARLY_DATA_MAX_DELAY)
-                *accept_early_data = 1;
-        }
-        /* check server-name */
-        if (ticket_server_name.len != 0) {
-            if (tls->server_name == NULL)
-                continue;
-            if (!vec_is_string(ticket_server_name, tls->server_name))
-                continue;
-        } else {
-            if (tls->server_name != NULL)
-                continue;
-        }
-        { /* check key-exchange */
-            ptls_key_exchange_algorithm_t **a;
-            for (a = tls->ctx->key_exchanges; *a != NULL && (*a)->id != ticket_key_exchange_id; ++a)
-                ;
-            if (*a == NULL)
-                continue;
-            tls->key_share = *a;
-        }
-        /* check cipher-suite */
-        if (ticket_csid != tls->cipher_suite->id)
-            continue;
-        /* check negotiated-protocol */
-        if (ticket_negotiated_protocol.len != 0) {
-            if (tls->negotiated_protocol == NULL)
-                continue;
-            if (!vec_is_string(ticket_negotiated_protocol, tls->negotiated_protocol))
-                continue;
-        }
-        /* check the length of the decrypted psk and the PSK binder */
-        if (ticket_psk.len != tls->key_schedule->hashes[0].algo->digest_size)
-            continue;
-        if (ch->psk.identities.list[*psk_index].binder.len != tls->key_schedule->hashes[0].algo->digest_size)
-            continue;
-
-        /* found */
-        goto Found;
-    }
-
-    /* not found */
-    *psk_index = SIZE_MAX;
-    *accept_early_data = 0;
-    tls->key_share = NULL;
-    ret = 0;
-    goto Exit;
-
-Found:
-    if ((ret = key_schedule_extract(tls->key_schedule, ticket_psk)) != 0)
-        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);
-    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;
-    if (!ptls_mem_equal(ch->psk.identities.list[*psk_index].binder.base, binder_key,
-                        tls->key_schedule->hashes[0].algo->digest_size)) {
-        ret = PTLS_ALERT_DECRYPT_ERROR;
-        goto Exit;
-    }
-    ret = 0;
-
-Exit:
-    ptls_buffer_dispose(&decbuf);
-    ptls_clear_memory(binder_key, sizeof(binder_key));
-    return ret;
-}
-
-static int calc_cookie_signature(ptls_t *tls, ptls_handshake_properties_t *properties,
-                                 ptls_key_exchange_algorithm_t *negotiated_group, ptls_iovec_t tbs, uint8_t *sig)
-{
-    ptls_hash_algorithm_t *algo = tls->ctx->cipher_suites[0]->hash;
-    ptls_hash_context_t *hctx;
-
-    if ((hctx = ptls_hmac_create(algo, properties->server.cookie.key, algo->digest_size)) == NULL)
-        return PTLS_ERROR_NO_MEMORY;
-
-#define UPDATE_BLOCK(p, _len)                                                                                                      \
-    do {                                                                                                                           \
-        size_t len = (_len);                                                                                                       \
-        assert(len < UINT8_MAX);                                                                                                   \
-        uint8_t len8 = (uint8_t)len;                                                                                               \
-        hctx->update(hctx, &len8, 1);                                                                                              \
-        hctx->update(hctx, (p), len);                                                                                              \
-    } while (0)
-#define UPDATE16(_v)                                                                                                               \
-    do {                                                                                                                           \
-        uint16_t v = (_v);                                                                                                         \
-        uint8_t b[2] = {v >> 8, v & 0xff};                                                                                         \
-        hctx->update(hctx, b, 2);                                                                                                  \
-    } while (0)
-
-    UPDATE_BLOCK(tls->client_random, sizeof(tls->client_random));
-    UPDATE_BLOCK(tls->server_name, tls->server_name != NULL ? strlen(tls->server_name) : 0);
-    UPDATE16(tls->cipher_suite->id);
-    UPDATE16(negotiated_group->id);
-    UPDATE_BLOCK(properties->server.cookie.additional_data.base, properties->server.cookie.additional_data.len);
-
-    UPDATE_BLOCK(tbs.base, tbs.len);
-
-#undef UPDATE_BLOCK
-#undef UPDATE16
-
-    hctx->final(hctx, sig, PTLS_HASH_FINAL_MODE_FREE);
-    return 0;
-}
-
-static int certificate_type_exists(uint8_t *list, size_t count, uint8_t desired_type)
-{
-    /* empty type list means that we default to x509 */
-    if (desired_type == PTLS_CERTIFICATE_TYPE_X509 && count == 0)
-        return 1;
-    for (size_t i = 0; i < count; i++) {
-        if (list[i] == desired_type)
-            return 1;
-    }
-    return 0;
-}
-
-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); });                                    \
-            do {                                                                                                                   \
-                extensions                                                                                                         \
-            } while (0);                                                                                                           \
-        });                                                                                                                        \
-    });
-
-#define EMIT_HELLO_RETRY_REQUEST(sched, negotiated_group, additional_extensions)                                                   \
-    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);                                   \
-                          if (_negotiated_group != NULL) {                                                                         \
-                              buffer_push_extension(emitter->buf, PTLS_EXTENSION_TYPE_KEY_SHARE,                                   \
-                                                    { ptls_buffer_push16(emitter->buf, _negotiated_group->id); });                 \
-                          }                                                                                                        \
-                          do {                                                                                                     \
-                              additional_extensions                                                                                \
-                          } while (0);                                                                                             \
-                      })
-    struct st_ptls_client_hello_t *ch;
-    struct {
-        ptls_key_exchange_algorithm_t *algorithm;
-        ptls_iovec_t peer_key;
-    } key_share = {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;
-
-    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}}};
-
-    /* decode ClientHello */
-    if ((ret = decode_client_hello(tls, ch, message.base + PTLS_HANDSHAKE_HEADER_SIZE, message.base + message.len, properties)) !=
-        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)
-                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;
-    }
-    /* 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) {
-            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)
-            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)) {
-            ret = PTLS_ALERT_UNSUPPORTED_CERTIFICATE;
-            goto Exit;
-        }
-    } else {
-        if (ch->psk.early_data_indication) {
-            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) {
-            size_t l = strlen(tls->server_name);
-            if (!(ch->server_name.len == l && memcmp(ch->server_name.base, tls->server_name, l) == 0)) {
-                ret = PTLS_ALERT_HANDSHAKE_FAILURE;
-                goto Exit;
-            }
-        }
-    }
-
-    { /* select (or check) cipher-suite, create key_schedule */
-        ptls_cipher_suite_t *cs;
-        if ((ret = select_cipher(&cs, tls->ctx->cipher_suites, ch->cipher_suites.base,
-                                 ch->cipher_suites.base + ch->cipher_suites.len, tls->ctx->server_cipher_preference)) != 0)
-            goto Exit;
-        if (!is_second_flight) {
-            tls->cipher_suite = cs;
-            tls->key_schedule = key_schedule_new(cs, NULL, tls->ctx->hkdf_label_prefix__obsolete);
-        } else {
-            if (tls->cipher_suite != cs) {
-                ret = PTLS_ALERT_HANDSHAKE_FAILURE;
-                goto Exit;
-            }
-        }
-    }
-
-    /* select key_share */
-    if (key_share.algorithm == NULL && ch->key_shares.base != NULL) {
-        const uint8_t *src = ch->key_shares.base, *const end = src + ch->key_shares.len;
-        ptls_decode_block(src, end, 2, {
-            if ((ret = select_key_share(&key_share.algorithm, &key_share.peer_key, tls->ctx->key_exchanges, &src, end, 0)) != 0)
-                goto Exit;
-        });
-    }
-
-    if (!is_second_flight) {
-        if (ch->cookie.all.len != 0 && key_share.algorithm != NULL) {
-
-            /* use cookie to check the integrity of the handshake, and update the context */
-            size_t sigsize = tls->ctx->cipher_suites[0]->hash->digest_size;
-            uint8_t *sig = alloca(sigsize);
-            if ((ret = calc_cookie_signature(tls, properties, key_share.algorithm, ch->cookie.tbs, sig)) != 0)
-                goto Exit;
-            if (!(ch->cookie.signature.len == sigsize && ptls_mem_equal(ch->cookie.signature.base, sig, sigsize))) {
-                ret = PTLS_ALERT_HANDSHAKE_FAILURE;
-                goto Exit;
-            }
-            /* 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);
-            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); });
-            });
-            emitter->buf->off = hrr_start;
-            is_second_flight = 1;
-
-        } else if (key_share.algorithm == NULL || (properties != NULL && properties->server.enforce_retry)) {
-
-            /* send HelloRetryRequest  */
-            if (ch->negotiated_groups.base == NULL) {
-                ret = PTLS_ALERT_MISSING_EXTENSION;
-                goto Exit;
-            }
-            ptls_key_exchange_algorithm_t *negotiated_group;
-            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);
-            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, {
-                    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;
-                            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->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 ((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;
-                ret = PTLS_ERROR_IN_PROGRESS;
-            }
-            goto Exit;
-        }
-    }
-
-    /* handle unknown extensions */
-    if ((ret = report_unknown_extensions(tls, properties, ch->unknown_extensions)) != 0)
-        goto Exit;
-
-    /* try psk handshake */
-    if (!is_second_flight && ch->psk.hash_end != 0 &&
-        (ch->psk.ke_modes & ((1u << PTLS_PSK_KE_MODE_PSK) | (1u << PTLS_PSK_KE_MODE_PSK_DHE))) != 0 &&
-        tls->ctx->encrypt_ticket != NULL && !tls->ctx->require_client_authentication) {
-        if ((ret = try_psk_handshake(tls, &psk_index, &accept_early_data, ch,
-                                     ptls_iovec_init(message.base, ch->psk.hash_end - message.base))) != 0) {
-            goto Exit;
-        }
-    }
-
-    /* If client authentication is enabled, we always force a full handshake.
-     * TODO: Check for `post_handshake_auth` extension and if that is present, do not force full handshake!
-     *       Remove also the check `!require_client_authentication` above.
-     *
-     * 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);
-        if (!is_second_flight) {
-            assert(tls->key_schedule->generation == 0);
-            key_schedule_extract(tls->key_schedule, ptls_iovec_init(NULL, 0));
-        }
-        mode = HANDSHAKE_MODE_FULL;
-        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);
-        if ((ch->psk.ke_modes & (1u << PTLS_PSK_KE_MODE_PSK)) != 0) {
-            mode = HANDSHAKE_MODE_PSK;
-        } else {
-            assert((ch->psk.ke_modes & (1u << PTLS_PSK_KE_MODE_PSK_DHE)) != 0);
-            mode = HANDSHAKE_MODE_PSK_DHE;
-        }
-        tls->is_psk_handshake = 1;
-        if (properties != NULL) {
-            ptls_iovec_t *selected = &ch->psk.identities.list[psk_index].binder;
-            memcpy(properties->server.selected_psk_binder.base, selected->base, selected->len);
-            properties->server.selected_psk_binder.len = selected->len;
-        }
-    }
-
-    if (accept_early_data && tls->ctx->max_early_data_size != 0 && psk_index == 0) {
-        if ((tls->pending_handshake_secret = malloc(PTLS_MAX_DIGEST_SIZE)) == NULL) {
-            ret = PTLS_ERROR_NO_MEMORY;
-            goto Exit;
-        }
-        if ((ret = derive_exporter_secret(tls, 1)) != 0)
-            goto Exit;
-        if ((ret = setup_traffic_protection(tls, 0, "c e traffic", 1, 0)) != 0)
-            goto Exit;
-    }
-
-    /* run key-exchange, to obtain pubkey and secret */
-    if (mode != HANDSHAKE_MODE_PSK) {
-        if (key_share.algorithm == NULL) {
-            ret = ch->key_shares.base != NULL ? PTLS_ALERT_HANDSHAKE_FAILURE : PTLS_ALERT_MISSING_EXTENSION;
-            goto Exit;
-        }
-        if ((ret = key_share.algorithm->exchange(key_share.algorithm, &pubkey, &ecdh_secret, key_share.peer_key)) != 0)
-            goto Exit;
-        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); });
-            }
-        });
-    if ((ret = push_change_cipher_spec(tls, emitter)) != 0)
-        goto Exit;
-
-    /* create protection contexts for the handshake */
-    assert(tls->key_schedule->generation == 1);
-    key_schedule_extract(tls->key_schedule, ecdh_secret);
-    if ((ret = setup_traffic_protection(tls, 1, "s hs traffic", 2, 0)) != 0)
-        goto Exit;
-    if (tls->pending_handshake_secret != NULL) {
-        if ((ret = derive_secret(tls->key_schedule, tls->pending_handshake_secret, "c hs traffic")) != 0)
-            goto Exit;
-        if (tls->ctx->update_traffic_key != NULL &&
-            (ret = tls->ctx->update_traffic_key->cb(tls->ctx->update_traffic_key, tls, 0, 2, tls->pending_handshake_secret)) != 0)
-            goto Exit;
-    } else {
-        if ((ret = setup_traffic_protection(tls, 0, "c hs traffic", 2, 0)) != 0)
-            goto Exit;
-        if (ch->psk.early_data_indication)
-            tls->server.early_data_skipped_bytes = 0;
-    }
-
-    /* send EncryptedExtensions */
-    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) {
-                /* 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, {});
-            }
-            if (tls->ctx->use_raw_public_keys) {
-                buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_SERVER_CERTIFICATE_TYPE,
-                                      { ptls_buffer_push(sendbuf, PTLS_CERTIFICATE_TYPE_RAW_PUBLIC_KEY); });
-            }
-            if (tls->negotiated_protocol != NULL) {
-                buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_ALPN, {
-                    ptls_buffer_push_block(sendbuf, 2, {
-                        ptls_buffer_push_block(sendbuf, 1, {
-                            ptls_buffer_pushv(sendbuf, tls->negotiated_protocol, strlen(tls->negotiated_protocol));
-                        });
-                    });
-                });
-            }
-            if (tls->pending_handshake_secret != NULL)
-                buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_EARLY_DATA, {});
-            if ((ret = push_additional_extensions(properties, sendbuf)) != 0)
-                goto Exit;
-        });
-    });
-
-    if (mode == HANDSHAKE_MODE_FULL) {
-        /* send certificate request if client authentication is activated */
-        if (tls->ctx->require_client_authentication) {
-            ptls_push_message(emitter, tls->key_schedule, PTLS_HANDSHAKE_TYPE_CERTIFICATE_REQUEST, {
-                /* certificate_request_context, this field SHALL be zero length, unless the certificate
-                 * request is used for post-handshake authentication.
-                 */
-                ptls_buffer_t *sendbuf = emitter->buf;
-                ptls_buffer_push(sendbuf, 0);
-                /* extensions */
-                ptls_buffer_push_block(sendbuf, 2, {
-                    buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_SIGNATURE_ALGORITHMS, {
-                        if ((ret = push_signature_algorithms(tls->ctx->verify_certificate, sendbuf)) != 0)
-                            goto Exit;
-                    });
-                });
-            });
-
-            if (ret != 0) {
-                goto Exit;
-            }
-        }
-
-        ret = send_certificate_and_certificate_verify(tls, emitter, &ch->signature_algorithms, ptls_iovec_init(NULL, 0),
-                                                      PTLS_SERVER_CERTIFICATE_VERIFY_CONTEXT_STRING, ch->status_request,
-                                                      ch->cert_compression_algos.list, ch->cert_compression_algos.count);
-
-        if (ret != 0) {
-            goto Exit;
-        }
-    }
-
-    if ((ret = send_finished(tls, emitter)) != 0)
-        goto Exit;
-
-    assert(tls->key_schedule->generation == 2);
-    if ((ret = key_schedule_extract(tls->key_schedule, ptls_iovec_init(NULL, 0))) != 0)
-        goto Exit;
-    if ((ret = setup_traffic_protection(tls, 1, "s ap traffic", 3, 0)) != 0)
-        goto Exit;
-    if ((ret = derive_secret(tls->key_schedule, tls->server.pending_traffic_secret, "c ap traffic")) != 0)
-        goto Exit;
-    if ((ret = derive_exporter_secret(tls, 0)) != 0)
-        goto Exit;
-
-    if (tls->pending_handshake_secret != NULL) {
-        if (tls->ctx->omit_end_of_early_data) {
-            if ((ret = commission_handshake_secret(tls)) != 0)
-                goto Exit;
-            tls->state = PTLS_STATE_SERVER_EXPECT_FINISHED;
-        } else {
-            tls->state = PTLS_STATE_SERVER_EXPECT_END_OF_EARLY_DATA;
-        }
-    } else if (tls->ctx->require_client_authentication) {
-        tls->state = PTLS_STATE_SERVER_EXPECT_CERTIFICATE;
-    } else {
-        tls->state = PTLS_STATE_SERVER_EXPECT_FINISHED;
-    }
-
-    /* send session ticket if necessary */
-    if (ch->psk.ke_modes != 0 && tls->ctx->ticket_lifetime != 0) {
-        if ((ret = send_session_ticket(tls, emitter)) != 0)
-            goto Exit;
-    }
-
-    if (tls->ctx->require_client_authentication) {
-        ret = PTLS_ERROR_IN_PROGRESS;
-    } else {
-        ret = 0;
-    }
-
-Exit:
-    free(pubkey.base);
-    if (ecdh_secret.base != NULL) {
-        ptls_clear_memory(ecdh_secret.base, ecdh_secret.len);
-        free(ecdh_secret.base);
-    }
-    free(ch);
-    return ret;
-
-#undef EMIT_SERVER_HELLO
-#undef EMIT_HELLO_RETRY_REQUEST
-}
-
-static int server_handle_end_of_early_data(ptls_t *tls, ptls_iovec_t message)
-{
-    int ret;
-
-    if ((ret = commission_handshake_secret(tls)) != 0)
-        goto Exit;
-
-    ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len);
-    tls->state = PTLS_STATE_SERVER_EXPECT_FINISHED;
-    ret = PTLS_ERROR_IN_PROGRESS;
-
-Exit:
-    return ret;
-}
-
-static int server_handle_finished(ptls_t *tls, ptls_iovec_t message)
-{
-    int ret;
-
-    if ((ret = verify_finished(tls, message)) != 0)
-        return ret;
-
-    memcpy(tls->traffic_protection.dec.secret, tls->server.pending_traffic_secret, sizeof(tls->server.pending_traffic_secret));
-    ptls_clear_memory(tls->server.pending_traffic_secret, sizeof(tls->server.pending_traffic_secret));
-    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);
-
-    tls->state = PTLS_STATE_SERVER_POST_HANDSHAKE;
-    return 0;
-}
-
-static int update_traffic_key(ptls_t *tls, int is_enc)
-{
-    struct st_ptls_traffic_protection_t *tp = is_enc ? &tls->traffic_protection.enc : &tls->traffic_protection.dec;
-    uint8_t secret[PTLS_MAX_DIGEST_SIZE];
-    int ret;
-
-    ptls_hash_algorithm_t *hash = tls->key_schedule->hashes[0].algo;
-    if ((ret = hkdf_expand_label(hash, secret, hash->digest_size, ptls_iovec_init(tp->secret, hash->digest_size), "traffic upd",
-                                 ptls_iovec_init(NULL, 0), tls->key_schedule->hkdf_label_prefix)) != 0)
-        goto Exit;
-    memcpy(tp->secret, secret, sizeof(secret));
-    ret = setup_traffic_protection(tls, is_enc, NULL, 3, 1);
-
-Exit:
-    ptls_clear_memory(secret, sizeof(secret));
-    return ret;
-}
-
-static int handle_key_update(ptls_t *tls, ptls_message_emitter_t *emitter, ptls_iovec_t message)
-{
-    const uint8_t *src = message.base + PTLS_HANDSHAKE_HEADER_SIZE, *const end = message.base + message.len;
-    int ret;
-
-    /* validate */
-    if (end - src != 1 || *src > 1)
-        return PTLS_ALERT_DECODE_ERROR;
-
-    /* update receive key */
-    if ((ret = update_traffic_key(tls, 0)) != 0)
-        return ret;
-
-    if (*src) {
-        if (tls->ctx->update_traffic_key != NULL)
-            return PTLS_ALERT_UNEXPECTED_MESSAGE;
-        tls->needs_key_update = 1;
-    }
-
-    return 0;
-}
-
-static int parse_record_header(struct st_ptls_record_t *rec, const uint8_t *src)
-{
-    rec->type = src[0];
-    rec->version = ntoh16(src + 1);
-    rec->length = ntoh16(src + 3);
-
-    if (rec->length >
-        (size_t)(rec->type == PTLS_CONTENT_TYPE_APPDATA ? PTLS_MAX_ENCRYPTED_RECORD_SIZE : PTLS_MAX_PLAINTEXT_RECORD_SIZE))
-        return PTLS_ALERT_DECODE_ERROR;
-
-    return 0;
-}
-
-static int parse_record(ptls_t *tls, struct st_ptls_record_t *rec, const uint8_t *src, size_t *len)
-{
-    int ret;
-
-    if (tls->recvbuf.rec.base == NULL && *len >= 5) {
-        /* fast path */
-        if ((ret = parse_record_header(rec, src)) != 0)
-            return ret;
-        if (5 + rec->length <= *len) {
-            rec->fragment = src + 5;
-            *len = rec->length + 5;
-            return 0;
-        }
-    }
-
-    /* slow path */
-    const uint8_t *const end = src + *len;
-    *rec = (struct st_ptls_record_t){0};
-
-    if (tls->recvbuf.rec.base == NULL) {
-        ptls_buffer_init(&tls->recvbuf.rec, "", 0);
-        if ((ret = ptls_buffer_reserve(&tls->recvbuf.rec, 5)) != 0)
-            return ret;
-    }
-
-    /* fill and parse the header */
-    while (tls->recvbuf.rec.off < 5) {
-        if (src == end)
-            return PTLS_ERROR_IN_PROGRESS;
-        tls->recvbuf.rec.base[tls->recvbuf.rec.off++] = *src++;
-    }
-    if ((ret = parse_record_header(rec, tls->recvbuf.rec.base)) != 0)
-        return ret;
-
-    /* fill the fragment */
-    size_t addlen = rec->length + 5 - tls->recvbuf.rec.off;
-    if (addlen != 0) {
-        if ((ret = ptls_buffer_reserve(&tls->recvbuf.rec, addlen)) != 0)
-            return ret;
-        if (addlen > (size_t)(end - src))
-            addlen = end - src;
-        if (addlen != 0) {
-            memcpy(tls->recvbuf.rec.base + tls->recvbuf.rec.off, src, addlen);
-            tls->recvbuf.rec.off += addlen;
-            src += addlen;
-        }
-    }
-
-    /* set rec->fragment if a complete record has been parsed */
-    if (tls->recvbuf.rec.off == rec->length + 5) {
-        rec->fragment = tls->recvbuf.rec.base + 5;
-        ret = 0;
-    } else {
-        ret = PTLS_ERROR_IN_PROGRESS;
-    }
-
-    *len -= end - src;
-    return ret;
-}
-
-static void update_open_count(ptls_context_t *ctx, ssize_t delta)
-{
-    if (ctx->update_open_count != NULL)
-        ctx->update_open_count->cb(ctx->update_open_count, delta);
-}
-
-static ptls_t *new_instance(ptls_context_t *ctx, int is_server)
-{
-    ptls_t *tls;
-
-    assert(ctx->get_time != NULL && "please set ctx->get_time to `&ptls_get_time`; see #92");
-
-    if ((tls = malloc(sizeof(*tls))) == NULL)
-        return NULL;
-
-    update_open_count(ctx, 1);
-    *tls = (ptls_t){ctx};
-    tls->is_server = is_server;
-    tls->send_change_cipher_spec = ctx->send_change_cipher_spec;
-    tls->skip_tracing = ptls_default_skip_tracing;
-    return tls;
-}
-
-ptls_t *ptls_client_new(ptls_context_t *ctx)
-{
-    ptls_t *tls = new_instance(ctx, 0);
-    tls->state = PTLS_STATE_CLIENT_HANDSHAKE_START;
-    tls->ctx->random_bytes(tls->client_random, sizeof(tls->client_random));
-    log_client_random(tls);
-    if (tls->send_change_cipher_spec) {
-        tls->client.legacy_session_id =
-            ptls_iovec_init(tls->client.legacy_session_id_buf, sizeof(tls->client.legacy_session_id_buf));
-        tls->ctx->random_bytes(tls->client.legacy_session_id.base, tls->client.legacy_session_id.len);
-    }
-
-    PTLS_PROBE(NEW, tls, 0);
-    return tls;
-}
-
-ptls_t *ptls_server_new(ptls_context_t *ctx)
-{
-    ptls_t *tls = new_instance(ctx, 1);
-    tls->state = PTLS_STATE_SERVER_EXPECT_CLIENT_HELLO;
-    tls->server.early_data_skipped_bytes = UINT32_MAX;
-
-    PTLS_PROBE(NEW, tls, 1);
-    return tls;
-}
-
-void ptls_free(ptls_t *tls)
-{
-    PTLS_PROBE0(FREE, tls);
-    ptls_buffer_dispose(&tls->recvbuf.rec);
-    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)
-        ptls_aead_free(tls->traffic_protection.dec.aead);
-    if (tls->traffic_protection.enc.aead != NULL)
-        ptls_aead_free(tls->traffic_protection.enc.aead);
-    free(tls->server_name);
-    free(tls->negotiated_protocol);
-    if (tls->is_server) {
-        /* nothing to do */
-    } else {
-        if (tls->client.key_share_ctx != NULL)
-            tls->client.key_share_ctx->on_exchange(&tls->client.key_share_ctx, 1, NULL, ptls_iovec_init(NULL, 0));
-        if (tls->client.certificate_request.context.base != NULL)
-            free(tls->client.certificate_request.context.base);
-    }
-    if (tls->certificate_verify.cb != NULL) {
-        tls->certificate_verify.cb(tls->certificate_verify.verify_ctx, 0, ptls_iovec_init(NULL, 0), ptls_iovec_init(NULL, 0));
-    }
-    if (tls->pending_handshake_secret != NULL) {
-        ptls_clear_memory(tls->pending_handshake_secret, PTLS_MAX_DIGEST_SIZE);
-        free(tls->pending_handshake_secret);
-    }
-    update_open_count(tls->ctx, -1);
-    ptls_clear_memory(tls, sizeof(*tls));
-    free(tls);
-}
-
-ptls_context_t *ptls_get_context(ptls_t *tls)
-{
-    return tls->ctx;
-}
-
-void ptls_set_context(ptls_t *tls, ptls_context_t *ctx)
-{
-    update_open_count(ctx, 1);
-    update_open_count(tls->ctx, -1);
-    tls->ctx = ctx;
-}
-
-ptls_iovec_t ptls_get_client_random(ptls_t *tls)
-{
-    return ptls_iovec_init(tls->client_random, PTLS_HELLO_RANDOM_SIZE);
-}
-
-ptls_cipher_suite_t *ptls_get_cipher(ptls_t *tls)
-{
-    return tls->cipher_suite;
-}
-
-const char *ptls_get_server_name(ptls_t *tls)
-{
-    return tls->server_name;
-}
-
-int ptls_set_server_name(ptls_t *tls, const char *server_name, size_t server_name_len)
-{
-    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';
-    }
-
-    free(tls->server_name);
-    tls->server_name = duped;
-
-    return 0;
-}
-
-const char *ptls_get_negotiated_protocol(ptls_t *tls)
-{
-    return tls->negotiated_protocol;
-}
-
-int ptls_set_negotiated_protocol(ptls_t *tls, const char *protocol, size_t protocol_len)
-{
-    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';
-    }
-
-    free(tls->negotiated_protocol);
-    tls->negotiated_protocol = duped;
-
-    return 0;
-}
-
-int ptls_handshake_is_complete(ptls_t *tls)
-{
-    return tls->state >= PTLS_STATE_POST_HANDSHAKE_MIN;
-}
-
-int ptls_is_psk_handshake(ptls_t *tls)
-{
-    return tls->is_psk_handshake;
-}
-
-void **ptls_get_data_ptr(ptls_t *tls)
-{
-    return &tls->data_ptr;
-}
-
-int ptls_skip_tracing(ptls_t *tls)
-{
-    return tls->skip_tracing;
-}
-
-void ptls_set_skip_tracing(ptls_t *tls, int skip_tracing)
-{
-    tls->skip_tracing = skip_tracing;
-}
-
-static int handle_client_handshake_message(ptls_t *tls, ptls_message_emitter_t *emitter, ptls_iovec_t message, int is_end_of_record,
-                                           ptls_handshake_properties_t *properties)
-{
-    uint8_t type = message.base[0];
-    int ret;
-
-    switch (tls->state) {
-    case PTLS_STATE_CLIENT_EXPECT_SERVER_HELLO:
-    case PTLS_STATE_CLIENT_EXPECT_SECOND_SERVER_HELLO:
-        if (type == PTLS_HANDSHAKE_TYPE_SERVER_HELLO && is_end_of_record) {
-            ret = client_handle_hello(tls, emitter, message, properties);
-        } else {
-            ret = PTLS_ALERT_UNEXPECTED_MESSAGE;
-        }
-        break;
-    case PTLS_STATE_CLIENT_EXPECT_ENCRYPTED_EXTENSIONS:
-        if (type == PTLS_HANDSHAKE_TYPE_ENCRYPTED_EXTENSIONS) {
-            ret = client_handle_encrypted_extensions(tls, message, properties);
-        } else {
-            ret = PTLS_ALERT_UNEXPECTED_MESSAGE;
-        }
-        break;
-    case PTLS_STATE_CLIENT_EXPECT_CERTIFICATE_REQUEST_OR_CERTIFICATE:
-        if (type == PTLS_HANDSHAKE_TYPE_CERTIFICATE_REQUEST) {
-            ret = client_handle_certificate_request(tls, message, properties);
-            break;
-        }
-    /* fall through */
-    case PTLS_STATE_CLIENT_EXPECT_CERTIFICATE:
-        switch (type) {
-        case PTLS_HANDSHAKE_TYPE_CERTIFICATE:
-            ret = client_handle_certificate(tls, message);
-            break;
-        case PTLS_HANDSHAKE_TYPE_COMPRESSED_CERTIFICATE:
-            ret = client_handle_compressed_certificate(tls, message);
-            break;
-        default:
-            ret = PTLS_ALERT_UNEXPECTED_MESSAGE;
-            break;
-        }
-        break;
-    case PTLS_STATE_CLIENT_EXPECT_CERTIFICATE_VERIFY:
-        if (type == PTLS_HANDSHAKE_TYPE_CERTIFICATE_VERIFY) {
-            ret = client_handle_certificate_verify(tls, message);
-        } else {
-            ret = PTLS_ALERT_UNEXPECTED_MESSAGE;
-        }
-        break;
-    case PTLS_STATE_CLIENT_EXPECT_FINISHED:
-        if (type == PTLS_HANDSHAKE_TYPE_FINISHED && is_end_of_record) {
-            ret = client_handle_finished(tls, emitter, message);
-        } else {
-            ret = PTLS_ALERT_UNEXPECTED_MESSAGE;
-        }
-        break;
-    case PTLS_STATE_CLIENT_POST_HANDSHAKE:
-        switch (type) {
-        case PTLS_HANDSHAKE_TYPE_NEW_SESSION_TICKET:
-            ret = client_handle_new_session_ticket(tls, message);
-            break;
-        case PTLS_HANDSHAKE_TYPE_KEY_UPDATE:
-            ret = handle_key_update(tls, emitter, message);
-            break;
-        default:
-            ret = PTLS_ALERT_UNEXPECTED_MESSAGE;
-            break;
-        }
-        break;
-    default:
-        assert(!"unexpected state");
-        ret = PTLS_ALERT_INTERNAL_ERROR;
-        break;
-    }
-
-    PTLS_PROBE(RECEIVE_MESSAGE, tls, message.base[0], message.base + PTLS_HANDSHAKE_HEADER_SIZE,
-               message.len - PTLS_HANDSHAKE_HEADER_SIZE, ret);
-
-    return ret;
-}
-
-static int handle_server_handshake_message(ptls_t *tls, ptls_message_emitter_t *emitter, ptls_iovec_t message, int is_end_of_record,
-                                           ptls_handshake_properties_t *properties)
-{
-    uint8_t type = message.base[0];
-    int ret;
-
-    switch (tls->state) {
-    case PTLS_STATE_SERVER_EXPECT_CLIENT_HELLO:
-    case PTLS_STATE_SERVER_EXPECT_SECOND_CLIENT_HELLO:
-        if (type == PTLS_HANDSHAKE_TYPE_CLIENT_HELLO && is_end_of_record) {
-            ret = server_handle_hello(tls, emitter, message, properties);
-        } else {
-            ret = PTLS_ALERT_HANDSHAKE_FAILURE;
-        }
-        break;
-    case PTLS_STATE_SERVER_EXPECT_CERTIFICATE:
-        if (type == PTLS_HANDSHAKE_TYPE_CERTIFICATE) {
-            ret = server_handle_certificate(tls, message);
-        } else {
-            ret = PTLS_ALERT_UNEXPECTED_MESSAGE;
-        }
-        break;
-    case PTLS_STATE_SERVER_EXPECT_CERTIFICATE_VERIFY:
-        if (type == PTLS_HANDSHAKE_TYPE_CERTIFICATE_VERIFY) {
-            ret = server_handle_certificate_verify(tls, message);
-        } else {
-            ret = PTLS_ALERT_UNEXPECTED_MESSAGE;
-        }
-        break;
-    case PTLS_STATE_SERVER_EXPECT_END_OF_EARLY_DATA:
-        assert(!tls->ctx->omit_end_of_early_data);
-        if (type == PTLS_HANDSHAKE_TYPE_END_OF_EARLY_DATA) {
-            ret = server_handle_end_of_early_data(tls, message);
-        } else {
-            ret = PTLS_ALERT_UNEXPECTED_MESSAGE;
-        }
-        break;
-    case PTLS_STATE_SERVER_EXPECT_FINISHED:
-        if (type == PTLS_HANDSHAKE_TYPE_FINISHED && is_end_of_record) {
-            ret = server_handle_finished(tls, message);
-        } else {
-            ret = PTLS_ALERT_HANDSHAKE_FAILURE;
-        }
-        break;
-    case PTLS_STATE_SERVER_POST_HANDSHAKE:
-        switch (type) {
-        case PTLS_HANDSHAKE_TYPE_KEY_UPDATE:
-            ret = handle_key_update(tls, emitter, message);
-            break;
-        default:
-            ret = PTLS_ALERT_UNEXPECTED_MESSAGE;
-            break;
-        }
-        break;
-    default:
-        assert(!"unexpected state");
-        ret = PTLS_ALERT_INTERNAL_ERROR;
-        break;
-    }
-
-    PTLS_PROBE(RECEIVE_MESSAGE, tls, message.base[0], message.base + PTLS_HANDSHAKE_HEADER_SIZE,
-               message.len - PTLS_HANDSHAKE_HEADER_SIZE, ret);
-
-    return ret;
-}
-
-static int handle_alert(ptls_t *tls, const uint8_t *src, size_t len)
-{
-    if (len != 2)
-        return PTLS_ALERT_DECODE_ERROR;
-
-    uint8_t desc = src[1];
-
-    /* all fatal alerts and USER_CANCELLED warning tears down the connection immediately, regardless of the transmitted level */
-    return PTLS_ALERT_TO_PEER_ERROR(desc);
-}
-
-static int message_buffer_is_overflow(ptls_context_t *ctx, size_t size)
-{
-    if (ctx->max_buffer_size == 0)
-        return 0;
-    if (size <= ctx->max_buffer_size)
-        return 0;
-    return 1;
-}
-
-static int handle_handshake_record(ptls_t *tls,
-                                   int (*cb)(ptls_t *tls, ptls_message_emitter_t *emitter, ptls_iovec_t message,
-                                             int is_end_of_record, ptls_handshake_properties_t *properties),
-                                   ptls_message_emitter_t *emitter, struct st_ptls_record_t *rec,
-                                   ptls_handshake_properties_t *properties)
-{
-    int ret;
-
-    /* handshake */
-    if (rec->type != PTLS_CONTENT_TYPE_HANDSHAKE)
-        return PTLS_ALERT_DECODE_ERROR;
-
-    /* flatten the unhandled messages */
-    const uint8_t *src, *src_end;
-    if (tls->recvbuf.mess.base == NULL) {
-        src = rec->fragment;
-        src_end = src + rec->length;
-    } else {
-        if (message_buffer_is_overflow(tls->ctx, tls->recvbuf.mess.off + rec->length))
-            return PTLS_ALERT_HANDSHAKE_FAILURE;
-        if ((ret = ptls_buffer_reserve(&tls->recvbuf.mess, rec->length)) != 0)
-            return ret;
-        memcpy(tls->recvbuf.mess.base + tls->recvbuf.mess.off, rec->fragment, rec->length);
-        tls->recvbuf.mess.off += rec->length;
-        src = tls->recvbuf.mess.base;
-        src_end = src + tls->recvbuf.mess.off;
-    }
-
-    /* handle the messages */
-    ret = PTLS_ERROR_IN_PROGRESS;
-    while (src_end - src >= 4) {
-        size_t mess_len = 4 + ntoh24(src + 1);
-        if (src_end - src < (int)mess_len)
-            break;
-        ret = cb(tls, emitter, ptls_iovec_init(src, mess_len), src_end - src == mess_len, properties);
-        switch (ret) {
-        case 0:
-        case PTLS_ERROR_IN_PROGRESS:
-            break;
-        default:
-            ptls_buffer_dispose(&tls->recvbuf.mess);
-            return ret;
-        }
-        src += mess_len;
-    }
-
-    /* keep last partial message in buffer */
-    if (src != src_end) {
-        size_t new_size = src_end - src;
-        if (message_buffer_is_overflow(tls->ctx, new_size))
-            return PTLS_ALERT_HANDSHAKE_FAILURE;
-        if (tls->recvbuf.mess.base == NULL) {
-            ptls_buffer_init(&tls->recvbuf.mess, "", 0);
-            if ((ret = ptls_buffer_reserve(&tls->recvbuf.mess, new_size)) != 0)
-                return ret;
-            memcpy(tls->recvbuf.mess.base, src, new_size);
-        } else {
-            memmove(tls->recvbuf.mess.base, src, new_size);
-        }
-        tls->recvbuf.mess.off = new_size;
-        ret = PTLS_ERROR_IN_PROGRESS;
-    } else {
-        ptls_buffer_dispose(&tls->recvbuf.mess);
-    }
-
-    return ret;
-}
-
-static int handle_input(ptls_t *tls, ptls_message_emitter_t *emitter, ptls_buffer_t *decryptbuf, const void *input, size_t *inlen,
-                        ptls_handshake_properties_t *properties)
-{
-    struct st_ptls_record_t rec;
-    int ret;
-
-    /* extract the record */
-    if ((ret = parse_record(tls, &rec, input, inlen)) != 0)
-        return ret;
-    assert(rec.fragment != NULL);
-
-    /* decrypt the record */
-    if (rec.type == PTLS_CONTENT_TYPE_CHANGE_CIPHER_SPEC) {
-        if (tls->state < PTLS_STATE_POST_HANDSHAKE_MIN) {
-            if (!(rec.length == 1 && rec.fragment[0] == 0x01))
-                return PTLS_ALERT_ILLEGAL_PARAMETER;
-        } else {
-            return PTLS_ALERT_HANDSHAKE_FAILURE;
-        }
-        ret = PTLS_ERROR_IN_PROGRESS;
-        goto NextRecord;
-    }
-    if (tls->traffic_protection.dec.aead != NULL && rec.type != PTLS_CONTENT_TYPE_ALERT) {
-        size_t decrypted_length;
-        if (rec.type != PTLS_CONTENT_TYPE_APPDATA)
-            return PTLS_ALERT_HANDSHAKE_FAILURE;
-        if ((ret = ptls_buffer_reserve(decryptbuf, 5 + rec.length)) != 0)
-            return ret;
-        if ((ret = aead_decrypt(&tls->traffic_protection.dec, decryptbuf->base + decryptbuf->off, &decrypted_length, rec.fragment,
-                                rec.length)) != 0) {
-            if (tls->is_server && tls->server.early_data_skipped_bytes != UINT32_MAX)
-                goto ServerSkipEarlyData;
-            return ret;
-        }
-        rec.length = decrypted_length;
-        rec.fragment = decryptbuf->base + decryptbuf->off;
-        /* skip padding */
-        for (; rec.length != 0; --rec.length)
-            if (rec.fragment[rec.length - 1] != 0)
-                break;
-        if (rec.length == 0)
-            return PTLS_ALERT_UNEXPECTED_MESSAGE;
-        rec.type = rec.fragment[--rec.length];
-    } else if (rec.type == PTLS_CONTENT_TYPE_APPDATA && tls->is_server && tls->server.early_data_skipped_bytes != UINT32_MAX) {
-        goto ServerSkipEarlyData;
-    }
-
-    if (tls->recvbuf.mess.base != NULL || rec.type == PTLS_CONTENT_TYPE_HANDSHAKE) {
-        /* handshake record */
-        ret = handle_handshake_record(tls, tls->is_server ? handle_server_handshake_message : handle_client_handshake_message,
-                                      emitter, &rec, properties);
-    } else {
-        /* handling of an alert or an application record */
-        switch (rec.type) {
-        case PTLS_CONTENT_TYPE_APPDATA:
-            if (tls->state >= PTLS_STATE_POST_HANDSHAKE_MIN) {
-                decryptbuf->off += rec.length;
-                ret = 0;
-            } else if (tls->state == PTLS_STATE_SERVER_EXPECT_END_OF_EARLY_DATA) {
-                if (tls->traffic_protection.dec.aead != NULL)
-                    decryptbuf->off += rec.length;
-                ret = 0;
-            } else {
-                ret = PTLS_ALERT_UNEXPECTED_MESSAGE;
-            }
-            break;
-        case PTLS_CONTENT_TYPE_ALERT:
-            ret = handle_alert(tls, rec.fragment, rec.length);
-            break;
-        default:
-            ret = PTLS_ALERT_UNEXPECTED_MESSAGE;
-            break;
-        }
-    }
-
-NextRecord:
-    ptls_buffer_dispose(&tls->recvbuf.rec);
-    return ret;
-
-ServerSkipEarlyData:
-    tls->server.early_data_skipped_bytes += (uint32_t)rec.length;
-    if (tls->server.early_data_skipped_bytes > PTLS_MAX_EARLY_DATA_SKIP_SIZE)
-        return PTLS_ALERT_HANDSHAKE_FAILURE;
-    ret = PTLS_ERROR_IN_PROGRESS;
-    goto NextRecord;
-}
-
-static void init_record_message_emitter(ptls_t *tls, struct st_ptls_record_message_emitter_t *emitter, ptls_buffer_t *sendbuf)
-{
-    *emitter = (struct st_ptls_record_message_emitter_t){
-        {sendbuf, &tls->traffic_protection.enc, 5, begin_record_message, commit_record_message}};
-}
-
-int ptls_handshake(ptls_t *tls, ptls_buffer_t *_sendbuf, const void *input, size_t *inlen, ptls_handshake_properties_t *properties)
-{
-    struct st_ptls_record_message_emitter_t emitter;
-    int ret;
-
-    assert(tls->state < PTLS_STATE_POST_HANDSHAKE_MIN);
-
-    init_record_message_emitter(tls, &emitter, _sendbuf);
-    size_t sendbuf_orig_off = emitter.super.buf->off;
-
-    /* special handlings */
-    switch (tls->state) {
-    case PTLS_STATE_CLIENT_HANDSHAKE_START: {
-        assert(input == NULL || *inlen == 0);
-        assert(tls->ctx->key_exchanges[0] != NULL);
-        return send_client_hello(tls, &emitter.super, properties, NULL);
-    }
-    default:
-        break;
-    }
-
-    const uint8_t *src = input, *const src_end = src + *inlen;
-    ptls_buffer_t decryptbuf;
-
-    ptls_buffer_init(&decryptbuf, "", 0);
-
-    /* perform handhake until completion or until all the input has been swallowed */
-    ret = PTLS_ERROR_IN_PROGRESS;
-    while (ret == PTLS_ERROR_IN_PROGRESS && src != src_end) {
-        size_t consumed = src_end - src;
-        ret = handle_input(tls, &emitter.super, &decryptbuf, src, &consumed, properties);
-        src += consumed;
-        assert(decryptbuf.off == 0);
-    }
-
-    ptls_buffer_dispose(&decryptbuf);
-
-    switch (ret) {
-    case 0:
-    case PTLS_ERROR_IN_PROGRESS:
-    case PTLS_ERROR_STATELESS_RETRY:
-        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;
-        /* 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,
-                                PTLS_ERROR_GET_CLASS(ret) == PTLS_ERROR_CLASS_SELF_ALERT ? ret : PTLS_ALERT_INTERNAL_ERROR) != 0)
-                emitter.super.buf->off = sendbuf_orig_off;
-        break;
-    }
-
-    *inlen -= src_end - src;
-    return ret;
-}
-
-int ptls_receive(ptls_t *tls, ptls_buffer_t *decryptbuf, const void *_input, size_t *inlen)
-{
-    const uint8_t *input = (const uint8_t *)_input, *const end = input + *inlen;
-    size_t decryptbuf_orig_size = decryptbuf->off;
-    int ret = 0;
-
-    assert(tls->state >= PTLS_STATE_SERVER_EXPECT_END_OF_EARLY_DATA);
-
-    /* loop until we decrypt some application data (or an error) */
-    while (ret == 0 && input != end && decryptbuf_orig_size == decryptbuf->off) {
-        size_t consumed = end - input;
-        ret = handle_input(tls, NULL, decryptbuf, input, &consumed, NULL);
-        input += consumed;
-
-        switch (ret) {
-        case 0:
-            break;
-        case PTLS_ERROR_IN_PROGRESS:
-            ret = 0;
-            break;
-        case PTLS_ERROR_CLASS_PEER_ALERT + PTLS_ALERT_CLOSE_NOTIFY:
-            /* TODO send close alert */
-            break;
-        default:
-            if (PTLS_ERROR_GET_CLASS(ret) == PTLS_ERROR_CLASS_SELF_ALERT) {
-                /* TODO send alert */
-            }
-            break;
-        }
-    }
-
-    *inlen -= end - input;
-
-    return ret;
-}
-
-static int update_send_key(ptls_t *tls, ptls_buffer_t *_sendbuf, int request_update)
-{
-    struct st_ptls_record_message_emitter_t emitter;
-    int ret;
-
-    init_record_message_emitter(tls, &emitter, _sendbuf);
-    size_t sendbuf_orig_off = emitter.super.buf->off;
-
-    ptls_push_message(&emitter.super, NULL, PTLS_HANDSHAKE_TYPE_KEY_UPDATE,
-                      { ptls_buffer_push(emitter.super.buf, !!request_update); });
-    if ((ret = update_traffic_key(tls, 1)) != 0)
-        goto Exit;
-    ret = 0;
-
-Exit:
-    if (ret != 0)
-        emitter.super.buf->off = sendbuf_orig_off;
-    return ret;
-}
-
-int ptls_send(ptls_t *tls, ptls_buffer_t *sendbuf, const void *input, size_t inlen)
-{
-    assert(tls->traffic_protection.enc.aead != NULL);
-
-    /* "For AES-GCM, up to 2^24.5 full-size records (about 24 million) may be encrypted on a given connection while keeping a
-     * safety margin of approximately 2^-57 for Authenticated Encryption (AE) security." (RFC 8446 section 5.5)
-     */
-    if (tls->traffic_protection.enc.seq >= 16777216)
-        tls->needs_key_update = 1;
-
-    if (tls->needs_key_update) {
-        int ret;
-        if ((ret = update_send_key(tls, sendbuf, tls->key_update_send_request)) != 0)
-            return ret;
-        tls->needs_key_update = 0;
-        tls->key_update_send_request = 0;
-    }
-
-    return buffer_push_encrypted_records(sendbuf, PTLS_CONTENT_TYPE_APPDATA, input, inlen, &tls->traffic_protection.enc);
-}
-
-int ptls_update_key(ptls_t *tls, int request_update)
-{
-    assert(tls->ctx->update_traffic_key == NULL);
-    tls->needs_key_update = 1;
-    tls->key_update_send_request = request_update;
-    return 0;
-}
-
-size_t ptls_get_record_overhead(ptls_t *tls)
-{
-    return 6 + tls->traffic_protection.enc.aead->algo->tag_size;
-}
-
-int ptls_send_alert(ptls_t *tls, ptls_buffer_t *sendbuf, uint8_t level, uint8_t description)
-{
-    size_t rec_start = sendbuf->off;
-    int ret = 0;
-
-    buffer_push_record(sendbuf, PTLS_CONTENT_TYPE_ALERT, { ptls_buffer_push(sendbuf, level, description); });
-    /* encrypt the alert if we have the encryption keys, unless when it is the early data key */
-    if (tls->traffic_protection.enc.aead != NULL && !(tls->state <= PTLS_STATE_CLIENT_EXPECT_FINISHED)) {
-        if ((ret = buffer_encrypt_record(sendbuf, rec_start, &tls->traffic_protection.enc)) != 0)
-            goto Exit;
-    }
-
-Exit:
-    return ret;
-}
-
-int ptls_export_secret(ptls_t *tls, void *output, size_t outlen, const char *label, ptls_iovec_t context_value, int is_early)
-{
-    ptls_hash_algorithm_t *algo = tls->key_schedule->hashes[0].algo;
-    uint8_t *master_secret = is_early ? tls->exporter_master_secret.early : tls->exporter_master_secret.one_rtt,
-            derived_secret[PTLS_MAX_DIGEST_SIZE], context_value_hash[PTLS_MAX_DIGEST_SIZE];
-    int ret;
-
-    if (master_secret == NULL) {
-        if (is_early) {
-            switch (tls->state) {
-            case PTLS_STATE_CLIENT_HANDSHAKE_START:
-            case PTLS_STATE_SERVER_EXPECT_CLIENT_HELLO:
-                ret = PTLS_ERROR_IN_PROGRESS;
-                break;
-            default:
-                ret = PTLS_ERROR_NOT_AVAILABLE;
-                break;
-            }
-        } else {
-            ret = PTLS_ERROR_IN_PROGRESS;
-        }
-        return ret;
-    }
-
-    if ((ret = ptls_calc_hash(algo, context_value_hash, context_value.base, context_value.len)) != 0)
-        return ret;
-
-    if ((ret = hkdf_expand_label(algo, derived_secret, algo->digest_size, ptls_iovec_init(master_secret, algo->digest_size), label,
-                                 ptls_iovec_init(algo->empty_digest, algo->digest_size), tls->key_schedule->hkdf_label_prefix)) !=
-        0)
-        goto Exit;
-    ret = hkdf_expand_label(algo, output, outlen, ptls_iovec_init(derived_secret, algo->digest_size), "exporter",
-                            ptls_iovec_init(context_value_hash, algo->digest_size), tls->key_schedule->hkdf_label_prefix);
-
-Exit:
-    ptls_clear_memory(derived_secret, sizeof(derived_secret));
-    ptls_clear_memory(context_value_hash, sizeof(context_value_hash));
-    return ret;
-}
-
-struct st_picotls_hmac_context_t {
-    ptls_hash_context_t super;
-    ptls_hash_algorithm_t *algo;
-    ptls_hash_context_t *hash;
-    uint8_t key[1];
-};
-
-static void hmac_update(ptls_hash_context_t *_ctx, const void *src, size_t len)
-{
-    struct st_picotls_hmac_context_t *ctx = (struct st_picotls_hmac_context_t *)_ctx;
-    ctx->hash->update(ctx->hash, src, len);
-}
-
-static void hmac_apply_key(struct st_picotls_hmac_context_t *ctx, uint8_t pad)
-{
-    size_t i;
-
-    for (i = 0; i != ctx->algo->block_size; ++i)
-        ctx->key[i] ^= pad;
-    ctx->hash->update(ctx->hash, ctx->key, ctx->algo->block_size);
-    for (i = 0; i != ctx->algo->block_size; ++i)
-        ctx->key[i] ^= pad;
-}
-
-static void hmac_final(ptls_hash_context_t *_ctx, void *md, ptls_hash_final_mode_t mode)
-{
-    struct st_picotls_hmac_context_t *ctx = (struct st_picotls_hmac_context_t *)_ctx;
-
-    assert(mode != PTLS_HASH_FINAL_MODE_SNAPSHOT || !"not supported");
-
-    if (md != NULL) {
-        ctx->hash->final(ctx->hash, md, PTLS_HASH_FINAL_MODE_RESET);
-        hmac_apply_key(ctx, 0x5c);
-        ctx->hash->update(ctx->hash, md, ctx->algo->digest_size);
-    }
-    ctx->hash->final(ctx->hash, md, mode);
-
-    switch (mode) {
-    case PTLS_HASH_FINAL_MODE_FREE:
-        ptls_clear_memory(ctx->key, ctx->algo->block_size);
-        free(ctx);
-        break;
-    case PTLS_HASH_FINAL_MODE_RESET:
-        hmac_apply_key(ctx, 0x36);
-        break;
-    default:
-        assert(!"FIXME");
-        break;
-    }
-}
-
-int ptls_calc_hash(ptls_hash_algorithm_t *algo, void *output, const void *src, size_t len)
-{
-    ptls_hash_context_t *ctx;
-
-    if ((ctx = algo->create()) == NULL)
-        return PTLS_ERROR_NO_MEMORY;
-    ctx->update(ctx, src, len);
-    ctx->final(ctx, output, PTLS_HASH_FINAL_MODE_FREE);
-    return 0;
-}
-
-ptls_hash_context_t *ptls_hmac_create(ptls_hash_algorithm_t *algo, const void *key, size_t key_size)
-{
-    struct st_picotls_hmac_context_t *ctx;
-
-    assert(key_size <= algo->block_size);
-
-    if ((ctx = malloc(offsetof(struct st_picotls_hmac_context_t, key) + algo->block_size)) == NULL)
-        return NULL;
-
-    *ctx = (struct st_picotls_hmac_context_t){{hmac_update, hmac_final}, algo};
-    if ((ctx->hash = algo->create()) == NULL) {
-        free(ctx);
-        return NULL;
-    }
-    memset(ctx->key, 0, algo->block_size);
-    memcpy(ctx->key, key, key_size);
-
-    hmac_apply_key(ctx, 0x36);
-
-    return &ctx->super;
-}
-
-int ptls_hkdf_extract(ptls_hash_algorithm_t *algo, void *output, ptls_iovec_t salt, ptls_iovec_t ikm)
-{
-    ptls_hash_context_t *hash;
-
-    if (salt.len == 0)
-        salt = ptls_iovec_init(zeroes_of_max_digest_size, algo->digest_size);
-
-    if ((hash = ptls_hmac_create(algo, salt.base, salt.len)) == NULL)
-        return PTLS_ERROR_NO_MEMORY;
-    hash->update(hash, ikm.base, ikm.len);
-    hash->final(hash, output, PTLS_HASH_FINAL_MODE_FREE);
-    return 0;
-}
-
-int ptls_hkdf_expand(ptls_hash_algorithm_t *algo, void *output, size_t outlen, ptls_iovec_t prk, ptls_iovec_t info)
-{
-    ptls_hash_context_t *hmac = NULL;
-    size_t i;
-    uint8_t digest[PTLS_MAX_DIGEST_SIZE];
-
-    for (i = 0; (i * algo->digest_size) < outlen; ++i) {
-        if (hmac == NULL) {
-            if ((hmac = ptls_hmac_create(algo, prk.base, prk.len)) == NULL)
-                return PTLS_ERROR_NO_MEMORY;
-        } else {
-            hmac->update(hmac, digest, algo->digest_size);
-        }
-        hmac->update(hmac, info.base, info.len);
-        uint8_t gen = (uint8_t)(i + 1);
-        hmac->update(hmac, &gen, 1);
-        hmac->final(hmac, digest, 1);
-
-        size_t off_start = i * algo->digest_size, off_end = off_start + algo->digest_size;
-        if (off_end > outlen)
-            off_end = outlen;
-        memcpy((uint8_t *)output + off_start, digest, off_end - off_start);
-    }
-
-    if (hmac != NULL)
-        hmac->final(hmac, NULL, PTLS_HASH_FINAL_MODE_FREE);
-
-    ptls_clear_memory(digest, algo->digest_size);
-
-    return 0;
-}
-
-int hkdf_expand_label(ptls_hash_algorithm_t *algo, void *output, size_t outlen, ptls_iovec_t secret, const char *label,
-                      ptls_iovec_t hash_value, const char *label_prefix)
-{
-    ptls_buffer_t hkdf_label;
-    uint8_t hkdf_label_buf[80];
-    int ret;
-
-    assert(label_prefix != NULL);
-
-    ptls_buffer_init(&hkdf_label, hkdf_label_buf, sizeof(hkdf_label_buf));
-
-    ptls_buffer_push16(&hkdf_label, (uint16_t)outlen);
-    ptls_buffer_push_block(&hkdf_label, 1, {
-        ptls_buffer_pushv(&hkdf_label, label_prefix, strlen(label_prefix));
-        ptls_buffer_pushv(&hkdf_label, label, strlen(label));
-    });
-    ptls_buffer_push_block(&hkdf_label, 1, { ptls_buffer_pushv(&hkdf_label, hash_value.base, hash_value.len); });
-
-    ret = ptls_hkdf_expand(algo, output, outlen, secret, ptls_iovec_init(hkdf_label.base, hkdf_label.off));
-
-Exit:
-    ptls_buffer_dispose(&hkdf_label);
-    return ret;
-}
-
-int ptls_hkdf_expand_label(ptls_hash_algorithm_t *algo, void *output, size_t outlen, ptls_iovec_t secret, const char *label,
-                           ptls_iovec_t hash_value, const char *label_prefix)
-{
-    /* the handshake layer should call hkdf_expand_label directly, always setting key_schedule->hkdf_label_prefix as the
-     * argument */
-    if (label_prefix == NULL)
-        label_prefix = PTLS_HKDF_EXPAND_LABEL_PREFIX;
-    return hkdf_expand_label(algo, output, outlen, secret, label, hash_value, label_prefix);
-}
-
-ptls_cipher_context_t *ptls_cipher_new(ptls_cipher_algorithm_t *algo, int is_enc, const void *key)
-{
-    ptls_cipher_context_t *ctx;
-
-    if ((ctx = (ptls_cipher_context_t *)malloc(algo->context_size)) == NULL)
-        return NULL;
-    *ctx = (ptls_cipher_context_t){algo};
-    if (algo->setup_crypto(ctx, is_enc, key) != 0) {
-        free(ctx);
-        ctx = NULL;
-    }
-    return ctx;
-}
-
-void ptls_cipher_free(ptls_cipher_context_t *ctx)
-{
-    ctx->do_dispose(ctx);
-    free(ctx);
-}
-
-ptls_aead_context_t *new_aead(ptls_aead_algorithm_t *aead, ptls_hash_algorithm_t *hash, int is_enc, const void *secret,
-                              ptls_iovec_t hash_value, const char *label_prefix)
-{
-    ptls_aead_context_t *ctx = NULL;
-    uint8_t key_iv[PTLS_MAX_SECRET_SIZE + PTLS_MAX_IV_SIZE];
-    int ret;
-
-    if ((ret = get_traffic_key(hash, key_iv, aead->key_size, 0, secret, hash_value, label_prefix)) != 0)
-        goto Exit;
-    if ((ret = get_traffic_key(hash, key_iv + aead->key_size, aead->iv_size, 1, secret, hash_value, label_prefix)) != 0)
-        goto Exit;
-    ctx = ptls_aead_new_direct(aead, is_enc, key_iv, key_iv + aead->key_size);
-
-Exit:
-    ptls_clear_memory(key_iv, sizeof(key_iv));
-    return ctx;
-}
-
-ptls_aead_context_t *ptls_aead_new(ptls_aead_algorithm_t *aead, ptls_hash_algorithm_t *hash, int is_enc, const void *secret,
-                                   const char *label_prefix)
-{
-    return new_aead(aead, hash, is_enc, secret, ptls_iovec_init(NULL, 0), label_prefix);
-}
-
-ptls_aead_context_t *ptls_aead_new_direct(ptls_aead_algorithm_t *aead, int is_enc, const void *key, const void *iv)
-{
-    ptls_aead_context_t *ctx;
-
-    if ((ctx = (ptls_aead_context_t *)malloc(aead->context_size)) == NULL)
-        return NULL;
-
-    *ctx = (ptls_aead_context_t){aead};
-
-    if (aead->setup_crypto(ctx, is_enc, key, iv) != 0) {
-        free(ctx);
-        return NULL;
-    }
-
-    return ctx;
-}
-
-void ptls_aead_free(ptls_aead_context_t *ctx)
-{
-    ctx->dispose_crypto(ctx);
-    free(ctx);
-}
-
-void ptls_aead__build_iv(ptls_aead_algorithm_t *algo, uint8_t *iv, const uint8_t *static_iv, uint64_t seq)
-{
-    size_t iv_size = algo->iv_size, i;
-    const uint8_t *s = static_iv;
-    uint8_t *d = iv;
-
-    /* build iv */
-    for (i = iv_size - 8; i != 0; --i)
-        *d++ = *s++;
-    i = 64;
-    do {
-        i -= 8;
-        *d++ = *s++ ^ (uint8_t)(seq >> i);
-    } while (i != 0);
-}
-
-static void clear_memory(void *p, size_t len)
-{
-    if (len != 0)
-        memset(p, 0, len);
-}
-
-void (*volatile ptls_clear_memory)(void *p, size_t len) = clear_memory;
-
-static int mem_equal(const void *_x, const void *_y, size_t len)
-{
-    const volatile uint8_t *x = _x, *y = _y;
-    uint8_t t = 0;
-
-    for (; len != 0; --len)
-        t |= *x++ ^ *y++;
-
-    return t == 0;
-}
-
-int (*volatile ptls_mem_equal)(const void *x, const void *y, size_t len) = mem_equal;
-
-static uint64_t get_time(ptls_get_time_t *self)
-{
-#if 0
-    struct timeval tv;
-    gettimeofday(&tv, NULL);
-    return (uint64_t)tv.tv_sec * 1000 + tv.tv_usec / 1000;
-#else
-    return 0;
-#endif
-}
-
-ptls_get_time_t ptls_get_time = {get_time};
-#if PICOTLS_USE_DTRACE
-PTLS_THREADLOCAL unsigned ptls_default_skip_tracing = 0;
-#endif
-
-int ptls_is_server(ptls_t *tls)
-{
-    return tls->is_server;
-}
-
-struct st_ptls_raw_message_emitter_t {
-    ptls_message_emitter_t super;
-    size_t start_off;
-    size_t *epoch_offsets;
-};
-
-static int begin_raw_message(ptls_message_emitter_t *_self)
-{
-    struct st_ptls_raw_message_emitter_t *self = (void *)_self;
-
-    self->start_off = self->super.buf->off;
-    return 0;
-}
-
-static int commit_raw_message(ptls_message_emitter_t *_self)
-{
-    struct st_ptls_raw_message_emitter_t *self = (void *)_self;
-    size_t epoch;
-
-    /* epoch is the key epoch, with the only exception being 2nd CH generated after 0-RTT key */
-    epoch = self->super.enc->epoch;
-    if (epoch == 1 && self->super.buf->base[self->start_off] == PTLS_HANDSHAKE_TYPE_CLIENT_HELLO)
-        epoch = 0;
-
-    for (++epoch; epoch < 5; ++epoch) {
-        assert(self->epoch_offsets[epoch] == self->start_off);
-        self->epoch_offsets[epoch] = self->super.buf->off;
-    }
-
-    self->start_off = SIZE_MAX;
-
-    return 0;
-}
-
-size_t ptls_get_read_epoch(ptls_t *tls)
-{
-    switch (tls->state) {
-    case PTLS_STATE_CLIENT_HANDSHAKE_START:
-    case PTLS_STATE_CLIENT_EXPECT_SERVER_HELLO:
-    case PTLS_STATE_CLIENT_EXPECT_SECOND_SERVER_HELLO:
-    case PTLS_STATE_SERVER_EXPECT_CLIENT_HELLO:
-    case PTLS_STATE_SERVER_EXPECT_SECOND_CLIENT_HELLO:
-        return 0; /* plaintext */
-    case PTLS_STATE_SERVER_EXPECT_END_OF_EARLY_DATA:
-        assert(!tls->ctx->omit_end_of_early_data);
-        return 1; /* 0-rtt */
-    case PTLS_STATE_CLIENT_EXPECT_ENCRYPTED_EXTENSIONS:
-    case PTLS_STATE_CLIENT_EXPECT_CERTIFICATE_REQUEST_OR_CERTIFICATE:
-    case PTLS_STATE_CLIENT_EXPECT_CERTIFICATE:
-    case PTLS_STATE_CLIENT_EXPECT_CERTIFICATE_VERIFY:
-    case PTLS_STATE_CLIENT_EXPECT_FINISHED:
-    case PTLS_STATE_SERVER_EXPECT_CERTIFICATE:
-    case PTLS_STATE_SERVER_EXPECT_CERTIFICATE_VERIFY:
-    case PTLS_STATE_SERVER_EXPECT_FINISHED:
-        return 2; /* handshake */
-    case PTLS_STATE_CLIENT_POST_HANDSHAKE:
-    case PTLS_STATE_SERVER_POST_HANDSHAKE:
-        return 3; /* 1-rtt */
-    default:
-        assert(!"invalid state");
-        return SIZE_MAX;
-    }
-}
-
-int ptls_handle_message(ptls_t *tls, ptls_buffer_t *sendbuf, size_t epoch_offsets[5], size_t in_epoch, const void *input,
-                        size_t inlen, ptls_handshake_properties_t *properties)
-{
-    return tls->is_server ? ptls_server_handle_message(tls, sendbuf, epoch_offsets, in_epoch, input, inlen, properties)
-                          : ptls_client_handle_message(tls, sendbuf, epoch_offsets, in_epoch, input, inlen, properties);
-}
-
-int ptls_client_handle_message(ptls_t *tls, ptls_buffer_t *sendbuf, size_t epoch_offsets[5], size_t in_epoch, const void *input,
-                               size_t inlen, ptls_handshake_properties_t *properties)
-{
-    assert(!tls->is_server);
-
-    struct st_ptls_raw_message_emitter_t emitter = {
-        {sendbuf, &tls->traffic_protection.enc, 0, begin_raw_message, commit_raw_message}, SIZE_MAX, epoch_offsets};
-    struct st_ptls_record_t rec = {PTLS_CONTENT_TYPE_HANDSHAKE, 0, inlen, input};
-
-    if (input == NULL)
-        return send_client_hello(tls, &emitter.super, properties, NULL);
-
-    if (ptls_get_read_epoch(tls) != in_epoch)
-        return PTLS_ALERT_UNEXPECTED_MESSAGE;
-
-    return handle_handshake_record(tls, handle_client_handshake_message, &emitter.super, &rec, properties);
-}
-
-int ptls_server_handle_message(ptls_t *tls, ptls_buffer_t *sendbuf, size_t epoch_offsets[5], size_t in_epoch, const void *input,
-                               size_t inlen, ptls_handshake_properties_t *properties)
-{
-    assert(tls->is_server);
-
-    struct st_ptls_raw_message_emitter_t emitter = {
-        {sendbuf, &tls->traffic_protection.enc, 0, begin_raw_message, commit_raw_message}, SIZE_MAX, epoch_offsets};
-    struct st_ptls_record_t rec = {PTLS_CONTENT_TYPE_HANDSHAKE, 0, inlen, input};
-
-    assert(input);
-
-    if (ptls_get_read_epoch(tls) != in_epoch)
-        return PTLS_ALERT_UNEXPECTED_MESSAGE;
-
-    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
- */
-int ptls_server_name_is_ipaddr(const char *name)
-{
-#ifdef AF_INET
-    struct sockaddr_in sin;
-    if (inet_pton(AF_INET, name, &sin) == 1)
-        return 1;
-#endif
-#ifdef AF_INET6
-    struct sockaddr_in6 sin6;
-    if (inet_pton(AF_INET6, name, &sin6) == 1)
-        return 1;
-#endif
-    return 0;
-}
-
-char *ptls_hexdump(char *buf, const void *_src, size_t len)
-{
-    char *dst = buf;
-    const uint8_t *src = _src;
-    size_t i;
-
-    for (i = 0; i != len; ++i) {
-        *dst++ = "0123456789abcdef"[src[i] >> 4];
-        *dst++ = "0123456789abcdef"[src[i] & 0xf];
-    }
-    *dst++ = '\0';
-    return buf;
+#ifdef _WINDOWS
+#include "winsock.h"
+#endif
+#include <assert.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#ifndef _WINDOWS
+#include <errno.h>
+#include <pthread.h>
+#include <unistd.h>
+#include <arpa/inet.h>
+#include <sys/time.h>
+#endif
+#include "picotls.h"
+#if PICOTLS_USE_DTRACE
+#include "picotls-probes.h"
+#endif
+
+#define PTLS_MAX_PLAINTEXT_RECORD_SIZE 16384
+#define PTLS_MAX_ENCRYPTED_RECORD_SIZE (16384 + 256)
+
+#define PTLS_RECORD_VERSION_MAJOR 3
+#define PTLS_RECORD_VERSION_MINOR 3
+
+#define PTLS_CONTENT_TYPE_CHANGE_CIPHER_SPEC 20
+#define PTLS_CONTENT_TYPE_ALERT 21
+#define PTLS_CONTENT_TYPE_HANDSHAKE 22
+#define PTLS_CONTENT_TYPE_APPDATA 23
+
+#define PTLS_PSK_KE_MODE_PSK 0
+#define PTLS_PSK_KE_MODE_PSK_DHE 1
+
+#define PTLS_HANDSHAKE_HEADER_SIZE 4
+
+#define PTLS_EXTENSION_TYPE_SERVER_NAME 0
+#define PTLS_EXTENSION_TYPE_STATUS_REQUEST 5
+#define PTLS_EXTENSION_TYPE_SUPPORTED_GROUPS 10
+#define PTLS_EXTENSION_TYPE_SIGNATURE_ALGORITHMS 13
+#define PTLS_EXTENSION_TYPE_ALPN 16
+#define PTLS_EXTENSION_TYPE_SERVER_CERTIFICATE_TYPE 20
+#define PTLS_EXTENSION_TYPE_COMPRESS_CERTIFICATE 27
+#define PTLS_EXTENSION_TYPE_PRE_SHARED_KEY 41
+#define PTLS_EXTENSION_TYPE_EARLY_DATA 42
+#define PTLS_EXTENSION_TYPE_SUPPORTED_VERSIONS 43
+#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_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                                                                                  \
+    (64 + sizeof(PTLS_SERVER_CERTIFICATE_VERIFY_CONTEXT_STRING) + PTLS_MAX_DIGEST_SIZE * 2)
+
+#define PTLS_EARLY_DATA_MAX_DELAY 10000 /* max. RTT (in msec) to permit early data */
+
+#ifndef PTLS_MAX_EARLY_DATA_SKIP_SIZE
+#define PTLS_MAX_EARLY_DATA_SKIP_SIZE 65536
+#endif
+#if defined(PTLS_DEBUG) && PTLS_DEBUG
+#define PTLS_DEBUGF(...) fprintf(stderr, __VA_ARGS__)
+#else
+#define PTLS_DEBUGF(...)
+#endif
+
+#ifndef PTLS_MEMORY_DEBUG
+#define PTLS_MEMORY_DEBUG 0
+#endif
+
+#if PICOTLS_USE_DTRACE
+#define PTLS_SHOULD_PROBE(LABEL, tls) (PTLS_UNLIKELY(PICOTLS_##LABEL##_ENABLED()) && !(tls)->skip_tracing)
+#define PTLS_PROBE0(LABEL, tls)                                                                                                    \
+    do {                                                                                                                           \
+        ptls_t *_tls = (tls);                                                                                                      \
+        if (PTLS_SHOULD_PROBE(LABEL, _tls))                                                                                        \
+            PICOTLS_##LABEL(_tls);                                                                                                 \
+    } while (0)
+#define PTLS_PROBE(LABEL, tls, ...)                                                                                                \
+    do {                                                                                                                           \
+        ptls_t *_tls = (tls);                                                                                                      \
+        if (PTLS_SHOULD_PROBE(LABEL, _tls))                                                                                        \
+            PICOTLS_##LABEL(_tls, __VA_ARGS__);                                                                                    \
+    } while (0)
+#else
+#define PTLS_PROBE0(LABEL, tls)
+#define PTLS_PROBE(LABEL, tls, ...)
+#endif
+
+/**
+ * list of supported versions in the preferred order
+ */
+static const uint16_t supported_versions[] = {PTLS_PROTOCOL_VERSION_TLS13};
+
+static const uint8_t hello_retry_random[PTLS_HELLO_RANDOM_SIZE] = {0xCF, 0x21, 0xAD, 0x74, 0xE5, 0x9A, 0x61, 0x11, 0xBE, 0x1D, 0x8C,
+                                                                   0x02, 0x1E, 0x65, 0xB8, 0x91, 0xC2, 0xA2, 0x11, 0x16, 0x7A, 0xBB,
+                                                                   0x8C, 0x5E, 0x07, 0x9E, 0x09, 0xE2, 0xC8, 0xA8, 0x33, 0x9C};
+
+struct st_ptls_traffic_protection_t {
+    uint8_t secret[PTLS_MAX_DIGEST_SIZE];
+    size_t epoch;
+    /* the following fields are not used if the key_change callback is set */
+    ptls_aead_context_t *aead;
+    uint64_t seq;
+    unsigned tls12 : 1;
+    uint64_t tls12_enc_record_iv;
+};
+
+struct st_ptls_record_message_emitter_t {
+    ptls_message_emitter_t super;
+    size_t rec_start;
+};
+
+struct st_ptls_signature_algorithms_t {
+    uint16_t list[16]; /* expand? */
+    size_t count;
+};
+
+struct st_ptls_certificate_request_t {
+    /**
+     * context.base becomes non-NULL when a CertificateRequest is pending for processing
+     */
+    ptls_iovec_t context;
+    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 offered_grease : 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
+     */
+    ptls_context_t *ctx;
+    /**
+     * the state
+     */
+    enum en_ptls_state_t {
+        PTLS_STATE_CLIENT_HANDSHAKE_START,
+        PTLS_STATE_CLIENT_EXPECT_SERVER_HELLO,
+        PTLS_STATE_CLIENT_EXPECT_SECOND_SERVER_HELLO,
+        PTLS_STATE_CLIENT_EXPECT_ENCRYPTED_EXTENSIONS,
+        PTLS_STATE_CLIENT_EXPECT_CERTIFICATE_REQUEST_OR_CERTIFICATE,
+        PTLS_STATE_CLIENT_EXPECT_CERTIFICATE,
+        PTLS_STATE_CLIENT_EXPECT_CERTIFICATE_VERIFY,
+        PTLS_STATE_CLIENT_EXPECT_FINISHED,
+        PTLS_STATE_SERVER_EXPECT_CLIENT_HELLO,
+        PTLS_STATE_SERVER_EXPECT_SECOND_CLIENT_HELLO,
+        PTLS_STATE_SERVER_GENERATING_CERTIFICATE_VERIFY,
+        PTLS_STATE_SERVER_EXPECT_CERTIFICATE,
+        PTLS_STATE_SERVER_EXPECT_CERTIFICATE_VERIFY,
+        /* ptls_send can be called if the state is below here */
+        PTLS_STATE_SERVER_EXPECT_END_OF_EARLY_DATA,
+        PTLS_STATE_SERVER_EXPECT_FINISHED,
+        PTLS_STATE_POST_HANDSHAKE_MIN,
+        PTLS_STATE_CLIENT_POST_HANDSHAKE = PTLS_STATE_POST_HANDSHAKE_MIN,
+        PTLS_STATE_SERVER_POST_HANDSHAKE
+    } state;
+    /**
+     * receive buffers
+     */
+    struct {
+        ptls_buffer_t rec;
+        ptls_buffer_t mess;
+    } recvbuf;
+    /**
+     * key schedule
+     */
+    ptls_key_schedule_t *key_schedule;
+    /**
+     * values used for record protection
+     */
+    struct {
+        struct st_ptls_traffic_protection_t dec;
+        struct st_ptls_traffic_protection_t enc;
+    } traffic_protection;
+    /**
+     * server-name passed using SNI
+     */
+    char *server_name;
+    /**
+     * result of ALPN
+     */
+    char *negotiated_protocol;
+    /**
+     * selected key-exchange
+     */
+    ptls_key_exchange_algorithm_t *key_share;
+    /**
+     * selected cipher-suite
+     */
+    ptls_cipher_suite_t *cipher_suite;
+    /**
+     * 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];
+    /**
+     * 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;
+    unsigned send_change_cipher_spec : 1;
+    unsigned needs_key_update : 1;
+    unsigned key_update_send_request : 1;
+    unsigned skip_tracing : 1;
+    /**
+     * misc.
+     */
+    union {
+        struct {
+            ptls_iovec_t legacy_session_id;
+            uint8_t legacy_session_id_buf[32];
+            ptls_key_exchange_context_t *key_share_ctx;
+            unsigned offered_psk : 1;
+            /**
+             * if 1-RTT write key is active
+             */
+            unsigned using_early_data : 1;
+            struct st_ptls_certificate_request_t certificate_request;
+        } client;
+        struct {
+            uint8_t pending_traffic_secret[PTLS_MAX_DIGEST_SIZE];
+            uint32_t early_data_skipped_bytes; /* if not UINT32_MAX, the server is skipping early data */
+            unsigned can_send_session_ticket : 1;
+            ptls_async_job_t *async_job;
+        } server;
+    };
+    /**
+     * certificate verify; will be used by the client and the server (if require_client_authentication is set)
+     */
+    struct {
+        int (*cb)(void *verify_ctx, uint16_t algo, ptls_iovec_t data, ptls_iovec_t signature);
+        void *verify_ctx;
+    } certificate_verify;
+    /**
+     * handshake traffic secret to be commisioned (an array of `uint8_t [PTLS_MAX_DIGEST_SIZE]` or NULL)
+     */
+    uint8_t *pending_handshake_secret;
+    /**
+     * user data
+     */
+    void *data_ptr;
+};
+
+struct st_ptls_record_t {
+    uint8_t type;
+    uint16_t version;
+    size_t length;
+    const uint8_t *fragment;
+};
+
+struct st_ptls_client_hello_psk_t {
+    ptls_iovec_t identity;
+    uint32_t obfuscated_ticket_age;
+    ptls_iovec_t binder;
+};
+
+#define MAX_UNKNOWN_EXTENSIONS 16
+#define MAX_CERTIFICATE_TYPES 8
+
+struct st_ptls_client_hello_t {
+    uint16_t legacy_version;
+    const uint8_t *random_bytes;
+    ptls_iovec_t legacy_session_id;
+    struct {
+        const uint8_t *ids;
+        size_t count;
+    } compression_methods;
+    uint16_t selected_version;
+    ptls_iovec_t cipher_suites;
+    ptls_iovec_t negotiated_groups;
+    ptls_iovec_t key_shares;
+    struct st_ptls_signature_algorithms_t signature_algorithms;
+    ptls_iovec_t server_name;
+    struct {
+        ptls_iovec_t list[16];
+        size_t count;
+    } alpn;
+    struct {
+        uint16_t list[16];
+        size_t count;
+    } cert_compression_algos;
+    struct {
+        ptls_iovec_t all;
+        ptls_iovec_t tbs;
+        ptls_iovec_t ch1_hash;
+        ptls_iovec_t signature;
+        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];
+            size_t count;
+        } identities;
+        unsigned ke_modes;
+        unsigned early_data_indication : 1;
+        unsigned is_last_extension : 1;
+    } psk;
+    ptls_raw_extension_t unknown_extensions[MAX_UNKNOWN_EXTENSIONS + 1];
+    size_t first_extension_at;
+};
+
+struct st_ptls_server_hello_t {
+    uint8_t random_[PTLS_HELLO_RANDOM_SIZE];
+    ptls_iovec_t legacy_session_id;
+    int is_retry_request;
+    union {
+        ptls_iovec_t peerkey;
+        struct {
+            uint16_t selected_group;
+            ptls_iovec_t cookie;
+            const uint8_t *ech;
+        } retry_request;
+    };
+};
+
+struct st_ptls_key_schedule_t {
+    unsigned generation; /* early secret (1), hanshake secret (2), master secret (3) */
+    uint8_t secret[PTLS_MAX_DIGEST_SIZE];
+    size_t num_hashes;
+    struct {
+        ptls_hash_algorithm_t *algo;
+        ptls_hash_context_t *ctx, *ctx_outer;
+    } hashes[1];
+};
+
+struct st_ptls_extension_decoder_t {
+    uint16_t type;
+    int (*cb)(ptls_t *tls, void *arg, const uint8_t *src, const uint8_t *const end);
+};
+
+struct st_ptls_extension_bitmap_t {
+    uint64_t bits;
+};
+
+static const uint8_t zeroes_of_max_digest_size[PTLS_MAX_DIGEST_SIZE] = {0};
+
+static ptls_aead_context_t *new_aead(ptls_aead_algorithm_t *aead, ptls_hash_algorithm_t *hash, int is_enc, const void *secret,
+                                     ptls_iovec_t hash_value, const char *label_prefix);
+static int server_finish_handshake(ptls_t *tls, ptls_message_emitter_t *emitter, int send_cert_verify,
+                                   struct st_ptls_signature_algorithms_t *signature_algorithms);
+
+static int is_supported_version(uint16_t v)
+{
+    size_t i;
+    for (i = 0; i != PTLS_ELEMENTSOF(supported_versions); ++i)
+        if (supported_versions[i] == v)
+            return 1;
+    return 0;
+}
+
+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(candext, allowed_bits)                                                                                                 \
+    do {                                                                                                                           \
+        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);
+    DEFINE_BIT(SH, SERVER_HELLO);
+    DEFINE_BIT(HRR, PSEUDO_HRR);
+    DEFINE_BIT(EE, ENCRYPTED_EXTENSIONS);
+    DEFINE_BIT(CR, CERTIFICATE_REQUEST);
+    DEFINE_BIT(CT, CERTIFICATE);
+    DEFINE_BIT(NST, NEW_SESSION_TICKET);
+
+    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
+     * receives an extension which it recognizes and which is not specified for the message in which it appears, it MUST abort the
+     * handshake with an "illegal_parameter" alert.
+     *
+     * +-------------------------+---------------+
+     * +        Extension        |    Allowed    |
+     * +-------------------------+---------------+ */
+    EXT( SERVER_NAME             , CH + EE       );
+    EXT( STATUS_REQUEST          , CH + CR + CT  );
+    EXT( SUPPORTED_GROUPS        , CH + EE       );
+    EXT( SIGNATURE_ALGORITHMS    , CH + CR       );
+    EXT( ALPN                    , CH + EE       );
+    EXT( SERVER_CERTIFICATE_TYPE , CH + EE       );
+    EXT( KEY_SHARE               , CH + SH + HRR );
+    EXT( PRE_SHARED_KEY          , CH + SH       );
+    EXT( PSK_KEY_EXCHANGE_MODES  , CH            );
+    EXT( EARLY_DATA              , CH + EE + NST );
+    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
+}
+
+#ifndef ntoh16
+static uint16_t ntoh16(const uint8_t *src)
+{
+    return (uint16_t)src[0] << 8 | src[1];
+}
+#endif
+
+#ifndef ntoh24
+static uint32_t ntoh24(const uint8_t *src)
+{
+    return (uint32_t)src[0] << 16 | (uint32_t)src[1] << 8 | src[2];
+}
+#endif
+
+#ifndef ntoh32
+static uint32_t ntoh32(const uint8_t *src)
+{
+    return (uint32_t)src[0] << 24 | (uint32_t)src[1] << 16 | (uint32_t)src[2] << 8 | src[3];
+}
+#endif
+
+#ifndef ntoh64
+static uint64_t ntoh64(const uint8_t *src)
+{
+    return (uint64_t)src[0] << 56 | (uint64_t)src[1] << 48 | (uint64_t)src[2] << 40 | (uint64_t)src[3] << 32 |
+           (uint64_t)src[4] << 24 | (uint64_t)src[5] << 16 | (uint64_t)src[6] << 8 | src[7];
+}
+#endif
+
+static void encode64(uint8_t *dst, uint64_t v)
+{
+    for (size_t i = 0; i < 8; ++i)
+        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);
+    if (buf->is_allocated) {
+#ifdef _WINDOWS
+        if (buf->align_bits != 0) {
+            _aligned_free(buf->base);
+        } else {
+            free(buf->base);
+        }
+#else
+        free(buf->base);
+#endif
+    }
+}
+
+int ptls_buffer_reserve(ptls_buffer_t *buf, size_t delta)
+{
+    return ptls_buffer_reserve_aligned(buf, delta, 0);
+}
+
+int ptls_buffer_reserve_aligned(ptls_buffer_t *buf, size_t delta, uint8_t align_bits)
+{
+    if (buf->base == NULL)
+        return PTLS_ERROR_NO_MEMORY;
+
+    if (PTLS_MEMORY_DEBUG || buf->capacity < buf->off + delta ||
+        (buf->align_bits < align_bits && ((uintptr_t)buf->base & (((uintptr_t)1 << align_bits) - 1)) != 0)) {
+        void *newp;
+        size_t new_capacity = buf->capacity;
+        if (new_capacity < 1024)
+            new_capacity = 1024;
+        while (new_capacity < buf->off + delta) {
+            new_capacity *= 2;
+        }
+        if (align_bits != 0) {
+#ifdef _WINDOWS
+            if ((newp = _aligned_malloc(new_capacity, (size_t)1 << align_bits)) == NULL)
+                return PTLS_ERROR_NO_MEMORY;
+#else
+            if (posix_memalign(&newp, 1 << align_bits, new_capacity) != 0)
+                return PTLS_ERROR_NO_MEMORY;
+#endif
+        } else {
+            if ((newp = malloc(new_capacity)) == NULL)
+                return PTLS_ERROR_NO_MEMORY;
+        }
+        memcpy(newp, buf->base, buf->off);
+        ptls_buffer__release_memory(buf);
+        buf->base = newp;
+        buf->capacity = new_capacity;
+        buf->is_allocated = 1;
+        buf->align_bits = align_bits;
+    }
+
+    return 0;
+}
+
+int ptls_buffer__do_pushv(ptls_buffer_t *buf, const void *src, size_t len)
+{
+    int ret;
+
+    if (len == 0)
+        return 0;
+    if ((ret = ptls_buffer_reserve(buf, len)) != 0)
+        return ret;
+    memcpy(buf->base + buf->off, src, len);
+    buf->off += len;
+    return 0;
+}
+
+int ptls_buffer__adjust_quic_blocksize(ptls_buffer_t *buf, size_t body_size)
+{
+    uint8_t sizebuf[PTLS_ENCODE_QUICINT_CAPACITY];
+    size_t sizelen = ptls_encode_quicint(sizebuf, body_size) - sizebuf;
+
+    /* adjust amount of space before body_size to `sizelen` bytes */
+    if (sizelen != 1) {
+        int ret;
+        if ((ret = ptls_buffer_reserve(buf, sizelen - 1)) != 0)
+            return ret;
+        memmove(buf->base + buf->off - body_size - 1 + sizelen, buf->base + buf->off - body_size, body_size);
+        buf->off += sizelen - 1;
+    }
+
+    /* write the size */
+    memcpy(buf->base + buf->off - body_size - sizelen, sizebuf, sizelen);
+
+    return 0;
+}
+
+int ptls_buffer__adjust_asn1_blocksize(ptls_buffer_t *buf, size_t body_size)
+{
+    fprintf(stderr, "unimplemented\n");
+    abort();
+}
+
+int ptls_buffer_push_asn1_ubigint(ptls_buffer_t *buf, const void *bignum, size_t size)
+{
+    const uint8_t *p = bignum, *const end = p + size;
+    int ret;
+
+    /* skip zeroes */
+    for (; end - p >= 1; ++p)
+        if (*p != 0)
+            break;
+
+    /* emit */
+    ptls_buffer_push(buf, 2);
+    ptls_buffer_push_asn1_block(buf, {
+        if (*p >= 0x80)
+            ptls_buffer_push(buf, 0);
+        if (p != end) {
+            ptls_buffer_pushv(buf, p, end - p);
+        } else {
+            ptls_buffer_pushv(buf, "", 1);
+        }
+    });
+    ret = 0;
+
+Exit:
+    return ret;
+}
+
+#if PTLS_FUZZ_HANDSHAKE
+
+static size_t aead_encrypt(struct st_ptls_traffic_protection_t *ctx, void *output, const void *input, size_t inlen,
+                           uint8_t content_type)
+{
+    memcpy(output, input, inlen);
+    memcpy(output + inlen, &content_type, 1);
+    return inlen + 1 + 16;
+}
+
+static int aead_decrypt(struct st_ptls_traffic_protection_t *ctx, void *output, size_t *outlen, const void *input, size_t inlen)
+{
+    if (inlen < 16) {
+        return PTLS_ALERT_BAD_RECORD_MAC;
+    }
+    memcpy(output, input, inlen - 16);
+    *outlen = inlen - 16; /* removing the 16 bytes of tag */
+    return 0;
+}
+
+#else
+
+static void build_aad(uint8_t aad[5], size_t reclen)
+{
+    aad[0] = PTLS_CONTENT_TYPE_APPDATA;
+    aad[1] = PTLS_RECORD_VERSION_MAJOR;
+    aad[2] = PTLS_RECORD_VERSION_MINOR;
+    aad[3] = (uint8_t)(reclen >> 8);
+    aad[4] = (uint8_t)reclen;
+}
+
+static size_t aead_encrypt(struct st_ptls_traffic_protection_t *ctx, void *output, const void *input, size_t inlen,
+                           uint8_t content_type)
+{
+    ptls_iovec_t invec[2] = {ptls_iovec_init(input, inlen), ptls_iovec_init(&content_type, 1)};
+    uint8_t aad[5];
+
+    build_aad(aad, inlen + 1 + ctx->aead->algo->tag_size);
+    ptls_aead_encrypt_v(ctx->aead, output, invec, PTLS_ELEMENTSOF(invec), ctx->seq++, aad, sizeof(aad));
+
+    return inlen + 1 + ctx->aead->algo->tag_size;
+}
+
+static int aead_decrypt(struct st_ptls_traffic_protection_t *ctx, void *output, size_t *outlen, const void *input, size_t inlen)
+{
+    uint8_t aad[5];
+
+    build_aad(aad, inlen);
+    if ((*outlen = ptls_aead_decrypt(ctx->aead, output, input, inlen, ctx->seq, aad, sizeof(aad))) == SIZE_MAX)
+        return PTLS_ALERT_BAD_RECORD_MAC;
+    ++ctx->seq;
+    return 0;
+}
+
+#endif /* #if PTLS_FUZZ_HANDSHAKE */
+
+static void build_tls12_aad(uint8_t *aad, uint8_t type, uint64_t seq, uint16_t length)
+{
+    for (size_t i = 0; i < 8; ++i)
+        aad[i] = (uint8_t)(seq >> (56 - i * 8));
+    aad[8] = type;
+    aad[9] = PTLS_RECORD_VERSION_MAJOR;
+    aad[10] = PTLS_RECORD_VERSION_MINOR;
+    aad[11] = length >> 8;
+    aad[12] = (uint8_t)length;
+}
+
+#define buffer_push_record(buf, type, block)                                                                                       \
+    do {                                                                                                                           \
+        ptls_buffer_push((buf), (type), PTLS_RECORD_VERSION_MAJOR, PTLS_RECORD_VERSION_MINOR);                                     \
+        ptls_buffer_push_block((buf), 2, block);                                                                                   \
+    } while (0)
+
+static int buffer_push_encrypted_records(ptls_buffer_t *buf, uint8_t type, const uint8_t *src, size_t len,
+                                         struct st_ptls_traffic_protection_t *enc)
+{
+    int ret = 0;
+
+    while (len != 0) {
+        size_t chunk_size = len;
+        if (chunk_size > PTLS_MAX_PLAINTEXT_RECORD_SIZE)
+            chunk_size = PTLS_MAX_PLAINTEXT_RECORD_SIZE;
+        if (enc->tls12) {
+            buffer_push_record(buf, type, {
+                /* reserve memory */
+                if ((ret = ptls_buffer_reserve_aligned(
+                         buf, enc->aead->algo->tls12.record_iv_size + chunk_size + enc->aead->algo->tag_size,
+                         enc->aead->algo->align_bits)) != 0)
+                    goto Exit;
+                /* determine nonce, as well as prepending that walue as the record IV (AES-GCM) */
+                uint64_t nonce;
+                if (enc->aead->algo->tls12.record_iv_size != 0) {
+                    assert(enc->aead->algo->tls12.record_iv_size == 8);
+                    nonce = enc->tls12_enc_record_iv++;
+                    encode64(buf->base + buf->off, nonce);
+                    buf->off += 8;
+                } else {
+                    nonce = enc->seq;
+                }
+                /* build AAD */
+                uint8_t aad[PTLS_TLS12_AAD_SIZE];
+                build_tls12_aad(aad, type, enc->seq, (uint16_t)chunk_size);
+                /* encrypt */
+                buf->off += ptls_aead_encrypt(enc->aead, buf->base + buf->off, src, chunk_size, nonce, aad, sizeof(aad));
+                ++enc->seq;
+            });
+        } else {
+            buffer_push_record(buf, PTLS_CONTENT_TYPE_APPDATA, {
+                if ((ret = ptls_buffer_reserve_aligned(buf, chunk_size + enc->aead->algo->tag_size + 1,
+                                                       enc->aead->algo->align_bits)) != 0)
+                    goto Exit;
+                buf->off += aead_encrypt(enc, buf->base + buf->off, src, chunk_size, type);
+            });
+        }
+        src += chunk_size;
+        len -= chunk_size;
+    }
+
+Exit:
+    return ret;
+}
+
+static int buffer_encrypt_record(ptls_buffer_t *buf, size_t rec_start, struct st_ptls_traffic_protection_t *enc)
+{
+    size_t bodylen = buf->off - rec_start - 5;
+    uint8_t *tmpbuf, type = buf->base[rec_start];
+    int ret;
+
+    /* Fast path: do in-place encryption if only one record needs to be emitted. (For simplicity, do not take this path if TLS 1.2
+     * is used, as this function will be called no more than once per connection, for encrypting an alert.) */
+    if (!enc->tls12 && bodylen <= PTLS_MAX_PLAINTEXT_RECORD_SIZE) {
+        size_t overhead = 1 + enc->aead->algo->tag_size;
+        if ((ret = ptls_buffer_reserve_aligned(buf, overhead, enc->aead->algo->align_bits)) != 0)
+            return ret;
+        size_t encrypted_len = aead_encrypt(enc, buf->base + rec_start + 5, buf->base + rec_start + 5, bodylen, type);
+        assert(encrypted_len == bodylen + overhead);
+        buf->off += overhead;
+        buf->base[rec_start] = PTLS_CONTENT_TYPE_APPDATA;
+        buf->base[rec_start + 3] = (encrypted_len >> 8) & 0xff;
+        buf->base[rec_start + 4] = encrypted_len & 0xff;
+        return 0;
+    }
+
+    /* move plaintext to temporary buffer */
+    if ((tmpbuf = malloc(bodylen)) == NULL) {
+        ret = PTLS_ERROR_NO_MEMORY;
+        goto Exit;
+    }
+    memcpy(tmpbuf, buf->base + rec_start + 5, bodylen);
+    ptls_clear_memory(buf->base + rec_start, bodylen + 5);
+    buf->off = rec_start;
+
+    /* push encrypted records */
+    ret = buffer_push_encrypted_records(buf, type, tmpbuf, bodylen, enc);
+
+Exit:
+    if (tmpbuf != NULL) {
+        ptls_clear_memory(tmpbuf, bodylen);
+        free(tmpbuf);
+    }
+    return ret;
+}
+
+static int begin_record_message(ptls_message_emitter_t *_self)
+{
+    struct st_ptls_record_message_emitter_t *self = (void *)_self;
+    int ret;
+
+    self->rec_start = self->super.buf->off;
+    ptls_buffer_push(self->super.buf, PTLS_CONTENT_TYPE_HANDSHAKE, PTLS_RECORD_VERSION_MAJOR, PTLS_RECORD_VERSION_MINOR, 0, 0);
+    ret = 0;
+Exit:
+    return ret;
+}
+
+static int commit_record_message(ptls_message_emitter_t *_self)
+{
+    struct st_ptls_record_message_emitter_t *self = (void *)_self;
+    int ret;
+
+    if (self->super.enc->aead != NULL) {
+        ret = buffer_encrypt_record(self->super.buf, self->rec_start, self->super.enc);
+    } else {
+        /* TODO allow CH,SH,HRR above 16KB */
+        size_t sz = self->super.buf->off - self->rec_start - 5;
+        assert(sz <= PTLS_MAX_PLAINTEXT_RECORD_SIZE);
+        self->super.buf->base[self->rec_start + 3] = (uint8_t)(sz >> 8);
+        self->super.buf->base[self->rec_start + 4] = (uint8_t)(sz);
+        ret = 0;
+    }
+
+    return ret;
+}
+
+#define buffer_push_extension(buf, type, block)                                                                                    \
+    do {                                                                                                                           \
+        ptls_buffer_push16((buf), (type));                                                                                         \
+        ptls_buffer_push_block((buf), 2, block);                                                                                   \
+    } while (0);
+
+#define decode_open_extensions(src, end, hstype, exttype, block)                                                                   \
+    do {                                                                                                                           \
+        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_testandset(&bitmap, (hstype), *(exttype))) {                                                 \
+                    ret = PTLS_ALERT_ILLEGAL_PARAMETER;                                                                            \
+                    goto Exit;                                                                                                     \
+                }                                                                                                                  \
+                ptls_decode_open_block((src), end, 2, block);                                                                      \
+            }                                                                                                                      \
+        });                                                                                                                        \
+    } while (0)
+
+#define decode_extensions(src, end, hstype, exttype, block)                                                                        \
+    do {                                                                                                                           \
+        decode_open_extensions((src), end, hstype, exttype, block);                                                                \
+        ptls_decode_assert_block_close((src), end);                                                                                \
+    } while (0)
+
+int ptls_decode8(uint8_t *value, const uint8_t **src, const uint8_t *end)
+{
+    if (*src == end)
+        return PTLS_ALERT_DECODE_ERROR;
+    *value = *(*src)++;
+    return 0;
+}
+
+int ptls_decode16(uint16_t *value, const uint8_t **src, const uint8_t *end)
+{
+    if (end - *src < 2)
+        return PTLS_ALERT_DECODE_ERROR;
+    *value = ntoh16(*src);
+    *src += 2;
+    return 0;
+}
+
+int ptls_decode24(uint32_t *value, const uint8_t **src, const uint8_t *end)
+{
+    if (end - *src < 3)
+        return PTLS_ALERT_DECODE_ERROR;
+    *value = ((uint32_t)(*src)[0] << 16) | ((uint32_t)(*src)[1] << 8) | (*src)[2];
+    *src += 3;
+    return 0;
+}
+
+int ptls_decode32(uint32_t *value, const uint8_t **src, const uint8_t *end)
+{
+    if (end - *src < 4)
+        return PTLS_ALERT_DECODE_ERROR;
+    *value = ntoh32(*src);
+    *src += 4;
+    return 0;
+}
+
+int ptls_decode64(uint64_t *value, const uint8_t **src, const uint8_t *end)
+{
+    if (end - *src < 8)
+        return PTLS_ALERT_DECODE_ERROR;
+    *value = ntoh64(*src);
+    *src += 8;
+    return 0;
+}
+
+uint64_t ptls_decode_quicint(const uint8_t **src, const uint8_t *end)
+{
+    if (PTLS_UNLIKELY(*src == end))
+        return UINT64_MAX;
+
+    uint8_t b = *(*src)++;
+
+    if (PTLS_LIKELY(b <= 0x3f))
+        return b;
+
+    uint64_t v = b & 0x3f;
+    unsigned bytes_left = (1 << (b >> 6)) - 1;
+    if (PTLS_UNLIKELY((size_t)(end - *src) < bytes_left))
+        return UINT64_MAX;
+    do {
+        v = (v << 8) | *(*src)++;
+    } while (--bytes_left != 0);
+    return v;
+}
+
+static void log_secret(ptls_t *tls, const char *type, ptls_iovec_t secret)
+{
+    char hexbuf[PTLS_MAX_DIGEST_SIZE * 2 + 1];
+
+    PTLS_PROBE(NEW_SECRET, tls, type, ptls_hexdump(hexbuf, secret.base, secret.len));
+    PTLS_LOG_CONN(new_secret, tls, { PTLS_LOG_ELEMENT_SAFESTR(label, type); });
+
+    if (tls->ctx->log_event != NULL)
+        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;
+}
+
+static void client_setup_ech_grease(struct st_ptls_ech_t *ech, void (*random_bytes)(void *, size_t), ptls_hpke_kem_t **kems,
+                                    ptls_hpke_cipher_suite_t **ciphers, const char *sni_name)
+{
+    static const size_t x25519_key_size = 32;
+    uint8_t random_secret[PTLS_AES128_KEY_SIZE + PTLS_AES_IV_SIZE];
+
+    /* pick up X25519, AES-128-GCM or bail out */
+    for (size_t i = 0; kems[i] != NULL; ++i) {
+        if (kems[i]->id == PTLS_HPKE_KEM_X25519_SHA256) {
+            ech->kem = kems[i];
+            break;
+        }
+    }
+    for (size_t i = 0; ciphers[i] != NULL; ++i) {
+        if (ciphers[i]->id.kdf == PTLS_HPKE_HKDF_SHA256 && ciphers[i]->id.aead == PTLS_HPKE_AEAD_AES_128_GCM) {
+            ech->cipher = ciphers[i];
+            break;
+        }
+    }
+    if (ech->kem == NULL || ech->cipher == NULL)
+        goto Fail;
+
+    /* aead is generated from random */
+    random_bytes(random_secret, sizeof(random_secret));
+    ech->aead = ptls_aead_new_direct(ech->cipher->aead, 1, random_secret, random_secret + PTLS_AES128_KEY_SIZE);
+
+    /* `enc` is random bytes */
+    if ((ech->client.enc.base = malloc(x25519_key_size)) == NULL)
+        goto Fail;
+    ech->client.enc.len = x25519_key_size;
+    random_bytes(ech->client.enc.base, ech->client.enc.len);
+
+    /* setup the rest (inner_client_random is left zeros) */
+    random_bytes(&ech->config_id, sizeof(ech->config_id));
+    ech->client.max_name_length = 64;
+    if ((ech->client.public_name = duplicate_as_str(sni_name, strlen(sni_name))) == NULL)
+        goto Fail;
+
+    return;
+
+Fail:
+    clear_ech(ech, 0);
+}
+
+#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) {
+        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, int use_outer)
+{
+#define FOREACH_HASH(block)                                                                                                        \
+    do {                                                                                                                           \
+        ptls_cipher_suite_t *cs;                                                                                                   \
+        if ((cs = preferred) != NULL) {                                                                                            \
+            block                                                                                                                  \
+        }                                                                                                                          \
+        if (offered != NULL) {                                                                                                     \
+            size_t i, j;                                                                                                           \
+            for (i = 0; (cs = offered[i]) != NULL; ++i) {                                                                          \
+                if (preferred == NULL || cs->hash != preferred->hash) {                                                            \
+                    for (j = 0; j != i; ++j)                                                                                       \
+                        if (cs->hash == offered[j]->hash)                                                                          \
+                            break;                                                                                                 \
+                    if (j == i) {                                                                                                  \
+                        block                                                                                                      \
+                    }                                                                                                              \
+                }                                                                                                                  \
+            }                                                                                                                      \
+        }                                                                                                                          \
+    } while (0)
+
+    ptls_key_schedule_t *sched;
+
+    { /* allocate */
+        size_t num_hashes = 0;
+        FOREACH_HASH({ ++num_hashes; });
+        if ((sched = malloc(offsetof(ptls_key_schedule_t, hashes) + sizeof(sched->hashes[0]) * num_hashes)) == NULL)
+            return NULL;
+        *sched = (ptls_key_schedule_t){0};
+    }
+
+    /* setup the hash algos and contexts */
+    FOREACH_HASH({
+        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;
+    });
+
+    return sched;
+Fail:
+    key_schedule_free(sched);
+    return NULL;
+
+#undef FOREACH_HASH
+}
+
+static int key_schedule_extract(ptls_key_schedule_t *sched, ptls_iovec_t ikm)
+{
+    int ret;
+
+    if (ikm.base == NULL)
+        ikm = ptls_iovec_init(zeroes_of_max_digest_size, sched->hashes[0].algo->digest_size);
+
+    if (sched->generation != 0 &&
+        (ret = ptls_hkdf_expand_label(sched->hashes[0].algo, sched->secret, sched->hashes[0].algo->digest_size,
+                                      ptls_iovec_init(sched->secret, sched->hashes[0].algo->digest_size), "derived",
+                                      ptls_iovec_init(sched->hashes[0].algo->empty_digest, sched->hashes[0].algo->digest_size),
+                                      NULL)) != 0)
+        return ret;
+
+    ++sched->generation;
+    ret = ptls_hkdf_extract(sched->hashes[0].algo, sched->secret,
+                            ptls_iovec_init(sched->secret, sched->hashes[0].algo->digest_size), ikm);
+    PTLS_DEBUGF("%s: %u, %02x%02x\n", __FUNCTION__, sched->generation, (int)sched->secret[0], (int)sched->secret[1]);
+    return ret;
+}
+
+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;
+
+    assert(sched->generation == 1);
+
+    /* find the one, while freeing others */
+    for (i = 0; i != sched->num_hashes; ++i) {
+        if (sched->hashes[i].algo == cs->hash) {
+            assert(found_slot == SIZE_MAX);
+            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) {
+        sched->hashes[0] = sched->hashes[found_slot];
+        reset = 1;
+    }
+    sched->num_hashes = 1;
+
+    /* recalculate the hash if a different hash as been selected than the one we used for calculating the early secrets */
+    if (reset) {
+        --sched->generation;
+        memset(sched->secret, 0, sizeof(sched->secret));
+        if ((ret = key_schedule_extract(sched, ptls_iovec_init(NULL, 0))) != 0)
+            goto Exit;
+    }
+
+    ret = 0;
+Exit:
+    return ret;
+}
+
+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) {
+        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), 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};
+
+    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);
+    }
+
+    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)
+{
+    int ret = ptls_hkdf_expand_label(sched->hashes[0].algo, secret, sched->hashes[0].algo->digest_size,
+                                     ptls_iovec_init(sched->secret, sched->hashes[0].algo->digest_size), label,
+                                     ptls_iovec_init(hash, sched->hashes[0].algo->digest_size), NULL);
+    PTLS_DEBUGF("%s: (label=%s, hash=%02x%02x) => %02x%02x\n", __FUNCTION__, label, hash[0], hash[1], ((uint8_t *)secret)[0],
+                ((uint8_t *)secret)[1]);
+    return ret;
+}
+
+static int derive_secret(ptls_key_schedule_t *sched, void *secret, const char *label)
+{
+    uint8_t hash_value[PTLS_MAX_DIGEST_SIZE];
+
+    sched->hashes[0].ctx->final(sched->hashes[0].ctx, hash_value, PTLS_HASH_FINAL_MODE_SNAPSHOT);
+    int ret = derive_secret_with_hash(sched, secret, label, hash_value);
+    ptls_clear_memory(hash_value, sizeof(hash_value));
+    return ret;
+}
+
+static int derive_secret_with_empty_digest(ptls_key_schedule_t *sched, void *secret, const char *label)
+{
+    return derive_secret_with_hash(sched, secret, label, sched->hashes[0].algo->empty_digest);
+}
+
+static int derive_exporter_secret(ptls_t *tls, int is_early)
+{
+    int ret;
+
+    if (!tls->ctx->use_exporter)
+        return 0;
+
+    uint8_t **slot = is_early ? &tls->exporter_master_secret.early : &tls->exporter_master_secret.one_rtt;
+    assert(*slot == NULL);
+    if ((*slot = malloc(tls->key_schedule->hashes[0].algo->digest_size)) == NULL)
+        return PTLS_ERROR_NO_MEMORY;
+
+    if ((ret = derive_secret(tls->key_schedule, *slot, is_early ? "e exp master" : "exp master")) != 0)
+        return ret;
+
+    log_secret(tls, is_early ? "EARLY_EXPORTER_SECRET" : "EXPORTER_SECRET",
+               ptls_iovec_init(*slot, tls->key_schedule->hashes[0].algo->digest_size));
+
+    return 0;
+}
+
+static void free_exporter_master_secret(ptls_t *tls, int is_early)
+{
+    uint8_t *slot = is_early ? tls->exporter_master_secret.early : tls->exporter_master_secret.one_rtt;
+    if (slot == NULL)
+        return;
+    assert(tls->key_schedule != NULL);
+    ptls_clear_memory(slot, tls->key_schedule->hashes[0].algo->digest_size);
+    free(slot);
+}
+
+static int derive_resumption_secret(ptls_key_schedule_t *sched, uint8_t *secret, ptls_iovec_t nonce)
+{
+    int ret;
+
+    if ((ret = derive_secret(sched, secret, "res master")) != 0)
+        goto Exit;
+    if ((ret = ptls_hkdf_expand_label(sched->hashes[0].algo, secret, sched->hashes[0].algo->digest_size,
+                                      ptls_iovec_init(secret, sched->hashes[0].algo->digest_size), "resumption", nonce, NULL)) != 0)
+        goto Exit;
+
+Exit:
+    if (ret != 0)
+        ptls_clear_memory(secret, sched->hashes[0].algo->digest_size);
+    return ret;
+}
+
+static int decode_new_session_ticket(ptls_t *tls, uint32_t *lifetime, uint32_t *age_add, ptls_iovec_t *nonce, ptls_iovec_t *ticket,
+                                     uint32_t *max_early_data_size, const uint8_t *src, const uint8_t *const end)
+{
+    uint16_t exttype;
+    int ret;
+
+    if ((ret = ptls_decode32(lifetime, &src, end)) != 0)
+        goto Exit;
+    if ((ret = ptls_decode32(age_add, &src, end)) != 0)
+        goto Exit;
+    ptls_decode_open_block(src, end, 1, {
+        *nonce = 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;
+        }
+        *ticket = ptls_iovec_init(src, end - src);
+        src = end;
+    });
+
+    *max_early_data_size = 0;
+    decode_extensions(src, end, PTLS_HANDSHAKE_TYPE_NEW_SESSION_TICKET, &exttype, {
+        if (tls->ctx->on_extension != NULL &&
+            (ret = tls->ctx->on_extension->cb(tls->ctx->on_extension, tls, PTLS_HANDSHAKE_TYPE_NEW_SESSION_TICKET, exttype,
+                                              ptls_iovec_init(src, end - src)) != 0))
+            goto Exit;
+        switch (exttype) {
+        case PTLS_EXTENSION_TYPE_EARLY_DATA:
+            if ((ret = ptls_decode32(max_early_data_size, &src, end)) != 0)
+                goto Exit;
+            break;
+        default:
+            src = end;
+            break;
+        }
+    });
+
+    ret = 0;
+Exit:
+    return ret;
+}
+
+static int decode_stored_session_ticket(ptls_t *tls, ptls_key_exchange_algorithm_t **key_share, ptls_cipher_suite_t **cs,
+                                        ptls_iovec_t *secret, uint32_t *obfuscated_ticket_age, ptls_iovec_t *ticket,
+                                        uint32_t *max_early_data_size, const uint8_t *src, const uint8_t *const end)
+{
+    uint16_t kxid, csid;
+    uint32_t lifetime, age_add;
+    uint64_t obtained_at, now;
+    ptls_iovec_t nonce;
+    int ret;
+
+    /* decode */
+    if ((ret = ptls_decode64(&obtained_at, &src, end)) != 0)
+        goto Exit;
+    if ((ret = ptls_decode16(&kxid, &src, end)) != 0)
+        goto Exit;
+    if ((ret = ptls_decode16(&csid, &src, end)) != 0)
+        goto Exit;
+    ptls_decode_open_block(src, end, 3, {
+        if ((ret = decode_new_session_ticket(tls, &lifetime, &age_add, &nonce, ticket, max_early_data_size, src, end)) != 0)
+            goto Exit;
+        src = end;
+    });
+    ptls_decode_block(src, end, 2, {
+        *secret = ptls_iovec_init(src, end - src);
+        src = end;
+    });
+
+    { /* determine the key-exchange */
+        ptls_key_exchange_algorithm_t **cand;
+        for (cand = tls->ctx->key_exchanges; *cand != NULL; ++cand)
+            if ((*cand)->id == kxid)
+                break;
+        if (*cand == NULL) {
+            ret = PTLS_ERROR_LIBRARY;
+            goto Exit;
+        }
+        *key_share = *cand;
+    }
+
+    { /* determine the cipher-suite */
+        ptls_cipher_suite_t **cand;
+        for (cand = tls->ctx->cipher_suites; *cand != NULL; ++cand)
+            if ((*cand)->id == csid)
+                break;
+        if (*cand == NULL) {
+            ret = PTLS_ERROR_LIBRARY;
+            goto Exit;
+        }
+        *cs = *cand;
+    }
+
+    /* calculate obfuscated_ticket_age */
+    now = tls->ctx->get_time->cb(tls->ctx->get_time);
+    if (!(obtained_at <= now && now - obtained_at < 7 * 86400 * 1000)) {
+        ret = PTLS_ERROR_LIBRARY;
+        goto Exit;
+    }
+    *obfuscated_ticket_age = (uint32_t)(now - obtained_at) + age_add;
+
+    ret = 0;
+Exit:
+    return ret;
+}
+
+static int get_traffic_key(ptls_hash_algorithm_t *algo, void *key, size_t key_size, int is_iv, const void *secret,
+                           ptls_iovec_t hash_value, const char *label_prefix)
+{
+    return ptls_hkdf_expand_label(algo, key, key_size, ptls_iovec_init(secret, algo->digest_size), is_iv ? "iv" : "key", hash_value,
+                                  label_prefix);
+}
+
+static int get_traffic_keys(ptls_aead_algorithm_t *aead, ptls_hash_algorithm_t *hash, void *key, void *iv, const void *secret,
+                            ptls_iovec_t hash_value, const char *label_prefix)
+{
+    int ret;
+
+    if ((ret = get_traffic_key(hash, key, aead->key_size, 0, secret, hash_value, label_prefix)) != 0 ||
+        (ret = get_traffic_key(hash, iv, aead->iv_size, 1, secret, hash_value, label_prefix)) != 0) {
+        ptls_clear_memory(key, aead->key_size);
+        ptls_clear_memory(iv, aead->iv_size);
+    }
+
+    return ret;
+}
+
+static int setup_traffic_protection(ptls_t *tls, int is_enc, const char *secret_label, size_t epoch, int skip_notify)
+{
+    static const char *log_labels[2][4] = {
+        {NULL, "CLIENT_EARLY_TRAFFIC_SECRET", "CLIENT_HANDSHAKE_TRAFFIC_SECRET", "CLIENT_TRAFFIC_SECRET_0"},
+        {NULL, NULL, "SERVER_HANDSHAKE_TRAFFIC_SECRET", "SERVER_TRAFFIC_SECRET_0"}};
+    struct st_ptls_traffic_protection_t *ctx = is_enc ? &tls->traffic_protection.enc : &tls->traffic_protection.dec;
+
+    if (secret_label != NULL) {
+        int ret;
+        if ((ret = derive_secret(tls->key_schedule, ctx->secret, secret_label)) != 0)
+            return ret;
+    }
+
+    ctx->epoch = epoch;
+
+    log_secret(tls, log_labels[ptls_is_server(tls) == is_enc][epoch],
+               ptls_iovec_init(ctx->secret, tls->key_schedule->hashes[0].algo->digest_size));
+
+    /* special path for applications having their own record layer */
+    if (tls->ctx->update_traffic_key != NULL) {
+        if (skip_notify)
+            return 0;
+        return tls->ctx->update_traffic_key->cb(tls->ctx->update_traffic_key, tls, is_enc, epoch, ctx->secret);
+    }
+
+    if (ctx->aead != NULL)
+        ptls_aead_free(ctx->aead);
+    if ((ctx->aead = ptls_aead_new(tls->cipher_suite->aead, tls->cipher_suite->hash, is_enc, ctx->secret,
+                                   tls->ctx->hkdf_label_prefix__obsolete)) == NULL)
+        return PTLS_ERROR_NO_MEMORY; /* TODO obtain error from ptls_aead_new */
+    ctx->seq = 0;
+
+    PTLS_DEBUGF("[%s] %02x%02x,%02x%02x\n", log_labels[ptls_is_server(tls)][epoch], (unsigned)ctx->secret[0],
+                (unsigned)ctx->secret[1], (unsigned)ctx->aead->static_iv[0], (unsigned)ctx->aead->static_iv[1]);
+
+    return 0;
+}
+
+static int commission_handshake_secret(ptls_t *tls)
+{
+    int is_enc = !ptls_is_server(tls);
+
+    assert(tls->pending_handshake_secret != NULL);
+    memcpy((is_enc ? &tls->traffic_protection.enc : &tls->traffic_protection.dec)->secret, tls->pending_handshake_secret,
+           PTLS_MAX_DIGEST_SIZE);
+    ptls_clear_memory(tls->pending_handshake_secret, PTLS_MAX_DIGEST_SIZE);
+    free(tls->pending_handshake_secret);
+    tls->pending_handshake_secret = NULL;
+
+    return setup_traffic_protection(tls, is_enc, NULL, 2, 1);
+}
+
+static void log_client_random(ptls_t *tls)
+{
+    char buf[sizeof(tls->client_random) * 2 + 1];
+
+    PTLS_PROBE(CLIENT_RANDOM, tls, ptls_hexdump(buf, tls->client_random, sizeof(tls->client_random)));
+    PTLS_LOG_CONN(client_random, tls, { PTLS_LOG_ELEMENT_HEXDUMP(bytes, tls->client_random, sizeof(tls->client_random)); });
+}
+
+#define SESSION_IDENTIFIER_MAGIC "ptls0001" /* the number should be changed upon incompatible format change */
+#define SESSION_IDENTIFIER_MAGIC_SIZE (sizeof(SESSION_IDENTIFIER_MAGIC) - 1)
+
+static int encode_session_identifier(ptls_context_t *ctx, ptls_buffer_t *buf, uint32_t ticket_age_add, ptls_iovec_t ticket_nonce,
+                                     ptls_key_schedule_t *sched, const char *server_name, uint16_t key_exchange_id, uint16_t csid,
+                                     const char *negotiated_protocol)
+{
+    int ret = 0;
+
+    ptls_buffer_push_block(buf, 2, {
+        /* format id */
+        ptls_buffer_pushv(buf, SESSION_IDENTIFIER_MAGIC, SESSION_IDENTIFIER_MAGIC_SIZE);
+        /* date */
+        ptls_buffer_push64(buf, ctx->get_time->cb(ctx->get_time));
+        /* resumption master secret */
+        ptls_buffer_push_block(buf, 2, {
+            if ((ret = ptls_buffer_reserve(buf, sched->hashes[0].algo->digest_size)) != 0)
+                goto Exit;
+            if ((ret = derive_resumption_secret(sched, buf->base + buf->off, ticket_nonce)) != 0)
+                goto Exit;
+            buf->off += sched->hashes[0].algo->digest_size;
+        });
+        /* key-exchange */
+        ptls_buffer_push16(buf, key_exchange_id);
+        /* cipher-suite */
+        ptls_buffer_push16(buf, csid);
+        /* ticket_age_add */
+        ptls_buffer_push32(buf, ticket_age_add);
+        /* server-name */
+        ptls_buffer_push_block(buf, 2, {
+            if (server_name != NULL)
+                ptls_buffer_pushv(buf, server_name, strlen(server_name));
+        });
+        /* alpn */
+        ptls_buffer_push_block(buf, 1, {
+            if (negotiated_protocol != NULL)
+                ptls_buffer_pushv(buf, negotiated_protocol, strlen(negotiated_protocol));
+        });
+    });
+
+Exit:
+    return ret;
+}
+
+int decode_session_identifier(uint64_t *issued_at, ptls_iovec_t *psk, uint32_t *ticket_age_add, ptls_iovec_t *server_name,
+                              uint16_t *key_exchange_id, uint16_t *csid, ptls_iovec_t *negotiated_protocol, const uint8_t *src,
+                              const uint8_t *const end)
+{
+    int ret = 0;
+
+    ptls_decode_block(src, end, 2, {
+        if (end - src < SESSION_IDENTIFIER_MAGIC_SIZE ||
+            memcmp(src, SESSION_IDENTIFIER_MAGIC, SESSION_IDENTIFIER_MAGIC_SIZE) != 0) {
+            ret = PTLS_ALERT_DECODE_ERROR;
+            goto Exit;
+        }
+        src += SESSION_IDENTIFIER_MAGIC_SIZE;
+        if ((ret = ptls_decode64(issued_at, &src, end)) != 0)
+            goto Exit;
+        ptls_decode_open_block(src, end, 2, {
+            *psk = ptls_iovec_init(src, end - src);
+            src = end;
+        });
+        if ((ret = ptls_decode16(key_exchange_id, &src, end)) != 0)
+            goto Exit;
+        if ((ret = ptls_decode16(csid, &src, end)) != 0)
+            goto Exit;
+        if ((ret = ptls_decode32(ticket_age_add, &src, end)) != 0)
+            goto Exit;
+        ptls_decode_open_block(src, end, 2, {
+            *server_name = ptls_iovec_init(src, end - src);
+            src = end;
+        });
+        ptls_decode_open_block(src, end, 1, {
+            *negotiated_protocol = ptls_iovec_init(src, end - src);
+            src = end;
+        });
+    });
+
+Exit:
+    return ret;
+}
+
+static size_t build_certificate_verify_signdata(uint8_t *data, ptls_key_schedule_t *sched, const char *context_string)
+{
+    size_t datalen = 0;
+
+    memset(data + datalen, 32, 64);
+    datalen += 64;
+    memcpy(data + datalen, context_string, strlen(context_string) + 1);
+    datalen += strlen(context_string) + 1;
+    sched->hashes[0].ctx->final(sched->hashes[0].ctx, data + datalen, PTLS_HASH_FINAL_MODE_SNAPSHOT);
+    datalen += sched->hashes[0].algo->digest_size;
+    assert(datalen <= PTLS_MAX_CERTIFICATE_VERIFY_SIGNDATA_SIZE);
+
+    return datalen;
+}
+
+static int calc_verify_data(void *output, ptls_key_schedule_t *sched, const void *secret)
+{
+    ptls_hash_context_t *hmac;
+    uint8_t digest[PTLS_MAX_DIGEST_SIZE];
+    int ret;
+
+    if ((ret = ptls_hkdf_expand_label(sched->hashes[0].algo, digest, sched->hashes[0].algo->digest_size,
+                                      ptls_iovec_init(secret, sched->hashes[0].algo->digest_size), "finished",
+                                      ptls_iovec_init(NULL, 0), NULL)) != 0)
+        return ret;
+    if ((hmac = ptls_hmac_create(sched->hashes[0].algo, digest, sched->hashes[0].algo->digest_size)) == NULL) {
+        ptls_clear_memory(digest, sizeof(digest));
+        return PTLS_ERROR_NO_MEMORY;
+    }
+
+    sched->hashes[0].ctx->final(sched->hashes[0].ctx, digest, PTLS_HASH_FINAL_MODE_SNAPSHOT);
+    PTLS_DEBUGF("%s: %02x%02x,%02x%02x\n", __FUNCTION__, ((uint8_t *)secret)[0], ((uint8_t *)secret)[1], digest[0], digest[1]);
+    hmac->update(hmac, digest, sched->hashes[0].algo->digest_size);
+    ptls_clear_memory(digest, sizeof(digest));
+    hmac->final(hmac, output, PTLS_HASH_FINAL_MODE_FREE);
+
+    return 0;
+}
+
+static int verify_finished(ptls_t *tls, ptls_iovec_t message)
+{
+    uint8_t verify_data[PTLS_MAX_DIGEST_SIZE];
+    int ret;
+
+    if (PTLS_HANDSHAKE_HEADER_SIZE + tls->key_schedule->hashes[0].algo->digest_size != message.len) {
+        ret = PTLS_ALERT_DECODE_ERROR;
+        goto Exit;
+    }
+
+    if ((ret = calc_verify_data(verify_data, tls->key_schedule, tls->traffic_protection.dec.secret)) != 0)
+        goto Exit;
+    if (!ptls_mem_equal(message.base + PTLS_HANDSHAKE_HEADER_SIZE, verify_data, tls->key_schedule->hashes[0].algo->digest_size)) {
+        ret = PTLS_ALERT_HANDSHAKE_FAILURE;
+        goto Exit;
+    }
+
+Exit:
+    ptls_clear_memory(verify_data, sizeof(verify_data));
+    return ret;
+}
+
+static int send_finished(ptls_t *tls, ptls_message_emitter_t *emitter)
+{
+    int ret;
+
+    ptls_push_message(emitter, tls->key_schedule, PTLS_HANDSHAKE_TYPE_FINISHED, {
+        if ((ret = ptls_buffer_reserve(emitter->buf, tls->key_schedule->hashes[0].algo->digest_size)) != 0)
+            goto Exit;
+        if ((ret = calc_verify_data(emitter->buf->base + emitter->buf->off, tls->key_schedule,
+                                    tls->traffic_protection.enc.secret)) != 0)
+            goto Exit;
+        emitter->buf->off += tls->key_schedule->hashes[0].algo->digest_size;
+    });
+
+Exit:
+    return ret;
+}
+
+static int send_session_ticket(ptls_t *tls, ptls_message_emitter_t *emitter)
+{
+    ptls_hash_context_t *msghash_backup = tls->key_schedule->hashes[0].ctx->clone_(tls->key_schedule->hashes[0].ctx);
+    ptls_buffer_t session_id;
+    char session_id_smallbuf[128];
+    uint32_t ticket_age_add;
+    int ret = 0;
+
+    assert(tls->ctx->ticket_lifetime != 0);
+    assert(tls->ctx->encrypt_ticket != NULL);
+
+    ptls_buffer_init(&session_id, session_id_smallbuf, sizeof(session_id_smallbuf));
+
+    { /* calculate verify-data that will be sent by the client */
+        size_t orig_off = emitter->buf->off;
+        if (tls->pending_handshake_secret != NULL && !tls->ctx->omit_end_of_early_data) {
+            assert(tls->state == PTLS_STATE_SERVER_EXPECT_END_OF_EARLY_DATA);
+            ptls_buffer_push_message_body(emitter->buf, tls->key_schedule, PTLS_HANDSHAKE_TYPE_END_OF_EARLY_DATA, {});
+            emitter->buf->off = orig_off;
+        }
+        ptls_buffer_push_message_body(emitter->buf, tls->key_schedule, PTLS_HANDSHAKE_TYPE_FINISHED, {
+            if ((ret = ptls_buffer_reserve(emitter->buf, tls->key_schedule->hashes[0].algo->digest_size)) != 0)
+                goto Exit;
+            if ((ret = calc_verify_data(emitter->buf->base + emitter->buf->off, tls->key_schedule,
+                                        tls->pending_handshake_secret != NULL ? tls->pending_handshake_secret
+                                                                              : tls->traffic_protection.dec.secret)) != 0)
+                goto Exit;
+            emitter->buf->off += tls->key_schedule->hashes[0].algo->digest_size;
+        });
+        emitter->buf->off = orig_off;
+    }
+
+    tls->ctx->random_bytes(&ticket_age_add, sizeof(ticket_age_add));
+
+    /* build the raw nsk */
+    ret = encode_session_identifier(tls->ctx, &session_id, ticket_age_add, ptls_iovec_init(NULL, 0), tls->key_schedule,
+                                    tls->server_name, tls->key_share->id, tls->cipher_suite->id, tls->negotiated_protocol);
+    if (ret != 0)
+        goto Exit;
+
+    /* encrypt and send */
+    ptls_push_message(emitter, tls->key_schedule, PTLS_HANDSHAKE_TYPE_NEW_SESSION_TICKET, {
+        ptls_buffer_push32(emitter->buf, tls->ctx->ticket_lifetime);
+        ptls_buffer_push32(emitter->buf, ticket_age_add);
+        ptls_buffer_push_block(emitter->buf, 1, {});
+        ptls_buffer_push_block(emitter->buf, 2, {
+            if ((ret = tls->ctx->encrypt_ticket->cb(tls->ctx->encrypt_ticket, tls, 1, emitter->buf,
+                                                    ptls_iovec_init(session_id.base, session_id.off))) != 0)
+                goto Exit;
+        });
+        ptls_buffer_push_block(emitter->buf, 2, {
+            if (tls->ctx->max_early_data_size != 0)
+                buffer_push_extension(emitter->buf, PTLS_EXTENSION_TYPE_EARLY_DATA,
+                                      { ptls_buffer_push32(emitter->buf, tls->ctx->max_early_data_size); });
+        });
+    });
+
+Exit:
+    ptls_buffer_dispose(&session_id);
+
+    /* restore handshake state */
+    tls->key_schedule->hashes[0].ctx->final(tls->key_schedule->hashes[0].ctx, NULL, PTLS_HASH_FINAL_MODE_FREE);
+    tls->key_schedule->hashes[0].ctx = msghash_backup;
+
+    return ret;
+}
+
+static int push_change_cipher_spec(ptls_t *tls, ptls_message_emitter_t *emitter)
+{
+    int ret;
+
+    /* check if we are requested to (or still need to) */
+    if (!tls->send_change_cipher_spec) {
+        ret = 0;
+        goto Exit;
+    }
+
+    /* CCS is a record, can only be sent when using a record-based protocol. */
+    if (emitter->begin_message != begin_record_message) {
+        ret = PTLS_ALERT_UNEXPECTED_MESSAGE;
+        goto Exit;
+    }
+
+    /* emit CCS */
+    buffer_push_record(emitter->buf, PTLS_CONTENT_TYPE_CHANGE_CIPHER_SPEC, { ptls_buffer_push(emitter->buf, 1); });
+
+    tls->send_change_cipher_spec = 0;
+    ret = 0;
+Exit:
+    return ret;
+}
+
+static int push_additional_extensions(ptls_handshake_properties_t *properties, ptls_buffer_t *sendbuf)
+{
+    int ret;
+
+    if (properties != NULL && properties->additional_extensions != NULL) {
+        ptls_raw_extension_t *ext;
+        for (ext = properties->additional_extensions; ext->type != UINT16_MAX; ++ext) {
+            buffer_push_extension(sendbuf, ext->type, { ptls_buffer_pushv(sendbuf, ext->data.base, ext->data.len); });
+        }
+    }
+    ret = 0;
+Exit:
+    return ret;
+}
+
+static int push_signature_algorithms(ptls_verify_certificate_t *vc, ptls_buffer_t *sendbuf)
+{
+    /* The list sent when verify callback is not registered */
+    static const uint16_t default_algos[] = {PTLS_SIGNATURE_RSA_PSS_RSAE_SHA256, PTLS_SIGNATURE_ECDSA_SECP256R1_SHA256,
+                                             PTLS_SIGNATURE_RSA_PKCS1_SHA256, PTLS_SIGNATURE_RSA_PKCS1_SHA1, UINT16_MAX};
+    int ret;
+
+    ptls_buffer_push_block(sendbuf, 2, {
+        for (const uint16_t *p = vc != NULL ? vc->algos : default_algos; *p != UINT16_MAX; ++p)
+            ptls_buffer_push16(sendbuf, *p);
+    });
+
+    ret = 0;
+Exit:
+    return ret;
+}
+
+static int decode_signature_algorithms(struct st_ptls_signature_algorithms_t *sa, const uint8_t **src, const uint8_t *end)
+{
+    int ret;
+
+    ptls_decode_block(*src, end, 2, {
+        do {
+            uint16_t id;
+            if ((ret = ptls_decode16(&id, src, end)) != 0)
+                goto Exit;
+            if (sa->count < PTLS_ELEMENTSOF(sa->list))
+                sa->list[sa->count++] = id;
+        } while (*src != end);
+    });
+
+    ret = 0;
+Exit:
+    return ret;
+}
+
+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, int server_chacha_priority)
+{
+    size_t found_index = SIZE_MAX;
+    int ret;
+
+    while (src != end) {
+        uint16_t id;
+        if ((ret = ptls_decode16(&id, &src, end)) != 0)
+            goto Exit;
+        for (size_t i = 0; candidates[i] != NULL; ++i) {
+            if (candidates[i]->id == id) {
+                if (server_preference && !(server_chacha_priority && id == PTLS_CIPHER_SUITE_CHACHA20_POLY1305_SHA256)) {
+                    /* preserve smallest matching index, and proceed to the next input */
+                    if (i < found_index) {
+                        found_index = i;
+                        break;
+                    }
+                } else {
+                    /* return the pointer matching to the first input that can be used */
+                    *selected = candidates[i];
+                    goto Exit;
+                }
+            }
+        }
+        /* first position of the server list matched (server_preference) */
+        if (found_index == 0)
+            break;
+        /* server preference is overridden only if the first entry of client-provided list is chachapoly */
+        server_chacha_priority = 0;
+    }
+    if (found_index != SIZE_MAX) {
+        *selected = candidates[found_index];
+        ret = 0;
+    } else {
+        ret = PTLS_ALERT_HANDSHAKE_FAILURE;
+    }
+
+Exit:
+    return ret;
+}
+
+static int push_key_share_entry(ptls_buffer_t *buf, uint16_t group, ptls_iovec_t pubkey)
+{
+    int ret;
+
+    ptls_buffer_push16(buf, group);
+    ptls_buffer_push_block(buf, 2, { ptls_buffer_pushv(buf, pubkey.base, pubkey.len); });
+    ret = 0;
+Exit:
+    return ret;
+}
+
+static int decode_key_share_entry(uint16_t *group, ptls_iovec_t *key_exchange, const uint8_t **src, const uint8_t *const end)
+{
+    int ret;
+
+    if ((ret = ptls_decode16(group, src, end)) != 0)
+        goto Exit;
+    ptls_decode_open_block(*src, end, 2, {
+        *key_exchange = ptls_iovec_init(*src, end - *src);
+        *src = end;
+    });
+
+Exit:
+    return ret;
+}
+
+static int select_key_share(ptls_key_exchange_algorithm_t **selected, ptls_iovec_t *peer_key,
+                            ptls_key_exchange_algorithm_t **candidates, const uint8_t **src, const uint8_t *const end,
+                            int expect_one)
+{
+    int ret;
+
+    *selected = NULL;
+
+    if (expect_one && *src == end) {
+        ret = PTLS_ALERT_ILLEGAL_PARAMETER;
+        goto Exit;
+    }
+
+    while (*src != end) {
+        uint16_t group;
+        ptls_iovec_t key;
+        if ((ret = decode_key_share_entry(&group, &key, src, end)) != 0)
+            goto Exit;
+        ptls_key_exchange_algorithm_t **c = candidates;
+        for (; *c != NULL; ++c) {
+            if (*selected == NULL && (*c)->id == group) {
+                *selected = *c;
+                *peer_key = key;
+            }
+        }
+        if (expect_one) {
+            ret = *selected != NULL ? 0 : PTLS_ALERT_ILLEGAL_PARAMETER;
+            goto Exit;
+        }
+    }
+
+    ret = 0;
+
+Exit:
+    return ret;
+}
+
+static int emit_server_name_extension(ptls_buffer_t *buf, const char *server_name)
+{
+    int ret;
+
+    ptls_buffer_push_block(buf, 2, {
+        ptls_buffer_push(buf, PTLS_SERVER_NAME_TYPE_HOSTNAME);
+        ptls_buffer_push_block(buf, 2, { ptls_buffer_pushv(buf, server_name, strlen(server_name)); });
+    });
+
+    ret = 0;
+Exit:
+    return ret;
+}
+
+/**
+ * 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)
+{
+    return 10 + enc_size;
+}
+
+/**
+ * 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 };
+
+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;
+            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;
+                        });
+                    });
+                });
+            }
+        });
+    });
+
+Exit:
+    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 = {NULL};
+    uint32_t obfuscated_ticket_age = 0;
+    const char *sni_name = NULL;
+    size_t mess_start, msghash_off;
+    uint8_t binder_key[PTLS_MAX_DIGEST_SIZE];
+    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 ECH (ignore broken ECHConfigList; it is delivered insecurely) */
+        if (!is_second_flight && sni_name != NULL && tls->ctx->ech.client.ciphers != NULL) {
+            if (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;
+                }
+            } else {
+                /* zero-length config indicates ECH greasing */
+                client_setup_ech_grease(&tls->ech, tls->ctx->random_bytes, tls->ctx->ech.client.kems, tls->ctx->ech.client.ciphers,
+                                        sni_name);
+            }
+        }
+        /* setup resumption-related data. If successful, resumption_secret becomes a non-zero value. */
+        if (properties->client.session_ticket.base != NULL) {
+            ptls_key_exchange_algorithm_t *key_share = NULL;
+            ptls_cipher_suite_t *cipher_suite = NULL;
+            uint32_t max_early_data_size;
+            if (decode_stored_session_ticket(tls, &key_share, &cipher_suite, &resumption_secret, &obfuscated_ticket_age,
+                                             &resumption_ticket, &max_early_data_size, properties->client.session_ticket.base,
+                                             properties->client.session_ticket.base + properties->client.session_ticket.len) == 0) {
+                tls->client.offered_psk = 1;
+                /* key-share selected by HRR should not be overridden */
+                if (tls->key_share == NULL)
+                    tls->key_share = key_share;
+                tls->cipher_suite = cipher_suite;
+                if (!is_second_flight && max_early_data_size != 0 && properties->client.max_early_data_size != NULL) {
+                    tls->client.using_early_data = 1;
+                    *properties->client.max_early_data_size = max_early_data_size;
+                }
+            } else {
+                resumption_secret = ptls_iovec_init(NULL, 0);
+            }
+        }
+        if (tls->client.using_early_data) {
+            properties->client.early_data_acceptance = PTLS_EARLY_DATA_ACCEPTANCE_UNKNOWN;
+        } else {
+            if (properties->client.max_early_data_size != NULL)
+                *properties->client.max_early_data_size = 0;
+            properties->client.early_data_acceptance = PTLS_EARLY_DATA_REJECTED;
+        }
+    }
+
+    /* use the default key share if still not undetermined */
+    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->ech.aead != NULL);
+        if ((ret = key_schedule_extract(tls->key_schedule, resumption_secret)) != 0)
+            goto Exit;
+    }
+
+    /* 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, 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, 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;
+            if (properties->client.ech.configs.len != 0) {
+                tls->ech.offered = 1;
+            } else {
+                tls->ech.offered_grease = 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);
+        if ((ret = setup_traffic_protection(tls, 1, "c e traffic", 1, 0)) != 0)
+            goto Exit;
+        if ((ret = push_change_cipher_spec(tls, emitter)) != 0)
+            goto Exit;
+    }
+    if (resumption_secret.base != NULL && !is_second_flight) {
+        if ((ret = derive_exporter_secret(tls, 1)) != 0)
+            goto Exit;
+    }
+    tls->state = cookie == NULL ? PTLS_STATE_CLIENT_EXPECT_SERVER_HELLO : PTLS_STATE_CLIENT_EXPECT_SECOND_SERVER_HELLO;
+    ret = PTLS_ERROR_IN_PROGRESS;
+
+Exit:
+    ptls_buffer_dispose(&encoded_ch_inner);
+    ptls_clear_memory(binder_key, sizeof(binder_key));
+    return ret;
+}
+
+ptls_cipher_suite_t *ptls_find_cipher_suite(ptls_cipher_suite_t **cipher_suites, uint16_t id)
+{
+    ptls_cipher_suite_t **cs;
+    if (cipher_suites == NULL)
+        return NULL;
+    for (cs = cipher_suites; *cs != NULL && (*cs)->id != id; ++cs)
+        ;
+    return *cs;
+}
+
+static int decode_server_hello(ptls_t *tls, struct st_ptls_server_hello_t *sh, const uint8_t *src, const uint8_t *const end)
+{
+    int ret;
+
+    *sh = (struct st_ptls_server_hello_t){{0}};
+
+    /* ignore legacy-version */
+    if (end - src < 2) {
+        ret = PTLS_ALERT_DECODE_ERROR;
+        goto Exit;
+    }
+    src += 2;
+
+    /* random */
+    if (end - src < PTLS_HELLO_RANDOM_SIZE) {
+        ret = PTLS_ALERT_DECODE_ERROR;
+        goto Exit;
+    }
+    sh->is_retry_request = memcmp(src, hello_retry_random, PTLS_HELLO_RANDOM_SIZE) == 0;
+    src += PTLS_HELLO_RANDOM_SIZE;
+
+    /* legacy_session_id */
+    ptls_decode_open_block(src, end, 1, {
+        if (end - src > 32) {
+            ret = PTLS_ALERT_DECODE_ERROR;
+            goto Exit;
+        }
+        sh->legacy_session_id = ptls_iovec_init(src, end - src);
+        src = end;
+    });
+
+    { /* select cipher_suite */
+        uint16_t csid;
+        if ((ret = ptls_decode16(&csid, &src, end)) != 0)
+            goto Exit;
+        if ((tls->cipher_suite = ptls_find_cipher_suite(tls->ctx->cipher_suites, csid)) == NULL) {
+            ret = PTLS_ALERT_ILLEGAL_PARAMETER;
+            goto Exit;
+        }
+    }
+
+    { /* legacy_compression_method */
+        uint8_t method;
+        if ((ret = ptls_decode8(&method, &src, end)) != 0)
+            goto Exit;
+        if (method != 0) {
+            ret = PTLS_ALERT_ILLEGAL_PARAMETER;
+            goto Exit;
+        }
+    }
+
+    if (sh->is_retry_request)
+        sh->retry_request.selected_group = UINT16_MAX;
+
+    uint16_t exttype, found_version = UINT16_MAX, selected_psk_identity = UINT16_MAX;
+    decode_extensions(src, end, sh->is_retry_request ? PTLS_HANDSHAKE_TYPE_PSEUDO_HRR : PTLS_HANDSHAKE_TYPE_SERVER_HELLO, &exttype,
+                      {
+                          if (tls->ctx->on_extension != NULL &&
+                              (ret = tls->ctx->on_extension->cb(tls->ctx->on_extension, tls, PTLS_HANDSHAKE_TYPE_SERVER_HELLO,
+                                                                exttype, ptls_iovec_init(src, end - src)) != 0))
+                              goto Exit;
+                          switch (exttype) {
+                          case PTLS_EXTENSION_TYPE_SUPPORTED_VERSIONS:
+                              if ((ret = ptls_decode16(&found_version, &src, end)) != 0)
+                                  goto Exit;
+                              break;
+                          case PTLS_EXTENSION_TYPE_KEY_SHARE:
+                              if (sh->is_retry_request) {
+                                  if ((ret = ptls_decode16(&sh->retry_request.selected_group, &src, end)) != 0)
+                                      goto Exit;
+                              } else {
+                                  uint16_t group;
+                                  if ((ret = decode_key_share_entry(&group, &sh->peerkey, &src, end)) != 0)
+                                      goto Exit;
+                                  if (src != end) {
+                                      ret = PTLS_ALERT_DECODE_ERROR;
+                                      goto Exit;
+                                  }
+                                  if (tls->key_share == NULL || tls->key_share->id != group) {
+                                      ret = PTLS_ALERT_ILLEGAL_PARAMETER;
+                                      goto Exit;
+                                  }
+                              }
+                              break;
+                          case PTLS_EXTENSION_TYPE_COOKIE:
+                              assert(sh->is_retry_request);
+                              ptls_decode_block(src, end, 2, {
+                                  if (src == end) {
+                                      ret = PTLS_ALERT_DECODE_ERROR;
+                                      goto Exit;
+                                  }
+                                  sh->retry_request.cookie = ptls_iovec_init(src, end - src);
+                                  src = end;
+                              });
+                              break;
+                          case PTLS_EXTENSION_TYPE_PRE_SHARED_KEY:
+                              assert(!sh->is_retry_request);
+                              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 || tls->ech.offered_grease)) {
+                                  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;
+                          }
+                      });
+
+    if (!is_supported_version(found_version)) {
+        ret = PTLS_ALERT_ILLEGAL_PARAMETER;
+        goto Exit;
+    }
+    if (!sh->is_retry_request) {
+        if (selected_psk_identity != UINT16_MAX) {
+            if (!tls->client.offered_psk) {
+                ret = PTLS_ALERT_ILLEGAL_PARAMETER;
+                goto Exit;
+            }
+            if (selected_psk_identity != 0) {
+                ret = PTLS_ALERT_ILLEGAL_PARAMETER;
+                goto Exit;
+            }
+            tls->is_psk_handshake = 1;
+        }
+        if (sh->peerkey.base == NULL && !tls->is_psk_handshake) {
+            ret = PTLS_ALERT_ILLEGAL_PARAMETER;
+            goto Exit;
+        }
+    }
+
+    ret = 0;
+Exit:
+    return ret;
+}
+
+static int handle_hello_retry_request(ptls_t *tls, ptls_message_emitter_t *emitter, struct st_ptls_server_hello_t *sh,
+                                      ptls_iovec_t message, ptls_handshake_properties_t *properties)
+{
+    int ret;
+
+    if (tls->client.key_share_ctx != NULL) {
+        tls->client.key_share_ctx->on_exchange(&tls->client.key_share_ctx, 1, NULL, ptls_iovec_init(NULL, 0));
+        tls->client.key_share_ctx = NULL;
+    }
+    if (tls->client.using_early_data) {
+        /* release traffic encryption key so that 2nd CH goes out in cleartext, but keep the epoch at 1 since we've already
+         * called derive-secret */
+        if (tls->ctx->update_traffic_key == NULL) {
+            assert(tls->traffic_protection.enc.aead != NULL);
+            ptls_aead_free(tls->traffic_protection.enc.aead);
+            tls->traffic_protection.enc.aead = NULL;
+        }
+        tls->client.using_early_data = 0;
+    }
+
+    if (sh->retry_request.selected_group != UINT16_MAX) {
+        /* we offer the first key_exchanges[0] as KEY_SHARE unless client.negotiate_before_key_exchange is set */
+        ptls_key_exchange_algorithm_t **cand;
+        for (cand = tls->ctx->key_exchanges; *cand != NULL; ++cand)
+            if ((*cand)->id == sh->retry_request.selected_group)
+                break;
+        if (*cand == NULL) {
+            ret = PTLS_ALERT_ILLEGAL_PARAMETER;
+            goto Exit;
+        }
+        tls->key_share = *cand;
+    } else if (tls->key_share != NULL) {
+        /* retain the key-share using in first CH, if server does not specify one */
+    } else {
+        ret = PTLS_ALERT_ILLEGAL_PARAMETER;
+        goto Exit;
+    }
+
+    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_PROBE(ECH_SELECTION, tls, !!tls->ech.accepted);
+    PTLS_LOG_CONN(ech_selection, tls, { PTLS_LOG_ELEMENT_BOOL(is_ech, tls->ech.accepted); });
+    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)
+{
+    struct st_ptls_server_hello_t sh;
+    ptls_iovec_t ecdh_secret = {NULL};
+    int ret;
+
+    if ((ret = decode_server_hello(tls, &sh, message.base + PTLS_HANDSHAKE_HEADER_SIZE, message.base + message.len)) != 0)
+        goto Exit;
+    if (!(sh.legacy_session_id.len == tls->client.legacy_session_id.len &&
+          ptls_mem_equal(sh.legacy_session_id.base, tls->client.legacy_session_id.base, tls->client.legacy_session_id.len))) {
+        ret = PTLS_ALERT_ILLEGAL_PARAMETER;
+        goto Exit;
+    }
+
+    if (sh.is_retry_request) {
+        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) {
+            size_t confirm_hash_off = 0;
+            if (tls->ech.offered) {
+                if (sh.retry_request.ech != NULL)
+                    confirm_hash_off = sh.retry_request.ech - message.base;
+            } else {
+                assert(tls->ech.offered_grease);
+            }
+            if ((ret = client_ech_select_hello(tls, message, confirm_hash_off, 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_cipher(tls->key_schedule, tls->cipher_suite,
+                                          tls->client.offered_psk && !tls->is_psk_handshake)) != 0)
+        goto Exit;
+
+    /* check if ECH is accepted */
+    if (tls->ech.aead != NULL) {
+        size_t confirm_hash_off = 0;
+        if (tls->ech.offered) {
+            confirm_hash_off =
+                PTLS_HANDSHAKE_HEADER_SIZE + 2 /* legacy_version */ + PTLS_HELLO_RANDOM_SIZE - PTLS_ECH_CONFIRM_LENGTH;
+        } else {
+            assert(tls->ech.offered_grease);
+        }
+        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;
+    }
+
+    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)
+        goto Exit;
+    if (tls->client.using_early_data) {
+        if ((tls->pending_handshake_secret = malloc(PTLS_MAX_DIGEST_SIZE)) == NULL) {
+            ret = PTLS_ERROR_NO_MEMORY;
+            goto Exit;
+        }
+        if ((ret = derive_secret(tls->key_schedule, tls->pending_handshake_secret, "c hs traffic")) != 0)
+            goto Exit;
+        if (tls->ctx->update_traffic_key != NULL &&
+            (ret = tls->ctx->update_traffic_key->cb(tls->ctx->update_traffic_key, tls, 1, 2, tls->pending_handshake_secret)) != 0)
+            goto Exit;
+    } else {
+        if ((ret = setup_traffic_protection(tls, 1, "c hs traffic", 2, 0)) != 0)
+            goto Exit;
+    }
+
+    tls->state = PTLS_STATE_CLIENT_EXPECT_ENCRYPTED_EXTENSIONS;
+    ret = PTLS_ERROR_IN_PROGRESS;
+
+Exit:
+    if (ecdh_secret.base != NULL) {
+        ptls_clear_memory(ecdh_secret.base, ecdh_secret.len);
+        free(ecdh_secret.base);
+    }
+    return ret;
+}
+
+static int should_collect_unknown_extension(ptls_t *tls, ptls_handshake_properties_t *properties, uint16_t type)
+{
+    return properties != NULL && properties->collect_extension != NULL && properties->collect_extension(tls, properties, type);
+}
+
+static int collect_unknown_extension(ptls_t *tls, uint16_t type, const uint8_t *src, const uint8_t *const end,
+                                     ptls_raw_extension_t *slots)
+{
+    size_t i;
+    for (i = 0; slots[i].type != UINT16_MAX; ++i) {
+        assert(i < MAX_UNKNOWN_EXTENSIONS);
+        if (slots[i].type == type)
+            return PTLS_ALERT_ILLEGAL_PARAMETER;
+    }
+    if (i < MAX_UNKNOWN_EXTENSIONS) {
+        slots[i].type = type;
+        slots[i].data = ptls_iovec_init(src, end - src);
+        slots[i + 1].type = UINT16_MAX;
+    }
+    return 0;
+}
+
+static int report_unknown_extensions(ptls_t *tls, ptls_handshake_properties_t *properties, ptls_raw_extension_t *slots)
+{
+    if (properties != NULL && properties->collect_extension != NULL) {
+        assert(properties->collected_extensions != NULL);
+        return properties->collected_extensions(tls, properties, slots);
+    } else {
+        return 0;
+    }
+}
+
+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;
+    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;
+    int ret, skip_early_data = 1;
+    uint8_t server_offered_cert_type = PTLS_CERTIFICATE_TYPE_X509;
+
+    decode_extensions(src, end, PTLS_HANDSHAKE_TYPE_ENCRYPTED_EXTENSIONS, &type, {
+        if (tls->ctx->on_extension != NULL &&
+            (ret = tls->ctx->on_extension->cb(tls->ctx->on_extension, tls, PTLS_HANDSHAKE_TYPE_ENCRYPTED_EXTENSIONS, type,
+                                              ptls_iovec_init(src, end - src)) != 0))
+            goto Exit;
+        switch (type) {
+        case PTLS_EXTENSION_TYPE_SERVER_NAME:
+            if (src != end) {
+                ret = PTLS_ALERT_DECODE_ERROR;
+                goto Exit;
+            }
+            if (!(tls->server_name != NULL && !ptls_server_name_is_ipaddr(tls->server_name))) {
+                ret = PTLS_ALERT_ILLEGAL_PARAMETER;
+                goto Exit;
+            }
+            break;
+        case PTLS_EXTENSION_TYPE_ALPN:
+            ptls_decode_block(src, end, 2, {
+                ptls_decode_open_block(src, end, 1, {
+                    if (src == end) {
+                        ret = PTLS_ALERT_DECODE_ERROR;
+                        goto Exit;
+                    }
+                    if ((ret = ptls_set_negotiated_protocol(tls, (const char *)src, end - src)) != 0)
+                        goto Exit;
+                    src = end;
+                });
+                if (src != end) {
+                    ret = PTLS_ALERT_HANDSHAKE_FAILURE;
+                    goto Exit;
+                }
+            });
+            break;
+        case PTLS_EXTENSION_TYPE_EARLY_DATA:
+            if (!tls->client.using_early_data) {
+                ret = PTLS_ALERT_ILLEGAL_PARAMETER;
+                goto Exit;
+            }
+            skip_early_data = 0;
+            break;
+        case PTLS_EXTENSION_TYPE_SERVER_CERTIFICATE_TYPE:
+            if (end - src != 1) {
+                ret = PTLS_ALERT_DECODE_ERROR;
+                goto Exit;
+            }
+            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 || tls->ech.offered_grease) && !ptls_is_ech_handshake(tls, NULL, 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) {
+                    if ((unknown_extensions = malloc(sizeof(*unknown_extensions) * (MAX_UNKNOWN_EXTENSIONS + 1))) == NULL) {
+                        ret = PTLS_ERROR_NO_MEMORY;
+                        goto Exit;
+                    }
+                    unknown_extensions[0].type = UINT16_MAX;
+                }
+                if ((ret = collect_unknown_extension(tls, type, src, end, unknown_extensions)) != 0)
+                    goto Exit;
+            }
+            break;
+        }
+        src = end;
+    });
+
+    if (server_offered_cert_type !=
+        (tls->ctx->use_raw_public_keys ? PTLS_CERTIFICATE_TYPE_RAW_PUBLIC_KEY : PTLS_CERTIFICATE_TYPE_X509)) {
+        ret = PTLS_ALERT_UNSUPPORTED_CERTIFICATE;
+        goto Exit;
+    }
+
+    if (tls->client.using_early_data) {
+        if (skip_early_data)
+            tls->client.using_early_data = 0;
+        if (properties != NULL)
+            properties->client.early_data_acceptance = skip_early_data ? PTLS_EARLY_DATA_REJECTED : PTLS_EARLY_DATA_ACCEPTED;
+    }
+    if ((ret = report_unknown_extensions(tls, properties, unknown_extensions)) != 0)
+        goto Exit;
+
+    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;
+
+Exit:
+    if (unknown_extensions != &no_unknown_extensions)
+        free(unknown_extensions);
+    return ret;
+}
+
+static int decode_certificate_request(ptls_t *tls, struct st_ptls_certificate_request_t *cr, const uint8_t *src,
+                                      const uint8_t *const end)
+{
+    int ret;
+    uint16_t exttype = 0;
+
+    /* certificate request context */
+    ptls_decode_open_block(src, end, 1, {
+        size_t len = end - src;
+        if (len > 255) {
+            ret = PTLS_ALERT_DECODE_ERROR;
+            goto Exit;
+        }
+        if ((cr->context.base = malloc(len != 0 ? len : 1)) == NULL) {
+            ret = PTLS_ERROR_NO_MEMORY;
+            goto Exit;
+        }
+        cr->context.len = len;
+        memcpy(cr->context.base, src, len);
+        src = end;
+    });
+
+    /* decode extensions */
+    decode_extensions(src, end, PTLS_HANDSHAKE_TYPE_CERTIFICATE_REQUEST, &exttype, {
+        if (tls->ctx->on_extension != NULL &&
+            (ret = tls->ctx->on_extension->cb(tls->ctx->on_extension, tls, PTLS_HANDSHAKE_TYPE_CERTIFICATE_REQUEST, exttype,
+                                              ptls_iovec_init(src, end - src)) != 0))
+            goto Exit;
+        switch (exttype) {
+        case PTLS_EXTENSION_TYPE_SIGNATURE_ALGORITHMS:
+            if ((ret = decode_signature_algorithms(&cr->signature_algorithms, &src, end)) != 0)
+                goto Exit;
+            break;
+        }
+        src = end;
+    });
+
+    if (cr->signature_algorithms.count == 0) {
+        ret = PTLS_ALERT_MISSING_EXTENSION;
+        goto Exit;
+    }
+
+    ret = 0;
+Exit:
+    return ret;
+}
+
+int ptls_build_certificate_message(ptls_buffer_t *buf, ptls_iovec_t context, ptls_iovec_t *certificates, size_t num_certificates,
+                                   ptls_iovec_t ocsp_status)
+{
+    int ret;
+
+    ptls_buffer_push_block(buf, 1, { ptls_buffer_pushv(buf, context.base, context.len); });
+    ptls_buffer_push_block(buf, 3, {
+        size_t i;
+        for (i = 0; i != num_certificates; ++i) {
+            ptls_buffer_push_block(buf, 3, { ptls_buffer_pushv(buf, certificates[i].base, certificates[i].len); });
+            ptls_buffer_push_block(buf, 2, {
+                if (i == 0 && ocsp_status.len != 0) {
+                    buffer_push_extension(buf, PTLS_EXTENSION_TYPE_STATUS_REQUEST, {
+                        ptls_buffer_push(buf, 1); /* status_type == ocsp */
+                        ptls_buffer_push_block(buf, 3, { ptls_buffer_pushv(buf, ocsp_status.base, ocsp_status.len); });
+                    });
+                }
+            });
+        }
+    });
+
+    ret = 0;
+Exit:
+    return ret;
+}
+
+static int default_emit_certificate_cb(ptls_emit_certificate_t *_self, ptls_t *tls, ptls_message_emitter_t *emitter,
+                                       ptls_key_schedule_t *key_sched, ptls_iovec_t context, int push_status_request,
+                                       const uint16_t *compress_algos, size_t num_compress_algos)
+{
+    int ret;
+
+    ptls_push_message(emitter, key_sched, PTLS_HANDSHAKE_TYPE_CERTIFICATE, {
+        if ((ret = ptls_build_certificate_message(emitter->buf, context, tls->ctx->certificates.list, tls->ctx->certificates.count,
+                                                  ptls_iovec_init(NULL, 0))) != 0)
+            goto Exit;
+    });
+
+    ret = 0;
+Exit:
+    return ret;
+}
+
+static int send_certificate(ptls_t *tls, ptls_message_emitter_t *emitter,
+                            struct st_ptls_signature_algorithms_t *signature_algorithms, ptls_iovec_t context,
+                            int push_status_request, const uint16_t *compress_algos, size_t num_compress_algos)
+{
+    int ret;
+
+    if (signature_algorithms->count == 0) {
+        ret = PTLS_ALERT_MISSING_EXTENSION;
+        goto Exit;
+    }
+
+    { /* send Certificate (or the equivalent) */
+        static ptls_emit_certificate_t default_emit_certificate = {default_emit_certificate_cb};
+        ptls_emit_certificate_t *emit_certificate =
+            tls->ctx->emit_certificate != NULL ? tls->ctx->emit_certificate : &default_emit_certificate;
+    Redo:
+        if ((ret = emit_certificate->cb(emit_certificate, tls, emitter, tls->key_schedule, context, push_status_request,
+                                        compress_algos, num_compress_algos)) != 0) {
+            if (ret == PTLS_ERROR_DELEGATE) {
+                assert(emit_certificate != &default_emit_certificate);
+                emit_certificate = &default_emit_certificate;
+                goto Redo;
+            }
+            goto Exit;
+        }
+    }
+
+Exit:
+    return ret;
+}
+
+static int send_certificate_verify(ptls_t *tls, ptls_message_emitter_t *emitter,
+                                   struct st_ptls_signature_algorithms_t *signature_algorithms, const char *context_string)
+{
+    size_t start_off = emitter->buf->off;
+    int ret;
+
+    if (tls->ctx->sign_certificate == NULL)
+        return 0;
+    /* build and send CertificateVerify */
+    ptls_push_message(emitter, tls->key_schedule, PTLS_HANDSHAKE_TYPE_CERTIFICATE_VERIFY, {
+        ptls_buffer_t *sendbuf = emitter->buf;
+        size_t algo_off = sendbuf->off;
+        ptls_buffer_push16(sendbuf, 0); /* filled in later */
+        ptls_buffer_push_block(sendbuf, 2, {
+            uint16_t algo;
+            uint8_t data[PTLS_MAX_CERTIFICATE_VERIFY_SIGNDATA_SIZE];
+            size_t datalen = build_certificate_verify_signdata(data, tls->key_schedule, context_string);
+            if ((ret = tls->ctx->sign_certificate->cb(
+                     tls->ctx->sign_certificate, tls, tls->is_server ? &tls->server.async_job : NULL, &algo, sendbuf,
+                     ptls_iovec_init(data, datalen), signature_algorithms != NULL ? signature_algorithms->list : NULL,
+                     signature_algorithms != NULL ? signature_algorithms->count : 0)) != 0) {
+                if (ret == PTLS_ERROR_ASYNC_OPERATION) {
+                    assert(tls->is_server || !"async operation only supported on the server-side");
+                    assert(tls->server.async_job != NULL);
+                    /* Reset the output to the end of the previous handshake message. CertificateVerify will be rebuilt when the
+                     * async operation completes. */
+                    emitter->buf->off = start_off;
+                } else {
+                    assert(tls->server.async_job == NULL);
+                }
+                goto Exit;
+            }
+            assert(tls->server.async_job == NULL);
+            sendbuf->base[algo_off] = (uint8_t)(algo >> 8);
+            sendbuf->base[algo_off + 1] = (uint8_t)algo;
+        });
+    });
+Exit:
+    return ret;
+}
+
+static int client_handle_certificate_request(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;
+    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;
+
+    /* This field SHALL be zero length unless used for the post-handshake authentication exchanges (section 4.3.2) */
+    if (tls->client.certificate_request.context.len != 0)
+        return PTLS_ALERT_ILLEGAL_PARAMETER;
+
+    tls->state = PTLS_STATE_CLIENT_EXPECT_CERTIFICATE;
+    ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len, 0);
+
+    return PTLS_ERROR_IN_PROGRESS;
+}
+
+static int handle_certificate(ptls_t *tls, const uint8_t *src, const uint8_t *end, int *got_certs)
+{
+    ptls_iovec_t certs[16];
+    size_t num_certs = 0;
+    int ret = 0;
+
+    /* certificate request context */
+    ptls_decode_open_block(src, end, 1, {
+        if (src != end) {
+            ret = PTLS_ALERT_ILLEGAL_PARAMETER;
+            goto Exit;
+        }
+    });
+    /* certificate_list */
+    ptls_decode_block(src, end, 3, {
+        while (src != end) {
+            ptls_decode_open_block(src, end, 3, {
+                if (num_certs < PTLS_ELEMENTSOF(certs))
+                    certs[num_certs++] = ptls_iovec_init(src, end - src);
+                src = end;
+            });
+            uint16_t type;
+            decode_open_extensions(src, end, PTLS_HANDSHAKE_TYPE_CERTIFICATE, &type, {
+                if (tls->ctx->on_extension != NULL &&
+                    (ret = tls->ctx->on_extension->cb(tls->ctx->on_extension, tls, PTLS_HANDSHAKE_TYPE_CERTIFICATE, type,
+                                                      ptls_iovec_init(src, end - src)) != 0))
+                    goto Exit;
+                src = end;
+            });
+        }
+    });
+
+    if (tls->ctx->verify_certificate != NULL) {
+        const char *server_name = NULL;
+        if (!ptls_is_server(tls)) {
+            if (tls->ech.offered && !ptls_is_ech_handshake(tls, NULL, 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;
+    }
+
+    *got_certs = num_certs != 0;
+
+Exit:
+    return ret;
+}
+
+static int client_do_handle_certificate(ptls_t *tls, const uint8_t *src, const uint8_t *end)
+{
+    int got_certs, ret;
+
+    if ((ret = handle_certificate(tls, src, end, &got_certs)) != 0)
+        return ret;
+    if (!got_certs)
+        return PTLS_ALERT_ILLEGAL_PARAMETER;
+
+    return 0;
+}
+
+static int client_handle_certificate(ptls_t *tls, ptls_iovec_t message)
+{
+    int ret;
+
+    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, 0);
+
+    tls->state = PTLS_STATE_CLIENT_EXPECT_CERTIFICATE_VERIFY;
+    return PTLS_ERROR_IN_PROGRESS;
+}
+
+static int client_handle_compressed_certificate(ptls_t *tls, ptls_iovec_t message)
+{
+    const uint8_t *src = message.base + PTLS_HANDSHAKE_HEADER_SIZE, *const end = message.base + message.len;
+    uint16_t algo;
+    uint32_t uncompressed_size;
+    uint8_t *uncompressed = NULL;
+    int ret;
+
+    if (tls->ctx->decompress_certificate == NULL) {
+        ret = PTLS_ALERT_UNEXPECTED_MESSAGE;
+        goto Exit;
+    }
+
+    /* decode */
+    if ((ret = ptls_decode16(&algo, &src, end)) != 0)
+        goto Exit;
+    if ((ret = ptls_decode24(&uncompressed_size, &src, end)) != 0)
+        goto Exit;
+    if (uncompressed_size > 65536) { /* TODO find a sensible number */
+        ret = PTLS_ALERT_BAD_CERTIFICATE;
+        goto Exit;
+    }
+    if ((uncompressed = malloc(uncompressed_size)) == NULL) {
+        ret = PTLS_ERROR_NO_MEMORY;
+        goto Exit;
+    }
+    ptls_decode_block(src, end, 3, {
+        if ((ret = tls->ctx->decompress_certificate->cb(tls->ctx->decompress_certificate, tls, algo,
+                                                        ptls_iovec_init(uncompressed, uncompressed_size),
+                                                        ptls_iovec_init(src, end - src))) != 0)
+            goto Exit;
+        src = end;
+    });
+
+    /* handle */
+    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, 0);
+    tls->state = PTLS_STATE_CLIENT_EXPECT_CERTIFICATE_VERIFY;
+    ret = PTLS_ERROR_IN_PROGRESS;
+
+Exit:
+    free(uncompressed);
+    return ret;
+}
+
+static int server_handle_certificate(ptls_t *tls, ptls_iovec_t message)
+{
+    int got_certs, ret;
+
+    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, 0);
+
+    if (got_certs) {
+        tls->state = PTLS_STATE_SERVER_EXPECT_CERTIFICATE_VERIFY;
+    } else {
+        /* Client did not provide certificate, and the verifier says we can fail open. Therefore, the next message is Finished. */
+        tls->state = PTLS_STATE_SERVER_EXPECT_FINISHED;
+    }
+
+    return PTLS_ERROR_IN_PROGRESS;
+}
+
+static int handle_certificate_verify(ptls_t *tls, ptls_iovec_t message, const char *context_string)
+{
+    const uint8_t *src = message.base + PTLS_HANDSHAKE_HEADER_SIZE, *const end = message.base + message.len;
+    uint16_t algo;
+    ptls_iovec_t signature;
+    uint8_t signdata[PTLS_MAX_CERTIFICATE_VERIFY_SIGNDATA_SIZE];
+    size_t signdata_size;
+    int ret;
+
+    /* decode */
+    if ((ret = ptls_decode16(&algo, &src, end)) != 0)
+        goto Exit;
+    ptls_decode_block(src, end, 2, {
+        signature = ptls_iovec_init(src, end - src);
+        src = end;
+    });
+
+    signdata_size = build_certificate_verify_signdata(signdata, tls->key_schedule, context_string);
+    if (tls->certificate_verify.cb != NULL) {
+        ret = tls->certificate_verify.cb(tls->certificate_verify.verify_ctx, algo, ptls_iovec_init(signdata, signdata_size),
+                                         signature);
+    } else {
+        ret = 0;
+    }
+    ptls_clear_memory(signdata, signdata_size);
+    tls->certificate_verify.cb = NULL;
+    if (ret != 0) {
+        goto Exit;
+    }
+
+    ptls__key_schedule_update_hash(tls->key_schedule, message.base, message.len, 0);
+
+Exit:
+    return ret;
+}
+
+static int client_handle_certificate_verify(ptls_t *tls, ptls_iovec_t message)
+{
+    int ret = handle_certificate_verify(tls, message, PTLS_SERVER_CERTIFICATE_VERIFY_CONTEXT_STRING);
+
+    if (ret == 0) {
+        tls->state = PTLS_STATE_CLIENT_EXPECT_FINISHED;
+        ret = PTLS_ERROR_IN_PROGRESS;
+    }
+
+    return ret;
+}
+
+static int server_handle_certificate_verify(ptls_t *tls, ptls_iovec_t message)
+{
+    int ret = handle_certificate_verify(tls, message, PTLS_CLIENT_CERTIFICATE_VERIFY_CONTEXT_STRING);
+
+    if (ret == 0) {
+        tls->state = PTLS_STATE_SERVER_EXPECT_FINISHED;
+        ret = PTLS_ERROR_IN_PROGRESS;
+    }
+
+    return ret;
+}
+
+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 alert_ech_required = tls->ech.offered && !ptls_is_ech_handshake(tls, NULL, NULL, NULL), ret;
+
+    if ((ret = verify_finished(tls, message)) != 0)
+        goto Exit;
+    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)
+        goto Exit;
+    if ((ret = setup_traffic_protection(tls, 0, "s ap traffic", 3, 0)) != 0)
+        goto Exit;
+    if ((ret = derive_secret(tls->key_schedule, send_secret, "c ap traffic")) != 0)
+        goto Exit;
+    if ((ret = derive_exporter_secret(tls, 0)) != 0)
+        goto Exit;
+
+    /* if sending early data, emit EOED and commision the client handshake traffic secret */
+    if (tls->pending_handshake_secret != NULL) {
+        assert(tls->traffic_protection.enc.aead != NULL || tls->ctx->update_traffic_key != NULL);
+        if (tls->client.using_early_data && !tls->ctx->omit_end_of_early_data)
+            ptls_push_message(emitter, tls->key_schedule, PTLS_HANDSHAKE_TYPE_END_OF_EARLY_DATA, {});
+        tls->client.using_early_data = 0;
+        if ((ret = commission_handshake_secret(tls)) != 0)
+            goto Exit;
+    }
+
+    if ((ret = push_change_cipher_spec(tls, emitter)) != 0)
+        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,
+                                          PTLS_CLIENT_CERTIFICATE_VERIFY_CONTEXT_STRING);
+        free(tls->client.certificate_request.context.base);
+        tls->client.certificate_request.context = ptls_iovec_init(NULL, 0);
+        if (ret != 0)
+            goto Exit;
+    }
+
+    ret = send_finished(tls, emitter);
+
+    memcpy(tls->traffic_protection.enc.secret, send_secret, sizeof(send_secret));
+    if ((ret = setup_traffic_protection(tls, 1, NULL, 3, 0)) != 0)
+        goto Exit;
+
+    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;
+}
+
+static int client_handle_new_session_ticket(ptls_t *tls, ptls_iovec_t message)
+{
+    const uint8_t *src = message.base + PTLS_HANDSHAKE_HEADER_SIZE, *const end = message.base + message.len;
+    ptls_iovec_t ticket_nonce;
+    int ret;
+
+    { /* verify the format */
+        uint32_t ticket_lifetime, ticket_age_add, max_early_data_size;
+        ptls_iovec_t ticket;
+        if ((ret = decode_new_session_ticket(tls, &ticket_lifetime, &ticket_age_add, &ticket_nonce, &ticket, &max_early_data_size,
+                                             src, end)) != 0)
+            return ret;
+    }
+
+    /* do nothing if use of session ticket is disabled */
+    if (tls->ctx->save_ticket == NULL)
+        return 0;
+
+    /* save the extension, along with the key of myself */
+    ptls_buffer_t ticket_buf;
+    ptls_buffer_init(&ticket_buf, "", 0);
+    ptls_buffer_push64(&ticket_buf, tls->ctx->get_time->cb(tls->ctx->get_time));
+    ptls_buffer_push16(&ticket_buf, tls->key_share->id);
+    ptls_buffer_push16(&ticket_buf, tls->cipher_suite->id);
+    ptls_buffer_push_block(&ticket_buf, 3, { ptls_buffer_pushv(&ticket_buf, src, end - src); });
+    ptls_buffer_push_block(&ticket_buf, 2, {
+        if ((ret = ptls_buffer_reserve(&ticket_buf, tls->key_schedule->hashes[0].algo->digest_size)) != 0)
+            goto Exit;
+        if ((ret = derive_resumption_secret(tls->key_schedule, ticket_buf.base + ticket_buf.off, ticket_nonce)) != 0)
+            goto Exit;
+        ticket_buf.off += tls->key_schedule->hashes[0].algo->digest_size;
+    });
+
+    if ((ret = tls->ctx->save_ticket->cb(tls->ctx->save_ticket, tls, ptls_iovec_init(ticket_buf.base, ticket_buf.off))) != 0)
+        goto Exit;
+
+    ret = 0;
+Exit:
+    ptls_buffer_dispose(&ticket_buf);
+    return ret;
+}
+
+static int client_hello_decode_server_name(ptls_iovec_t *name, const uint8_t **src, const uint8_t *const end)
+{
+    int ret = 0;
+
+    ptls_decode_open_block(*src, end, 2, {
+        do {
+            uint8_t type;
+            if ((ret = ptls_decode8(&type, src, end)) != 0)
+                goto Exit;
+            ptls_decode_open_block(*src, end, 2, {
+                switch (type) {
+                case PTLS_SERVER_NAME_TYPE_HOSTNAME:
+                    if (memchr(*src, '\0', end - *src) != 0) {
+                        ret = PTLS_ALERT_ILLEGAL_PARAMETER;
+                        goto Exit;
+                    }
+                    *name = ptls_iovec_init(*src, end - *src);
+                    break;
+                default:
+                    break;
+                }
+                *src = end;
+            });
+        } while (*src != end);
+    });
+
+Exit:
+    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)
+{
+    int ret;
+
+    ptls_decode_block(src, end, 2, {
+        while (src != end) {
+            uint16_t group;
+            if ((ret = ptls_decode16(&group, &src, end)) != 0)
+                goto Exit;
+            ptls_key_exchange_algorithm_t **c = candidates;
+            for (; *c != NULL; ++c) {
+                if ((*c)->id == group) {
+                    *selected = *c;
+                    return 0;
+                }
+            }
+        }
+    });
+
+    ret = PTLS_ALERT_HANDSHAKE_FAILURE;
+
+Exit:
+    return ret;
+}
+
+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;
+
+    /* decode protocol version (do not bare to decode something older than TLS 1.0) */
+    if ((ret = ptls_decode16(&ch->legacy_version, &src, end)) != 0)
+        goto Exit;
+    if (ch->legacy_version < 0x0301) {
+        ret = PTLS_ALERT_PROTOCOL_VERSION;
+        goto Exit;
+    }
+
+    /* skip random */
+    if (end - src < PTLS_HELLO_RANDOM_SIZE) {
+        ret = PTLS_ALERT_DECODE_ERROR;
+        goto Exit;
+    }
+    ch->random_bytes = src;
+    src += PTLS_HELLO_RANDOM_SIZE;
+
+    /* skip legacy_session_id */
+    ptls_decode_open_block(src, end, 1, {
+        if (end - src > 32) {
+            ret = PTLS_ALERT_DECODE_ERROR;
+            goto Exit;
+        }
+        ch->legacy_session_id = ptls_iovec_init(src, end - src);
+        src = end;
+    });
+
+    /* decode and select from ciphersuites */
+    ptls_decode_open_block(src, end, 2, {
+        if ((end - src) % 2 != 0) {
+            ret = PTLS_ALERT_DECODE_ERROR;
+            goto Exit;
+        }
+        ch->cipher_suites = ptls_iovec_init(src, end - src);
+        src = end;
+    });
+
+    /* decode legacy_compression_methods */
+    ptls_decode_open_block(src, end, 1, {
+        if (src == end) {
+            ret = PTLS_ALERT_DECODE_ERROR;
+            goto Exit;
+        }
+        ch->compression_methods.ids = src;
+        ch->compression_methods.count = end - src;
+        src = end;
+    });
+
+    /* CH defined in TLS versions below 1.2 might not have extensions (or they might, see what OpenSSL 1.0.0 sends); so bail out
+     * after parsing the main variables. Zero is returned as it is a valid ClientHello. However `ptls_t::selected_version` remains
+     * zero indicating that no compatible version were found. */
+    if (ch->legacy_version < 0x0303 && src == end) {
+        ret = 0;
+        goto Exit;
+    }
+
+    /* decode extensions */
+    ch->first_extension_at = src - start + 2;
+    decode_extensions(src, end, PTLS_HANDSHAKE_TYPE_CLIENT_HELLO, &exttype, {
+        ch->psk.is_last_extension = 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:
+            if ((ret = client_hello_decode_server_name(&ch->server_name, &src, end)) != 0)
+                goto Exit;
+            if (src != end) {
+                ret = PTLS_ALERT_DECODE_ERROR;
+                goto Exit;
+            }
+            break;
+        case PTLS_EXTENSION_TYPE_ALPN:
+            ptls_decode_block(src, end, 2, {
+                do {
+                    ptls_decode_open_block(src, end, 1, {
+                        /* rfc7301 3.1: empty strings MUST NOT be included */
+                        if (src == end) {
+                            ret = PTLS_ALERT_DECODE_ERROR;
+                            goto Exit;
+                        }
+                        if (ch->alpn.count < PTLS_ELEMENTSOF(ch->alpn.list))
+                            ch->alpn.list[ch->alpn.count++] = ptls_iovec_init(src, end - src);
+                        src = end;
+                    });
+                } while (src != end);
+            });
+            break;
+        case PTLS_EXTENSION_TYPE_SERVER_CERTIFICATE_TYPE:
+            ptls_decode_block(src, end, 1, {
+                size_t list_size = end - src;
+
+                /* RFC7250 4.1: No empty list, no list with single x509 element */
+                if (list_size == 0 || (list_size == 1 && *src == PTLS_CERTIFICATE_TYPE_X509)) {
+                    ret = PTLS_ALERT_DECODE_ERROR;
+                    goto Exit;
+                }
+
+                do {
+                    if (ch->server_certificate_types.count < PTLS_ELEMENTSOF(ch->server_certificate_types.list))
+                        ch->server_certificate_types.list[ch->server_certificate_types.count++] = *src;
+                    src++;
+                } while (src != end);
+            });
+            break;
+        case PTLS_EXTENSION_TYPE_COMPRESS_CERTIFICATE:
+            ptls_decode_block(src, end, 1, {
+                do {
+                    uint16_t id;
+                    if ((ret = ptls_decode16(&id, &src, end)) != 0)
+                        goto Exit;
+                    if (ch->cert_compression_algos.count < PTLS_ELEMENTSOF(ch->cert_compression_algos.list))
+                        ch->cert_compression_algos.list[ch->cert_compression_algos.count++] = id;
+                } while (src != end);
+            });
+            break;
+        case PTLS_EXTENSION_TYPE_SUPPORTED_GROUPS:
+            ch->negotiated_groups = ptls_iovec_init(src, end - src);
+            break;
+        case PTLS_EXTENSION_TYPE_SIGNATURE_ALGORITHMS:
+            if ((ret = decode_signature_algorithms(&ch->signature_algorithms, &src, end)) != 0)
+                goto Exit;
+            break;
+        case PTLS_EXTENSION_TYPE_KEY_SHARE:
+            ch->key_shares = ptls_iovec_init(src, end - src);
+            break;
+        case PTLS_EXTENSION_TYPE_SUPPORTED_VERSIONS:
+            ptls_decode_block(src, end, 1, {
+                size_t selected_index = PTLS_ELEMENTSOF(supported_versions);
+                do {
+                    size_t i;
+                    uint16_t v;
+                    if ((ret = ptls_decode16(&v, &src, end)) != 0)
+                        goto Exit;
+                    for (i = 0; i != selected_index; ++i) {
+                        if (supported_versions[i] == v) {
+                            selected_index = i;
+                            break;
+                        }
+                    }
+                } while (src != end);
+                if (selected_index != PTLS_ELEMENTSOF(supported_versions))
+                    ch->selected_version = supported_versions[selected_index];
+            });
+            break;
+        case PTLS_EXTENSION_TYPE_COOKIE:
+            if (properties == NULL || properties->server.cookie.key == NULL) {
+                ret = PTLS_ALERT_ILLEGAL_PARAMETER;
+                goto Exit;
+            }
+            ch->cookie.all = ptls_iovec_init(src, end - src);
+            ptls_decode_block(src, end, 2, {
+                ch->cookie.tbs.base = (void *)src;
+                ptls_decode_open_block(src, end, 2, {
+                    ptls_decode_open_block(src, end, 1, {
+                        ch->cookie.ch1_hash = ptls_iovec_init(src, end - src);
+                        src = end;
+                    });
+                    uint8_t sent_key_share;
+                    if ((ret = ptls_decode8(&sent_key_share, &src, end)) != 0)
+                        goto Exit;
+                    switch (sent_key_share) {
+                    case 0:
+                        assert(!ch->cookie.sent_key_share);
+                        break;
+                    case 1:
+                        ch->cookie.sent_key_share = 1;
+                        break;
+                    default:
+                        ret = PTLS_ALERT_DECODE_ERROR;
+                        goto Exit;
+                    }
+                });
+                ch->cookie.tbs.len = src - ch->cookie.tbs.base;
+                ptls_decode_block(src, end, 1, {
+                    ch->cookie.signature = ptls_iovec_init(src, end - src);
+                    src = end;
+                });
+            });
+            break;
+        case PTLS_EXTENSION_TYPE_PRE_SHARED_KEY: {
+            size_t num_identities = 0;
+            ptls_decode_open_block(src, end, 2, {
+                do {
+                    struct st_ptls_client_hello_psk_t psk = {{NULL}};
+                    ptls_decode_open_block(src, end, 2, {
+                        psk.identity = ptls_iovec_init(src, end - src);
+                        src = end;
+                    });
+                    if ((ret = ptls_decode32(&psk.obfuscated_ticket_age, &src, end)) != 0)
+                        goto Exit;
+                    if (ch->psk.identities.count < PTLS_ELEMENTSOF(ch->psk.identities.list))
+                        ch->psk.identities.list[ch->psk.identities.count++] = psk;
+                    ++num_identities;
+                } while (src != end);
+            });
+            ch->psk.hash_end = src;
+            ptls_decode_block(src, end, 2, {
+                size_t num_binders = 0;
+                do {
+                    ptls_decode_open_block(src, end, 1, {
+                        if (num_binders < ch->psk.identities.count)
+                            ch->psk.identities.list[num_binders].binder = ptls_iovec_init(src, end - src);
+                        src = end;
+                    });
+                    ++num_binders;
+                } while (src != end);
+                if (num_identities != num_binders) {
+                    ret = PTLS_ALERT_ILLEGAL_PARAMETER;
+                    goto Exit;
+                }
+            });
+            ch->psk.is_last_extension = 1;
+        } break;
+        case PTLS_EXTENSION_TYPE_PSK_KEY_EXCHANGE_MODES:
+            ptls_decode_block(src, end, 1, {
+                do {
+                    uint8_t mode;
+                    if ((ret = ptls_decode8(&mode, &src, end)) != 0)
+                        goto Exit;
+                    if (mode < sizeof(ch->psk.ke_modes) * 8)
+                        ch->psk.ke_modes |= 1u << mode;
+                } while (src != end);
+            });
+            break;
+        case PTLS_EXTENSION_TYPE_EARLY_DATA:
+            ch->psk.early_data_indication = 1;
+            break;
+        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 (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;
+        }
+        src = end;
+    });
+
+    ret = 0;
+Exit:
+    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 cipher_suites, 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 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,
+                                                cipher_suites,
+                                                {alpns, num_alpns},
+                                                {sig_algos, num_sig_algos},
+                                                {cert_comp_algos, num_cert_comp_algos},
+                                                {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->cipher_suites, ch->alpn.list,
+                                               ch->alpn.count, 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';
+}
+
+static int try_psk_handshake(ptls_t *tls, size_t *psk_index, int *accept_early_data, struct st_ptls_client_hello_t *ch,
+                             ptls_iovec_t ch_trunc)
+{
+    ptls_buffer_t decbuf;
+    ptls_iovec_t ticket_psk, ticket_server_name, ticket_negotiated_protocol;
+    uint64_t issue_at, now = tls->ctx->get_time->cb(tls->ctx->get_time);
+    uint32_t age_add;
+    uint16_t ticket_key_exchange_id, ticket_csid;
+    uint8_t binder_key[PTLS_MAX_DIGEST_SIZE];
+    int ret;
+
+    ptls_buffer_init(&decbuf, "", 0);
+
+    for (*psk_index = 0; *psk_index < ch->psk.identities.count; ++*psk_index) {
+        struct st_ptls_client_hello_psk_t *identity = ch->psk.identities.list + *psk_index;
+        /* decrypt and decode */
+        int can_accept_early_data = 1;
+        decbuf.off = 0;
+        switch (tls->ctx->encrypt_ticket->cb(tls->ctx->encrypt_ticket, tls, 0, &decbuf, identity->identity)) {
+        case 0: /* decrypted */
+            break;
+        case PTLS_ERROR_REJECT_EARLY_DATA: /* decrypted, but early data is rejected */
+            can_accept_early_data = 0;
+            break;
+        default: /* decryption failure */
+            continue;
+        }
+        if (decode_session_identifier(&issue_at, &ticket_psk, &age_add, &ticket_server_name, &ticket_key_exchange_id, &ticket_csid,
+                                      &ticket_negotiated_protocol, decbuf.base, decbuf.base + decbuf.off) != 0)
+            continue;
+        /* check age */
+        if (now < issue_at)
+            continue;
+        if (now - issue_at > (uint64_t)tls->ctx->ticket_lifetime * 1000)
+            continue;
+        *accept_early_data = 0;
+        if (ch->psk.early_data_indication && can_accept_early_data) {
+            /* accept early-data if abs(diff) between the reported age and the actual age is within += 10 seconds */
+            int64_t delta = (now - issue_at) - (identity->obfuscated_ticket_age - age_add);
+            if (delta < 0)
+                delta = -delta;
+            if (tls->ctx->max_early_data_size != 0 && delta <= PTLS_EARLY_DATA_MAX_DELAY)
+                *accept_early_data = 1;
+        }
+        /* check server-name */
+        if (ticket_server_name.len != 0) {
+            if (tls->server_name == NULL)
+                continue;
+            if (!vec_is_string(ticket_server_name, tls->server_name))
+                continue;
+        } else {
+            if (tls->server_name != NULL)
+                continue;
+        }
+        { /* check key-exchange */
+            ptls_key_exchange_algorithm_t **a;
+            for (a = tls->ctx->key_exchanges; *a != NULL && (*a)->id != ticket_key_exchange_id; ++a)
+                ;
+            if (*a == NULL)
+                continue;
+            tls->key_share = *a;
+        }
+        /* check cipher-suite */
+        if (ticket_csid != tls->cipher_suite->id)
+            continue;
+        /* check negotiated-protocol */
+        if (ticket_negotiated_protocol.len != 0) {
+            if (tls->negotiated_protocol == NULL)
+                continue;
+            if (!vec_is_string(ticket_negotiated_protocol, tls->negotiated_protocol))
+                continue;
+        }
+        /* check the length of the decrypted psk and the PSK binder */
+        if (ticket_psk.len != tls->key_schedule->hashes[0].algo->digest_size)
+            continue;
+        if (ch->psk.identities.list[*psk_index].binder.len != tls->key_schedule->hashes[0].algo->digest_size)
+            continue;
+
+        /* found */
+        goto Found;
+    }
+
+    /* not found */
+    *psk_index = SIZE_MAX;
+    *accept_early_data = 0;
+    tls->key_share = NULL;
+    ret = 0;
+    goto Exit;
+
+Found:
+    if ((ret = key_schedule_extract(tls->key_schedule, ticket_psk)) != 0)
+        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, 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;
+    if (!ptls_mem_equal(ch->psk.identities.list[*psk_index].binder.base, binder_key,
+                        tls->key_schedule->hashes[0].algo->digest_size)) {
+        ret = PTLS_ALERT_DECRYPT_ERROR;
+        goto Exit;
+    }
+    ret = 0;
+
+Exit:
+    ptls_buffer_dispose(&decbuf);
+    ptls_clear_memory(binder_key, sizeof(binder_key));
+    return ret;
+}
+
+static int calc_cookie_signature(ptls_t *tls, ptls_handshake_properties_t *properties,
+                                 ptls_key_exchange_algorithm_t *negotiated_group, ptls_iovec_t tbs, uint8_t *sig)
+{
+    ptls_hash_algorithm_t *algo = tls->ctx->cipher_suites[0]->hash;
+    ptls_hash_context_t *hctx;
+
+    if ((hctx = ptls_hmac_create(algo, properties->server.cookie.key, algo->digest_size)) == NULL)
+        return PTLS_ERROR_NO_MEMORY;
+
+#define UPDATE_BLOCK(p, _len)                                                                                                      \
+    do {                                                                                                                           \
+        size_t len = (_len);                                                                                                       \
+        assert(len < UINT8_MAX);                                                                                                   \
+        uint8_t len8 = (uint8_t)len;                                                                                               \
+        hctx->update(hctx, &len8, 1);                                                                                              \
+        hctx->update(hctx, (p), len);                                                                                              \
+    } while (0)
+#define UPDATE16(_v)                                                                                                               \
+    do {                                                                                                                           \
+        uint16_t v = (_v);                                                                                                         \
+        uint8_t b[2] = {v >> 8, v & 0xff};                                                                                         \
+        hctx->update(hctx, b, 2);                                                                                                  \
+    } while (0)
+
+    UPDATE_BLOCK(tls->client_random, sizeof(tls->client_random));
+    UPDATE_BLOCK(tls->server_name, tls->server_name != NULL ? strlen(tls->server_name) : 0);
+    UPDATE16(tls->cipher_suite->id);
+    UPDATE16(negotiated_group->id);
+    UPDATE_BLOCK(properties->server.cookie.additional_data.base, properties->server.cookie.additional_data.len);
+
+    UPDATE_BLOCK(tbs.base, tbs.len);
+
+#undef UPDATE_BLOCK
+#undef UPDATE16
+
+    hctx->final(hctx, sig, PTLS_HASH_FINAL_MODE_FREE);
+    return 0;
+}
+
+static int certificate_type_exists(uint8_t *list, size_t count, uint8_t desired_type)
+{
+    /* empty type list means that we default to x509 */
+    if (desired_type == PTLS_CERTIFICATE_TYPE_X509 && count == 0)
+        return 1;
+    for (size_t i = 0; i < count; i++) {
+        if (list[i] == desired_type)
+            return 1;
+    }
+    return 0;
+}
+
+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, 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 {                                                                                                                   \
+                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, 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);                                   \
+                          if (_negotiated_group != NULL) {                                                                         \
+                              buffer_push_extension(emitter->buf, PTLS_EXTENSION_TYPE_KEY_SHARE,                                   \
+                                                    { ptls_buffer_push16(emitter->buf, _negotiated_group->id); });                 \
+                          }                                                                                                        \
+                          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){.unknown_extensions = {{UINT16_MAX}}};
+
+    /* decode ClientHello */
+    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 ? 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;
+            }
+        }
+    }
+
+    /* 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 (!is_second_flight) {
+            PTLS_PROBE(ECH_SELECTION, tls, tls->ech.aead != NULL);
+            PTLS_LOG_CONN(ech_selection, tls, { PTLS_LOG_ELEMENT_BOOL(is_ech, tls->ech.aead != NULL); });
+        }
+        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;
+    }
+
+    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) {
+        if (ch->legacy_session_id.len != 0)
+            tls->send_change_cipher_spec = 1;
+        ptls_iovec_t server_name = {NULL};
+        if (ch->server_name.base != NULL)
+            server_name = ch->server_name;
+        if ((ret = call_on_client_hello_cb(tls, server_name, message, ch->cipher_suites, 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->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)) {
+            ret = PTLS_ALERT_UNSUPPORTED_CERTIFICATE;
+            goto Exit;
+        }
+    } else {
+        if (ch->psk.early_data_indication) {
+            ret = PTLS_ALERT_DECODE_ERROR;
+            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) {
+            size_t l = strlen(tls->server_name);
+            if (!(ch->server_name.len == l && memcmp(ch->server_name.base, tls->server_name, l) == 0)) {
+                ret = PTLS_ALERT_HANDSHAKE_FAILURE;
+                goto Exit;
+            }
+        }
+    }
+
+    { /* select (or check) cipher-suite, create key_schedule */
+        ptls_cipher_suite_t *cs;
+        if ((ret = select_cipher(&cs, tls->ctx->cipher_suites, ch->cipher_suites.base,
+                                 ch->cipher_suites.base + ch->cipher_suites.len, tls->ctx->server_cipher_preference, tls->ctx->server_cipher_chacha_priority)) != 0)
+            goto Exit;
+        if (!is_second_flight) {
+            tls->cipher_suite = cs;
+            tls->key_schedule = key_schedule_new(cs, NULL, 0);
+        } else {
+            if (tls->cipher_suite != cs) {
+                ret = PTLS_ALERT_HANDSHAKE_FAILURE;
+                goto Exit;
+            }
+        }
+    }
+
+    /* select key_share */
+    if (key_share.algorithm == NULL && ch->key_shares.base != NULL) {
+        const uint8_t *src = ch->key_shares.base, *const end = src + ch->key_shares.len;
+        ptls_decode_block(src, end, 2, {
+            if ((ret = select_key_share(&key_share.algorithm, &key_share.peer_key, tls->ctx->key_exchanges, &src, end, 0)) != 0)
+                goto Exit;
+        });
+    }
+
+    if (!is_second_flight) {
+        if (ch->cookie.all.len != 0 && key_share.algorithm != NULL) {
+
+            { /* use cookie to check the integrity of the handshake, and update the context */
+                uint8_t sig[PTLS_MAX_DIGEST_SIZE];
+                size_t sigsize = tls->ctx->cipher_suites[0]->hash->digest_size;
+                if ((ret = calc_cookie_signature(tls, properties, key_share.algorithm, ch->cookie.tbs, sig)) != 0)
+                    goto Exit;
+                if (!(ch->cookie.signature.len == sigsize && ptls_mem_equal(ch->cookie.signature.base, sig, sigsize))) {
+                    ret = PTLS_ALERT_HANDSHAKE_FAILURE;
+                    goto Exit;
+                }
+            }
+            /* 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, 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);
+                                         });
+                                     },
+                                     {});
+            emitter->buf->off = hrr_start;
+            is_second_flight = 1;
+
+        } else if (key_share.algorithm == NULL || (properties != NULL && properties->server.enforce_retry)) {
+
+            /* send HelloRetryRequest  */
+            if (ch->negotiated_groups.base == NULL) {
+                ret = PTLS_ALERT_MISSING_EXTENSION;
+                goto Exit;
+            }
+            ptls_key_exchange_algorithm_t *negotiated_group;
+            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, 0);
+            assert(tls->key_schedule->generation == 0);
+
+            /* 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, 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;
+                    if (ptls_is_ech_handshake(tls, NULL, 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, {
+                                /* 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->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 {
+                tls->state = PTLS_STATE_SERVER_EXPECT_SECOND_CLIENT_HELLO;
+                if (ch->psk.early_data_indication)
+                    tls->server.early_data_skipped_bytes = 0;
+                ret = PTLS_ERROR_IN_PROGRESS;
+            }
+            goto Exit;
+        }
+    }
+
+    /* handle unknown extensions */
+    if ((ret = report_unknown_extensions(tls, properties, ch->unknown_extensions)) != 0)
+        goto Exit;
+
+    /* try psk handshake */
+    if (!is_second_flight && ch->psk.hash_end != 0 &&
+        (ch->psk.ke_modes & ((1u << PTLS_PSK_KE_MODE_PSK) | (1u << PTLS_PSK_KE_MODE_PSK_DHE))) != 0 &&
+        tls->ctx->encrypt_ticket != NULL && !tls->ctx->require_client_authentication) {
+        if ((ret = try_psk_handshake(tls, &psk_index, &accept_early_data, ch,
+                                     ptls_iovec_init(message.base, ch->psk.hash_end - message.base))) != 0) {
+            goto Exit;
+        }
+    }
+
+    /* If client authentication is enabled, we always force a full handshake.
+     * TODO: Check for `post_handshake_auth` extension and if that is present, do not force full handshake!
+     *       Remove also the check `!require_client_authentication` above.
+     *
+     * 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, 0);
+        if (!is_second_flight) {
+            assert(tls->key_schedule->generation == 0);
+            key_schedule_extract(tls->key_schedule, ptls_iovec_init(NULL, 0));
+        }
+        mode = HANDSHAKE_MODE_FULL;
+        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, 0);
+        if ((ch->psk.ke_modes & (1u << PTLS_PSK_KE_MODE_PSK)) != 0) {
+            mode = HANDSHAKE_MODE_PSK;
+        } else {
+            assert((ch->psk.ke_modes & (1u << PTLS_PSK_KE_MODE_PSK_DHE)) != 0);
+            mode = HANDSHAKE_MODE_PSK_DHE;
+        }
+        tls->is_psk_handshake = 1;
+        if (properties != NULL) {
+            ptls_iovec_t *selected = &ch->psk.identities.list[psk_index].binder;
+            memcpy(properties->server.selected_psk_binder.base, selected->base, selected->len);
+            properties->server.selected_psk_binder.len = selected->len;
+        }
+    }
+    tls->server.can_send_session_ticket = ch->psk.ke_modes != 0;
+
+    if (accept_early_data && tls->ctx->max_early_data_size != 0 && psk_index == 0) {
+        if ((tls->pending_handshake_secret = malloc(PTLS_MAX_DIGEST_SIZE)) == NULL) {
+            ret = PTLS_ERROR_NO_MEMORY;
+            goto Exit;
+        }
+        if ((ret = derive_exporter_secret(tls, 1)) != 0)
+            goto Exit;
+        if ((ret = setup_traffic_protection(tls, 0, "c e traffic", 1, 0)) != 0)
+            goto Exit;
+    }
+
+    /* run key-exchange, to obtain pubkey and secret */
+    if (mode != HANDSHAKE_MODE_PSK) {
+        if (key_share.algorithm == NULL) {
+            ret = ch->key_shares.base != NULL ? PTLS_ALERT_HANDSHAKE_FAILURE : PTLS_ALERT_MISSING_EXTENSION;
+            goto Exit;
+        }
+        if ((ret = key_share.algorithm->exchange(key_share.algorithm, &pubkey, &ecdh_secret, key_share.peer_key)) != 0)
+            goto Exit;
+        tls->key_share = key_share.algorithm;
+    }
+
+    { /* 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, 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;
+
+    /* create protection contexts for the handshake */
+    assert(tls->key_schedule->generation == 1);
+    key_schedule_extract(tls->key_schedule, ecdh_secret);
+    if ((ret = setup_traffic_protection(tls, 1, "s hs traffic", 2, 0)) != 0)
+        goto Exit;
+    if (tls->pending_handshake_secret != NULL) {
+        if ((ret = derive_secret(tls->key_schedule, tls->pending_handshake_secret, "c hs traffic")) != 0)
+            goto Exit;
+        if (tls->ctx->update_traffic_key != NULL &&
+            (ret = tls->ctx->update_traffic_key->cb(tls->ctx->update_traffic_key, tls, 0, 2, tls->pending_handshake_secret)) != 0)
+            goto Exit;
+    } else {
+        if ((ret = setup_traffic_protection(tls, 0, "c hs traffic", 2, 0)) != 0)
+            goto Exit;
+        if (ch->psk.early_data_indication)
+            tls->server.early_data_skipped_bytes = 0;
+    }
+
+    /* send EncryptedExtensions */
+    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->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, {});
+            }
+            if (tls->ctx->use_raw_public_keys) {
+                buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_SERVER_CERTIFICATE_TYPE,
+                                      { ptls_buffer_push(sendbuf, PTLS_CERTIFICATE_TYPE_RAW_PUBLIC_KEY); });
+            }
+            if (tls->negotiated_protocol != NULL) {
+                buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_ALPN, {
+                    ptls_buffer_push_block(sendbuf, 2, {
+                        ptls_buffer_push_block(sendbuf, 1, {
+                            ptls_buffer_pushv(sendbuf, tls->negotiated_protocol, strlen(tls->negotiated_protocol));
+                        });
+                    });
+                });
+            }
+            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, 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;
+        });
+    });
+
+    if (mode == HANDSHAKE_MODE_FULL) {
+        /* send certificate request if client authentication is activated */
+        if (tls->ctx->require_client_authentication) {
+            ptls_push_message(emitter, tls->key_schedule, PTLS_HANDSHAKE_TYPE_CERTIFICATE_REQUEST, {
+                /* certificate_request_context, this field SHALL be zero length, unless the certificate
+                 * request is used for post-handshake authentication.
+                 */
+                ptls_buffer_t *sendbuf = emitter->buf;
+                ptls_buffer_push(sendbuf, 0);
+                /* extensions */
+                ptls_buffer_push_block(sendbuf, 2, {
+                    buffer_push_extension(sendbuf, PTLS_EXTENSION_TYPE_SIGNATURE_ALGORITHMS, {
+                        if ((ret = push_signature_algorithms(tls->ctx->verify_certificate, sendbuf)) != 0)
+                            goto Exit;
+                    });
+                });
+            });
+
+            if (ret != 0) {
+                goto Exit;
+            }
+        }
+
+        /* send certificate */
+        if ((ret = send_certificate(tls, emitter, &ch->signature_algorithms, ptls_iovec_init(NULL, 0), ch->status_request,
+                                    ch->cert_compression_algos.list, ch->cert_compression_algos.count)) != 0)
+            goto Exit;
+        /* send certificateverify, finished, and complete the handshake */
+        if ((ret = server_finish_handshake(tls, emitter, 1, &ch->signature_algorithms)) != 0)
+            goto Exit;
+    } else {
+        /* send finished, and complete the handshake */
+        if ((ret = server_finish_handshake(tls, emitter, 0, NULL)) != 0)
+            goto Exit;
+    }
+
+Exit:
+    free(pubkey.base);
+    if (ecdh_secret.base != NULL) {
+        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;
+
+#undef EMIT_SERVER_HELLO
+#undef EMIT_HELLO_RETRY_REQUEST
+}
+
+static int server_finish_handshake(ptls_t *tls, ptls_message_emitter_t *emitter, int send_cert_verify,
+                                   struct st_ptls_signature_algorithms_t *signature_algorithms)
+{
+    int ret;
+
+    if (send_cert_verify) {
+        if ((ret = send_certificate_verify(tls, emitter, signature_algorithms, PTLS_SERVER_CERTIFICATE_VERIFY_CONTEXT_STRING)) !=
+            0) {
+            if (ret == PTLS_ERROR_ASYNC_OPERATION) {
+                tls->state = PTLS_STATE_SERVER_GENERATING_CERTIFICATE_VERIFY;
+            }
+            goto Exit;
+        }
+    }
+
+    if ((ret = send_finished(tls, emitter)) != 0)
+        goto Exit;
+
+    assert(tls->key_schedule->generation == 2);
+    if ((ret = key_schedule_extract(tls->key_schedule, ptls_iovec_init(NULL, 0))) != 0)
+        goto Exit;
+    if ((ret = setup_traffic_protection(tls, 1, "s ap traffic", 3, 0)) != 0)
+        goto Exit;
+    if ((ret = derive_secret(tls->key_schedule, tls->server.pending_traffic_secret, "c ap traffic")) != 0)
+        goto Exit;
+    if ((ret = derive_exporter_secret(tls, 0)) != 0)
+        goto Exit;
+
+    if (tls->pending_handshake_secret != NULL) {
+        if (tls->ctx->omit_end_of_early_data) {
+            if ((ret = commission_handshake_secret(tls)) != 0)
+                goto Exit;
+            tls->state = PTLS_STATE_SERVER_EXPECT_FINISHED;
+        } else {
+            tls->state = PTLS_STATE_SERVER_EXPECT_END_OF_EARLY_DATA;
+        }
+    } else if (tls->ctx->require_client_authentication) {
+        tls->state = PTLS_STATE_SERVER_EXPECT_CERTIFICATE;
+    } else {
+        tls->state = PTLS_STATE_SERVER_EXPECT_FINISHED;
+    }
+
+    /* send session ticket if necessary */
+    if (tls->server.can_send_session_ticket && tls->ctx->ticket_lifetime != 0) {
+        if ((ret = send_session_ticket(tls, emitter)) != 0)
+            goto Exit;
+    }
+
+    if (tls->ctx->require_client_authentication) {
+        ret = PTLS_ERROR_IN_PROGRESS;
+    } else {
+        ret = 0;
+    }
+
+Exit:
+    return ret;
+}
+
+static int server_handle_end_of_early_data(ptls_t *tls, ptls_iovec_t message)
+{
+    int ret;
+
+    if ((ret = commission_handshake_secret(tls)) != 0)
+        goto Exit;
+
+    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;
+
+Exit:
+    return ret;
+}
+
+static int server_handle_finished(ptls_t *tls, ptls_iovec_t message)
+{
+    int ret;
+
+    if ((ret = verify_finished(tls, message)) != 0)
+        return ret;
+
+    memcpy(tls->traffic_protection.dec.secret, tls->server.pending_traffic_secret, sizeof(tls->server.pending_traffic_secret));
+    ptls_clear_memory(tls->server.pending_traffic_secret, sizeof(tls->server.pending_traffic_secret));
+    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, 0);
+
+    tls->state = PTLS_STATE_SERVER_POST_HANDSHAKE;
+    return 0;
+}
+
+static int update_traffic_key(ptls_t *tls, int is_enc)
+{
+    struct st_ptls_traffic_protection_t *tp = is_enc ? &tls->traffic_protection.enc : &tls->traffic_protection.dec;
+    uint8_t secret[PTLS_MAX_DIGEST_SIZE];
+    int ret;
+
+    ptls_hash_algorithm_t *hash = tls->key_schedule->hashes[0].algo;
+    if ((ret = ptls_hkdf_expand_label(hash, secret, hash->digest_size, ptls_iovec_init(tp->secret, hash->digest_size),
+                                      "traffic upd", ptls_iovec_init(NULL, 0), NULL)) != 0)
+        goto Exit;
+    memcpy(tp->secret, secret, sizeof(secret));
+    ret = setup_traffic_protection(tls, is_enc, NULL, 3, 1);
+
+Exit:
+    ptls_clear_memory(secret, sizeof(secret));
+    return ret;
+}
+
+static int handle_key_update(ptls_t *tls, ptls_message_emitter_t *emitter, ptls_iovec_t message)
+{
+    const uint8_t *src = message.base + PTLS_HANDSHAKE_HEADER_SIZE, *const end = message.base + message.len;
+    int ret;
+
+    /* validate */
+    if (end - src != 1 || *src > 1)
+        return PTLS_ALERT_DECODE_ERROR;
+
+    /* update receive key */
+    if ((ret = update_traffic_key(tls, 0)) != 0)
+        return ret;
+
+    if (*src) {
+        if (tls->ctx->update_traffic_key != NULL)
+            return PTLS_ALERT_UNEXPECTED_MESSAGE;
+        tls->needs_key_update = 1;
+    }
+
+    return 0;
+}
+
+static int parse_record_header(struct st_ptls_record_t *rec, const uint8_t *src)
+{
+    rec->type = src[0];
+    rec->version = ntoh16(src + 1);
+    rec->length = ntoh16(src + 3);
+
+    if (rec->length >
+        (size_t)(rec->type == PTLS_CONTENT_TYPE_APPDATA ? PTLS_MAX_ENCRYPTED_RECORD_SIZE : PTLS_MAX_PLAINTEXT_RECORD_SIZE))
+        return PTLS_ALERT_DECODE_ERROR;
+
+    return 0;
+}
+
+static int parse_record(ptls_t *tls, struct st_ptls_record_t *rec, const uint8_t *src, size_t *len)
+{
+    int ret;
+
+    assert(*len != 0);
+
+    /* Check if the first byte is something that we can handle, otherwise do not bother parsing / buffering the entire record as it
+     * is obviously broken. SSL 2.0 handshakes fall into this path as well. */
+    if (tls->recvbuf.rec.base == NULL) {
+        uint8_t type = src[0];
+        switch (type) {
+        case PTLS_CONTENT_TYPE_CHANGE_CIPHER_SPEC:
+        case PTLS_CONTENT_TYPE_ALERT:
+        case PTLS_CONTENT_TYPE_HANDSHAKE:
+        case PTLS_CONTENT_TYPE_APPDATA:
+            break;
+        default:
+            return PTLS_ALERT_DECODE_ERROR;
+        }
+    }
+
+    if (tls->recvbuf.rec.base == NULL && *len >= 5) {
+        /* fast path */
+        if ((ret = parse_record_header(rec, src)) != 0)
+            return ret;
+        if (5 + rec->length <= *len) {
+            rec->fragment = src + 5;
+            *len = rec->length + 5;
+            return 0;
+        }
+    }
+
+    /* slow path */
+    const uint8_t *const end = src + *len;
+    *rec = (struct st_ptls_record_t){0};
+
+    if (tls->recvbuf.rec.base == NULL) {
+        ptls_buffer_init(&tls->recvbuf.rec, "", 0);
+        if ((ret = ptls_buffer_reserve(&tls->recvbuf.rec, 5)) != 0)
+            return ret;
+    }
+
+    /* fill and parse the header */
+    while (tls->recvbuf.rec.off < 5) {
+        if (src == end)
+            return PTLS_ERROR_IN_PROGRESS;
+        tls->recvbuf.rec.base[tls->recvbuf.rec.off++] = *src++;
+    }
+    if ((ret = parse_record_header(rec, tls->recvbuf.rec.base)) != 0)
+        return ret;
+
+    /* fill the fragment */
+    size_t addlen = rec->length + 5 - tls->recvbuf.rec.off;
+    if (addlen != 0) {
+        if ((ret = ptls_buffer_reserve(&tls->recvbuf.rec, addlen)) != 0)
+            return ret;
+        if (addlen > (size_t)(end - src))
+            addlen = end - src;
+        if (addlen != 0) {
+            memcpy(tls->recvbuf.rec.base + tls->recvbuf.rec.off, src, addlen);
+            tls->recvbuf.rec.off += addlen;
+            src += addlen;
+        }
+    }
+
+    /* set rec->fragment if a complete record has been parsed */
+    if (tls->recvbuf.rec.off == rec->length + 5) {
+        rec->fragment = tls->recvbuf.rec.base + 5;
+        ret = 0;
+    } else {
+        ret = PTLS_ERROR_IN_PROGRESS;
+    }
+
+    *len -= end - src;
+    return ret;
+}
+
+static void update_open_count(ptls_context_t *ctx, ssize_t delta)
+{
+    if (ctx->update_open_count != NULL)
+        ctx->update_open_count->cb(ctx->update_open_count, delta);
+}
+
+static ptls_t *new_instance(ptls_context_t *ctx, int is_server)
+{
+    ptls_t *tls;
+
+    assert(ctx->get_time != NULL && "please set ctx->get_time to `&ptls_get_time`; see #92");
+
+    if ((tls = malloc(sizeof(*tls))) == NULL)
+        return NULL;
+
+    update_open_count(ctx, 1);
+    *tls = (ptls_t){ctx};
+    tls->is_server = is_server;
+    tls->send_change_cipher_spec = ctx->send_change_cipher_spec;
+    tls->skip_tracing = ptls_default_skip_tracing;
+    return tls;
+}
+
+ptls_t *ptls_client_new(ptls_context_t *ctx)
+{
+    ptls_t *tls = new_instance(ctx, 0);
+    tls->state = PTLS_STATE_CLIENT_HANDSHAKE_START;
+    tls->ctx->random_bytes(tls->client_random, sizeof(tls->client_random));
+    log_client_random(tls);
+    if (tls->send_change_cipher_spec) {
+        tls->client.legacy_session_id =
+            ptls_iovec_init(tls->client.legacy_session_id_buf, sizeof(tls->client.legacy_session_id_buf));
+        tls->ctx->random_bytes(tls->client.legacy_session_id.base, tls->client.legacy_session_id.len);
+    }
+
+    PTLS_PROBE(NEW, tls, 0);
+    PTLS_LOG_CONN(new, tls, { PTLS_LOG_ELEMENT_BOOL(is_server, 0); });
+    return tls;
+}
+
+ptls_t *ptls_server_new(ptls_context_t *ctx)
+{
+    ptls_t *tls = new_instance(ctx, 1);
+    tls->state = PTLS_STATE_SERVER_EXPECT_CLIENT_HELLO;
+    tls->server.early_data_skipped_bytes = UINT32_MAX;
+
+    PTLS_PROBE(NEW, tls, 1);
+    PTLS_LOG_CONN(new, tls, { PTLS_LOG_ELEMENT_BOOL(is_server, 1); });
+    return tls;
+}
+
+static int export_tls12_params(ptls_buffer_t *output, int is_server, int session_reused, ptls_cipher_suite_t *cipher,
+                               const void *client_random, const char *server_name, ptls_iovec_t negotiated_protocol,
+                               const void *enc_key, const void *enc_iv, uint64_t enc_seq, uint64_t enc_record_iv,
+                               const void *dec_key, const void *dec_iv, uint64_t dec_seq)
+{
+    int ret;
+
+    ptls_buffer_push_block(output, 2, {
+        ptls_buffer_push(output, is_server);
+        ptls_buffer_push(output, session_reused);
+        ptls_buffer_push16(output, PTLS_PROTOCOL_VERSION_TLS12);
+        ptls_buffer_push16(output, cipher->id);
+        ptls_buffer_pushv(output, client_random, PTLS_HELLO_RANDOM_SIZE);
+        ptls_buffer_push_block(output, 2, {
+            size_t len = server_name != NULL ? strlen(server_name) : 0;
+            ptls_buffer_pushv(output, server_name, len);
+        });
+        ptls_buffer_push_block(output, 2, { ptls_buffer_pushv(output, negotiated_protocol.base, negotiated_protocol.len); });
+        ptls_buffer_push_block(output, 2, {
+            ptls_buffer_pushv(output, enc_key, cipher->aead->key_size);
+            ptls_buffer_pushv(output, enc_iv, cipher->aead->tls12.fixed_iv_size);
+            ptls_buffer_push64(output, enc_seq);
+            if (cipher->aead->tls12.record_iv_size != 0)
+                ptls_buffer_push64(output, enc_record_iv);
+            ptls_buffer_pushv(output, dec_key, cipher->aead->key_size);
+            ptls_buffer_pushv(output, dec_iv, cipher->aead->tls12.fixed_iv_size);
+            ptls_buffer_push64(output, dec_seq);
+        });
+        ptls_buffer_push_block(output, 2, {}); /* for future extensions */
+    });
+
+Exit:
+    return ret;
+}
+
+int ptls_build_tls12_export_params(ptls_context_t *ctx, ptls_buffer_t *output, int is_server, int session_reused,
+                                   ptls_cipher_suite_t *cipher, const void *master_secret, const void *hello_randoms,
+                                   uint64_t next_send_record_iv, const char *server_name, ptls_iovec_t negotiated_protocol)
+{
+    assert(cipher->aead->tls12.fixed_iv_size + cipher->aead->tls12.record_iv_size != 0 || !"given cipher-suite supports TLS/1.2");
+
+    uint8_t key_block[(PTLS_MAX_SECRET_SIZE + PTLS_MAX_IV_SIZE) * 2];
+    size_t key_block_len = (cipher->aead->key_size + cipher->aead->tls12.fixed_iv_size) * 2;
+    int ret;
+
+    assert(key_block_len <= sizeof(key_block));
+
+    /* generate key block */
+    if ((ret =
+             ptls_tls12_phash(cipher->hash, key_block, key_block_len, ptls_iovec_init(master_secret, PTLS_TLS12_MASTER_SECRET_SIZE),
+                              "key expansion", ptls_iovec_init(hello_randoms, PTLS_HELLO_RANDOM_SIZE * 2))) != 0)
+        goto Exit;
+
+    /* determine key locations */
+    struct {
+        const void *key;
+        const void *iv;
+    } client_secret, server_secret, *enc_secret = is_server ? &server_secret : &client_secret,
+                                    *dec_secret = is_server ? &client_secret : &server_secret;
+    client_secret.key = key_block;
+    server_secret.key = key_block + cipher->aead->key_size;
+    client_secret.iv = key_block + cipher->aead->key_size * 2;
+    server_secret.iv = key_block + cipher->aead->key_size * 2 + cipher->aead->tls12.fixed_iv_size;
+
+    /* Serialize prams. Sequence number of the first application record is 1, because Finished is the only message sent after
+     * ChangeCipherSpec. */
+    ret = export_tls12_params(output, is_server, session_reused, cipher, (uint8_t *)hello_randoms + PTLS_HELLO_RANDOM_SIZE,
+                              server_name, negotiated_protocol, enc_secret->key, enc_secret->iv, 1, next_send_record_iv,
+                              dec_secret->key, dec_secret->iv, 1);
+
+Exit:
+    ptls_clear_memory(key_block, sizeof(key_block));
+    return ret;
+}
+
+int ptls_export(ptls_t *tls, ptls_buffer_t *output)
+{
+    /* TODO add tls13 support */
+    if (!tls->traffic_protection.enc.tls12)
+        return PTLS_ERROR_LIBRARY;
+
+    ptls_iovec_t negotiated_protocol =
+        ptls_iovec_init(tls->negotiated_protocol, tls->negotiated_protocol != NULL ? strlen(tls->negotiated_protocol) : 0);
+    return export_tls12_params(output, tls->is_server, tls->is_psk_handshake, tls->cipher_suite, tls->client_random,
+                               tls->server_name, negotiated_protocol, tls->traffic_protection.enc.secret,
+                               tls->traffic_protection.enc.secret + PTLS_MAX_SECRET_SIZE, tls->traffic_protection.enc.seq,
+                               tls->traffic_protection.enc.tls12_enc_record_iv, tls->traffic_protection.dec.secret,
+                               tls->traffic_protection.dec.secret + PTLS_MAX_SECRET_SIZE, tls->traffic_protection.dec.seq);
+}
+
+static int build_tls12_traffic_protection(ptls_t *tls, int is_enc, const uint8_t **src, const uint8_t *const end)
+{
+    struct st_ptls_traffic_protection_t *tp = is_enc ? &tls->traffic_protection.enc : &tls->traffic_protection.dec;
+
+    if ((size_t)(end - *src) < tls->cipher_suite->aead->key_size + tls->cipher_suite->aead->tls12.fixed_iv_size + sizeof(uint64_t))
+        return PTLS_ALERT_DECODE_ERROR;
+
+    /* set properties */
+    memcpy(tp->secret, *src, tls->cipher_suite->aead->key_size);
+    *src += tls->cipher_suite->aead->key_size;
+    memcpy(tp->secret + PTLS_MAX_SECRET_SIZE, *src, tls->cipher_suite->aead->tls12.fixed_iv_size);
+    *src += tls->cipher_suite->aead->tls12.fixed_iv_size;
+    if (ptls_decode64(&tp->seq, src, end) != 0)
+        return PTLS_ALERT_DECODE_ERROR;
+    if (is_enc && tls->cipher_suite->aead->tls12.record_iv_size != 0) {
+        if (ptls_decode64(&tp->tls12_enc_record_iv, src, end) != 0)
+            return PTLS_ALERT_DECODE_ERROR;
+    }
+    tp->tls12 = 1;
+
+    /* instantiate aead */
+    if ((tp->aead = ptls_aead_new_direct(tls->cipher_suite->aead, is_enc, tp->secret, tp->secret + PTLS_MAX_SECRET_SIZE)) == NULL)
+        return PTLS_ERROR_NO_MEMORY;
+
+    return 0;
+}
+
+int ptls_import(ptls_context_t *ctx, ptls_t **tls, ptls_iovec_t params)
+{
+    const uint8_t *src = params.base, *const end = src + params.len;
+    uint16_t protocol_version, csid;
+    int ret;
+
+    *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) {
+            ret = PTLS_ALERT_DECODE_ERROR;
+            goto Exit;
+        }
+        if ((*tls = new_instance(ctx, *src++)) == NULL) {
+            ret = PTLS_ERROR_NO_MEMORY;
+            goto Exit;
+        }
+        (*tls)->is_psk_handshake = *src++;
+        /* determine protocol version and cipher suite */
+        if ((ret = ptls_decode16(&protocol_version, &src, end)) != 0)
+            goto Exit;
+        if ((ret = ptls_decode16(&csid, &src, end)) != 0)
+            goto Exit;
+        (*tls)->cipher_suite = ptls_find_cipher_suite(ctx->tls12_cipher_suites, csid);
+        if ((*tls)->cipher_suite == NULL) {
+            ret = PTLS_ALERT_HANDSHAKE_FAILURE;
+            goto Exit;
+        }
+        /* other version-independent stuff */
+        if (end - src < PTLS_HELLO_RANDOM_SIZE) {
+            ret = PTLS_ALERT_DECODE_ERROR;
+            goto Exit;
+        }
+        memcpy((*tls)->client_random, src, PTLS_HELLO_RANDOM_SIZE);
+        src += PTLS_HELLO_RANDOM_SIZE;
+        ptls_decode_open_block(src, end, 2, {
+            if (src != end) {
+                if ((ret = ptls_set_server_name(*tls, (const char *)src, end - src)) != 0)
+                    goto Exit;
+                src = end;
+            }
+        });
+        ptls_decode_open_block(src, end, 2, {
+            if (src != end) {
+                if ((ret = ptls_set_negotiated_protocol(*tls, (const char *)src, end - src)) != 0)
+                    goto Exit;
+                src = end;
+            }
+        });
+        /* version-dependent stuff */
+        ptls_decode_open_block(src, end, 2, {
+            switch (protocol_version) {
+            case PTLS_PROTOCOL_VERSION_TLS12:
+                /* setup AEAD keys */
+                if ((ret = build_tls12_traffic_protection(*tls, 1, &src, end)) != 0)
+                    goto Exit;
+                if ((ret = build_tls12_traffic_protection(*tls, 0, &src, end)) != 0)
+                    goto Exit;
+                break;
+            default:
+                ret = PTLS_ALERT_ILLEGAL_PARAMETER;
+                break;
+            }
+        });
+        /* extensions */
+        ptls_decode_open_block(src, end, 2, {
+            src = end; /* unused */
+        });
+    });
+
+    (*tls)->state = ptls_is_server(*tls) ? PTLS_STATE_SERVER_POST_HANDSHAKE : PTLS_STATE_CLIENT_POST_HANDSHAKE;
+
+Exit:
+    if (ret != 0) {
+        if (*tls != NULL) {
+            ptls_free(*tls);
+            *tls = NULL;
+        }
+    }
+    return ret;
+}
+
+void ptls_free(ptls_t *tls)
+{
+    PTLS_PROBE0(FREE, tls);
+    PTLS_LOG_CONN(free, tls, {});
+
+    ptls_buffer_dispose(&tls->recvbuf.rec);
+    ptls_buffer_dispose(&tls->recvbuf.mess);
+    free_exporter_master_secret(tls, 1);
+    free_exporter_master_secret(tls, 0);
+    if (tls->key_schedule != NULL)
+        key_schedule_free(tls->key_schedule);
+    if (tls->traffic_protection.dec.aead != NULL)
+        ptls_aead_free(tls->traffic_protection.dec.aead);
+    if (tls->traffic_protection.enc.aead != NULL)
+        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);
+    } else {
+        if (tls->client.key_share_ctx != NULL)
+            tls->client.key_share_ctx->on_exchange(&tls->client.key_share_ctx, 1, NULL, ptls_iovec_init(NULL, 0));
+        if (tls->client.certificate_request.context.base != NULL)
+            free(tls->client.certificate_request.context.base);
+    }
+    if (tls->certificate_verify.cb != NULL)
+        tls->certificate_verify.cb(tls->certificate_verify.verify_ctx, 0, ptls_iovec_init(NULL, 0), ptls_iovec_init(NULL, 0));
+    if (tls->pending_handshake_secret != NULL) {
+        ptls_clear_memory(tls->pending_handshake_secret, PTLS_MAX_DIGEST_SIZE);
+        free(tls->pending_handshake_secret);
+    }
+    update_open_count(tls->ctx, -1);
+    ptls_clear_memory(tls, sizeof(*tls));
+    free(tls);
+}
+
+ptls_context_t *ptls_get_context(ptls_t *tls)
+{
+    return tls->ctx;
+}
+
+void ptls_set_context(ptls_t *tls, ptls_context_t *ctx)
+{
+    update_open_count(ctx, 1);
+    update_open_count(tls->ctx, -1);
+    tls->ctx = ctx;
+}
+
+ptls_async_job_t *ptls_get_async_job(ptls_t *tls)
+{
+    return tls->server.async_job;
+}
+
+ptls_iovec_t ptls_get_client_random(ptls_t *tls)
+{
+    return ptls_iovec_init(tls->client_random, PTLS_HELLO_RANDOM_SIZE);
+}
+
+ptls_cipher_suite_t *ptls_get_cipher(ptls_t *tls)
+{
+    return tls->cipher_suite;
+}
+
+uint16_t ptls_get_protocol_version(ptls_t *tls)
+{
+    if (tls->traffic_protection.enc.tls12)
+        return PTLS_PROTOCOL_VERSION_TLS12;
+
+    return PTLS_PROTOCOL_VERSION_TLS13;
+}
+
+int ptls_get_traffic_keys(ptls_t *tls, int is_enc, uint8_t *key, uint8_t *iv, uint64_t *seq)
+{
+    struct st_ptls_traffic_protection_t *ctx = is_enc ? &tls->traffic_protection.enc : &tls->traffic_protection.dec;
+    int ret;
+
+    if ((ret = get_traffic_keys(tls->cipher_suite->aead, tls->cipher_suite->hash, key, iv, ctx->secret, ptls_iovec_init(NULL, 0),
+                                NULL)) != 0)
+        return ret;
+    *seq = ctx->seq;
+    return 0;
+}
+
+const char *ptls_get_server_name(ptls_t *tls)
+{
+    return tls->server_name;
+}
+
+int ptls_set_server_name(ptls_t *tls, const char *server_name, size_t server_name_len)
+{
+    char *duped = NULL;
+
+    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;
+
+    return 0;
+}
+
+const char *ptls_get_negotiated_protocol(ptls_t *tls)
+{
+    return tls->negotiated_protocol;
+}
+
+int ptls_set_negotiated_protocol(ptls_t *tls, const char *protocol, size_t protocol_len)
+{
+    char *duped = NULL;
+
+    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;
+
+    return 0;
+}
+
+int ptls_handshake_is_complete(ptls_t *tls)
+{
+    return tls->state >= PTLS_STATE_POST_HANDSHAKE_MIN;
+}
+
+int ptls_is_psk_handshake(ptls_t *tls)
+{
+    return tls->is_psk_handshake;
+}
+
+int ptls_is_ech_handshake(ptls_t *tls, uint8_t *config_id, ptls_hpke_kem_t **kem, ptls_hpke_cipher_suite_t **cipher)
+{
+    if (tls->ech.accepted) {
+        if (config_id != NULL)
+            *config_id = tls->ech.config_id;
+        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;
+}
+
+int ptls_skip_tracing(ptls_t *tls)
+{
+    return tls->skip_tracing;
+}
+
+void ptls_set_skip_tracing(ptls_t *tls, int skip_tracing)
+{
+    tls->skip_tracing = skip_tracing;
+}
+
+static int handle_client_handshake_message(ptls_t *tls, ptls_message_emitter_t *emitter, ptls_iovec_t message, int is_end_of_record,
+                                           ptls_handshake_properties_t *properties)
+{
+    uint8_t type = message.base[0];
+    int ret;
+
+    switch (tls->state) {
+    case PTLS_STATE_CLIENT_EXPECT_SERVER_HELLO:
+    case PTLS_STATE_CLIENT_EXPECT_SECOND_SERVER_HELLO:
+        if (type == PTLS_HANDSHAKE_TYPE_SERVER_HELLO && is_end_of_record) {
+            ret = client_handle_hello(tls, emitter, message, properties);
+        } else {
+            ret = PTLS_ALERT_UNEXPECTED_MESSAGE;
+        }
+        break;
+    case PTLS_STATE_CLIENT_EXPECT_ENCRYPTED_EXTENSIONS:
+        if (type == PTLS_HANDSHAKE_TYPE_ENCRYPTED_EXTENSIONS) {
+            ret = client_handle_encrypted_extensions(tls, message, properties);
+        } else {
+            ret = PTLS_ALERT_UNEXPECTED_MESSAGE;
+        }
+        break;
+    case PTLS_STATE_CLIENT_EXPECT_CERTIFICATE_REQUEST_OR_CERTIFICATE:
+        if (type == PTLS_HANDSHAKE_TYPE_CERTIFICATE_REQUEST) {
+            ret = client_handle_certificate_request(tls, message, properties);
+            break;
+        }
+    /* fall through */
+    case PTLS_STATE_CLIENT_EXPECT_CERTIFICATE:
+        switch (type) {
+        case PTLS_HANDSHAKE_TYPE_CERTIFICATE:
+            ret = client_handle_certificate(tls, message);
+            break;
+        case PTLS_HANDSHAKE_TYPE_COMPRESSED_CERTIFICATE:
+            ret = client_handle_compressed_certificate(tls, message);
+            break;
+        default:
+            ret = PTLS_ALERT_UNEXPECTED_MESSAGE;
+            break;
+        }
+        break;
+    case PTLS_STATE_CLIENT_EXPECT_CERTIFICATE_VERIFY:
+        if (type == PTLS_HANDSHAKE_TYPE_CERTIFICATE_VERIFY) {
+            ret = client_handle_certificate_verify(tls, message);
+        } else {
+            ret = PTLS_ALERT_UNEXPECTED_MESSAGE;
+        }
+        break;
+    case PTLS_STATE_CLIENT_EXPECT_FINISHED:
+        if (type == PTLS_HANDSHAKE_TYPE_FINISHED && is_end_of_record) {
+            ret = client_handle_finished(tls, emitter, message);
+        } else {
+            ret = PTLS_ALERT_UNEXPECTED_MESSAGE;
+        }
+        break;
+    case PTLS_STATE_CLIENT_POST_HANDSHAKE:
+        switch (type) {
+        case PTLS_HANDSHAKE_TYPE_NEW_SESSION_TICKET:
+            ret = client_handle_new_session_ticket(tls, message);
+            break;
+        case PTLS_HANDSHAKE_TYPE_KEY_UPDATE:
+            ret = handle_key_update(tls, emitter, message);
+            break;
+        default:
+            ret = PTLS_ALERT_UNEXPECTED_MESSAGE;
+            break;
+        }
+        break;
+    default:
+        assert(!"unexpected state");
+        ret = PTLS_ALERT_INTERNAL_ERROR;
+        break;
+    }
+
+    PTLS_PROBE(RECEIVE_MESSAGE, tls, message.base[0], message.base + PTLS_HANDSHAKE_HEADER_SIZE,
+               message.len - PTLS_HANDSHAKE_HEADER_SIZE, ret);
+    PTLS_LOG_CONN(receive_message, tls, {
+        PTLS_LOG_ELEMENT_UNSIGNED(message, message.base[0]);
+        PTLS_LOG_ELEMENT_UNSIGNED(len, message.len - PTLS_HANDSHAKE_HEADER_SIZE);
+        PTLS_LOG_ELEMENT_SIGNED(result, ret);
+    });
+
+    return ret;
+}
+
+static int handle_server_handshake_message(ptls_t *tls, ptls_message_emitter_t *emitter, ptls_iovec_t message, int is_end_of_record,
+                                           ptls_handshake_properties_t *properties)
+{
+    uint8_t type = message.base[0];
+    int ret;
+
+    switch (tls->state) {
+    case PTLS_STATE_SERVER_EXPECT_CLIENT_HELLO:
+    case PTLS_STATE_SERVER_EXPECT_SECOND_CLIENT_HELLO:
+        if (type == PTLS_HANDSHAKE_TYPE_CLIENT_HELLO && is_end_of_record) {
+            ret = server_handle_hello(tls, emitter, message, properties);
+        } else {
+            ret = PTLS_ALERT_HANDSHAKE_FAILURE;
+        }
+        break;
+    case PTLS_STATE_SERVER_EXPECT_CERTIFICATE:
+        if (type == PTLS_HANDSHAKE_TYPE_CERTIFICATE) {
+            ret = server_handle_certificate(tls, message);
+        } else {
+            ret = PTLS_ALERT_UNEXPECTED_MESSAGE;
+        }
+        break;
+    case PTLS_STATE_SERVER_EXPECT_CERTIFICATE_VERIFY:
+        if (type == PTLS_HANDSHAKE_TYPE_CERTIFICATE_VERIFY) {
+            ret = server_handle_certificate_verify(tls, message);
+        } else {
+            ret = PTLS_ALERT_UNEXPECTED_MESSAGE;
+        }
+        break;
+    case PTLS_STATE_SERVER_EXPECT_END_OF_EARLY_DATA:
+        assert(!tls->ctx->omit_end_of_early_data);
+        if (type == PTLS_HANDSHAKE_TYPE_END_OF_EARLY_DATA) {
+            ret = server_handle_end_of_early_data(tls, message);
+        } else {
+            ret = PTLS_ALERT_UNEXPECTED_MESSAGE;
+        }
+        break;
+    case PTLS_STATE_SERVER_EXPECT_FINISHED:
+        if (type == PTLS_HANDSHAKE_TYPE_FINISHED && is_end_of_record) {
+            ret = server_handle_finished(tls, message);
+        } else {
+            ret = PTLS_ALERT_HANDSHAKE_FAILURE;
+        }
+        break;
+    case PTLS_STATE_SERVER_POST_HANDSHAKE:
+        switch (type) {
+        case PTLS_HANDSHAKE_TYPE_KEY_UPDATE:
+            ret = handle_key_update(tls, emitter, message);
+            break;
+        default:
+            ret = PTLS_ALERT_UNEXPECTED_MESSAGE;
+            break;
+        }
+        break;
+    default:
+        assert(!"unexpected state");
+        ret = PTLS_ALERT_INTERNAL_ERROR;
+        break;
+    }
+
+    PTLS_PROBE(RECEIVE_MESSAGE, tls, message.base[0], message.base + PTLS_HANDSHAKE_HEADER_SIZE,
+               message.len - PTLS_HANDSHAKE_HEADER_SIZE, ret);
+    PTLS_LOG_CONN(receive_message, tls, {
+        PTLS_LOG_ELEMENT_UNSIGNED(message, message.base[0]);
+        PTLS_LOG_ELEMENT_UNSIGNED(len, message.len - PTLS_HANDSHAKE_HEADER_SIZE);
+        PTLS_LOG_ELEMENT_SIGNED(result, ret);
+    });
+
+    return ret;
+}
+
+static int handle_alert(ptls_t *tls, const uint8_t *src, size_t len)
+{
+    if (len != 2)
+        return PTLS_ALERT_DECODE_ERROR;
+
+    uint8_t desc = src[1];
+
+    /* all fatal alerts and USER_CANCELLED warning tears down the connection immediately, regardless of the transmitted level */
+    return PTLS_ALERT_TO_PEER_ERROR(desc);
+}
+
+static int message_buffer_is_overflow(ptls_context_t *ctx, size_t size)
+{
+    if (ctx->max_buffer_size == 0)
+        return 0;
+    if (size <= ctx->max_buffer_size)
+        return 0;
+    return 1;
+}
+
+static int handle_handshake_record(ptls_t *tls,
+                                   int (*cb)(ptls_t *tls, ptls_message_emitter_t *emitter, ptls_iovec_t message,
+                                             int is_end_of_record, ptls_handshake_properties_t *properties),
+                                   ptls_message_emitter_t *emitter, struct st_ptls_record_t *rec,
+                                   ptls_handshake_properties_t *properties)
+{
+    int ret;
+
+    /* handshake */
+    if (rec->type != PTLS_CONTENT_TYPE_HANDSHAKE)
+        return PTLS_ALERT_DECODE_ERROR;
+
+    /* flatten the unhandled messages */
+    const uint8_t *src, *src_end;
+    if (tls->recvbuf.mess.base == NULL) {
+        src = rec->fragment;
+        src_end = src + rec->length;
+    } else {
+        if (message_buffer_is_overflow(tls->ctx, tls->recvbuf.mess.off + rec->length))
+            return PTLS_ALERT_HANDSHAKE_FAILURE;
+        if ((ret = ptls_buffer_reserve(&tls->recvbuf.mess, rec->length)) != 0)
+            return ret;
+        memcpy(tls->recvbuf.mess.base + tls->recvbuf.mess.off, rec->fragment, rec->length);
+        tls->recvbuf.mess.off += rec->length;
+        src = tls->recvbuf.mess.base;
+        src_end = src + tls->recvbuf.mess.off;
+    }
+
+    /* handle the messages */
+    ret = PTLS_ERROR_IN_PROGRESS;
+    while (src_end - src >= 4) {
+        size_t mess_len = 4 + ntoh24(src + 1);
+        if (src_end - src < (int)mess_len)
+            break;
+        ret = cb(tls, emitter, ptls_iovec_init(src, mess_len), src_end - src == mess_len, properties);
+        switch (ret) {
+        case 0:
+        case PTLS_ERROR_ASYNC_OPERATION:
+        case PTLS_ERROR_IN_PROGRESS:
+            break;
+        default:
+            ptls_buffer_dispose(&tls->recvbuf.mess);
+            return ret;
+        }
+        src += mess_len;
+    }
+
+    /* keep last partial message in buffer */
+    if (src != src_end) {
+        size_t new_size = src_end - src;
+        if (message_buffer_is_overflow(tls->ctx, new_size))
+            return PTLS_ALERT_HANDSHAKE_FAILURE;
+        if (tls->recvbuf.mess.base == NULL) {
+            ptls_buffer_init(&tls->recvbuf.mess, "", 0);
+            if ((ret = ptls_buffer_reserve(&tls->recvbuf.mess, new_size)) != 0)
+                return ret;
+            memcpy(tls->recvbuf.mess.base, src, new_size);
+        } else {
+            memmove(tls->recvbuf.mess.base, src, new_size);
+        }
+        tls->recvbuf.mess.off = new_size;
+        ret = PTLS_ERROR_IN_PROGRESS;
+    } else {
+        ptls_buffer_dispose(&tls->recvbuf.mess);
+    }
+
+    return ret;
+}
+
+static int handle_input(ptls_t *tls, ptls_message_emitter_t *emitter, ptls_buffer_t *decryptbuf, const void *input, size_t *inlen,
+                        ptls_handshake_properties_t *properties)
+{
+    struct st_ptls_record_t rec;
+    int ret;
+
+    /* extract the record */
+    if ((ret = parse_record(tls, &rec, input, inlen)) != 0)
+        return ret;
+    assert(rec.fragment != NULL);
+
+    /* decrypt the record */
+    if (rec.type == PTLS_CONTENT_TYPE_CHANGE_CIPHER_SPEC) {
+        if (tls->state < PTLS_STATE_POST_HANDSHAKE_MIN) {
+            if (!(rec.length == 1 && rec.fragment[0] == 0x01))
+                return PTLS_ALERT_ILLEGAL_PARAMETER;
+        } else {
+            return PTLS_ALERT_HANDSHAKE_FAILURE;
+        }
+        ret = PTLS_ERROR_IN_PROGRESS;
+        goto NextRecord;
+    }
+    if (tls->traffic_protection.dec.aead != NULL && rec.type != PTLS_CONTENT_TYPE_ALERT) {
+        size_t decrypted_length;
+        if (rec.type != PTLS_CONTENT_TYPE_APPDATA)
+            return PTLS_ALERT_HANDSHAKE_FAILURE;
+        if ((ret = ptls_buffer_reserve(decryptbuf, 5 + rec.length)) != 0)
+            return ret;
+        if ((ret = aead_decrypt(&tls->traffic_protection.dec, decryptbuf->base + decryptbuf->off, &decrypted_length, rec.fragment,
+                                rec.length)) != 0) {
+            if (tls->is_server && tls->server.early_data_skipped_bytes != UINT32_MAX)
+                goto ServerSkipEarlyData;
+            return ret;
+        }
+        rec.length = decrypted_length;
+        rec.fragment = decryptbuf->base + decryptbuf->off;
+        /* skip padding */
+        for (; rec.length != 0; --rec.length)
+            if (rec.fragment[rec.length - 1] != 0)
+                break;
+        if (rec.length == 0)
+            return PTLS_ALERT_UNEXPECTED_MESSAGE;
+        rec.type = rec.fragment[--rec.length];
+    } else if (rec.type == PTLS_CONTENT_TYPE_APPDATA && tls->is_server && tls->server.early_data_skipped_bytes != UINT32_MAX) {
+        goto ServerSkipEarlyData;
+    }
+
+    if (tls->recvbuf.mess.base != NULL || rec.type == PTLS_CONTENT_TYPE_HANDSHAKE) {
+        /* handshake record */
+        ret = handle_handshake_record(tls, tls->is_server ? handle_server_handshake_message : handle_client_handshake_message,
+                                      emitter, &rec, properties);
+    } else {
+        /* handling of an alert or an application record */
+        switch (rec.type) {
+        case PTLS_CONTENT_TYPE_APPDATA:
+            if (tls->state >= PTLS_STATE_POST_HANDSHAKE_MIN) {
+                decryptbuf->off += rec.length;
+                ret = 0;
+            } else if (tls->state == PTLS_STATE_SERVER_EXPECT_END_OF_EARLY_DATA) {
+                if (tls->traffic_protection.dec.aead != NULL)
+                    decryptbuf->off += rec.length;
+                ret = 0;
+            } else {
+                ret = PTLS_ALERT_UNEXPECTED_MESSAGE;
+            }
+            break;
+        case PTLS_CONTENT_TYPE_ALERT:
+            ret = handle_alert(tls, rec.fragment, rec.length);
+            break;
+        default:
+            ret = PTLS_ALERT_UNEXPECTED_MESSAGE;
+            break;
+        }
+    }
+
+NextRecord:
+    ptls_buffer_dispose(&tls->recvbuf.rec);
+    return ret;
+
+ServerSkipEarlyData:
+    tls->server.early_data_skipped_bytes += (uint32_t)rec.length;
+    if (tls->server.early_data_skipped_bytes > PTLS_MAX_EARLY_DATA_SKIP_SIZE)
+        return PTLS_ALERT_HANDSHAKE_FAILURE;
+    ret = PTLS_ERROR_IN_PROGRESS;
+    goto NextRecord;
+}
+
+static int handle_input_tls12(ptls_t *tls, ptls_buffer_t *decryptbuf, const void *input, size_t *inlen)
+{
+    struct st_ptls_record_t rec;
+    int ret;
+
+    /* extract the record, or bail out */
+    if ((ret = parse_record(tls, &rec, input, inlen)) != 0)
+        return ret;
+    assert(rec.fragment != NULL);
+
+    const uint8_t *src = rec.fragment, *end = src + rec.length;
+    uint64_t nonce;
+    uint8_t aad[PTLS_TLS12_AAD_SIZE];
+
+    /* determine the nonce */
+    if (tls->traffic_protection.dec.aead->algo->tls12.record_iv_size != 0) {
+        assert(tls->traffic_protection.dec.aead->algo->tls12.record_iv_size == 8);
+        if ((ret = ptls_decode64(&nonce, &src, end)) != 0)
+            goto Exit;
+    } else {
+        nonce = tls->traffic_protection.dec.seq;
+    }
+
+    /* determine cleartext length */
+    size_t textlen = end - src;
+    if (textlen < tls->traffic_protection.dec.aead->algo->tag_size) {
+        ret = PTLS_ALERT_BAD_RECORD_MAC;
+        goto Exit;
+    }
+    textlen -= tls->traffic_protection.dec.aead->algo->tag_size;
+
+    /* build aad */
+    build_tls12_aad(aad, rec.type, tls->traffic_protection.dec.seq, (uint16_t)textlen);
+
+    /* decrypt input to decryptbuf */
+    if ((ret = ptls_buffer_reserve(decryptbuf, textlen)) != 0)
+        goto Exit;
+    if (ptls_aead_decrypt(tls->traffic_protection.dec.aead, decryptbuf->base + decryptbuf->off, src, end - src, nonce, aad,
+                          sizeof(aad)) != textlen) {
+        ret = PTLS_ALERT_BAD_RECORD_MAC;
+        goto Exit;
+    }
+    ++tls->traffic_protection.dec.seq;
+
+    /* record-type specific action */
+    switch (rec.type) {
+    case PTLS_CONTENT_TYPE_APPDATA:
+        /* if application data, retain the bytes being decrypted */
+        decryptbuf->off += textlen;
+        break;
+    case PTLS_CONTENT_TYPE_ALERT:
+        /* submit alert without adjusting decryptbuf, so that the decrypted data would be dropped after handling the alert */
+        ret = handle_alert(tls, decryptbuf->base + decryptbuf->off, textlen);
+        break;
+    default:
+        ret = PTLS_ALERT_UNEXPECTED_MESSAGE;
+        break;
+    }
+
+Exit:
+    ptls_buffer_dispose(&tls->recvbuf.rec);
+    ptls_clear_memory(aad, sizeof(aad));
+    return 0;
+}
+
+static void init_record_message_emitter(ptls_t *tls, struct st_ptls_record_message_emitter_t *emitter, ptls_buffer_t *sendbuf)
+{
+    *emitter = (struct st_ptls_record_message_emitter_t){
+        {sendbuf, &tls->traffic_protection.enc, 5, begin_record_message, commit_record_message}};
+}
+
+int ptls_handshake(ptls_t *tls, ptls_buffer_t *_sendbuf, const void *input, size_t *inlen, ptls_handshake_properties_t *properties)
+{
+    struct st_ptls_record_message_emitter_t emitter;
+    int ret;
+
+    assert(tls->state < PTLS_STATE_POST_HANDSHAKE_MIN);
+
+    init_record_message_emitter(tls, &emitter, _sendbuf);
+    size_t sendbuf_orig_off = emitter.super.buf->off;
+
+    /* special handlings */
+    switch (tls->state) {
+    case PTLS_STATE_CLIENT_HANDSHAKE_START: {
+        assert(input == NULL || *inlen == 0);
+        assert(tls->ctx->key_exchanges[0] != NULL);
+        return send_client_hello(tls, &emitter.super, properties, NULL);
+    }
+    case PTLS_STATE_SERVER_GENERATING_CERTIFICATE_VERIFY:
+        return server_finish_handshake(tls, &emitter.super, 1, NULL);
+    default:
+        break;
+    }
+
+    const uint8_t *src = input, *const src_end = src + *inlen;
+    ptls_buffer_t decryptbuf;
+
+    ptls_buffer_init(&decryptbuf, "", 0);
+
+    /* perform handhake until completion or until all the input has been swallowed */
+    ret = PTLS_ERROR_IN_PROGRESS;
+    while (ret == PTLS_ERROR_IN_PROGRESS && src != src_end) {
+        size_t consumed = src_end - src;
+        ret = handle_input(tls, &emitter.super, &decryptbuf, src, &consumed, properties);
+        src += consumed;
+        assert(decryptbuf.off == 0);
+    }
+
+    ptls_buffer_dispose(&decryptbuf);
+
+    switch (ret) {
+    case 0:
+    case PTLS_ERROR_IN_PROGRESS:
+    case PTLS_ERROR_STATELESS_RETRY:
+    case PTLS_ERROR_ASYNC_OPERATION:
+        break;
+    default:
+        /* 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,
+                                PTLS_ERROR_GET_CLASS(ret) == PTLS_ERROR_CLASS_SELF_ALERT ? ret : PTLS_ALERT_INTERNAL_ERROR) != 0)
+                emitter.super.buf->off = sendbuf_orig_off;
+        break;
+    }
+
+    *inlen -= src_end - src;
+    return ret;
+}
+
+int ptls_receive(ptls_t *tls, ptls_buffer_t *decryptbuf, const void *_input, size_t *inlen)
+{
+    const uint8_t *input = (const uint8_t *)_input, *const end = input + *inlen;
+    size_t decryptbuf_orig_size = decryptbuf->off;
+    int ret = 0;
+
+    assert(tls->state >= PTLS_STATE_SERVER_EXPECT_END_OF_EARLY_DATA);
+
+    /* loop until we decrypt some application data (or an error) */
+    while (ret == 0 && input != end && decryptbuf_orig_size == decryptbuf->off) {
+        size_t consumed = end - input;
+        if (tls->traffic_protection.dec.tls12) {
+            ret = handle_input_tls12(tls, decryptbuf, input, &consumed);
+        } else {
+            ret = handle_input(tls, NULL, decryptbuf, input, &consumed, NULL);
+        }
+        input += consumed;
+
+        switch (ret) {
+        case 0:
+            break;
+        case PTLS_ERROR_IN_PROGRESS:
+            ret = 0;
+            break;
+        case PTLS_ERROR_CLASS_PEER_ALERT + PTLS_ALERT_CLOSE_NOTIFY:
+            /* TODO send close alert */
+            break;
+        default:
+            if (PTLS_ERROR_GET_CLASS(ret) == PTLS_ERROR_CLASS_SELF_ALERT) {
+                /* TODO send alert */
+            }
+            break;
+        }
+    }
+
+    *inlen -= end - input;
+
+    return ret;
+}
+
+static int update_send_key(ptls_t *tls, ptls_buffer_t *_sendbuf, int request_update)
+{
+    struct st_ptls_record_message_emitter_t emitter;
+    int ret;
+
+    init_record_message_emitter(tls, &emitter, _sendbuf);
+    size_t sendbuf_orig_off = emitter.super.buf->off;
+
+    ptls_push_message(&emitter.super, NULL, PTLS_HANDSHAKE_TYPE_KEY_UPDATE,
+                      { ptls_buffer_push(emitter.super.buf, !!request_update); });
+    if ((ret = update_traffic_key(tls, 1)) != 0)
+        goto Exit;
+    ret = 0;
+
+Exit:
+    if (ret != 0)
+        emitter.super.buf->off = sendbuf_orig_off;
+    return ret;
+}
+
+int ptls_send(ptls_t *tls, ptls_buffer_t *sendbuf, const void *input, size_t inlen)
+{
+    assert(tls->traffic_protection.enc.aead != NULL);
+
+    /* "For AES-GCM, up to 2^24.5 full-size records (about 24 million) may be encrypted on a given connection while keeping a
+     * safety margin of approximately 2^-57 for Authenticated Encryption (AE) security." (RFC 8446 section 5.5)
+     */
+    if (tls->traffic_protection.enc.seq >= 16777216)
+        tls->needs_key_update = 1;
+
+    if (tls->needs_key_update) {
+        int ret;
+        if ((ret = update_send_key(tls, sendbuf, tls->key_update_send_request)) != 0)
+            return ret;
+        tls->needs_key_update = 0;
+        tls->key_update_send_request = 0;
+    }
+
+    return buffer_push_encrypted_records(sendbuf, PTLS_CONTENT_TYPE_APPDATA, input, inlen, &tls->traffic_protection.enc);
+}
+
+int ptls_update_key(ptls_t *tls, int request_update)
+{
+    assert(tls->ctx->update_traffic_key == NULL);
+    tls->needs_key_update = 1;
+    tls->key_update_send_request = request_update;
+    return 0;
+}
+
+size_t ptls_get_record_overhead(ptls_t *tls)
+{
+    ptls_aead_algorithm_t *algo = tls->traffic_protection.enc.aead->algo;
+
+    if (tls->traffic_protection.enc.tls12) {
+        return 5 + algo->tls12.record_iv_size + algo->tag_size;
+    } else {
+        return 6 + algo->tag_size;
+    }
+}
+
+int ptls_send_alert(ptls_t *tls, ptls_buffer_t *sendbuf, uint8_t level, uint8_t description)
+{
+    size_t rec_start = sendbuf->off;
+    int ret = 0;
+
+    buffer_push_record(sendbuf, PTLS_CONTENT_TYPE_ALERT, { ptls_buffer_push(sendbuf, level, description); });
+    /* encrypt the alert if we have the encryption keys, unless when it is the early data key */
+    if (tls->traffic_protection.enc.aead != NULL && !(tls->state <= PTLS_STATE_CLIENT_EXPECT_FINISHED)) {
+        if ((ret = buffer_encrypt_record(sendbuf, rec_start, &tls->traffic_protection.enc)) != 0)
+            goto Exit;
+    }
+
+Exit:
+    return ret;
+}
+
+int ptls_export_secret(ptls_t *tls, void *output, size_t outlen, const char *label, ptls_iovec_t context_value, int is_early)
+{
+    ptls_hash_algorithm_t *algo = tls->key_schedule->hashes[0].algo;
+    uint8_t *master_secret = is_early ? tls->exporter_master_secret.early : tls->exporter_master_secret.one_rtt,
+            derived_secret[PTLS_MAX_DIGEST_SIZE], context_value_hash[PTLS_MAX_DIGEST_SIZE];
+    int ret;
+
+    if (master_secret == NULL) {
+        if (is_early) {
+            switch (tls->state) {
+            case PTLS_STATE_CLIENT_HANDSHAKE_START:
+            case PTLS_STATE_SERVER_EXPECT_CLIENT_HELLO:
+                ret = PTLS_ERROR_IN_PROGRESS;
+                break;
+            default:
+                ret = PTLS_ERROR_NOT_AVAILABLE;
+                break;
+            }
+        } else {
+            ret = PTLS_ERROR_IN_PROGRESS;
+        }
+        return ret;
+    }
+
+    if ((ret = ptls_calc_hash(algo, context_value_hash, context_value.base, context_value.len)) != 0)
+        return ret;
+
+    if ((ret = ptls_hkdf_expand_label(algo, derived_secret, algo->digest_size, ptls_iovec_init(master_secret, algo->digest_size),
+                                      label, ptls_iovec_init(algo->empty_digest, algo->digest_size), NULL)) != 0)
+        goto Exit;
+    ret = ptls_hkdf_expand_label(algo, output, outlen, ptls_iovec_init(derived_secret, algo->digest_size), "exporter",
+                                 ptls_iovec_init(context_value_hash, algo->digest_size), NULL);
+
+Exit:
+    ptls_clear_memory(derived_secret, sizeof(derived_secret));
+    ptls_clear_memory(context_value_hash, sizeof(context_value_hash));
+    return ret;
+}
+
+struct st_picotls_hmac_context_t {
+    ptls_hash_context_t super;
+    ptls_hash_algorithm_t *algo;
+    ptls_hash_context_t *hash;
+    uint8_t key[1];
+};
+
+static void hmac_update(ptls_hash_context_t *_ctx, const void *src, size_t len)
+{
+    struct st_picotls_hmac_context_t *ctx = (struct st_picotls_hmac_context_t *)_ctx;
+    ctx->hash->update(ctx->hash, src, len);
+}
+
+static void hmac_apply_key(struct st_picotls_hmac_context_t *ctx, uint8_t pad)
+{
+    size_t i;
+
+    for (i = 0; i != ctx->algo->block_size; ++i)
+        ctx->key[i] ^= pad;
+    ctx->hash->update(ctx->hash, ctx->key, ctx->algo->block_size);
+    for (i = 0; i != ctx->algo->block_size; ++i)
+        ctx->key[i] ^= pad;
+}
+
+static void hmac_final(ptls_hash_context_t *_ctx, void *md, ptls_hash_final_mode_t mode)
+{
+    struct st_picotls_hmac_context_t *ctx = (struct st_picotls_hmac_context_t *)_ctx;
+
+    assert(mode != PTLS_HASH_FINAL_MODE_SNAPSHOT || !"not supported");
+
+    if (md != NULL) {
+        ctx->hash->final(ctx->hash, md, PTLS_HASH_FINAL_MODE_RESET);
+        hmac_apply_key(ctx, 0x5c);
+        ctx->hash->update(ctx->hash, md, ctx->algo->digest_size);
+    }
+    ctx->hash->final(ctx->hash, md, mode);
+
+    switch (mode) {
+    case PTLS_HASH_FINAL_MODE_FREE:
+        ptls_clear_memory(ctx->key, ctx->algo->block_size);
+        free(ctx);
+        break;
+    case PTLS_HASH_FINAL_MODE_RESET:
+        hmac_apply_key(ctx, 0x36);
+        break;
+    default:
+        assert(!"FIXME");
+        break;
+    }
+}
+
+int ptls_calc_hash(ptls_hash_algorithm_t *algo, void *output, const void *src, size_t len)
+{
+    ptls_hash_context_t *ctx;
+
+    if ((ctx = algo->create()) == NULL)
+        return PTLS_ERROR_NO_MEMORY;
+    ctx->update(ctx, src, len);
+    ctx->final(ctx, output, PTLS_HASH_FINAL_MODE_FREE);
+    return 0;
+}
+
+ptls_hash_context_t *ptls_hmac_create(ptls_hash_algorithm_t *algo, const void *key, size_t key_size)
+{
+    struct st_picotls_hmac_context_t *ctx;
+
+    assert(key_size <= algo->block_size);
+
+    if ((ctx = malloc(offsetof(struct st_picotls_hmac_context_t, key) + algo->block_size)) == NULL)
+        return NULL;
+
+    *ctx = (struct st_picotls_hmac_context_t){{hmac_update, hmac_final}, algo};
+    if ((ctx->hash = algo->create()) == NULL) {
+        free(ctx);
+        return NULL;
+    }
+    memset(ctx->key, 0, algo->block_size);
+    memcpy(ctx->key, key, key_size);
+
+    hmac_apply_key(ctx, 0x36);
+
+    return &ctx->super;
+}
+
+int ptls_hkdf_extract(ptls_hash_algorithm_t *algo, void *output, ptls_iovec_t salt, ptls_iovec_t ikm)
+{
+    ptls_hash_context_t *hash;
+
+    if (salt.len == 0)
+        salt = ptls_iovec_init(zeroes_of_max_digest_size, algo->digest_size);
+
+    if ((hash = ptls_hmac_create(algo, salt.base, salt.len)) == NULL)
+        return PTLS_ERROR_NO_MEMORY;
+    hash->update(hash, ikm.base, ikm.len);
+    hash->final(hash, output, PTLS_HASH_FINAL_MODE_FREE);
+    return 0;
+}
+
+int ptls_hkdf_expand(ptls_hash_algorithm_t *algo, void *output, size_t outlen, ptls_iovec_t prk, ptls_iovec_t info)
+{
+    ptls_hash_context_t *hmac = NULL;
+    size_t i;
+    uint8_t digest[PTLS_MAX_DIGEST_SIZE];
+
+    for (i = 0; (i * algo->digest_size) < outlen; ++i) {
+        if (hmac == NULL) {
+            if ((hmac = ptls_hmac_create(algo, prk.base, prk.len)) == NULL)
+                return PTLS_ERROR_NO_MEMORY;
+        } else {
+            hmac->update(hmac, digest, algo->digest_size);
+        }
+        hmac->update(hmac, info.base, info.len);
+        uint8_t gen = (uint8_t)(i + 1);
+        hmac->update(hmac, &gen, 1);
+        hmac->final(hmac, digest, 1);
+
+        size_t off_start = i * algo->digest_size, off_end = off_start + algo->digest_size;
+        if (off_end > outlen)
+            off_end = outlen;
+        memcpy((uint8_t *)output + off_start, digest, off_end - off_start);
+    }
+
+    if (hmac != NULL)
+        hmac->final(hmac, NULL, PTLS_HASH_FINAL_MODE_FREE);
+
+    ptls_clear_memory(digest, algo->digest_size);
+
+    return 0;
+}
+
+int ptls_hkdf_expand_label(ptls_hash_algorithm_t *algo, void *output, size_t outlen, ptls_iovec_t secret, const char *label,
+                           ptls_iovec_t hash_value, const char *label_prefix)
+{
+    ptls_buffer_t hkdf_label;
+    uint8_t hkdf_label_buf[80];
+    int ret;
+
+    ptls_buffer_init(&hkdf_label, hkdf_label_buf, sizeof(hkdf_label_buf));
+
+    ptls_buffer_push16(&hkdf_label, (uint16_t)outlen);
+    ptls_buffer_push_block(&hkdf_label, 1, {
+        if (label_prefix == NULL)
+            label_prefix = PTLS_HKDF_EXPAND_LABEL_PREFIX;
+        ptls_buffer_pushv(&hkdf_label, label_prefix, strlen(label_prefix));
+        ptls_buffer_pushv(&hkdf_label, label, strlen(label));
+    });
+    ptls_buffer_push_block(&hkdf_label, 1, { ptls_buffer_pushv(&hkdf_label, hash_value.base, hash_value.len); });
+
+    ret = ptls_hkdf_expand(algo, output, outlen, secret, ptls_iovec_init(hkdf_label.base, hkdf_label.off));
+
+Exit:
+    ptls_buffer_dispose(&hkdf_label);
+    return ret;
+}
+
+int ptls_tls12_phash(ptls_hash_algorithm_t *algo, void *output, size_t outlen, ptls_iovec_t secret, const char *label,
+                     ptls_iovec_t seed)
+{
+    ptls_hash_context_t *hmac;
+    uint8_t An[PTLS_MAX_DIGEST_SIZE];
+    size_t output_off = 0;
+
+    if ((hmac = ptls_hmac_create(algo, secret.base, secret.len)) == NULL)
+        return PTLS_ERROR_NO_MEMORY;
+
+    /* A(1) = HMAC_hash(secret, label + seed) */
+    if (label != NULL)
+        hmac->update(hmac, label, strlen(label));
+    hmac->update(hmac, seed.base, seed.len);
+    hmac->final(hmac, An, PTLS_HASH_FINAL_MODE_RESET);
+
+    while (1) {
+        /* output += HMAC_hash(secret, A(i) + label + seed) */
+        hmac->update(hmac, An, algo->digest_size);
+        if (label != NULL)
+            hmac->update(hmac, label, strlen(label));
+        hmac->update(hmac, seed.base, seed.len);
+        if (outlen - output_off <= algo->digest_size) {
+            /* digest of last chunk is at first written to An then the necessary bytes are copied to output */
+            hmac->final(hmac, An, PTLS_HASH_FINAL_MODE_FREE);
+            memcpy((uint8_t *)output + output_off, An, outlen - output_off);
+            break;
+        }
+        hmac->final(hmac, (uint8_t *)output + output_off, PTLS_HASH_FINAL_MODE_RESET);
+        output_off += algo->digest_size;
+
+        /* A(i) = HMAC_hash(secret, A(i-1)) */
+        hmac->update(hmac, An, algo->digest_size);
+        hmac->final(hmac, An, PTLS_HASH_FINAL_MODE_RESET);
+    }
+
+    ptls_clear_memory(An, algo->digest_size);
+
+    return 0;
+}
+
+ptls_cipher_context_t *ptls_cipher_new(ptls_cipher_algorithm_t *algo, int is_enc, const void *key)
+{
+    ptls_cipher_context_t *ctx;
+
+    if ((ctx = (ptls_cipher_context_t *)malloc(algo->context_size)) == NULL)
+        return NULL;
+    *ctx = (ptls_cipher_context_t){algo};
+    if (algo->setup_crypto(ctx, is_enc, key) != 0) {
+        free(ctx);
+        ctx = NULL;
+    }
+    return ctx;
+}
+
+void ptls_cipher_free(ptls_cipher_context_t *ctx)
+{
+    ctx->do_dispose(ctx);
+    free(ctx);
+}
+
+ptls_aead_context_t *new_aead(ptls_aead_algorithm_t *aead, ptls_hash_algorithm_t *hash, int is_enc, const void *secret,
+                              ptls_iovec_t hash_value, const char *label_prefix)
+{
+    ptls_aead_context_t *ctx = NULL;
+    struct {
+        uint8_t key[PTLS_MAX_SECRET_SIZE];
+        uint8_t iv[PTLS_MAX_IV_SIZE];
+    } key_iv;
+    int ret;
+
+    if ((ret = get_traffic_keys(aead, hash, key_iv.key, key_iv.iv, secret, hash_value, label_prefix)) != 0)
+        goto Exit;
+    ctx = ptls_aead_new_direct(aead, is_enc, key_iv.key, key_iv.iv);
+
+Exit:
+    ptls_clear_memory(&key_iv, sizeof(key_iv));
+    return ctx;
+}
+
+ptls_aead_context_t *ptls_aead_new(ptls_aead_algorithm_t *aead, ptls_hash_algorithm_t *hash, int is_enc, const void *secret,
+                                   const char *label_prefix)
+{
+    return new_aead(aead, hash, is_enc, secret, ptls_iovec_init(NULL, 0), label_prefix);
+}
+
+ptls_aead_context_t *ptls_aead_new_direct(ptls_aead_algorithm_t *aead, int is_enc, const void *key, const void *iv)
+{
+    ptls_aead_context_t *ctx;
+
+    if ((ctx = (ptls_aead_context_t *)malloc(aead->context_size)) == NULL)
+        return NULL;
+
+    *ctx = (ptls_aead_context_t){aead};
+
+    if (aead->setup_crypto(ctx, is_enc, key, iv) != 0) {
+        free(ctx);
+        return NULL;
+    }
+
+    return ctx;
+}
+
+void ptls_aead_free(ptls_aead_context_t *ctx)
+{
+    ctx->dispose_crypto(ctx);
+    free(ctx);
+}
+
+void ptls_aead_xor_iv(ptls_aead_context_t *ctx, const void *_bytes, size_t len)
+{
+    const uint8_t *bytes = _bytes;
+    uint8_t iv[PTLS_MAX_IV_SIZE];
+
+    ptls_aead_get_iv(ctx, iv);
+    for (size_t i = 0; i < len; ++i)
+        iv[i] ^= bytes[i];
+    ptls_aead_set_iv(ctx, iv);
+}
+
+void ptls_aead__build_iv(ptls_aead_algorithm_t *algo, uint8_t *iv, const uint8_t *static_iv, uint64_t seq)
+{
+    size_t iv_size = algo->iv_size, i;
+    const uint8_t *s = static_iv;
+    uint8_t *d = iv;
+
+    /* build iv */
+    for (i = iv_size - 8; i != 0; --i)
+        *d++ = *s++;
+    i = 64;
+    do {
+        i -= 8;
+        *d++ = *s++ ^ (uint8_t)(seq >> i);
+    } while (i != 0);
+}
+
+static void clear_memory(void *p, size_t len)
+{
+    if (len != 0)
+        memset(p, 0, len);
+}
+
+void (*volatile ptls_clear_memory)(void *p, size_t len) = clear_memory;
+
+static int mem_equal(const void *_x, const void *_y, size_t len)
+{
+    const volatile uint8_t *x = _x, *y = _y;
+    uint8_t t = 0;
+
+    for (; len != 0; --len)
+        t |= *x++ ^ *y++;
+
+    return t == 0;
+}
+
+int (*volatile ptls_mem_equal)(const void *x, const void *y, size_t len) = mem_equal;
+
+static uint64_t get_time(ptls_get_time_t *self)
+{
+    struct timeval tv;
+    gettimeofday(&tv, NULL);
+    return (uint64_t)tv.tv_sec * 1000 + tv.tv_usec / 1000;
+}
+
+ptls_get_time_t ptls_get_time = {get_time};
+#if PICOTLS_USE_DTRACE
+PTLS_THREADLOCAL unsigned ptls_default_skip_tracing = 0;
+#endif
+
+int ptls_is_server(ptls_t *tls)
+{
+    return tls->is_server;
+}
+
+struct st_ptls_raw_message_emitter_t {
+    ptls_message_emitter_t super;
+    size_t start_off;
+    size_t *epoch_offsets;
+};
+
+static int begin_raw_message(ptls_message_emitter_t *_self)
+{
+    struct st_ptls_raw_message_emitter_t *self = (void *)_self;
+
+    self->start_off = self->super.buf->off;
+    return 0;
+}
+
+static int commit_raw_message(ptls_message_emitter_t *_self)
+{
+    struct st_ptls_raw_message_emitter_t *self = (void *)_self;
+    size_t epoch;
+
+    /* epoch is the key epoch, with the only exception being 2nd CH generated after 0-RTT key */
+    epoch = self->super.enc->epoch;
+    if (epoch == 1 && self->super.buf->base[self->start_off] == PTLS_HANDSHAKE_TYPE_CLIENT_HELLO)
+        epoch = 0;
+
+    for (++epoch; epoch < 5; ++epoch) {
+        assert(self->epoch_offsets[epoch] == self->start_off);
+        self->epoch_offsets[epoch] = self->super.buf->off;
+    }
+
+    self->start_off = SIZE_MAX;
+
+    return 0;
+}
+
+size_t ptls_get_read_epoch(ptls_t *tls)
+{
+    switch (tls->state) {
+    case PTLS_STATE_CLIENT_HANDSHAKE_START:
+    case PTLS_STATE_CLIENT_EXPECT_SERVER_HELLO:
+    case PTLS_STATE_CLIENT_EXPECT_SECOND_SERVER_HELLO:
+    case PTLS_STATE_SERVER_EXPECT_CLIENT_HELLO:
+    case PTLS_STATE_SERVER_EXPECT_SECOND_CLIENT_HELLO:
+        return 0; /* plaintext */
+    case PTLS_STATE_SERVER_EXPECT_END_OF_EARLY_DATA:
+        assert(!tls->ctx->omit_end_of_early_data);
+        return 1; /* 0-rtt */
+    case PTLS_STATE_CLIENT_EXPECT_ENCRYPTED_EXTENSIONS:
+    case PTLS_STATE_CLIENT_EXPECT_CERTIFICATE_REQUEST_OR_CERTIFICATE:
+    case PTLS_STATE_CLIENT_EXPECT_CERTIFICATE:
+    case PTLS_STATE_CLIENT_EXPECT_CERTIFICATE_VERIFY:
+    case PTLS_STATE_CLIENT_EXPECT_FINISHED:
+    case PTLS_STATE_SERVER_GENERATING_CERTIFICATE_VERIFY:
+    case PTLS_STATE_SERVER_EXPECT_CERTIFICATE:
+    case PTLS_STATE_SERVER_EXPECT_CERTIFICATE_VERIFY:
+    case PTLS_STATE_SERVER_EXPECT_FINISHED:
+        return 2; /* handshake */
+    case PTLS_STATE_CLIENT_POST_HANDSHAKE:
+    case PTLS_STATE_SERVER_POST_HANDSHAKE:
+        return 3; /* 1-rtt */
+    default:
+        assert(!"invalid state");
+        return SIZE_MAX;
+    }
+}
+
+int ptls_handle_message(ptls_t *tls, ptls_buffer_t *sendbuf, size_t epoch_offsets[5], size_t in_epoch, const void *input,
+                        size_t inlen, ptls_handshake_properties_t *properties)
+{
+    return tls->is_server ? ptls_server_handle_message(tls, sendbuf, epoch_offsets, in_epoch, input, inlen, properties)
+                          : ptls_client_handle_message(tls, sendbuf, epoch_offsets, in_epoch, input, inlen, properties);
+}
+
+int ptls_client_handle_message(ptls_t *tls, ptls_buffer_t *sendbuf, size_t epoch_offsets[5], size_t in_epoch, const void *input,
+                               size_t inlen, ptls_handshake_properties_t *properties)
+{
+    assert(!tls->is_server);
+
+    struct st_ptls_raw_message_emitter_t emitter = {
+        {sendbuf, &tls->traffic_protection.enc, 0, begin_raw_message, commit_raw_message}, SIZE_MAX, epoch_offsets};
+    struct st_ptls_record_t rec = {PTLS_CONTENT_TYPE_HANDSHAKE, 0, inlen, input};
+
+    if (input == NULL)
+        return send_client_hello(tls, &emitter.super, properties, NULL);
+
+    if (ptls_get_read_epoch(tls) != in_epoch)
+        return PTLS_ALERT_UNEXPECTED_MESSAGE;
+
+    return handle_handshake_record(tls, handle_client_handshake_message, &emitter.super, &rec, properties);
+}
+
+int ptls_server_handle_message(ptls_t *tls, ptls_buffer_t *sendbuf, size_t epoch_offsets[5], size_t in_epoch, const void *input,
+                               size_t inlen, ptls_handshake_properties_t *properties)
+{
+    assert(tls->is_server);
+
+    struct st_ptls_raw_message_emitter_t emitter = {
+        {sendbuf, &tls->traffic_protection.enc, 0, begin_raw_message, commit_raw_message}, SIZE_MAX, epoch_offsets};
+    struct st_ptls_record_t rec = {PTLS_CONTENT_TYPE_HANDSHAKE, 0, inlen, input};
+
+    if (tls->state == PTLS_STATE_SERVER_GENERATING_CERTIFICATE_VERIFY) {
+        assert(input == NULL || inlen == 0);
+        return server_finish_handshake(tls, &emitter.super, 1, NULL);
+    }
+
+    assert(input != NULL);
+
+    if (ptls_get_read_epoch(tls) != in_epoch)
+        return PTLS_ALERT_UNEXPECTED_MESSAGE;
+
+    return handle_handshake_record(tls, handle_server_handshake_message, &emitter.super, &rec, properties);
+}
+
+/**
+ * checks if given name looks like an IP address
+ */
+int ptls_server_name_is_ipaddr(const char *name)
+{
+#ifdef AF_INET
+    struct sockaddr_in sin;
+    if (inet_pton(AF_INET, name, &sin) == 1)
+        return 1;
+#endif
+#ifdef AF_INET6
+    struct sockaddr_in6 sin6;
+    if (inet_pton(AF_INET6, name, &sin6) == 1)
+        return 1;
+#endif
+    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];
+    *dst++ = "0123456789abcdef"[v & 0xf];
+    return dst;
+}
+
+char *ptls_hexdump(char *dst, const void *_src, size_t len)
+{
+    char *buf = dst;
+    const uint8_t *src = _src;
+
+    for (size_t i = 0; i != len; ++i)
+        dst = byte_to_hex(dst, src[i]);
+    *dst = '\0';
+    return buf;
+}
+
+char *ptls_jsonescape(char *buf, const char *unsafe_str, size_t len)
+{
+    char *dst = buf;
+    const uint8_t *src = (const uint8_t *)unsafe_str, *end = src + len;
+
+    for (; src != end; ++src) {
+        switch (*src) {
+#define MAP(ch, escaped)                                                                                                           \
+    case ch:                                                                                                                       \
+        memcpy(dst, (escaped), sizeof(escaped) - 1);                                                                               \
+        dst += sizeof(escaped) - 1;                                                                                                \
+        break
+            MAP('"', "\\\"");
+            MAP('\\', "\\\\");
+            MAP('/', "\\/");
+            MAP('\b', "\\b");
+            MAP('\f', "\\f");
+            MAP('\n', "\\n");
+            MAP('\r', "\\r");
+            MAP('\t', "\\t");
+#undef MAP
+        default:
+            if (*src < 0x20 || *src == 0x7f) {
+                *dst++ = '\\';
+                *dst++ = 'u';
+                *dst++ = '0';
+                *dst++ = '0';
+                dst = byte_to_hex(dst, *src);
+            } else {
+                *dst++ = *src;
+            }
+            break;
+        }
+    }
+    *dst = '\0';
+
+    return dst;
+}
+
+int ptls_log__do_pushv(ptls_buffer_t *buf, const void *p, size_t l)
+{
+    if (ptls_buffer_reserve(buf, l) != 0)
+        return 0;
+
+    memcpy(buf->base + buf->off, p, l);
+    buf->off += l;
+    return 1;
+}
+
+int ptls_log__do_push_unsafestr(ptls_buffer_t *buf, const char *s, size_t l)
+{
+    if (ptls_buffer_reserve(buf, l * (sizeof("\\uXXXX") - 1) + 1) != 0)
+        return 0;
+
+    buf->off = (uint8_t *)ptls_jsonescape((char *)(buf->base + buf->off), s, l) - buf->base;
+
+    return 1;
+}
+
+int ptls_log__do_push_hexdump(ptls_buffer_t *buf, const void *s, size_t l)
+{
+    if (ptls_buffer_reserve(buf, l * 2 + 1) != 0)
+        return 0;
+
+    ptls_hexdump((char *)(buf->base + buf->off), s, l);
+    buf->off += l * 2;
+
+    return 1;
+}
+
+int ptls_log__do_push_signed32(ptls_buffer_t *buf, int32_t v)
+{
+    /* TODO optimize */
+    char s[sizeof("-2147483648")];
+    int len = snprintf(s, sizeof(s), "%" PRId32, v);
+    return ptls_log__do_pushv(buf, s, (size_t)len);
+}
+
+int ptls_log__do_push_signed64(ptls_buffer_t *buf, int64_t v)
+{
+    /* TODO optimize */
+    char s[sizeof("-9223372036854775808")];
+    int len = snprintf(s, sizeof(s), "%" PRId64, v);
+    return ptls_log__do_pushv(buf, s, (size_t)len);
+}
+
+int ptls_log__do_push_unsigned32(ptls_buffer_t *buf, uint32_t v)
+{
+    /* TODO optimize */
+    char s[sizeof("4294967295")];
+    int len = snprintf(s, sizeof(s), "%" PRIu32, v);
+    return ptls_log__do_pushv(buf, s, (size_t)len);
+}
+
+int ptls_log__do_push_unsigned64(ptls_buffer_t *buf, uint64_t v)
+{
+    /* TODO optimize */
+    char s[sizeof("18446744073709551615")];
+    int len = snprintf(s, sizeof(s), "%" PRIu64, v);
+    return ptls_log__do_pushv(buf, s, (size_t)len);
+}
+
+#if PTLS_HAVE_LOG
+
+volatile ptls_log_t ptls_log = {};
+
+static struct {
+    int *fds;
+    size_t num_fds;
+    size_t num_lost;
+    pthread_mutex_t mutex;
+} logctx = {.mutex = PTHREAD_MUTEX_INITIALIZER};
+
+size_t ptls_log_num_lost(void)
+{
+    return logctx.num_lost;
+}
+
+int ptls_log_add_fd(int fd)
+{
+    int ret;
+
+    pthread_mutex_lock(&logctx.mutex);
+
+    int *newfds;
+    if ((newfds = realloc(logctx.fds, sizeof(logctx.fds[0]) * (logctx.num_fds + 1))) == NULL) {
+        ret = PTLS_ERROR_NO_MEMORY;
+        goto Exit;
+    }
+    logctx.fds = newfds;
+    logctx.fds[logctx.num_fds++] = fd;
+    ptls_log.is_active = 1;
+
+    ret = 0; /* success */
+
+Exit:
+    pthread_mutex_unlock(&logctx.mutex);
+    return ret;
+}
+
+#endif
+
+void ptls_log__do_write(const ptls_buffer_t *buf)
+{
+#if PTLS_HAVE_LOG
+    pthread_mutex_lock(&logctx.mutex);
+
+    for (size_t fd_index = 0; fd_index < logctx.num_fds;) {
+        ssize_t ret;
+        while ((ret = write(logctx.fds[fd_index], buf->base, buf->off)) == -1 && errno == EINTR)
+            ;
+        if (ret == buf->off) {
+            /* success */
+            ++fd_index;
+        } else if (ret > 0 || (ret == -1 && (errno == EAGAIN || errno == EWOULDBLOCK))) {
+            /* partial write or buffer full */
+            ++logctx.num_lost;
+            ++fd_index;
+        } else {
+            /* write error; close and remove that fd from array */
+            close(logctx.fds[fd_index]);
+            logctx.fds[fd_index] = logctx.fds[logctx.num_fds - 1];
+            --logctx.num_fds;
+            if (logctx.num_fds == 0)
+                ptls_log.is_active = 0;
+        }
+    }
+
+    pthread_mutex_unlock(&logctx.mutex);
+#endif
 }
diff --git a/cbits/picotls.h b/cbits/picotls.h
--- a/cbits/picotls.h
+++ b/cbits/picotls.h
@@ -64,6 +64,7 @@
 #define PTLS_THREADLOCAL __declspec(thread)
 #else
 #define PTLS_THREADLOCAL __thread
+#define PTLS_HAVE_LOG 1
 #endif
 
 #ifndef PTLS_FUZZ_HANDSHAKE
@@ -84,7 +85,7 @@
 #define PTLS_AESCCM_INTEGRITY_LIMIT 0xB504F3                   /* 2^23.5 */
 
 #define PTLS_CHACHA20_KEY_SIZE 32
-#define PTLS_CHACHA20_IV_SIZE 16
+#define PTLS_CHACHA20_IV_SIZE 16 /* contrary to RFC 7539, follow OpenSSL way of using first 32 bits as ctr and latter 96 as IV */
 #define PTLS_CHACHA20POLY1305_IV_SIZE 12
 #define PTLS_CHACHA20POLY1305_TAG_SIZE 16
 #define PTLS_CHACHA20POLY1305_CONFIDENTIALITY_LIMIT UINT64_MAX       /* at least 2^64 */
@@ -99,10 +100,17 @@
 #define PTLS_SHA384_BLOCK_SIZE 128
 #define PTLS_SHA384_DIGEST_SIZE 48
 
+#define PTLS_SHA512_BLOCK_SIZE 128
+#define PTLS_SHA512_DIGEST_SIZE 64
+
 #define PTLS_MAX_SECRET_SIZE 32
 #define PTLS_MAX_IV_SIZE 16
 #define PTLS_MAX_DIGEST_SIZE 64
 
+/* versions */
+#define PTLS_PROTOCOL_VERSION_TLS12 0x0303
+#define PTLS_PROTOCOL_VERSION_TLS13 0x0304
+
 /* cipher-suites */
 #define PTLS_CIPHER_SUITE_AES_128_GCM_SHA256 0x1301
 #define PTLS_CIPHER_SUITE_NAME_AES_128_GCM_SHA256 "TLS_AES_128_GCM_SHA256"
@@ -111,9 +119,23 @@
 #define PTLS_CIPHER_SUITE_CHACHA20_POLY1305_SHA256 0x1303
 #define PTLS_CIPHER_SUITE_NAME_CHACHA20_POLY1305_SHA256 "TLS_CHACHA20_POLY1305_SHA256"
 
+/* TLS/1.2 cipher-suites that we support (for compatibility, OpenSSL names are used) */
+#define PTLS_CIPHER_SUITE_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 0xc02b
+#define PTLS_CIPHER_SUITE_NAME_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 "ECDHE-ECDSA-AES128-GCM-SHA256"
+#define PTLS_CIPHER_SUITE_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 0xc02c
+#define PTLS_CIPHER_SUITE_NAME_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 "ECDHE-ECDSA-AES256-GCM-SHA384"
+#define PTLS_CIPHER_SUITE_ECDHE_RSA_WITH_AES_128_GCM_SHA256 0xc02f
+#define PTLS_CIPHER_SUITE_NAME_ECDHE_RSA_WITH_AES_128_GCM_SHA256 "ECDHE-RSA-AES128-GCM-SHA256"
+#define PTLS_CIPHER_SUITE_ECDHE_RSA_WITH_AES_256_GCM_SHA384 0xc030
+#define PTLS_CIPHER_SUITE_NAME_ECDHE_RSA_WITH_AES_256_GCM_SHA384 "ECDHE-RSA-AES256-GCM-SHA384"
+#define PTLS_CIPHER_SUITE_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 0xcca8
+#define PTLS_CIPHER_SUITE_NAME_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 "ECDHE-RSA-CHACHA20-POLY1305"
+#define PTLS_CIPHER_SUITE_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 0xcca9
+#define PTLS_CIPHER_SUITE_NAME_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 "ECDHE-ECDSA-CHACHA20-POLY1305"
+
 /* negotiated_groups */
 #define PTLS_GROUP_SECP256R1 23
-#define PTLS_GROUP_NAME_SECP256R1 "scep256r1"
+#define PTLS_GROUP_NAME_SECP256R1 "secp256r1"
 #define PTLS_GROUP_SECP384R1 24
 #define PTLS_GROUP_NAME_SECP384R1 "secp384r1"
 #define PTLS_GROUP_SECP521R1 25
@@ -134,11 +156,20 @@
 #define PTLS_SIGNATURE_RSA_PSS_RSAE_SHA512 0x0806
 #define PTLS_SIGNATURE_ED25519 0x0807
 
-/* ESNI */
-#define PTLS_ESNI_VERSION_DRAFT03 0xff02
-
-#define PTLS_ESNI_RESPONSE_TYPE_ACCEPT 0
-#define PTLS_ESNI_RESPONSE_TYPE_RETRY_REQUEST 1
+/* HPKE */
+#define PTLS_HPKE_MODE_BASE 0
+#define PTLS_HPKE_MODE_PSK 1
+#define PTLS_HPKE_MODE_AUTH 2
+#define PTLS_HPKE_MODE_AUTH_PSK 3
+#define PTLS_HPKE_KEM_P256_SHA256 16
+#define PTLS_HPKE_KEM_P384_SHA384 17
+#define PTLS_HPKE_KEM_X25519_SHA256 32
+#define PTLS_HPKE_HKDF_SHA256 1
+#define PTLS_HPKE_HKDF_SHA384 2
+#define PTLS_HPKE_HKDF_SHA512 3
+#define PTLS_HPKE_AEAD_AES_128_GCM 1
+#define PTLS_HPKE_AEAD_AES_256_GCM 2
+#define PTLS_HPKE_AEAD_CHACHA20POLY1305 3
 
 /* error classes and macros */
 #define PTLS_ERROR_CLASS_SELF_ALERT 0
@@ -168,16 +199,27 @@
 #define PTLS_ALERT_CERTIFICATE_UNKNOWN 46
 #define PTLS_ALERT_ILLEGAL_PARAMETER 47
 #define PTLS_ALERT_UNKNOWN_CA 48
+#define PTLS_ALERT_ACCESS_DENIED 49
 #define PTLS_ALERT_DECODE_ERROR 50
 #define PTLS_ALERT_DECRYPT_ERROR 51
 #define PTLS_ALERT_PROTOCOL_VERSION 70
 #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
+#define PTLS_TLS12_AAD_SIZE 13
+#define PTLS_TLS12_AESGCM_FIXED_IV_SIZE 4
+#define PTLS_TLS12_AESGCM_RECORD_IV_SIZE 8
+#define PTLS_TLS12_CHACHAPOLY_FIXED_IV_SIZE 12
+#define PTLS_TLS12_CHACHAPOLY_RECORD_IV_SIZE 0
+
 /* internal errors */
 #define PTLS_ERROR_NO_MEMORY (PTLS_ERROR_CLASS_INTERNAL + 1)
 #define PTLS_ERROR_IN_PROGRESS (PTLS_ERROR_CLASS_INTERNAL + 2)
@@ -187,9 +229,9 @@
 #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)
 
 #define PTLS_ERROR_INCORRECT_BASE64 (PTLS_ERROR_CLASS_INTERNAL + 50)
 #define PTLS_ERROR_PEM_LABEL_NOT_FOUND (PTLS_ERROR_CLASS_INTERNAL + 51)
@@ -219,6 +261,7 @@
 #define PTLS_HANDSHAKE_TYPE_KEY_UPDATE 24
 #define PTLS_HANDSHAKE_TYPE_COMPRESSED_CERTIFICATE 25
 #define PTLS_HANDSHAKE_TYPE_MESSAGE_HASH 254
+#define PTLS_HANDSHAKE_TYPE_PSEUDO_HRR -1
 
 #define PTLS_CERTIFICATE_TYPE_X509 0
 #define PTLS_CERTIFICATE_TYPE_RAW_PUBLIC_KEY 2
@@ -236,6 +279,17 @@
             0xfb, 0xd5, 0x1a, 0xd2, 0xf1, 0x48, 0x98, 0xb9, 0x5b                                                                   \
     }
 
+#define PTLS_ZERO_DIGEST_SHA512                                                                                                    \
+    {                                                                                                                              \
+        0xcf, 0x83, 0xe1, 0x35, 0x7e, 0xef, 0xb8, 0xbd, 0xf1, 0x54, 0x28, 0x50, 0xd6, 0x6d, 0x80, 0x07, 0xd6, 0x20, 0xe4, 0x05,    \
+            0x0b, 0x57, 0x15, 0xdc, 0x83, 0xf4, 0xa9, 0x21, 0xd3, 0x6c, 0xe9, 0xce, 0x47, 0xd0, 0xd1, 0x3c, 0x5d, 0x85, 0xf2,      \
+            0xb0, 0xff, 0x83, 0x18, 0xd2, 0x87, 0x7e, 0xec, 0x2f, 0x63, 0xb9, 0x31, 0xbd, 0x47, 0x41, 0x7a, 0x81, 0xa5, 0x38,      \
+            0x32, 0x7a, 0xf9, 0x27, 0xda, 0x3e                                                                                     \
+    }
+
+#define PTLS_TO__STR(n) #n
+#define PTLS_TO_STR(n) PTLS_TO__STR(n)
+
 typedef struct st_ptls_t ptls_t;
 typedef struct st_ptls_context_t ptls_context_t;
 typedef struct st_ptls_key_schedule_t ptls_key_schedule_t;
@@ -255,7 +309,8 @@
     uint8_t *base;
     size_t capacity;
     size_t off;
-    int is_allocated;
+    uint8_t is_allocated; /* boolean */
+    uint8_t align_bits;   /* if particular alignment is required, set to log2(alignment); otherwize zero */
 } ptls_buffer_t;
 
 /**
@@ -336,17 +391,22 @@
 /**
  * AEAD context. AEAD implementations are allowed to stuff data at the end of the struct. The size of the memory allocated for the
  * struct is governed by ptls_aead_algorithm_t::context_size.
+ * Ciphers for TLS over TCP MUST implement `do_encrypt`, `do_encrypt_v`, `do_decrypt`. `do_encrypt_init`, `~update`, `~final` are
+ * obsolete, and therefore may not be available.
  */
 typedef struct st_ptls_aead_context_t {
     const struct st_ptls_aead_algorithm_t *algo;
     /* field above this line must not be altered by the crypto binding */
     void (*dispose_crypto)(struct st_ptls_aead_context_t *ctx);
-    void (*do_xor_iv)(struct st_ptls_aead_context_t *ctx, const void *bytes, size_t len);
+    void (*do_get_iv)(struct st_ptls_aead_context_t *ctx, void *iv);
+    void (*do_set_iv)(struct st_ptls_aead_context_t *ctx, const void *iv);
     void (*do_encrypt_init)(struct st_ptls_aead_context_t *ctx, uint64_t seq, const void *aad, size_t aadlen);
     size_t (*do_encrypt_update)(struct st_ptls_aead_context_t *ctx, void *output, const void *input, size_t inlen);
     size_t (*do_encrypt_final)(struct st_ptls_aead_context_t *ctx, void *output);
     void (*do_encrypt)(struct st_ptls_aead_context_t *ctx, void *output, const void *input, size_t inlen, uint64_t seq,
                        const void *aad, size_t aadlen, ptls_aead_supplementary_encryption_t *supp);
+    void (*do_encrypt_v)(struct st_ptls_aead_context_t *ctx, void *output, ptls_iovec_t *input, size_t incnt, uint64_t seq,
+                         const void *aad, size_t aadlen);
     size_t (*do_decrypt)(struct st_ptls_aead_context_t *ctx, void *output, const void *input, size_t inlen, uint64_t seq,
                          const void *aad, size_t aadlen);
 } ptls_aead_context_t;
@@ -388,6 +448,21 @@
      */
     size_t tag_size;
     /**
+     * TLS/1.2 Security Parameters (AEAD without support for TLS 1.2 must set both values to 0)
+     */
+    struct {
+        size_t fixed_iv_size;
+        size_t record_iv_size;
+    } tls12;
+    /**
+     * if encrypted bytes are going to be written using non-temporal store instructions (i.e., skip cache)
+     */
+    unsigned non_temporal : 1;
+    /**
+     * log2(alignment) being required
+     */
+    uint8_t align_bits;
+    /**
      * size of memory allocated for ptls_aead_context_t. AEAD implementations can set this value to something greater than
      * sizeof(ptls_aead_context_t) and stuff additional data at the bottom of the struct.
      */
@@ -439,6 +514,10 @@
  */
 typedef const struct st_ptls_hash_algorithm_t {
     /**
+     * name of the hash algorithm
+     */
+    const char *name;
+    /**
      * block size
      */
     size_t block_size;
@@ -486,39 +565,25 @@
 } ptls_message_emitter_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
+ * HPKE KEM
  */
+typedef const struct st_ptls_hpke_kem_t {
+    uint16_t id;
+    ptls_key_exchange_algorithm_t *keyex;
+    ptls_hash_algorithm_t *hash;
+} ptls_hpke_kem_t;
 
-#define PTLS_ESNI_NONCE_SIZE 16
+typedef struct st_ptls_hpke_cipher_suite_id_t {
+    uint16_t kdf;
+    uint16_t aead;
+} ptls_hpke_cipher_suite_id_t;
 
-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;
+typedef const struct st_ptls_hpke_cipher_suite_t {
+    ptls_hpke_cipher_suite_id_t id;
+    const char *name; /* in form of "<kdf>/<aead>" using the sames specified in IANA HPKE registry */
+    ptls_hash_algorithm_t *hash;
+    ptls_aead_algorithm_t *aead;
+} ptls_hpke_cipher_suite_t;
 
 #define PTLS_CALLBACK_TYPE0(ret, name)                                                                                             \
     typedef struct st_ptls_##name##_t {                                                                                            \
@@ -543,6 +608,10 @@
      */
     ptls_iovec_t raw_message;
     /**
+     * points to the cipher-suites section of the raw_message (see above)
+     */
+    ptls_iovec_t cipher_suites;
+    /**
      *
      */
     struct {
@@ -558,18 +627,10 @@
         size_t count;
     } certificate_compression_algorithms;
     struct {
-        const uint16_t *list;
-        size_t count;
-    } cipher_suites;
-    struct {
         const uint8_t *list;
         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;
@@ -590,19 +651,41 @@
 PTLS_CALLBACK_TYPE(int, emit_certificate, ptls_t *tls, ptls_message_emitter_t *emitter, ptls_key_schedule_t *key_sched,
                    ptls_iovec_t context, int push_status_request, const uint16_t *compress_algos, size_t num_compress_algos);
 /**
- * when gerenating CertificateVerify, the core calls the callback to sign the handshake context using the certificate.
+ * An object that represents an asynchronous task (e.g., RSA signature generation).
+ * When `ptls_handshake` returns `PTLS_ERROR_ASYNC_OPERATION`, it has an associated task in flight. The user should obtain the
+ * reference to the associated task by calling `ptls_get_async_job`, then either wait for the file descriptor obtained from
+ * the `get_fd` callback to become readable, or set a completion callback via `set_completion_callback` and wait for its
+ * invocation. Once notified, the user should invoke `ptls_handshake` again.
+ * Async jobs typically provide support for only one of the two methods.
  */
-PTLS_CALLBACK_TYPE(int, sign_certificate, ptls_t *tls, uint16_t *selected_algorithm, ptls_buffer_t *output, ptls_iovec_t input,
-                   const uint16_t *algorithms, size_t num_algorithms);
+typedef struct st_ptls_async_job_t {
+    void (*destroy_)(struct st_ptls_async_job_t *self);
+    /**
+     * optional callback returning a file descriptor that becomes readable when the job is complete
+     */
+    int (*get_fd)(struct st_ptls_async_job_t *self);
+    /**
+     * optional callback for setting a completion callback
+     */
+    void (*set_completion_callback)(struct st_ptls_async_job_t *self, void (*cb)(void *), void *cbdata);
+} ptls_async_job_t;
 /**
+ * When gerenating CertificateVerify, the core calls the callback to sign the handshake context using the certificate. This callback
+ * supports asynchronous mode; see `ptls_openssl_sign_certificate_t` for more information.
+ */
+PTLS_CALLBACK_TYPE(int, sign_certificate, ptls_t *tls, ptls_async_job_t **async, uint16_t *selected_algorithm,
+                   ptls_buffer_t *output, ptls_iovec_t input, const uint16_t *algorithms, size_t num_algorithms);
+/**
  * after receiving Certificate, the core calls the callback to verify the certificate chain and to obtain a pointer to a
  * callback that should be used for verifying CertificateVerify. If an error occurs between a successful return from this
  * 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);
     /**
@@ -656,10 +739,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
@@ -689,9 +774,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;
     /**
      *
      */
@@ -721,8 +827,7 @@
      */
     size_t max_buffer_size;
     /**
-     * the field is obsolete; should be set to NULL for QUIC draft-17.  Note also that even though everybody did, it was incorrect
-     * to set the value to "quic " in the earlier versions of the draft.
+     * this field is obsolete and ignored
      */
     const char *hkdf_label_prefix__obsolete;
     /**
@@ -765,6 +870,11 @@
      */
     unsigned server_cipher_preference : 1;
     /**
+     * boolean indicating if ChaCha20-Poly1305 should be reprioritized to the top of the server cipher list if a ChaCha20-Poly1305
+     * cipher is at the top of the client cipher list
+     */
+    unsigned server_cipher_chacha_priority : 1;
+    /**
      *
      */
     ptls_encrypt_ticket_t *encrypt_ticket;
@@ -791,11 +901,11 @@
     /**
      *
      */
-    ptls_update_esni_key_t *update_esni_key;
+    ptls_on_extension_t *on_extension;
     /**
-     *
+     * (optional) list of supported tls12 cipher-suites terminated by NULL
      */
-    ptls_on_extension_t *on_extension;
+    ptls_cipher_suite_t **tls12_cipher_suites;
 };
 
 typedef struct st_ptls_raw_extension_t {
@@ -848,9 +958,19 @@
              */
             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. If config.base is non-NULL but config.len is zero, a grease ECH will
+                 * be sent, assuming that X25519-SHA256 KEM and SHA256-AES-128-GCM HPKE cipher is available.
+                 */
+                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 {
             /**
@@ -925,6 +1045,10 @@
  */
 int ptls_buffer_reserve(ptls_buffer_t *buf, size_t delta);
 /**
+ * reserves space for additional amount of memory, requiring `buf->base` to follow specified alignment
+ */
+int ptls_buffer_reserve_aligned(ptls_buffer_t *buf, size_t delta, uint8_t align_bits);
+/**
  * internal
  */
 int ptls_buffer__do_pushv(ptls_buffer_t *buf, const void *src, size_t len);
@@ -946,6 +1070,9 @@
 static uint8_t *ptls_encode_quicint(uint8_t *p, uint64_t v);
 #define PTLS_ENCODE_QUICINT_CAPACITY 8
 
+#define PTLS_QUICINT_MAX 4611686018427387903 // (1 << 62) - 1
+#define PTLS_QUICINT_LONGEST_STR "4611686018427387903"
+
 #define ptls_buffer_pushv(buf, src, len)                                                                                           \
     do {                                                                                                                           \
         if ((ret = ptls_buffer__do_pushv((buf), (src), (len))) != 0)                                                               \
@@ -1039,7 +1166,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)                                                                         \
@@ -1052,6 +1179,7 @@
             goto Exit;                                                                                                             \
     } while (0)
 
+int ptls_decode8(uint8_t *value, const uint8_t **src, const uint8_t *end);
 int ptls_decode16(uint16_t *value, const uint8_t **src, const uint8_t *end);
 int ptls_decode24(uint32_t *value, const uint8_t **src, const uint8_t *end);
 int ptls_decode32(uint32_t *value, const uint8_t **src, const uint8_t *end);
@@ -1112,7 +1240,166 @@
         ptls_decode_assert_block_close((src), end);                                                                                \
     } while (0)
 
+#define PTLS_LOG__DO_LOG(module, type, block)                                                                                      \
+    do {                                                                                                                           \
+        int ptlslog_skip = 0;                                                                                                      \
+        char smallbuf[128];                                                                                                        \
+        ptls_buffer_t ptlslogbuf;                                                                                                  \
+        ptls_buffer_init(&ptlslogbuf, smallbuf, sizeof(smallbuf));                                                                 \
+        PTLS_LOG__DO_PUSH_SAFESTR("{\"module\":\"" PTLS_TO_STR(module) "\",\"type\":\"" PTLS_TO_STR(type) "\"");                   \
+        do {                                                                                                                       \
+            block                                                                                                                  \
+        } while (0);                                                                                                               \
+        PTLS_LOG__DO_PUSH_SAFESTR("}\n");                                                                                          \
+        if (!ptlslog_skip)                                                                                                         \
+            ptls_log__do_write(&ptlslogbuf);                                                                                       \
+        ptls_buffer_dispose(&ptlslogbuf);                                                                                          \
+    } while (0)
+
+#define PTLS_LOG(module, type, block)                                                                                              \
+    do {                                                                                                                           \
+        if (!ptls_log.is_active)                                                                                                   \
+            break;                                                                                                                 \
+        PTLS_LOG__DO_LOG((module), (type), (block));                                                                               \
+    } while (0)
+
+#define PTLS_LOG_CONN(type, tls, block)                                                                                            \
+    do {                                                                                                                           \
+        ptls_t *_tls = (tls);                                                                                                      \
+        if (!ptls_log.is_active || ptls_skip_tracing(_tls))                                                                        \
+            break;                                                                                                                 \
+        PTLS_LOG__DO_LOG(picotls, type, {                                                                                          \
+            PTLS_LOG_ELEMENT_PTR(tls, _tls);                                                                                       \
+            do {                                                                                                                   \
+                block                                                                                                              \
+            } while (0);                                                                                                           \
+        });                                                                                                                        \
+    } while (0)
+
+#define PTLS_LOG_ELEMENT_SAFESTR(name, value)                                                                                      \
+    do {                                                                                                                           \
+        PTLS_LOG__DO_PUSH_SAFESTR(",\"" PTLS_TO_STR(name) "\":\"");                                                                \
+        PTLS_LOG__DO_PUSH_SAFESTR(value);                                                                                          \
+        PTLS_LOG__DO_PUSH_SAFESTR("\"");                                                                                           \
+    } while (0)
+#define PTLS_LOG_ELEMENT_UNSAFESTR(name, value, value_len)                                                                         \
+    do {                                                                                                                           \
+        PTLS_LOG__DO_PUSH_SAFESTR(",\"" PTLS_TO_STR(name) "\":\"");                                                                \
+        PTLS_LOG__DO_PUSH_UNSAFESTR(value, value_len);                                                                             \
+        PTLS_LOG__DO_PUSH_SAFESTR("\"");                                                                                           \
+    } while (0)
+#define PTLS_LOG_ELEMENT_HEXDUMP(name, value, value_len)                                                                           \
+    do {                                                                                                                           \
+        PTLS_LOG__DO_PUSH_SAFESTR(",\"" PTLS_TO_STR(name) "\":\"");                                                                \
+        PTLS_LOG__DO_PUSH_HEXDUMP(value, value_len);                                                                               \
+        PTLS_LOG__DO_PUSH_SAFESTR("\"");                                                                                           \
+    } while (0)
+#define PTLS_LOG_ELEMENT_PTR(name, value) PTLS_LOG_ELEMENT_UNSIGNED(name, (uint64_t)(value))
+#define PTLS_LOG_ELEMENT_SIGNED(name, value)                                                                                       \
+    do {                                                                                                                           \
+        PTLS_LOG__DO_PUSH_SAFESTR(",\"" PTLS_TO_STR(name) "\":");                                                                  \
+        PTLS_LOG__DO_PUSH_SIGNED(value);                                                                                           \
+    } while (0)
+#define PTLS_LOG_ELEMENT__DO_UNSIGNED(name, suffix, value)                                                                         \
+    do {                                                                                                                           \
+        PTLS_LOG__DO_PUSH_SAFESTR(",\"" PTLS_TO_STR(name) suffix "\":");                                                           \
+        PTLS_LOG__DO_PUSH_UNSIGNED(value);                                                                                         \
+    } while (0)
+#define PTLS_LOG_ELEMENT_UNSIGNED(name, value) PTLS_LOG_ELEMENT__DO_UNSIGNED(name, "", value)
+#define PTLS_LOG_ELEMENT_BOOL(name, value)                                                                                         \
+    do {                                                                                                                           \
+        PTLS_LOG__DO_PUSH_SAFESTR(",\"" PTLS_TO_STR(name) "\":");                                                                  \
+        PTLS_LOG__DO_PUSH_SAFESTR(value ? "true" : "false");                                                                       \
+    } while (0)
+
+#define PTLS_LOG_APPDATA_ELEMENT_UNSAFESTR(name, value, value_len)                                                                 \
+    do {                                                                                                                           \
+        size_t _len = (value_len);                                                                                                 \
+        if (ptls_log.include_appdata)                                                                                              \
+            PTLS_LOG_ELEMENT_UNSAFESTR(name, value, _len);                                                                         \
+        PTLS_LOG_ELEMENT__DO_UNSIGNED(name, "_len", _len);                                                                         \
+    } while (0)
+#define PTLS_LOG_APPDATA_ELEMENT_HEXDUMP(name, value, value_len)                                                                   \
+    do {                                                                                                                           \
+        size_t _len = (value_len);                                                                                                 \
+        if (ptls_log.include_appdata)                                                                                              \
+            PTLS_LOG_ELEMENT_HEXDUMP(name, value, _len);                                                                           \
+        PTLS_LOG_ELEMENT__DO_UNSIGNED(name, "_len", _len);                                                                         \
+    } while (0)
+
+#define PTLS_LOG__DO_PUSH_SAFESTR(v)                                                                                               \
+    do {                                                                                                                           \
+        if (PTLS_UNLIKELY(!ptlslog_skip && !ptls_log__do_push_safestr(&ptlslogbuf, (v))))                                          \
+            ptlslog_skip = 1;                                                                                                      \
+    } while (0)
+#define PTLS_LOG__DO_PUSH_UNSAFESTR(v, l)                                                                                          \
+    do {                                                                                                                           \
+        if (PTLS_UNLIKELY(!ptlslog_skip && !ptls_log__do_push_unsafestr(&ptlslogbuf, (v), (l))))                                   \
+            ptlslog_skip = 1;                                                                                                      \
+    } while (0)
+#define PTLS_LOG__DO_PUSH_HEXDUMP(v, l)                                                                                            \
+    do {                                                                                                                           \
+        if (PTLS_UNLIKELY(!ptlslog_skip && !ptls_log__do_push_hexdump(&ptlslogbuf, (v), (l))))                                     \
+            ptlslog_skip = 1;                                                                                                      \
+    } while (0)
+#define PTLS_LOG__DO_PUSH_SIGNED(v)                                                                                                \
+    do {                                                                                                                           \
+        if (PTLS_UNLIKELY(!ptlslog_skip)) {                                                                                        \
+            if (sizeof(v) <= sizeof(int32_t)) {                                                                                    \
+                if (PTLS_UNLIKELY(!ptls_log__do_push_signed32(&ptlslogbuf, (v))))                                                  \
+                    ptlslog_skip = 1;                                                                                              \
+            } else {                                                                                                               \
+                if (PTLS_UNLIKELY(!ptls_log__do_push_signed64(&ptlslogbuf, (v))))                                                  \
+                    ptlslog_skip = 1;                                                                                              \
+            }                                                                                                                      \
+        }                                                                                                                          \
+    } while (0)
+#define PTLS_LOG__DO_PUSH_UNSIGNED(v)                                                                                              \
+    do {                                                                                                                           \
+        if (PTLS_UNLIKELY(!ptlslog_skip)) {                                                                                        \
+            if (sizeof(v) <= sizeof(uint32_t)) {                                                                                   \
+                if (PTLS_UNLIKELY(!ptls_log__do_push_unsigned32(&ptlslogbuf, (uint32_t)(v))))                                      \
+                    ptlslog_skip = 1;                                                                                              \
+            } else {                                                                                                               \
+                if (PTLS_UNLIKELY(!ptls_log__do_push_unsigned64(&ptlslogbuf, (v))))                                                \
+                    ptlslog_skip = 1;                                                                                              \
+            }                                                                                                                      \
+        }                                                                                                                          \
+    } while (0)
+
 /**
+ * User API is exposed only when logging is supported by the platform.
+ */
+typedef struct st_ptls_log_t {
+    unsigned is_active : 1;
+    unsigned include_appdata : 1;
+} ptls_log_t;
+
+#if PTLS_HAVE_LOG
+extern volatile ptls_log_t ptls_log;
+/**
+ * Returns the number of log events that were unable to be emitted.
+ */
+size_t ptls_log_num_lost(void);
+/**
+ * Registers an fd to the logger. A registered fd is automatically closed and removed if it is invalidated.
+ */
+int ptls_log_add_fd(int fd);
+#else
+static const ptls_log_t ptls_log = {0};
+#endif
+
+static int ptls_log__do_push_safestr(ptls_buffer_t *buf, const char *s);
+int ptls_log__do_push_unsafestr(ptls_buffer_t *buf, const char *s, size_t l);
+int ptls_log__do_push_hexdump(ptls_buffer_t *buf, const void *s, size_t l);
+int ptls_log__do_pushv(ptls_buffer_t *buf, const void *p, size_t l);
+int ptls_log__do_push_signed32(ptls_buffer_t *buf, int32_t v);
+int ptls_log__do_push_signed64(ptls_buffer_t *buf, int64_t v);
+int ptls_log__do_push_unsigned32(ptls_buffer_t *buf, uint32_t v);
+int ptls_log__do_push_unsigned64(ptls_buffer_t *buf, uint64_t v);
+void ptls_log__do_write(const ptls_buffer_t *buf);
+
+/**
  * create a client object to handle new TLS connection
  */
 ptls_t *ptls_client_new(ptls_context_t *ctx);
@@ -1121,10 +1408,20 @@
  */
 ptls_t *ptls_server_new(ptls_context_t *ctx);
 /**
- * creates a object handle new TLS connection
+ * creates an object handle new TLS connection
  */
 static ptls_t *ptls_new(ptls_context_t *ctx, int is_server);
 /**
+ * creates TLS 1.2 record layer for post-handshake communication
+ */
+int ptls_build_tls12_export_params(ptls_context_t *ctx, ptls_buffer_t *output, int is_server, int session_reused,
+                                   ptls_cipher_suite_t *cipher, const void *master_secret, const void *hello_randoms,
+                                   uint64_t next_send_record_iv, const char *server_name, ptls_iovec_t negotiated_protocol);
+/**
+ * create a post-handshake TLS connection object using given parameters
+ */
+int ptls_import(ptls_context_t *ctx, ptls_t **tls, ptls_iovec_t params);
+/**
  * releases all resources associated to the object
  */
 void ptls_free(ptls_t *tls);
@@ -1137,6 +1434,10 @@
  */
 void ptls_set_context(ptls_t *tls, ptls_context_t *ctx);
 /**
+ * get the signature context
+ */
+ptls_async_job_t *ptls_get_async_job(ptls_t *tls);
+/**
  * returns the client-random
  */
 ptls_iovec_t ptls_get_client_random(ptls_t *tls);
@@ -1145,6 +1446,21 @@
  */
 ptls_cipher_suite_t *ptls_get_cipher(ptls_t *tls);
 /**
+ * returns a supported cipher-suite given an id
+ */
+ptls_cipher_suite_t *ptls_find_cipher_suite(ptls_cipher_suite_t **cipher_suites, uint16_t id);
+/**
+ * Returns protocol version (e.g., 0x0303 for TLS 1.2, 0x0304 for TLS 1.3). The result may be unstable prior to handshake
+ * completion.
+ */
+uint16_t ptls_get_protocol_version(ptls_t *tls);
+/**
+ * Returns current state of traffic keys. The cipher-suite being used, as well as the length of the traffic keys, can be obtained
+ * via `ptls_get_cipher`.
+ * TODO: Even in case of offloading just the TX side, there should be API for handling key updates, sending Close aleart.
+ */
+int ptls_get_traffic_keys(ptls_t *tls, int is_enc, uint8_t *key, uint8_t *iv, uint64_t *seq);
+/**
  * returns the server-name (NULL if SNI is not used or failed to negotiate)
  */
 const char *ptls_get_server_name(ptls_t *tls);
@@ -1173,6 +1489,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, uint8_t *config_id, 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);
@@ -1248,6 +1568,11 @@
 int ptls_hkdf_expand_label(ptls_hash_algorithm_t *algo, void *output, size_t outlen, ptls_iovec_t secret, const char *label,
                            ptls_iovec_t hash_value, const char *label_prefix);
 /**
+ * The expansion function of TLS 1.2 defined in RFC 5426 section 5. When `label` is NULL, acts as P_<hash>, or if non-NULL, as PRF.
+ */
+int ptls_tls12_phash(ptls_hash_algorithm_t *algo, void *output, size_t outlen, ptls_iovec_t secret, const char *label,
+                     ptls_iovec_t seed);
+/**
  * instantiates a symmetric cipher
  */
 ptls_cipher_context_t *ptls_cipher_new(ptls_cipher_algorithm_t *algo, int is_enc, const void *key);
@@ -1290,26 +1615,35 @@
  * Permutes the static IV by applying given bytes using bit-wise XOR. This API can be used for supplying nonces longer than 64-
  * bits.
  */
-static void ptls_aead_xor_iv(ptls_aead_context_t *ctx, const void *bytes, size_t len);
+void ptls_aead_xor_iv(ptls_aead_context_t *ctx, const void *bytes, size_t len);
+static void ptls_aead_get_iv(ptls_aead_context_t *ctx, void *iv);
+static void ptls_aead_set_iv(ptls_aead_context_t *ctx, const void *iv);
 /**
- *
+ * Encrypts one AEAD block, given input and output vectors.
  */
 static size_t ptls_aead_encrypt(ptls_aead_context_t *ctx, void *output, const void *input, size_t inlen, uint64_t seq,
                                 const void *aad, size_t aadlen);
+/**
+ * Encrypts one AEAD block, as well as one block of ECB (for QUIC / DTLS packet number encryption). Depending on the AEAD engine
+ * being used, the two operations might run simultaneously.
+ */
 static void ptls_aead_encrypt_s(ptls_aead_context_t *ctx, void *output, const void *input, size_t inlen, uint64_t seq,
                                 const void *aad, size_t aadlen, ptls_aead_supplementary_encryption_t *supp);
 /**
- * initializes the internal state of the encryptor
+ * Encrypts one AEAD block, given a vector of vectors.
  */
+static void ptls_aead_encrypt_v(ptls_aead_context_t *ctx, void *output, ptls_iovec_t *input, size_t incnt, uint64_t seq,
+                                const void *aad, size_t aadlen);
+/**
+ * Obsolete; new applications should use one of: `ptls_aead_encrypt`, `ptls_aead_encrypt_s`, `ptls_aead_encrypt_v`.
+ */
 static void ptls_aead_encrypt_init(ptls_aead_context_t *ctx, uint64_t seq, const void *aad, size_t aadlen);
 /**
- * encrypts the input and updates the GCM state
- * @return number of bytes emitted to output
+ * Obsolete; see `ptls_aead_encrypt_init`.
  */
 static size_t ptls_aead_encrypt_update(ptls_aead_context_t *ctx, void *output, const void *input, size_t inlen);
 /**
- * emits buffered data (if any) and the GCM tag
- * @return number of bytes emitted to output
+ * Obsolete; see `ptls_aead_encrypt_init`.
  */
 static size_t ptls_aead_encrypt_final(ptls_aead_context_t *ctx, void *output);
 /**
@@ -1319,7 +1653,7 @@
 static size_t ptls_aead_decrypt(ptls_aead_context_t *ctx, void *output, const void *input, size_t inlen, uint64_t seq,
                                 const void *aad, size_t aadlen);
 /**
- * Return the current read epoch.
+ * Return the current read epoch (i.e., that of the message being received or to be)
  */
 size_t ptls_get_read_epoch(ptls_t *tls);
 /**
@@ -1355,9 +1689,14 @@
 static void ptls_aead__do_encrypt(ptls_aead_context_t *ctx, void *output, const void *input, size_t inlen, uint64_t seq,
                                   const void *aad, size_t aadlen, ptls_aead_supplementary_encryption_t *supp);
 /**
+ *
+ */
+static void ptls_aead__do_encrypt_v(ptls_aead_context_t *ctx, void *_output, ptls_iovec_t *input, size_t incnt, uint64_t seq,
+                                    const void *aad, size_t aadlen);
+/**
  * 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
  */
@@ -1367,40 +1706,44 @@
  */
 extern int (*volatile ptls_mem_equal)(const void *x, const void *y, size_t len);
 /**
- *
- */
-static ptls_iovec_t ptls_iovec_init(const void *p, size_t len);
-/**
  * checks if a server name is an IP address.
  */
 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.
  */
 int ptls_load_certificates(ptls_context_t *ctx, char const *cert_pem_file);
 /**
- *
- */
-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);
-/**
- *
+ * SetupBaseS function of RFC 9180. Given `kem`, `algo`, `info`, and receiver's public key, returns an ephemeral public key and an
+ * AEAD context used for encrypting data.
  */
-void ptls_esni_dispose_context(ptls_esni_context_t *esni);
+int ptls_hpke_setup_base_s(ptls_hpke_kem_t *kem, ptls_hpke_cipher_suite_t *cipher, ptls_iovec_t *pk_s, ptls_aead_context_t **ctx,
+                           ptls_iovec_t pk_r, ptls_iovec_t info);
 /**
- * Obtain the ESNI secrets negotiated during the handshake.
+ * SetupBaseR function of RFC 9180. Given `kem`, `algo`, `info`, receiver's private key (`keyex`), and the esnder's public key,
+ * returns the AEAD context to be used for decrypting data.
  */
-ptls_esni_secret_t *ptls_get_esni_secret(ptls_t *ctx);
+int ptls_hpke_setup_base_r(ptls_hpke_kem_t *kem, ptls_hpke_cipher_suite_t *cipher, ptls_key_exchange_context_t *keyex,
+                           ptls_aead_context_t **ctx, ptls_iovec_t pk_s, ptls_iovec_t info);
 /**
  *
  */
 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.
+ */
+char *ptls_jsonescape(char *buf, const char *s, size_t len);
+/**
  * the default get_time callback
  */
 extern ptls_get_time_t ptls_get_time;
-#if PICOTLS_USE_DTRACE
+#if defined(PICOTLS_USE_DTRACE) && PICOTLS_USE_DTRACE
 /**
  *
  */
@@ -1411,6 +1754,11 @@
 
 /* inline functions */
 
+inline int ptls_log__do_push_safestr(ptls_buffer_t *buf, const char *s)
+{
+    return ptls_log__do_pushv(buf, s, strlen(s));
+}
+
 inline ptls_t *ptls_new(ptls_context_t *ctx, int is_server)
 {
     return is_server ? ptls_server_new(ctx) : ptls_client_new(ctx);
@@ -1434,12 +1782,13 @@
     buf->off = 0;
     buf->capacity = smallbuf_size;
     buf->is_allocated = 0;
+    buf->align_bits = 0;
 }
 
 inline void ptls_buffer_dispose(ptls_buffer_t *buf)
 {
     ptls_buffer__release_memory(buf);
-    *buf = (ptls_buffer_t){NULL};
+    *buf = (ptls_buffer_t){NULL, 0, 0, 0, 0};
 }
 
 inline uint8_t *ptls_encode_quicint(uint8_t *p, uint64_t v)
@@ -1476,11 +1825,16 @@
     ctx->do_transform(ctx, output, input, len);
 }
 
-inline void ptls_aead_xor_iv(ptls_aead_context_t *ctx, const void *bytes, size_t len)
+inline void ptls_aead_get_iv(ptls_aead_context_t *ctx, void *iv)
 {
-    ctx->do_xor_iv(ctx, bytes, len);
+    ctx->do_get_iv(ctx, iv);
 }
 
+inline void ptls_aead_set_iv(ptls_aead_context_t *ctx, const void *iv)
+{
+    ctx->do_set_iv(ctx, iv);
+}
+
 inline size_t ptls_aead_encrypt(ptls_aead_context_t *ctx, void *output, const void *input, size_t inlen, uint64_t seq,
                                 const void *aad, size_t aadlen)
 {
@@ -1494,6 +1848,12 @@
     ctx->do_encrypt(ctx, output, input, inlen, seq, aad, aadlen, supp);
 }
 
+inline void ptls_aead_encrypt_v(ptls_aead_context_t *ctx, void *output, ptls_iovec_t *input, size_t incnt, uint64_t seq,
+                                const void *aad, size_t aadlen)
+{
+    ctx->do_encrypt_v(ctx, output, input, incnt, seq, aad, aadlen);
+}
+
 inline void ptls_aead_encrypt_init(ptls_aead_context_t *ctx, uint64_t seq, const void *aad, size_t aadlen)
 {
     ctx->do_encrypt_init(ctx, seq, aad, aadlen);
@@ -1512,15 +1872,25 @@
 inline void ptls_aead__do_encrypt(ptls_aead_context_t *ctx, void *output, const void *input, size_t inlen, uint64_t seq,
                                   const void *aad, size_t aadlen, ptls_aead_supplementary_encryption_t *supp)
 {
-    ctx->do_encrypt_init(ctx, seq, aad, aadlen);
-    ctx->do_encrypt_update(ctx, output, input, inlen);
-    ctx->do_encrypt_final(ctx, (uint8_t *)output + inlen);
+    ptls_iovec_t invec = ptls_iovec_init(input, inlen);
+    ctx->do_encrypt_v(ctx, output, &invec, 1, seq, aad, aadlen);
 
     if (supp != NULL) {
         ptls_cipher_init(supp->ctx, supp->input);
         memset(supp->output, 0, sizeof(supp->output));
         ptls_cipher_encrypt(supp->ctx, supp->output, supp->output, sizeof(supp->output));
     }
+}
+
+inline void ptls_aead__do_encrypt_v(ptls_aead_context_t *ctx, void *_output, ptls_iovec_t *input, size_t incnt, uint64_t seq,
+                                    const void *aad, size_t aadlen)
+{
+    uint8_t *output = (uint8_t *)_output;
+
+    ctx->do_encrypt_init(ctx, seq, aad, aadlen);
+    for (size_t i = 0; i < incnt; ++i)
+        output += ctx->do_encrypt_update(ctx, output, input[i].base, input[i].len);
+    ctx->do_encrypt_final(ctx, output);
 }
 
 inline size_t ptls_aead_decrypt(ptls_aead_context_t *ctx, void *output, const void *input, size_t inlen, uint64_t seq,
diff --git a/cbits/picotls/fusion.h b/cbits/picotls/fusion.h
--- a/cbits/picotls/fusion.h
+++ b/cbits/picotls/fusion.h
@@ -28,19 +28,28 @@
 
 #include <stddef.h>
 #include <emmintrin.h>
+#include <immintrin.h>
 #include "../picotls.h"
 
 #define PTLS_FUSION_AES128_ROUNDS 10
 #define PTLS_FUSION_AES256_ROUNDS 14
 
+#ifndef PTLS_X86_CACHE_LINE_ALIGN_BITS
+#define PTLS_X86_CACHE_LINE_ALIGN_BITS 6 /* 64-bytes */
+#endif
+
 typedef struct ptls_fusion_aesecb_context {
-    __m128i keys[PTLS_FUSION_AES256_ROUNDS + 1];
+    union {
+        __m128i m128[PTLS_FUSION_AES256_ROUNDS + 1];
+        __m256i m256[PTLS_FUSION_AES256_ROUNDS + 1];
+    } keys;
     unsigned rounds;
-} ptls_fusion_aesecb_context_t;
+    uint8_t aesni256;
+} __attribute__((aligned(32))) ptls_fusion_aesecb_context_t;
 
 typedef struct ptls_fusion_aesgcm_context ptls_fusion_aesgcm_context_t;
 
-void ptls_fusion_aesecb_init(ptls_fusion_aesecb_context_t *ctx, int is_enc, const void *key, size_t key_size);
+void ptls_fusion_aesecb_init(ptls_fusion_aesecb_context_t *ctx, int is_enc, const void *key, size_t key_size, int avx256);
 void ptls_fusion_aesecb_dispose(ptls_fusion_aesecb_context_t *ctx);
 void ptls_fusion_aesecb_encrypt(ptls_fusion_aesecb_context_t *ctx, void *dst, const void *src);
 
@@ -84,8 +93,15 @@
 int ptls_fusion_aesgcm_decrypt(ptls_fusion_aesgcm_context_t *ctx, void *output, const void *input, size_t inlen, __m128i ctr,
                                const void *aad, size_t aadlen, const void *tag);
 
+/**
+ * A boolean flag indicating if vaes and vpclmulqdq (256-bit crypto instructions) should be used. This flag is set automatically
+ * when `ptls_fusion_is_supported_by_cpu` is called. Users can update the flag to enforce behavior. Engines that do not have support
+ * for these 256-bit instructions will continue using the 128-bit ones, even when this flag is set.
+ */
+extern int ptls_fusion_can_aesni256;
 extern ptls_cipher_algorithm_t ptls_fusion_aes128ctr, ptls_fusion_aes256ctr;
 extern ptls_aead_algorithm_t ptls_fusion_aes128gcm, ptls_fusion_aes256gcm;
+extern ptls_aead_algorithm_t ptls_non_temporal_aes128gcm, ptls_non_temporal_aes256gcm;
 
 /**
  * Returns a boolean indicating if fusion can be used.
diff --git a/quic.cabal b/quic.cabal
--- a/quic.cabal
+++ b/quic.cabal
@@ -1,5 +1,5 @@
 name:                quic
-version:             0.1.0
+version:             0.1.1
 synopsis:            QUIC
 description:         Library for QUIC: A UDP-Based Multiplexed and Secure Transport
 license:             BSD3
@@ -26,6 +26,11 @@
   Description:          Development commands
   Default:              False
 
+Flag fusion
+  Description:          Use fusion AES-GCM engine from picotls
+  Default:              True
+  Manual:               True
+
 ----------------------------------------------------------------
 
 library
@@ -147,9 +152,9 @@
   default-extensions:  Strict StrictData
   if os(windows)
     cc-options:        -D_WINDOWS
-  if arch(x86_64)
+  if flag(fusion) && arch(x86_64)
     cpp-options:       -DUSE_FUSION
-    cc-options:        -mavx2 -maes -mpclmul
+    cc-options:        -mavx2 -maes -mpclmul -mvaes -mvpclmulqdq
     c-sources:         cbits/fusion.c cbits/picotls.c
 
 test-suite spec
@@ -186,19 +191,6 @@
                       , unliftio
   default-extensions:  Strict StrictData
   build-tool-depends: hspec-discover:hspec-discover
-
-
-test-suite doctests
-  type:                 exitcode-stdio-1.0
-  default-language:     Haskell2010
-  hs-source-dirs:       test
-  main-is:              doctests.hs
-
-  build-depends:        base >= 4.9 && < 5
-                      , doctest >= 0.10.1
-
-  ghc-options: -Wall
-  default-extensions:  Strict StrictData
 
 executable server
   if flag(devel)
diff --git a/test/Config.hs b/test/Config.hs
--- a/test/Config.hs
+++ b/test/Config.hs
@@ -38,8 +38,7 @@
 testServerConfig :: ServerConfig
 testServerConfig = defaultServerConfig {
     -- Don't use "0.0.0.0" and "::" for Windows (UDP dispatching bug)
-    -- Don't use "127.0.0.1" for macOS (recvmsg bug)
-    scAddresses = [("::1",50003)]
+    scAddresses = [("127.0.0.1",50003)]
   }
 
 makeTestServerConfigR :: IO ServerConfig
@@ -54,13 +53,12 @@
 testServerConfigR :: ServerConfig
 testServerConfigR = defaultServerConfig {
     -- Don't use "0.0.0.0" and "::" for Windows (UDP dispatching bug)
-    -- Don't use "127.0.0.1" for macOS (recvmsg bug)
-    scAddresses = [("::1",50003)]
+    scAddresses = [("127.0.0.1",50003)]
   }
 
 testClientConfig :: ClientConfig
 testClientConfig = defaultClientConfig {
-    ccServerName = "::1"
+    ccServerName = "127.0.0.1"
   , ccPortName   = "50003"
   , ccValidate   = False
   , ccDebugLog   = True
@@ -68,7 +66,7 @@
 
 testClientConfigR :: ClientConfig
 testClientConfigR = defaultClientConfig {
-    ccServerName = "::1"
+    ccServerName = "127.0.0.1"
   , ccPortName   = "50002"
   , ccValidate   = False
   , ccDebugLog   = True
@@ -130,10 +128,10 @@
   where
     hints = defaultHints { addrSocketType = Datagram
                          , addrFlags = [AI_NUMERICHOST]
-                         , addrFamily = AF_INET6
+                         , addrFamily = AF_INET
                          }
     resolve port =
-        head <$> getAddrInfo (Just hints) (Just "::1") (Just port)
+        head <$> getAddrInfo (Just hints) (Just "127.0.0.1") (Just port)
     shouldDrop (Randomly n) _ _ = do
         w <- getRandomOneByte
         return ((w `mod` fromIntegral n) == 0)
diff --git a/test/HandshakeSpec.hs b/test/HandshakeSpec.hs
--- a/test/HandshakeSpec.hs
+++ b/test/HandshakeSpec.hs
@@ -84,10 +84,10 @@
 testHandshake :: ClientConfig -> ServerConfig -> HandshakeMode13 -> IO ()
 testHandshake cc sc mode = do
     concurrently_ server client
-    threadDelay 10000
+    threadDelay 50000
   where
     client = do
-        threadDelay 10000
+        threadDelay 50000
         C.run cc $ \conn -> do
             waitEstablished conn
             handshakeMode <$> getConnectionInfo conn `shouldReturn` mode
@@ -107,17 +107,17 @@
 testHandshake2 :: ClientConfig -> ServerConfig -> (HandshakeMode13, HandshakeMode13) -> Bool -> IO ()
 testHandshake2 cc1 sc (mode1, mode2) use0RTT = do
     concurrently_ server client
-    threadDelay 10000
+    threadDelay 50000
   where
     runClient cc mode action = C.run cc $ \conn -> do
         void $ action conn
         handshakeMode <$> getConnectionInfo conn `shouldReturn` mode
-        threadDelay 10000
+        threadDelay 50000
         getResumptionInfo conn
     client = do
         threadDelay 10000
         res <- runClient cc1 mode1 $ query "first"
-        threadDelay 10000
+        threadDelay 50000
         let cc2 = cc1 { ccResumption = res
                       , ccUse0RTT    = use0RTT
                       }
@@ -134,10 +134,10 @@
 testHandshake3 :: ClientConfig -> ClientConfig -> ServerConfig -> (QUICException -> Bool) -> IO ()
 testHandshake3 cc1 cc2 sc selector = do
     concurrently_ server client
-    threadDelay 10000
+    threadDelay 50000
   where
     client = do
-        threadDelay 10000
+        threadDelay 50000
         C.run cc1 (query "first")  `shouldThrow` selector
         C.run cc2 (query "second") `shouldReturn` ()
     server = S.run sc $ \conn -> do
diff --git a/test/IOSpec.hs b/test/IOSpec.hs
--- a/test/IOSpec.hs
+++ b/test/IOSpec.hs
@@ -69,7 +69,76 @@
             withPipe (DropClientPacket [10]) $ testSendRecv cc sc 20
         it "can exchange data on client 11" $ do
             withPipe (DropClientPacket [11]) $ testSendRecv cc sc 20
+    describe "recvStream" $ do
+        it "don't block if client stop sending first" $ do
+            withPipe (Randomly 20) $ testRecvStreamClientStopFirst cc sc
+        it "don't block if server stop sending first" $ do
+            withPipe (Randomly 20) $ testRecvStreamServerStopFirst cc sc
 
+consumeBytes :: Stream -> Int -> IO ()
+consumeBytes _ 0 = return ()
+consumeBytes strm left = do
+    bs <- recvStream strm 1024
+    when (BS.null bs) $ expectationFailure "no enough bytes received"
+    let len = BS.length bs
+    when (len > left) $ expectationFailure "extra bytes received"
+    consumeBytes strm (left - len)
+
+assertEndOfStream :: Stream -> IO ()
+assertEndOfStream strm = recvStream strm 1024 `shouldReturn` ""
+
+testRecvStreamClientStopFirst :: C.ClientConfig -> ServerConfig -> IO ()
+testRecvStreamClientStopFirst cc sc = do
+    mvar <- newEmptyMVar
+    void $ concurrently (client mvar) (server mvar)
+    threadDelay 10000
+  where
+    aerr = ApplicationProtocolError 0
+
+    client mvar = do
+        threadDelay 10000
+        C.run cc $ \conn -> do
+            strm <- stream conn
+            sendStream strm (BS.replicate 10000 0)
+            takeMVar mvar `shouldReturn` ()
+            stopStream strm aerr
+            resetStream strm aerr
+            takeMVar mvar `shouldReturn` ()
+    server mvar = run sc $ \conn -> do
+        strm <- acceptStream conn
+        consumeBytes strm 10000 `shouldReturn` ()
+        -- notify client to stop stream after all bytes are received.
+        putMVar mvar ()
+        -- verify that client has stopped sending.
+        assertEndOfStream strm
+        putMVar mvar ()
+        stop conn
+
+testRecvStreamServerStopFirst :: C.ClientConfig -> ServerConfig -> IO ()
+testRecvStreamServerStopFirst cc sc = do
+    mvar <- newEmptyMVar
+    void $ concurrently (client mvar) (server mvar)
+    threadDelay 10000
+  where
+    aerr = ApplicationProtocolError 0
+
+    client mvar = do
+        threadDelay 10000
+        C.run cc $ \conn -> do
+            strm <- stream conn
+            sendStream strm (BS.replicate 10000 0)
+            takeMVar mvar `shouldReturn` ()
+    server mvar = run sc $ \conn -> do
+        strm <- acceptStream conn
+        consumeBytes strm 10000 `shouldReturn` ()
+        -- ask client to stop sending.
+        stopStream strm aerr
+        -- verify that client has stopped sending.
+        assertEndOfStream strm
+        resetStream strm aerr
+        putMVar mvar ()
+        stop conn
+
 testSendRecv :: C.ClientConfig -> ServerConfig -> Int -> IO ()
 testSendRecv cc sc times = do
     mvar <- newEmptyMVar
@@ -86,16 +155,7 @@
             takeMVar mvar `shouldReturn` ()
     server mvar = run sc $ \conn -> do
         strm <- acceptStream conn
-        bs <- recvStream strm 1024
-        let len = BS.length bs
-        n <- loop strm bs len
-        n `shouldBe` (10000 * times)
+        consumeBytes strm (10000 * times)
+        assertEndOfStream strm
         putMVar mvar ()
         stop conn
-      where
-        loop _    "" n = return n
-        loop strm _  n = do
-            bs <- recvStream strm 1024
-            let len = BS.length bs
-                n' = n + len
-            loop strm bs n'
diff --git a/test/doctests.hs b/test/doctests.hs
deleted file mode 100644
--- a/test/doctests.hs
+++ /dev/null
@@ -1,4 +0,0 @@
-import Test.DocTest
-
-main :: IO ()
-main = doctest ["Network.QUIC.Client","Network.QUIC.Server"]
