diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,131 @@
 # Change log for "tls"
 
-## Version 2.3.1
+## Version 2.4.9
 
+* Include CertificateRequest in the post-handshake auth transcript.
+  This breaks PHA with hs-tls 2.4.8 or earlier.
+  [#565](https://github.com/haskell-tls/hs-tls/pull/565)
+* Send a TLS 1.2 session ticket only to a client that asked for one.
+  [#575](https://github.com/haskell-tls/hs-tls/pull/575)
+* Fall back to a full handshake when a PSK finds a TLS 1.2 session.
+  [#576](https://github.com/haskell-tls/hs-tls/pull/576)
+* Refuse SCSV or a missing renegotiation_info in secure renegotiation.
+  [#566](https://github.com/haskell-tls/hs-tls/pull/566)
+* Reject a hybrid key share whose classical part fails to derive.
+  [#550](https://github.com/haskell-tls/hs-tls/pull/550)
+* Send no SNI for an empty server name, and refuse a malformed or
+  invalid server_name.
+  [#568](https://github.com/haskell-tls/hs-tls/pull/568)
+* Refuse a CertificateVerify algorithm that is not offered or does not
+  fit the key with illegal_parameter.
+  [#562](https://github.com/haskell-tls/hs-tls/pull/562)
+  [#578](https://github.com/haskell-tls/hs-tls/pull/578)
+* Accept an empty TLS 1.2 client certificate when the hook does.
+  [#552](https://github.com/haskell-tls/hs-tls/pull/552)
+* Request EdDSA client certificates with ecdsa_sign in TLS 1.2.
+  [#553](https://github.com/haskell-tls/hs-tls/pull/553)
+* Accept a ClientHello whose legacy_version is above TLS 1.2.
+  [#564](https://github.com/haskell-tls/hs-tls/pull/564)
+* Answer malformed or misplaced messages with the alerts the RFCs ask for.
+  [#554](https://github.com/haskell-tls/hs-tls/pull/554)
+  [#555](https://github.com/haskell-tls/hs-tls/pull/555)
+  [#556](https://github.com/haskell-tls/hs-tls/pull/556)
+  [#557](https://github.com/haskell-tls/hs-tls/pull/557)
+  [#558](https://github.com/haskell-tls/hs-tls/pull/558)
+  [#559](https://github.com/haskell-tls/hs-tls/pull/559)
+  [#560](https://github.com/haskell-tls/hs-tls/pull/560)
+  [#561](https://github.com/haskell-tls/hs-tls/pull/561)
+  [#563](https://github.com/haskell-tls/hs-tls/pull/563)
+  [#567](https://github.com/haskell-tls/hs-tls/pull/567)
+  [#569](https://github.com/haskell-tls/hs-tls/pull/569)
+  [#570](https://github.com/haskell-tls/hs-tls/pull/570)
+  [#571](https://github.com/haskell-tls/hs-tls/pull/571)
+* tls-server: new options for tlsfuzzer, which CI runs weekly.
+  [#551](https://github.com/haskell-tls/hs-tls/pull/551)
+  [#572](https://github.com/haskell-tls/hs-tls/pull/572)
+  [#573](https://github.com/haskell-tls/hs-tls/pull/573)
+  [#574](https://github.com/haskell-tls/hs-tls/pull/574)
+  [#577](https://github.com/haskell-tls/hs-tls/pull/577)
+
+## Version 2.4.8
+
+* Stop printing traffic secrets and the session secret.
+  [#549](https://github.com/haskell-tls/hs-tls/pull/549)
+
+## Version 2.4.7
+
+* Use the one-call AES-GCM and ChaCha20-Poly1305 interfaces of crypton.
+  This needs crypton 2.1.1 or later.
+  [#548](https://github.com/haskell-tls/hs-tls/pull/548)
+
+## Version 2.4.6
+
+* Accept crypton 2.1.
+
+## Version 2.4.5
+
+* Fix the TLS 1.3 0-RTT session tests racing the NewSessionTicket.
+  [#547](https://github.com/haskell-tls/hs-tls/pull/547)
+* CI: drop macOS with GHC 9.12.
+  [#546](https://github.com/haskell-tls/hs-tls/pull/546)
+* CI: retry Hackage downloads, keep the cache when a test fails, and run
+  doctest on one job.
+  [#545](https://github.com/haskell-tls/hs-tls/pull/545)
+* `extensionDecode` returns `Nothing` instead of calling `error`.
+  [#544](https://github.com/haskell-tls/hs-tls/pull/544)
+* `getTLSUnique` and `getTLSExporter` return `Nothing` before a handshake.
+  [#543](https://github.com/haskell-tls/hs-tls/pull/543)
+* Take two timing signals out of the CBC record path.
+  [#542](https://github.com/haskell-tls/hs-tls/pull/542)
+* Bound the size of a handshake message reassembled from records.
+  [#541](https://github.com/haskell-tls/hs-tls/pull/541)
+* CI: speed up.
+  [#540](https://github.com/haskell-tls/hs-tls/pull/540)
+* Limit consecutive TLS 1.3 KeyUpdate messages with the new
+  `limitKeyUpdate` parameter.
+  [#539](https://github.com/haskell-tls/hs-tls/pull/539)
+* Validate the negotiated ALPN protocol.
+  [#538](https://github.com/haskell-tls/hs-tls/pull/538)
+* Validate the negotiated cipher suite.
+  [#537](https://github.com/haskell-tls/hs-tls/pull/537)
+* Make the session ticket tests deterministic.
+  [#536](https://github.com/haskell-tls/hs-tls/pull/536)
+
+## Version 2.4.4
+
+* Enforce server certificate purpose
+  [#534](https://github.com/haskell-tls/hs-tls/pull/534)
+* Use dedicated doctest REPL
+  [#533](https://github.com/haskell-tls/hs-tls/pull/533)
+* Bind early data to ALPN
+  [#532](https://github.com/haskell-tls/hs-tls/pull/532)
+* Bound certificate decompression
+  [#531](https://github.com/haskell-tls/hs-tls/pull/531)
+* Fix RecordOverflow race after TLS 1.3 client authentication
+  [#530](https://github.com/haskell-tls/hs-tls/pull/530)
+
+## Version 2.4.3
+
+* A server checks clientAuth of ExtendedKeyUsage in a client
+  certificate on client authentication.
+
+## Version 2.4.2
+
+* The `Network.TLS.Extra.CipherCBC` module is added.
+  [#526](https://github.com/haskell-tls/hs-tls/pull/526)
+
+## Version 2.4.1
+
+* Ensure same `supported_groups` before/after HRR.
+* New `clientWantTicket` parameter makes it possible to opt-out of soliciting
+  session tickets from servers.
+
+## Version 2.4.0
+
+* Identical to v2.3.1 but major version up as v2.3.1 breaks "quic".
+
+## Version 2.3.1 (deprecated)
+
 * Using ScrubbedBytes for secrets.
 * Key echange with ML-KEM.
   [#517](https://github.com/haskell-tls/hs-tls/pull/517)
@@ -81,14 +205,14 @@
   This feature is automatically used if the peer supports it.
 * More tests with `tlsfuzzer` especially for client authentication
   and 0-RTT.
-* Implementing a utility funcation, `validateClientCertificate`, for
+* Implementing a utility function, `validateClientCertificate`, for
   client authentication.
 * Bug fix for echo back logic of Cookie extension.
 * More pretty show for the internal `Handshake` structure for debugging.
 
 ## Version 2.1.6
 
-* Testing with "tlsfuzzer" again. Now don't send an alert agaist to
+* Testing with "tlsfuzzer" again. Now don't send an alert against to
   peer's alert. Double locking (aka self dead-lock) is fixed. Sending
   an alert for known-but-cannot-parse extensions. Other corner cases
   are also fixed.
@@ -352,7 +476,7 @@
 API CHANGES:
 
 - `SessionManager` implementations need to provide a `sessionResumeOnlyOnce`
-  function to accomodate resumption scenarios with 0-RTT data.  The function is
+  function to accommodate resumption scenarios with 0-RTT data.  The function is
   called only on the server side.
 - Data type `SessionData` is extended with four new fields for TLS version 1.3.
   `SessionManager` implementations that serializes/deserializes `SessionData`
diff --git a/Network/TLS.hs b/Network/TLS.hs
--- a/Network/TLS.hs
+++ b/Network/TLS.hs
@@ -11,7 +11,7 @@
 -- protocol, and support RSA and Ephemeral (Elliptic curve and
 -- regular) Diffie Hellman key exchanges, and many extensions.
 --
--- The tipical usage is:
+-- The typical usage is:
 --
 -- > socket <- ...
 -- > ctx <- contextNew socket <params>
@@ -46,6 +46,7 @@
     clientUseServerNameIndication,
     clientWantSessionResume,
     clientWantSessionResumeList,
+    clientWantTicket,
     clientShared,
     clientHooks,
     clientSupported,
@@ -138,6 +139,7 @@
     Limit,
     defaultLimit,
     limitHandshakeFragment,
+    limitKeyUpdate,
     limitRecordSize,
 
     -- * Shared parameters
diff --git a/Network/TLS/Compression.hs b/Network/TLS/Compression.hs
--- a/Network/TLS/Compression.hs
+++ b/Network/TLS/Compression.hs
@@ -51,7 +51,7 @@
     (==) c1 c2 = compressionID c1 == compressionID c2
 
 -- | intersect a list of ids commonly given by the other side with a list of compression
--- the function keeps the list of compression in order, to be able to find quickly the prefered
+-- the function keeps the list of compression in order, to be able to find quickly the preferred
 -- compression.
 compressionIntersectID :: [Compression] -> [Word8] -> [Compression]
 compressionIntersectID l ids = filter (\c -> compressionID c `elem` ids) l
diff --git a/Network/TLS/Context.hs b/Network/TLS/Context.hs
--- a/Network/TLS/Context.hs
+++ b/Network/TLS/Context.hs
@@ -8,6 +8,7 @@
     Context (..),
     Hooks (..),
     Established (..),
+    PendingRecv (..),
     RecordLayer (..),
     ctxEOF,
     ctxEstablished,
@@ -23,6 +24,7 @@
     updateMeasure,
     withMeasure,
     withReadLock,
+    tryWithReadLock,
     withWriteLock,
     withStateLock,
     withRWLock,
@@ -114,7 +116,7 @@
     doHandshake :: a -> Context -> IO ()
     doHandshakeWith :: a -> Context -> HandshakeR -> IO ()
     doRequestCertificate :: a -> Context -> IO Bool
-    doPostHandshakeAuthWith :: a -> Context -> Handshake13 -> IO ()
+    doPostHandshakeAuthWith :: a -> Context -> Handshake13R -> IO ()
 
 instance TLSParams ClientParams where
     getTLSCommonParams cparams =
@@ -265,8 +267,11 @@
 --   and use the "tls-exporter" channel binding via 'getTLSExporter'.
 getTLSUnique :: Context -> IO (Maybe ByteString)
 getTLSUnique ctx = do
-    ver <- liftIO $ usingState_ ctx getVersion
-    if ver == TLS12
+    -- Nothing rather than error before a version has been negotiated: this
+    -- can be called on a context whose handshake has not run, and it already
+    -- answers with Maybe.
+    mver <- liftIO $ usingState_ ctx getVersionMaybe
+    if mver == Just TLS12
         then do
             mx <- usingState_ ctx getFirstVerifyData
             case mx of
@@ -278,8 +283,9 @@
 --   For TLS 1.2, 'Nothing' is returned.
 getTLSExporter :: Context -> IO (Maybe ByteString)
 getTLSExporter ctx = do
-    ver <- liftIO $ usingState_ ctx getVersion
-    if ver == TLS13
+    -- As in 'getTLSUnique'.
+    mver <- liftIO $ usingState_ ctx getVersionMaybe
+    if mver == Just TLS13
         then exporter ctx "EXPORTER-Channel-Binding" "" 32
         else return Nothing
 
diff --git a/Network/TLS/Context/Internal.hs b/Network/TLS/Context/Internal.hs
--- a/Network/TLS/Context/Internal.hs
+++ b/Network/TLS/Context/Internal.hs
@@ -17,6 +17,7 @@
     Hooks (..),
     Limit (..),
     Established (..),
+    PendingRecv (..),
     PendingRecvAction (..),
     RecordLayer (..),
     Locks (..),
@@ -36,6 +37,7 @@
     updateMeasure,
     withMeasure,
     withReadLock,
+    tryWithReadLock,
     withWriteLock,
     withStateLock,
     withRWLock,
@@ -65,6 +67,8 @@
     defaultTLS13State,
     getTLS13State,
     modifyTLS13State,
+    incrementTLS13KeyUpdateCount,
+    resetTLS13KeyUpdateCount,
     CipherChoice (..),
     makeCipherChoice,
 
@@ -86,7 +90,7 @@
 ) where
 
 import Control.Concurrent.MVar
-import Control.Exception (throwIO)
+import qualified Control.Exception as E
 import Control.Monad.State.Strict
 import Data.ByteArray (convert)
 import qualified Data.ByteArray as BA
@@ -170,7 +174,7 @@
     { doHandshake_ :: Context -> IO ()
     , doHandshakeWith_ :: Context -> HandshakeR -> IO ()
     , doRequestCertificate_ :: Context -> IO Bool
-    , doPostHandshakeAuthWith_ :: Context -> Handshake13 -> IO ()
+    , doPostHandshakeAuthWith_ :: Context -> Handshake13R -> IO ()
     }
 
 data Locks = Locks
@@ -199,11 +203,12 @@
 
 data TLS13State = TLS13State
     { tls13stRecvNST :: Bool -- client
+    , tls13stKeyUpdateCount :: Int
     , tls13stSentClientCert :: Bool -- client
     , tls13stRecvSF :: Bool -- client
     , tls13stSentCF :: Bool -- client
     , tls13stRecvCF :: Bool -- server
-    , tls13stPendingRecvData :: Maybe ByteString -- client
+    , tls13stPendingRecv :: PendingRecv -- client
     , tls13stPendingSentData :: [ByteString] -> [ByteString] -- client
     , tls13stRTT :: Millisecond
     , tls13st0RTT :: Bool -- client
@@ -211,7 +216,7 @@
     , tls13stClientExtensions :: [ExtensionRaw] -- client
     , tls13stChoice :: ~CipherChoice -- client
     , tls13stHsKey :: Maybe (SecretTriple HandshakeSecret) -- client
-    -- Actuall session id for TLS 1.2, random value for TLS 1.3
+    -- Actual session id for TLS 1.2, random value for TLS 1.3
     , tls13stSession :: Session
     , tls13stSentExtensions :: [ExtensionID]
     }
@@ -220,11 +225,12 @@
 defaultTLS13State =
     TLS13State
         { tls13stRecvNST = False
+        , tls13stKeyUpdateCount = 0
         , tls13stSentClientCert = False
         , tls13stRecvSF = False
         , tls13stSentCF = False
         , tls13stRecvCF = False
-        , tls13stPendingRecvData = Nothing
+        , tls13stPendingRecv = NoPendingRecv
         , tls13stPendingSentData = id
         , tls13stRTT = 0
         , tls13st0RTT = False
@@ -242,6 +248,16 @@
 modifyTLS13State :: Context -> (TLS13State -> TLS13State) -> IO ()
 modifyTLS13State Context{..} f = atomicModifyIORef' ctxTLS13State $ \st -> (f st, ())
 
+incrementTLS13KeyUpdateCount :: Context -> IO Int
+incrementTLS13KeyUpdateCount Context{..} =
+    atomicModifyIORef' ctxTLS13State $ \st ->
+        let count = tls13stKeyUpdateCount st + 1
+         in (st{tls13stKeyUpdateCount = count}, count)
+
+resetTLS13KeyUpdateCount :: Context -> IO ()
+resetTLS13KeyUpdateCount ctx =
+    modifyTLS13State ctx $ \st -> st{tls13stKeyUpdateCount = 0}
+
 data HandshakeSync
     = HandshakeSync
         (Context -> ClientState -> IO ())
@@ -271,6 +287,15 @@
     | Established
     deriving (Eq, Show)
 
+-- | Outcome of a read that was started on behalf of a caller who is no longer
+-- waiting for it, held until the next receive hands it over.  Reads cannot be
+-- abandoned once started -- see 'Network.TLS.Core.handshake' -- so a reader that
+-- outlives its caller leaves its result here instead.
+data PendingRecv
+    = NoPendingRecv
+    | PendingRecvData ByteString
+    | PendingRecvError E.SomeException
+
 data PendingRecvAction
     = -- | simple pending action. The first 'Bool' is necessity of alignment.
       PendingRecvAction Bool (Handshake13 -> IO ())
@@ -367,7 +392,7 @@
 withLog ctx f = ctxWithHooks ctx (f . hookLogging)
 
 throwCore :: MonadIO m => TLSError -> m a
-throwCore = liftIO . throwIO . Uncontextualized
+throwCore = liftIO . E.throwIO . Uncontextualized
 
 failOnEitherError :: MonadIO m => m (Either TLSError a) -> m a
 failOnEitherError f = do
@@ -387,7 +412,7 @@
 
 usingHState :: MonadIO m => Context -> HandshakeM a -> m a
 usingHState ctx f = liftIO $ modifyMVar (ctxHandshakeState ctx) $ \case
-    Nothing -> liftIO $ throwIO MissingHandshake
+    Nothing -> liftIO $ E.throwIO MissingHandshake
     Just st -> return $ swap (Just <$> runHandshake st f)
 
 getHState :: MonadIO m => Context -> m (Maybe HandshakeState)
@@ -449,6 +474,26 @@
 
 withReadLock :: Context -> IO a -> IO a
 withReadLock ctx f = withMVar (lockRead $ ctxLocks ctx) (const f)
+
+-- | Like 'withReadLock', but returns 'Nothing' immediately instead of waiting
+-- when another thread already holds the read lock.
+--
+-- The read lock is what keeps a single thread reading the connection at a time.
+-- Records arrive length-prefixed, so two threads reading in parallel would each
+-- take a piece of whatever record the other was in the middle of, and neither
+-- would end up with a usable message.
+--
+-- Use this instead of 'withReadLock' when the read is optional and skipping it
+-- is better than waiting for the current reader, which may hold the lock for
+-- arbitrarily long.  'bye' is the only such caller; see the note there.
+tryWithReadLock :: Context -> IO a -> IO (Maybe a)
+tryWithReadLock ctx f = E.bracket acquire release $ \mlock -> case mlock of
+    Nothing -> return Nothing
+    Just _ -> Just <$> f
+  where
+    lock = lockRead $ ctxLocks ctx
+    acquire = tryTakeMVar lock
+    release = mapM_ (putMVar lock)
 
 withWriteLock :: Context -> IO a -> IO a
 withWriteLock ctx f = withMVar (lockWrite $ ctxLocks ctx) (const f)
diff --git a/Network/TLS/Core.hs b/Network/TLS/Core.hs
--- a/Network/TLS/Core.hs
+++ b/Network/TLS/Core.hs
@@ -27,6 +27,8 @@
     requestCertificate,
 ) where
 
+import Control.Concurrent (forkIO)
+import Control.Concurrent.MVar
 import qualified Control.Exception as E
 import Control.Monad.State.Strict
 import qualified Data.ByteString as B
@@ -36,6 +38,10 @@
 import System.Timeout
 
 import Network.TLS.Context
+import Network.TLS.Context.Internal (
+    incrementTLS13KeyUpdateCount,
+    resetTLS13KeyUpdateCount,
+ )
 import Network.TLS.Extension
 import Network.TLS.Handshake
 import Network.TLS.Handshake.Common
@@ -72,11 +78,39 @@
         sentClientCert <- tls13stSentClientCert <$> getTLS13State ctx
         when (role == ClientRole && tls13 && sentClientCert) $ do
             rtt <- getRTT ctx
-            -- This 'timeout' should work.
-            mdat <- timeout rtt $ recvData13 ctx
-            case mdat of
-                Nothing -> return ()
-                Just dat -> modifyTLS13State ctx $ \st -> st{tls13stPendingRecvData = Just dat}
+            -- We are only willing to wait 'rtt' for the alert, but a receive
+            -- must not be abandoned once it has started.  Records are read
+            -- length-prefixed and the record layer keeps no receive buffer, so
+            -- an aborted receive loses the bytes it has already taken off the
+            -- transport and leaves the stream positioned inside a record.
+            -- Every later read is then misframed, and the connection is dead
+            -- with a spurious protocol error.
+            --
+            -- So the receive runs in its own thread and we stop waiting for it
+            -- rather than interrupting it.  It holds the read lock, which keeps
+            -- it the only reader and makes the next receive wait for it to
+            -- finish; its outcome is left in 'tls13stPendingRecv' for that
+            -- receive to pick up.
+            done <- newEmptyMVar
+            void $ forkIO $ withReadLock ctx $ do
+                r <- E.try $ recvData13 ctx
+                modifyTLS13State ctx $ \st ->
+                    st
+                        { tls13stPendingRecv = case r of
+                            Right dat -> PendingRecvData dat
+                            Left err -> PendingRecvError err
+                        }
+                putMVar done ()
+            arrived <- timeout rtt $ takeMVar done
+            -- Still report the authentication failure from 'handshake' itself
+            -- whenever it did arrive in time.
+            when (isJust arrived) $ do
+                pending <- tls13stPendingRecv <$> getTLS13State ctx
+                case pending of
+                    PendingRecvError err -> do
+                        modifyTLS13State ctx $ \st -> st{tls13stPendingRecv = NoPendingRecv}
+                        E.throwIO err
+                    _ -> return ()
 
 rttFactor :: Int
 rttFactor = 3
@@ -118,7 +152,7 @@
                 recvNST <- chk
                 unless recvNST $ do
                     rtt <- getRTT ctx
-                    void $ timeout rtt $ recvHS13 ctx chk
+                    tryRecvHS13 rtt chk
             else do
                 -- receiving Client Finished
                 let chk = tls13stRecvCF <$> getTLS13State ctx
@@ -127,8 +161,28 @@
                     -- no chance to measure RTT before receiving CF
                     -- fixme: 1sec is good enough?
                     let rtt = 1000000
-                    void $ timeout rtt $ recvHS13 ctx chk
+                    tryRecvHS13 rtt chk
     bye_ ctx
+  where
+    -- Receiving these messages only improves the chances of a later session
+    -- resumption, so giving up on them costs nothing important.  We give up in
+    -- two different situations, for two different reasons.
+    --
+    -- First, we need the read lock, because only one thread at a time may read
+    -- the connection, but we take it only if it happens to be free.  Another
+    -- thread can be sitting in 'recvData' waiting for data that never arrives,
+    -- or the receive that 'handshake' starts can still be running, and either
+    -- holds the read lock for as long as it lasts.  Waiting for the lock would
+    -- therefore hang 'bye', and closing a connection that a reader is stuck on
+    -- is exactly what 'bye' is for, so we skip the receive in that case.
+    --
+    -- Second, if we do get the lock, we wait 'rtt' for the message and then
+    -- abandon the receive.  Abandoning it can stop the connection part way
+    -- through a record, after which nothing can be read from it again -- which
+    -- is acceptable only because we are closing the connection here anyway.
+    tryRecvHS13 :: Int -> IO Bool -> IO ()
+    tryRecvHS13 rtt chk =
+        void $ tryWithReadLock ctx $ timeout rtt $ recvHS13 ctx chk
 
 bye_ :: MonadIO m => Context -> m ()
 bye_ ctx = liftIO $ do
@@ -188,7 +242,7 @@
         -- All chunks are protected with the same write lock because we don't
         -- want to interleave writes from other threads in the middle of our
         -- possibly large write.
-        mlen <- getPeerRecordLimit ctx -- plaintext, dont' adjust for TLS 1.3
+        mlen <- getPeerRecordLimit ctx -- plaintext, don't adjust for TLS 1.3
         mapM_ (mapChunks_ mlen sendP) (L.toChunks dataToSend)
 
 -- | Get data out of Data packet, and automatically renegotiate if a Handshake
@@ -240,15 +294,20 @@
 
 recvData13 :: Context -> IO ByteString
 recvData13 ctx = do
-    mdat <- tls13stPendingRecvData <$> getTLS13State ctx
-    case mdat of
-        Nothing -> do
+    pending <- tls13stPendingRecv <$> getTLS13State ctx
+    case pending of
+        NoPendingRecv -> do
             pkt <- recvPacket13 ctx
             either (onError (terminate13 ctx)) process pkt
-        Just dat -> do
-            modifyTLS13State ctx $ \st -> st{tls13stPendingRecvData = Nothing}
+        PendingRecvData dat -> do
+            clearPending
             return dat
+        PendingRecvError err -> do
+            clearPending
+            E.throwIO err
   where
+    clearPending = modifyTLS13State ctx $ \st -> st{tls13stPendingRecv = NoPendingRecv}
+
     -- UserCanceled MUST be followed by a CloseNotify.
     process (Alert13 [(AlertLevel_Warning, UserCanceled)]) = return B.empty
     process (Alert13 [(AlertLevel_Warning, CloseNotify)]) = tryBye ctx >> setEOF ctx >> return B.empty
@@ -283,7 +342,7 @@
                 | otherwise -> do
                     let reason = "early data deprotect overflow"
                     terminate13 ctx (Error_Misc reason) AlertLevel_Fatal UnexpectedMessage reason
-            Established -> return x
+            Established -> resetTLS13KeyUpdateCount ctx >> return x
             _ -> throwCore $ Error_Protocol "data at not-established" UnexpectedMessage
     process ChangeCipherSpec13 = do
         established <- ctxEstablished ctx
@@ -345,6 +404,13 @@
         -- to key update (update_requested) which we sent.
         if established == Established
             then do
+                case limitKeyUpdate $ sharedLimit $ ctxShared ctx of
+                    Just limit | limit > 0 -> do
+                        count <- incrementTLS13KeyUpdateCount ctx
+                        when (count > limit) $ do
+                            let reason = "too many consecutive KeyUpdate messages"
+                            terminate13 ctx (Error_Misc reason) AlertLevel_Fatal UnexpectedMessage reason
+                    _ -> return ()
                 keyUpdate ctx getRxRecordState setRxRecordState
                 -- Write lock wraps both actions because we don't want another
                 -- packet to be sent by another thread before the Tx state is
@@ -357,8 +423,8 @@
                 let reason = "received key update before established"
                 terminate13 ctx (Error_Misc reason) AlertLevel_Fatal UnexpectedMessage reason
     -- Client only
-    loopHandshake13 ((h@CertRequest13{}, _b) : hbs) =
-        postHandshakeAuthWith ctx h >> loopHandshake13 hbs
+    loopHandshake13 (hb@(CertRequest13{}, _) : hbs) =
+        postHandshakeAuthWith ctx hb >> loopHandshake13 hbs
     loopHandshake13 (hb@(h, _) : hbs) = do
         rtt0 <- tls13st0RTT <$> getTLS13State ctx
         when rtt0 $ case h of
diff --git a/Network/TLS/Crypto/IES.hs b/Network/TLS/Crypto/IES.hs
--- a/Network/TLS/Crypto/IES.hs
+++ b/Network/TLS/Crypto/IES.hs
@@ -249,20 +249,32 @@
 groupEncapsulate (GroupPubA_MLKEM1024 pub) = do
     (sec, ct) <- ML.encapsulate pub
     return $ Just (GroupPubB_MLKEM1024 ct, convert sec)
+-- The classical part of a hybrid can fail as the group alone does: an
+-- all-zero X25519 public key decodes, but the shared secret derived from
+-- it is rejected.  Nothing is turned into illegal_parameter by the caller.
 groupEncapsulate (GroupPubA_X25519MLKEM768 (e1, e2)) = do
-    (c1, k1) <- fromJust <$> getECDHPubShared' x25519 e1
-    (k2, c2) <- ML.encapsulate e2
-    -- Sec 4.1: Specifically, the order of shares in the concatenation
-    -- has been reversed.
-    return $ Just (GroupPubB_X25519MLKEM768 (c1, c2), convert k2 <> k1)
+    mx <- getECDHPubShared' x25519 e1
+    case mx of
+        Nothing -> return Nothing
+        Just (c1, k1) -> do
+            (k2, c2) <- ML.encapsulate e2
+            -- Sec 4.1: Specifically, the order of shares in the concatenation
+            -- has been reversed.
+            return $ Just (GroupPubB_X25519MLKEM768 (c1, c2), convert k2 <> k1)
 groupEncapsulate (GroupPubA_P256MLKEM768 (e1, e2)) = do
-    (c1, k1) <- fromJust <$> getECDHPubShared' p256 e1
-    (k2, c2) <- ML.encapsulate e2
-    return $ Just (GroupPubB_P256MLKEM768 (c1, c2), k1 <> convert k2)
+    mx <- getECDHPubShared' p256 e1
+    case mx of
+        Nothing -> return Nothing
+        Just (c1, k1) -> do
+            (k2, c2) <- ML.encapsulate e2
+            return $ Just (GroupPubB_P256MLKEM768 (c1, c2), k1 <> convert k2)
 groupEncapsulate (GroupPubA_P384MLKEM1024 (e1, e2)) = do
-    (c1, k1) <- fromJust <$> getECDHPubShared' p384 e1
-    (k2, c2) <- ML.encapsulate e2
-    return $ Just (GroupPubB_P384MLKEM1024 (c1, c2), k1 <> convert k2)
+    mx <- getECDHPubShared' p384 e1
+    case mx of
+        Nothing -> return Nothing
+        Just (c1, k1) -> do
+            (k2, c2) <- ML.encapsulate e2
+            return $ Just (GroupPubB_P384MLKEM1024 (c1, c2), k1 <> convert k2)
 
 dhGroupGetPubShared
     :: MonadRandom r => Group -> PublicNumber -> r (Maybe (PublicNumber, GroupKey))
diff --git a/Network/TLS/Error.hs b/Network/TLS/Error.hs
--- a/Network/TLS/Error.hs
+++ b/Network/TLS/Error.hs
@@ -3,8 +3,7 @@
 
 module Network.TLS.Error where
 
-import Control.Exception (Exception (..))
-import Data.Typeable
+import qualified Control.Exception as E
 
 import Network.TLS.Imports
 
@@ -34,7 +33,7 @@
     | Error_Packet_unexpected String String
     | Error_Packet_Parsing String
     | Error_TCP_Terminate
-    deriving (Eq, Show, Typeable)
+    deriving (Eq, Show)
 
 ----------------------------------------------------------------
 
@@ -61,9 +60,9 @@
       --   handshake had occurred.
       --   Indicates that this library has been used incorrectly.
       MissingHandshake
-    deriving (Show, Eq, Typeable)
+    deriving (Show, Eq)
 
-instance Exception TLSException
+instance E.Exception TLSException
 
 ----------------------------------------------------------------
 
diff --git a/Network/TLS/Extension.hs b/Network/TLS/Extension.hs
--- a/Network/TLS/Extension.hs
+++ b/Network/TLS/Extension.hs
@@ -428,7 +428,22 @@
 -- | Extension class to transform bytes to and from a high level Extension type.
 class Extension a where
     extensionID :: a -> ExtensionID
+
+    -- | Decode an extension's body as it appears in the given message.
+    --
+    -- 'Nothing' covers both ways this can fail to produce a value: a body
+    -- that does not parse, and a message the extension is not defined in.
+    -- Both reach the peer the same way, as the decode_error alert that
+    -- 'lookupAndDecode' and 'lookupAndDecodeAndDo' raise, which is what
+    -- either case warrants.
+    --
+    -- So the last clause of an instance is @Nothing@, never @error@: the
+    -- message type is chosen by this library rather than by the peer, so an
+    -- unhandled one would be our own bug -- and turning our bug into an
+    -- ErrorCall thrown from pure code, out through the handshake and into
+    -- the application, is a worse answer than dropping the one connection.
     extensionDecode :: MessageType -> ByteString -> Maybe a
+
     extensionEncode :: a -> ByteString
 
 data MessageType
@@ -438,12 +453,12 @@
     | MsgTEncryptedExtensions
     | MsgTNewSessionTicket
     | MsgTCertificateRequest
-    deriving (Eq, Show)
+    deriving (Eq, Show, Enum, Bounded)
 
 ------------------------------------------------------------
 
 -- | Server Name extension including the name type and the associated name.
--- the associated name decoding is dependant of its name type.
+-- the associated name decoding is dependent of its name type.
 -- name type = 0 : hostname
 newtype ServerName = ServerName [ServerNameType] deriving (Show, Eq)
 
@@ -466,21 +481,32 @@
       where
         encodeNameType (ServerNameHostName hn) = putWord8 0 >> putOpaque16 (BC.pack hn) -- FIXME: should be puny code conversion
         encodeNameType (ServerNameOther (nt, opaque)) = putWord8 nt >> putBytes opaque
-    extensionDecode MsgTClientHello = decodeServerName
+    extensionDecode MsgTClientHello = decodeServerNameList
     extensionDecode MsgTServerHello = decodeServerName
     extensionDecode MsgTEncryptedExtensions = decodeServerName
-    extensionDecode _ = error "extensionDecode: ServerName"
+    extensionDecode _ = const Nothing
 
 decodeServerName :: ByteString -> Maybe ServerName
 decodeServerName "" = Just $ ServerName [] -- dirty hack for servers
-decodeServerName bs = runGetMaybe decode bs
+decodeServerName bs = decodeServerNameList bs
+
+-- RFC 6066 Section 3: server_name_list<1..2^16-1> of
+-- HostName<1..2^16-1>, which leaves no room for an empty extension, an
+-- empty list, an empty host name nor trailing data.
+decodeServerNameList :: ByteString -> Maybe ServerName
+decodeServerNameList = runGetMaybe decode
   where
     decode = do
         len <- fromIntegral <$> getWord16
-        ServerName <$> getList len getServerName
+        names <- getList len getServerName
+        when (null names) $ fail "empty server_name_list"
+        r <- remaining
+        when (r /= 0) $ fail "trailing data in server_name"
+        return $ ServerName names
     getServerName = do
         ty <- getWord8
         snameParsed <- getOpaque16
+        when (ty == 0 && B.null snameParsed) $ fail "empty host_name"
         let sname = B.copy snameParsed
             name = case ty of
                 0 -> ServerNameHostName $ BC.unpack sname -- FIXME: should be puny code conversion
@@ -521,7 +547,7 @@
     extensionDecode MsgTClientHello = decodeMaxFragmentLength
     extensionDecode MsgTServerHello = decodeMaxFragmentLength
     extensionDecode MsgTEncryptedExtensions = decodeMaxFragmentLength
-    extensionDecode _ = error "extensionDecode: MaxFragmentLength"
+    extensionDecode _ = const Nothing
 
 decodeMaxFragmentLength :: ByteString -> Maybe MaxFragmentLength
 decodeMaxFragmentLength = runGetMaybe $ toMaxFragmentEnum <$> getWord8
@@ -542,7 +568,7 @@
     extensionEncode (SupportedGroups groups) = runPut $ putWords16 $ map (\(Group g) -> g) groups
     extensionDecode MsgTClientHello = decodeSupportedGroups
     extensionDecode MsgTEncryptedExtensions = decodeSupportedGroups
-    extensionDecode _ = error "extensionDecode: SupportedGroups"
+    extensionDecode _ = const Nothing
 
 decodeSupportedGroups :: ByteString -> Maybe SupportedGroups
 decodeSupportedGroups =
@@ -577,11 +603,14 @@
     extensionEncode (EcPointFormatsSupported formats) = runPut $ putWords8 $ map fromEcPointFormat formats
     extensionDecode MsgTClientHello = decodeEcPointFormatsSupported
     extensionDecode MsgTServerHello = decodeEcPointFormatsSupported
-    extensionDecode _ = error "extensionDecode: EcPointFormatsSupported"
+    extensionDecode _ = const Nothing
 
 decodeEcPointFormatsSupported :: ByteString -> Maybe EcPointFormatsSupported
-decodeEcPointFormatsSupported =
-    runGetMaybe (EcPointFormatsSupported . map EcPointFormat <$> getWords8)
+decodeEcPointFormatsSupported = runGetMaybe $ do
+    formats <- getWords8
+    -- RFC 8422 Section 5.1.2: ec_point_format_list<1..2^8-1>
+    when (null formats) $ fail "empty ec_point_format_list"
+    return $ EcPointFormatsSupported $ map EcPointFormat formats
 
 ------------------------------------------------------------
 
@@ -596,7 +625,7 @@
                 >> mapM_ putSignatureHashAlgorithm algs
     extensionDecode MsgTClientHello = decodeSignatureAlgorithms
     extensionDecode MsgTCertificateRequest = decodeSignatureAlgorithms
-    extensionDecode _ = error "extensionDecode: SignatureAlgorithms"
+    extensionDecode _ = const Nothing
 
 decodeSignatureAlgorithms :: ByteString -> Maybe SignatureAlgorithms
 decodeSignatureAlgorithms = runGetMaybe $ do
@@ -632,7 +661,7 @@
     extensionEncode (HeartBeat mode) = runPut $ putWord8 $ fromHeartBeatMode mode
     extensionDecode MsgTClientHello = decodeHeartBeat
     extensionDecode MsgTServerHello = decodeHeartBeat
-    extensionDecode _ = error "extensionDecode: HeartBeat"
+    extensionDecode _ = const Nothing
 
 decodeHeartBeat :: ByteString -> Maybe HeartBeat
 decodeHeartBeat = runGetMaybe $ HeartBeat . HeartBeatMode <$> getWord8
@@ -651,16 +680,23 @@
     extensionDecode MsgTClientHello = decodeApplicationLayerProtocolNegotiation
     extensionDecode MsgTServerHello = decodeApplicationLayerProtocolNegotiation
     extensionDecode MsgTEncryptedExtensions = decodeApplicationLayerProtocolNegotiation
-    extensionDecode _ = error "extensionDecode: ApplicationLayerProtocolNegotiation"
+    extensionDecode _ = const Nothing
 
 decodeApplicationLayerProtocolNegotiation
     :: ByteString -> Maybe ApplicationLayerProtocolNegotiation
 decodeApplicationLayerProtocolNegotiation = runGetMaybe $ do
     len <- getWord16
-    ApplicationLayerProtocolNegotiation <$> getList (fromIntegral len) getALPN
+    protos <- getList (fromIntegral len) getALPN
+    -- RFC 7301 Section 3.1: protocol_name_list<2..2^16-1> of
+    -- ProtocolName<1..2^8-1>, with nothing after it.
+    when (null protos) $ fail "empty protocol_name_list"
+    r <- remaining
+    when (r /= 0) $ fail "trailing data in application_layer_protocol_negotiation"
+    return $ ApplicationLayerProtocolNegotiation protos
   where
     getALPN = do
         alpnParsed <- getOpaque8
+        when (B.null alpnParsed) $ fail "empty ProtocolName"
         let alpn = B.copy alpnParsed
         return (B.length alpn + 1, alpn)
 
@@ -674,7 +710,7 @@
     extensionEncode ExtendedMainSecret = B.empty
     extensionDecode MsgTClientHello "" = Just ExtendedMainSecret
     extensionDecode MsgTServerHello "" = Just ExtendedMainSecret
-    extensionDecode _ _ = error "extensionDecode: ExtendedMainSecret"
+    extensionDecode _ _ = Nothing
 
 ------------------------------------------------------------
 
@@ -749,7 +785,7 @@
     extensionEncode (SessionTicket ticket) = runPut $ putBytes ticket
     extensionDecode MsgTClientHello = decodeSessionTicket
     extensionDecode MsgTServerHello = decodeSessionTicket
-    extensionDecode _ = error "extensionDecode: SessionTicket"
+    extensionDecode _ = const Nothing
 
 decodeSessionTicket :: ByteString -> Maybe SessionTicket
 decodeSessionTicket = runGetMaybe $ SessionTicket <$> (remaining >>= getBytes)
@@ -792,7 +828,7 @@
                 fromIntegral w16
     extensionDecode MsgTClientHello = decodePreSharedKeyClientHello
     extensionDecode MsgTServerHello = decodePreSharedKeyServerHello
-    extensionDecode _ = error "extensionDecode: PreShareKey"
+    extensionDecode _ = const Nothing
 
 decodePreSharedKeyClientHello :: ByteString -> Maybe PreSharedKey
 decodePreSharedKeyClientHello = runGetMaybe $ do
@@ -837,7 +873,7 @@
     extensionDecode MsgTNewSessionTicket =
         runGetMaybe $
             EarlyDataIndication . Just <$> getWord32
-    extensionDecode _ = error "extensionDecode: EarlyDataIndication"
+    extensionDecode _ = const Nothing
 
 ------------------------------------------------------------
 
@@ -860,7 +896,7 @@
             putBinaryVersion ver
     extensionDecode MsgTClientHello = decodeSupportedVersionsClientHello
     extensionDecode MsgTServerHello = decodeSupportedVersionsServerHello
-    extensionDecode _ = error "extensionDecode: SupportedVersionsServerHello"
+    extensionDecode _ = const Nothing
 
 decodeSupportedVersionsClientHello :: ByteString -> Maybe SupportedVersions
 decodeSupportedVersionsClientHello = runGetMaybe $ do
@@ -888,7 +924,7 @@
     extensionID _ = EID_Cookie
     extensionEncode (Cookie opaque) = runPut $ putOpaque16 opaque
     extensionDecode MsgTServerHello = runGetMaybe (Cookie <$> getOpaque16)
-    extensionDecode _ = error "extensionDecode: Cookie"
+    extensionDecode _ = const Nothing
 
 ------------------------------------------------------------
 
@@ -916,7 +952,7 @@
             putWords8 $
                 map fromPskKexMode pkms
     extensionDecode MsgTClientHello = decodePskKeyExchangeModes
-    extensionDecode _ = error "extensionDecode: PskKeyExchangeModes"
+    extensionDecode _ = const Nothing
 
 decodePskKeyExchangeModes :: ByteString -> Maybe PskKeyExchangeModes
 decodePskKeyExchangeModes =
@@ -935,7 +971,7 @@
             putDNames names
     extensionDecode MsgTClientHello = decodeCertificateAuthorities
     extensionDecode MsgTCertificateRequest = decodeCertificateAuthorities
-    extensionDecode _ = error "extensionDecode: CertificateAuthorities"
+    extensionDecode _ = const Nothing
 
 decodeCertificateAuthorities :: ByteString -> Maybe CertificateAuthorities
 decodeCertificateAuthorities =
@@ -949,7 +985,7 @@
     extensionID _ = EID_PostHandshakeAuth
     extensionEncode _ = B.empty
     extensionDecode MsgTClientHello = runGetMaybe $ return PostHandshakeAuth
-    extensionDecode _ = error "extensionDecode: PostHandshakeAuth"
+    extensionDecode _ = const Nothing
 
 ------------------------------------------------------------
 
@@ -964,7 +1000,7 @@
                 >> mapM_ putSignatureHashAlgorithm algs
     extensionDecode MsgTClientHello = decodeSignatureAlgorithmsCert
     extensionDecode MsgTCertificateRequest = decodeSignatureAlgorithmsCert
-    extensionDecode _ = error "extensionDecode: SignatureAlgorithmsCert"
+    extensionDecode _ = const Nothing
 
 decodeSignatureAlgorithmsCert :: ByteString -> Maybe SignatureAlgorithmsCert
 decodeSignatureAlgorithmsCert = runGetMaybe $ do
@@ -1021,7 +1057,7 @@
     extensionDecode MsgTClientHello = decodeKeyShareClientHello
     extensionDecode MsgTServerHello = decodeKeyShareServerHello
     extensionDecode MsgTHelloRetryRequest = decodeKeyShareHRR
-    extensionDecode _ = error "extensionDecode: KeyShare"
+    extensionDecode _ = const Nothing
 
 decodeKeyShareClientHello :: ByteString -> Maybe KeyShare
 decodeKeyShareClientHello = runGetMaybe $ do
@@ -1059,7 +1095,7 @@
         putWord8 $ fromIntegral (length ids * 2)
         mapM_ (putWord16 . fromExtensionID) ids
     extensionDecode MsgTClientHello = decodeEchOuterExtensions
-    extensionDecode _ = error "extensionDecode: EchOuterExtensions"
+    extensionDecode _ = const Nothing
 
 decodeEchOuterExtensions :: ByteString -> Maybe EchOuterExtensions
 decodeEchOuterExtensions = runGetMaybe $ do
@@ -1120,7 +1156,7 @@
     extensionDecode MsgTClientHello = decodeECHClientHello
     extensionDecode MsgTEncryptedExtensions = decodeECHEncryptedExtensions
     extensionDecode MsgTHelloRetryRequest = decodeECHHelloRetryRequest
-    extensionDecode _ = error "extensionDecode: EncryptedClientHello"
+    extensionDecode _ = const Nothing
 
 decodeECH :: ByteString -> Maybe EncryptedClientHello
 decodeECH bs =
@@ -1172,4 +1208,4 @@
         opaque <- getOpaque8
         let (cvd, svd) = B.splitAt (B.length opaque `div` 2) opaque
         return $ SecureRenegotiation cvd svd
-    extensionDecode _ = error "extensionDecode: SecureRenegotiation"
+    extensionDecode _ = const Nothing
diff --git a/Network/TLS/Extra/Cipher.hs b/Network/TLS/Extra/Cipher.hs
--- a/Network/TLS/Extra/Cipher.hs
+++ b/Network/TLS/Extra/Cipher.hs
@@ -70,11 +70,12 @@
 ) where
 
 import Crypto.Cipher.AES
-import qualified Crypto.Cipher.ChaChaPoly1305 as ChaChaPoly1305
+import qualified Crypto.Cipher.AES.GCM as GCM
+import qualified Crypto.Cipher.ChaCha.Poly1305 as ChaChaOne
 import Crypto.Cipher.Types hiding (Cipher, cipherName)
 import Crypto.Error
-import qualified Crypto.MAC.Poly1305 as Poly1305
 import Crypto.System.CPU
+import Data.ByteArray (convert)
 import qualified Data.ByteString as B
 import Data.Tuple (swap)
 
@@ -619,20 +620,40 @@
              in simpleDecrypt aeadIni ad d 8
         )
 
+-- The AES-GCM and ChaCha20-Poly1305 ciphers go through the one-call
+-- interfaces crypton added for this, not the general AEAD one.  Two things
+-- are saved on every record.
+--
+-- The state the key alone determines -- the AES key schedule and the table of
+-- multiples of H -- was rebuilt for each record by aeadInit; newContext
+-- builds it once, here, where the key is fixed.
+--
+-- And the general interface reaches its cipher through AEADModeImpl, whose
+-- fields are @forall ba. ByteArray ba => ...@, so a dictionary is passed at
+-- every call and no pragma can remove it.
+--
+-- Measured in C on an idle Haswell, against what this did before: 64-byte
+-- record 0.169 -> 0.047 microseconds, 1400-byte 0.501 -> 0.301, 16 KiB
+-- 3.285 -> 3.207.  It is a fixed cost that goes, so it is most of a small
+-- record and little of a full one.
+aesgcm :: BulkDirection -> BulkKey -> BulkAEAD
+aesgcm BulkEncrypt key =
+    let ctx = noFail (GCM.newContext key)
+     in \nonce d ad ->
+            let sealed = GCM.encrypt ctx nonce ad d 16
+                (out, tag) = B.splitAt (B.length sealed - 16) sealed
+             in (out, AuthTag (convert tag))
+aesgcm BulkDecrypt key =
+    let ctx = noFail (GCM.newContext key)
+     in \nonce d ad -> GCM.decryptWithTag ctx nonce ad d 16
+
 aes128gcm :: BulkDirection -> BulkKey -> BulkAEAD
-aes128gcm BulkEncrypt key =
-    let ctx = noFail (cipherInit key) :: AES128
-     in ( \nonce d ad ->
-            let aeadIni = noFail (aeadInit AEAD_GCM ctx nonce)
-             in swap $ aeadSimpleEncrypt aeadIni ad d 16
-        )
-aes128gcm BulkDecrypt key =
-    let ctx = noFail (cipherInit key) :: AES128
-     in ( \nonce d ad ->
-            let aeadIni = noFail (aeadInit AEAD_GCM ctx nonce)
-             in simpleDecrypt aeadIni ad d 16
-        )
+aes128gcm = aesgcm
 
+aes256gcm :: BulkDirection -> BulkKey -> BulkAEAD
+aes256gcm = aesgcm
+
+
 aes256ccm :: BulkDirection -> BulkKey -> BulkAEAD
 aes256ccm BulkEncrypt key =
     let ctx = noFail (cipherInit key) :: AES256
@@ -665,20 +686,6 @@
              in simpleDecrypt aeadIni ad d 8
         )
 
-aes256gcm :: BulkDirection -> BulkKey -> BulkAEAD
-aes256gcm BulkEncrypt key =
-    let ctx = noFail (cipherInit key) :: AES256
-     in ( \nonce d ad ->
-            let aeadIni = noFail (aeadInit AEAD_GCM ctx nonce)
-             in swap $ aeadSimpleEncrypt aeadIni ad d 16
-        )
-aes256gcm BulkDecrypt key =
-    let ctx = noFail (cipherInit key) :: AES256
-     in ( \nonce d ad ->
-            let aeadIni = noFail (aeadInit AEAD_GCM ctx nonce)
-             in simpleDecrypt aeadIni ad d 16
-        )
-
 simpleDecrypt
     :: AEAD cipher -> ByteString -> ByteString -> Int -> (ByteString, AuthTag)
 simpleDecrypt aeadIni header input taglen = (output, tag)
@@ -690,23 +697,23 @@
 noFail :: CryptoFailable a -> a
 noFail = throwCryptoError
 
+-- The one-call interface here too, and for the same two reasons as the AES
+-- ciphers above: the step-at-a-time Crypto.Cipher.ChaChaPoly1305 is eight
+-- foreign calls and the allocations between them for a message that arrived
+-- whole, and the general AEAD interface passes a dictionary a call.
+--
+-- Measured through the Haskell interface on an Apple M4: 100 bytes 0.97 ->
+-- 0.415 microseconds, 1400 bytes 2.89 -> 2.36.
 chacha20poly1305 :: BulkDirection -> BulkKey -> BulkAEAD
-chacha20poly1305 BulkEncrypt key nonce =
-    let st = noFail (ChaChaPoly1305.nonce12 nonce >>= ChaChaPoly1305.initialize key)
-     in ( \input ad ->
-            let st2 = ChaChaPoly1305.finalizeAAD (ChaChaPoly1305.appendAAD ad st)
-                (output, st3) = ChaChaPoly1305.encrypt input st2
-                Poly1305.Auth tag = ChaChaPoly1305.finalize st3
-             in (output, AuthTag tag)
-        )
-chacha20poly1305 BulkDecrypt key nonce =
-    let st = noFail (ChaChaPoly1305.nonce12 nonce >>= ChaChaPoly1305.initialize key)
-     in ( \input ad ->
-            let st2 = ChaChaPoly1305.finalizeAAD (ChaChaPoly1305.appendAAD ad st)
-                (output, st3) = ChaChaPoly1305.decrypt input st2
-                Poly1305.Auth tag = ChaChaPoly1305.finalize st3
-             in (output, AuthTag tag)
-        )
+chacha20poly1305 BulkEncrypt key =
+    let ctx = noFail (ChaChaOne.newContext key)
+     in \nonce d ad ->
+            let sealed = noFail (ChaChaOne.encrypt ctx nonce ad d 16)
+                (out, tag) = B.splitAt (B.length sealed - 16) sealed
+             in (out, AuthTag (convert tag))
+chacha20poly1305 BulkDecrypt key =
+    let ctx = noFail (ChaChaOne.newContext key)
+     in \nonce d ad -> noFail (ChaChaOne.decryptWithTag ctx nonce ad d 16)
 
 ----------------------------------------------------------------
 
diff --git a/Network/TLS/Extra/CipherCBC.hs b/Network/TLS/Extra/CipherCBC.hs
new file mode 100644
--- /dev/null
+++ b/Network/TLS/Extra/CipherCBC.hs
@@ -0,0 +1,201 @@
+module Network.TLS.Extra.CipherCBC (
+    -- * TLS 1.2 CBC ciphers with PFS and SHA2
+    ciphersuite_pfs_sha2_cbc,
+    ciphersuite_ecdhe_sha2_cbc,
+    ciphersuite_dhe_rsa_sha2_cbc,
+
+    -- ** Individual CBC ciphers
+    cipher_DHE_RSA_AES128_SHA256,
+    cipher_DHE_RSA_AES256_SHA256,
+    cipher_ECDHE_RSA_AES128CBC_SHA256,
+    cipher_ECDHE_RSA_AES256CBC_SHA384,
+    cipher_ECDHE_ECDSA_AES128CBC_SHA256,
+) where
+
+import Crypto.Cipher.AES
+import Crypto.Cipher.Types hiding (Cipher, cipherName)
+import Crypto.Error
+-- import Crypto.System.CPU
+import qualified Data.ByteString as B
+
+import Network.TLS.Cipher
+import Network.TLS.Imports
+import Network.TLS.Types hiding (IV)
+
+----------------------------------------------------------------
+
+-- | TLS 1.2 AES CBC ciphers with DHE or ECDHE key exchange, ECDSA or RSA
+-- authentication and a SHA256 or SHA2384 MAC.
+-- For legacy applications only, deprecated in HTTPS.
+ciphersuite_pfs_sha2_cbc :: [Cipher]
+ciphersuite_pfs_sha2_cbc =
+    [ cipher_ECDHE_ECDSA_AES128CBC_SHA256
+    , cipher_ECDHE_ECDSA_AES256CBC_SHA384
+    , cipher_ECDHE_RSA_AES128CBC_SHA256
+    , cipher_ECDHE_RSA_AES256CBC_SHA384
+    , cipher_DHE_RSA_AES128_SHA256
+    , cipher_DHE_RSA_AES256_SHA256
+    ]
+
+-- | TLS 1.2 AES CBC ciphers with ECDHE key exchange, ECDSA or RSA
+-- authentication and a SHA256 or SHA2384 MAC.
+-- For legacy applications only, deprecated in HTTPS.
+ciphersuite_ecdhe_sha2_cbc :: [Cipher]
+ciphersuite_ecdhe_sha2_cbc =
+    [ cipher_ECDHE_ECDSA_AES128CBC_SHA256
+    , cipher_ECDHE_ECDSA_AES256CBC_SHA384
+    , cipher_ECDHE_RSA_AES128CBC_SHA256
+    , cipher_ECDHE_RSA_AES256CBC_SHA384
+    ]
+
+-- | TLS 1.2 AES CBC ciphers with DHE key exchange, RSA authentication and a
+-- SHA256 MAC.
+-- For legacy applications only, deprecated in HTTPS.
+ciphersuite_dhe_rsa_sha2_cbc :: [Cipher]
+ciphersuite_dhe_rsa_sha2_cbc =
+    [ cipher_DHE_RSA_AES256_SHA256
+    , cipher_DHE_RSA_AES128_SHA256
+    ]
+
+----------------------------------------------------------------
+
+-- | TLS 1.2 AES128 CBC, with DHE key exchange, RSA authentication and a SHA256 MAC.
+-- For legacy applications only, deprecated in HTTPS.
+cipher_DHE_RSA_AES128_SHA256 :: Cipher
+cipher_DHE_RSA_AES128_SHA256 =
+    Cipher
+        { cipherID = 0x0067
+        , cipherName = "DHE-RSA-AES128-SHA256"
+        , cipherBulk = bulk_aes128
+        , cipherHash = SHA256
+        , cipherPRFHash = Just SHA256
+        , cipherKeyExchange = CipherKeyExchange_DHE_RSA
+        , cipherMinVer = Just TLS12 -- RFC 5288 Sec 4
+        }
+
+-- | TLS 1.2 AES256 CBC, with DHE key exchange, RSA authentication and a SHA256 MAC.
+-- For legacy applications only, deprecated in HTTPS.
+cipher_DHE_RSA_AES256_SHA256 :: Cipher
+cipher_DHE_RSA_AES256_SHA256 =
+    cipher_DHE_RSA_AES128_SHA256
+        { cipherID = 0x006B
+        , cipherName = "DHE-RSA-AES256-SHA256"
+        , cipherBulk = bulk_aes256
+        }
+
+-- | TLS 1.2 AES128 CBC, with ECDHE key exchange, RSA authentication and a SHA256 MAC.
+-- For legacy applications only, deprecated in HTTPS.
+cipher_ECDHE_RSA_AES128CBC_SHA256 :: Cipher
+cipher_ECDHE_RSA_AES128CBC_SHA256 =
+    Cipher
+        { cipherID = 0xC027
+        , cipherName = "ECDHE-RSA-AES128CBC-SHA256"
+        , cipherBulk = bulk_aes128
+        , cipherHash = SHA256
+        , cipherPRFHash = Just SHA256
+        , cipherKeyExchange = CipherKeyExchange_ECDHE_RSA
+        , cipherMinVer = Just TLS12 -- RFC 5288 Sec 4
+        }
+
+-- | TLS 1.2 AES256 CBC, with ECDHE key exchange, RSA authentication and a SHA384 MAC.
+-- For legacy applications only, deprecated in HTTPS.
+cipher_ECDHE_RSA_AES256CBC_SHA384 :: Cipher
+cipher_ECDHE_RSA_AES256CBC_SHA384 =
+    Cipher
+        { cipherID = 0xC028
+        , cipherName = "ECDHE-RSA-AES256CBC-SHA384"
+        , cipherBulk = bulk_aes256
+        , cipherHash = SHA384
+        , cipherPRFHash = Just SHA384
+        , cipherKeyExchange = CipherKeyExchange_ECDHE_RSA
+        , cipherMinVer = Just TLS12 -- RFC 5288 Sec 4
+        }
+
+-- | TLS 1.2 AES128 CBC, with ECDHE key exchange, ECDSA authentication and a SHA256 MAC.
+-- For legacy applications only, deprecated in HTTPS.
+cipher_ECDHE_ECDSA_AES128CBC_SHA256 :: Cipher
+cipher_ECDHE_ECDSA_AES128CBC_SHA256 =
+    Cipher
+        { cipherID = 0xc023
+        , cipherName = "ECDHE-ECDSA-AES128CBC-SHA256"
+        , cipherBulk = bulk_aes128
+        , cipherHash = SHA256
+        , cipherPRFHash = Just SHA256
+        , cipherKeyExchange = CipherKeyExchange_ECDHE_ECDSA
+        , cipherMinVer = Just TLS12 -- RFC 5289
+        }
+
+-- | TLS 1.2 AES256 CBC, with ECDHE key exchange, ECDSA authentication and a SHA384 MAC.
+-- For legacy applications only, deprecated in HTTPS.
+cipher_ECDHE_ECDSA_AES256CBC_SHA384 :: Cipher
+cipher_ECDHE_ECDSA_AES256CBC_SHA384 =
+    Cipher
+        { cipherID = 0xC024
+        , cipherName = "ECDHE-ECDSA-AES256CBC-SHA384"
+        , cipherBulk = bulk_aes256
+        , cipherHash = SHA384
+        , cipherPRFHash = Just SHA384
+        , cipherKeyExchange = CipherKeyExchange_ECDHE_ECDSA
+        , cipherMinVer = Just TLS12 -- RFC 5289
+        }
+
+----------------------------------------------------------------
+
+aes128cbc :: BulkDirection -> BulkKey -> BulkBlock
+aes128cbc BulkEncrypt key =
+    let ctx = noFail (cipherInit key) :: AES128
+     in ( \iv input ->
+            let output = cbcEncrypt ctx (makeIV_ iv) input in (output, takelast 16 output)
+        )
+aes128cbc BulkDecrypt key =
+    let ctx = noFail (cipherInit key) :: AES128
+     in ( \iv input ->
+            let output = cbcDecrypt ctx (makeIV_ iv) input in (output, takelast 16 input)
+        )
+
+aes256cbc :: BulkDirection -> BulkKey -> BulkBlock
+aes256cbc BulkEncrypt key =
+    let ctx = noFail (cipherInit key) :: AES256
+     in ( \iv input ->
+            let output = cbcEncrypt ctx (makeIV_ iv) input in (output, takelast 16 output)
+        )
+aes256cbc BulkDecrypt key =
+    let ctx = noFail (cipherInit key) :: AES256
+     in ( \iv input ->
+            let output = cbcDecrypt ctx (makeIV_ iv) input in (output, takelast 16 input)
+        )
+
+makeIV_ :: BlockCipher a => B.ByteString -> IV a
+makeIV_ = fromMaybe (error "makeIV_") . makeIV
+
+takelast :: Int -> B.ByteString -> B.ByteString
+takelast i b = B.drop (B.length b - i) b
+
+noFail :: CryptoFailable a -> a
+noFail = throwCryptoError
+
+----------------------------------------------------------------
+
+bulk_aes128 :: Bulk
+bulk_aes128 =
+    Bulk
+        { bulkName = "AES128"
+        , bulkKeySize = 16
+        , bulkIVSize = 16
+        , bulkExplicitIV = 0
+        , bulkAuthTagLen = 0
+        , bulkBlockSize = 16
+        , bulkF = BulkBlockF aes128cbc
+        }
+
+bulk_aes256 :: Bulk
+bulk_aes256 =
+    Bulk
+        { bulkName = "AES256"
+        , bulkKeySize = 32
+        , bulkIVSize = 16
+        , bulkExplicitIV = 0
+        , bulkAuthTagLen = 0
+        , bulkBlockSize = 16
+        , bulkF = BulkBlockF aes256cbc
+        }
diff --git a/Network/TLS/Handshake/Certificate.hs b/Network/TLS/Handshake/Certificate.hs
--- a/Network/TLS/Handshake/Certificate.hs
+++ b/Network/TLS/Handshake/Certificate.hs
@@ -3,13 +3,20 @@
     badCertificate,
     rejectOnException,
     verifyLeafKeyUsage,
+    verifyLeafKeyUsagePurpose,
     extractCAname,
 ) where
 
-import Control.Exception (SomeException)
+import qualified Control.Exception as E
 import Control.Monad (unless)
 import Control.Monad.State.Strict
-import Data.X509 (ExtKeyUsage (..), ExtKeyUsageFlag, extensionGet)
+import Data.X509 (
+    ExtExtendedKeyUsage (..),
+    ExtKeyUsage (..),
+    ExtKeyUsageFlag,
+    ExtKeyUsagePurpose (..),
+    extensionGet,
+ )
 
 import Network.TLS.Context.Internal
 import Network.TLS.Struct
@@ -31,7 +38,7 @@
 badCertificate :: MonadIO m => String -> m a
 badCertificate msg = throwCore $ Error_Protocol msg BadCertificate
 
-rejectOnException :: SomeException -> IO CertificateUsage
+rejectOnException :: E.SomeException -> IO CertificateUsage
 rejectOnException e = return $ CertificateUsageReject $ CertificateRejectOther $ show e
 
 verifyLeafKeyUsage :: MonadIO m => [ExtKeyUsageFlag] -> CertificateChain -> m ()
@@ -46,6 +53,19 @@
         case extensionGet (certExtensions cert) of
             Nothing -> True -- unrestricted cert
             Just (ExtKeyUsage flags) -> any (`elem` validFlags) flags
+
+verifyLeafKeyUsagePurpose
+    :: MonadIO m => ExtKeyUsagePurpose -> CertificateChain -> m ()
+verifyLeafKeyUsagePurpose _ (CertificateChain []) = return ()
+verifyLeafKeyUsagePurpose validPurpose (CertificateChain (signed : _)) =
+    unless verified $
+        badCertificate $
+            "certificate is not allowed for " ++ show validPurpose
+  where
+    cert = getCertificate signed
+    verified = case extensionGet (certExtensions cert) of
+        Nothing -> True
+        Just (ExtExtendedKeyUsage purposes) -> validPurpose `elem` purposes
 
 extractCAname :: SignedCertificate -> DistinguishedName
 extractCAname cert = certSubjectDN $ getCertificate cert
diff --git a/Network/TLS/Handshake/Client.hs b/Network/TLS/Handshake/Client.hs
--- a/Network/TLS/Handshake/Client.hs
+++ b/Network/TLS/Handshake/Client.hs
@@ -102,7 +102,7 @@
         case ver of
             TLS13
                 | hrr ->
-                    helloRetry cparams ctx mparams ver crand (grpsSupported \\ grpsSelected)
+                    helloRetry cparams ctx mparams ver crand grpsSupported grpsSelected
                 | otherwise -> do
                     recvServerSecondFlight13 cparams ctx grpsSelected
                     sendClientSecondFlight13 cparams ctx
@@ -126,8 +126,9 @@
     -> Version
     -> ClientRandom
     -> [Group]
+    -> [Group]
     -> IO ()
-helloRetry cparams ctx mparams ver crand groupsSupported = do
+helloRetry cparams ctx mparams ver crand groupsSupported groupsSelected = do
     when (null groupsSupported) $
         throwCore $
             Error_Protocol "no supported groups on the client side" IllegalParameter
@@ -137,18 +138,22 @@
     mks <- usingState_ ctx getTLS13KeyShare
     case mks of
         Just (KeyShareHRR selectedGroup)
-            | selectedGroup `elem` groupsSupported -> do
+            -- RFC 8446 Sec 4.1.4: the selected_group MUST be in supported_groups
+            -- and MUST NOT already have been offered in the initial key_share.
+            | selectedGroup `elem` groupsSupported
+                && selectedGroup `notElem` groupsSelected -> do
                 usingHState ctx $ setTLS13HandshakeMode HelloRetryRequest
                 clearTxRecordState ctx
                 let cparams' = cparams{clientUseEarlyData = False}
                 runPacketFlight ctx $ sendChangeCipherSpec13 ctx
                 clientSession <- tls13stSession <$> getTLS13State ctx
-                let groupsSupported' = selectedGroup : filter (/= selectedGroup) groupsSupported
-                    groupsSelected' = [selectedGroup]
-                    grps =
+                -- RFC 8446 Sec 4.1.2: the second ClientHello MUST be identical
+                -- to the first except for the specific listed changes.
+                -- supported_groups is NOT on that list, so it must be unchanged.
+                let grps =
                         Groups
-                            { grpsSupported = groupsSupported'
-                            , grpsSelected = groupsSelected'
+                            { grpsSupported = groupsSupported
+                            , grpsSelected = [selectedGroup]
                             }
 
                 handshake
@@ -156,6 +161,11 @@
                     ctx
                     grps
                     (Just (crand, clientSession, ver))
+            | selectedGroup `elem` groupsSelected ->
+                throwCore $
+                    Error_Protocol
+                        "server selected a group already offered in key_share"
+                        IllegalParameter
             | otherwise ->
                 throwCore $
                     Error_Protocol "server-selected group is not supported" IllegalParameter
diff --git a/Network/TLS/Handshake/Client/ClientHello.hs b/Network/TLS/Handshake/Client/ClientHello.hs
--- a/Network/TLS/Handshake/Client/ClientHello.hs
+++ b/Network/TLS/Handshake/Client/ClientHello.hs
@@ -215,13 +215,16 @@
 
     --------------------
 
+    -- RFC 6066 Section 3: HostName is <1..2^16-1>, so no server_name
+    -- is sent for an empty name.
     sniExt =
-        if clientUseServerNameIndication cparams
+        if clientUseServerNameIndication cparams && not (null sni)
             then do
-                let sni = fst $ clientServerIdentification cparams
                 usingState_ ctx $ setClientSNI sni
                 return $ Just $ toExtensionRaw $ ServerName [ServerNameHostName sni]
             else return Nothing
+      where
+        sni = fst $ clientServerIdentification cparams
 
     -- RFC 8446 Sec 4.2.8 says: Each KeyShareEntry value MUST correspond
     -- to a group offered in the "supported_groups" extension and MUST
@@ -262,11 +265,13 @@
         Nothing -> return Nothing
         Just siz -> return $ Just $ toExtensionRaw $ RecordSizeLimit $ fromIntegral siz
 
-    sessionTicketExt = do
+    sessionTicketExt =
         case clientSessions cparams of
             (sidOrTkt, _) : _
                 | isTicket sidOrTkt -> return $ Just $ toExtensionRaw $ SessionTicket sidOrTkt
-            _ -> return $ Just $ toExtensionRaw $ SessionTicket ""
+            _
+                | clientWantTicket cparams -> return $ Just $ toExtensionRaw $ SessionTicket ""
+                | otherwise -> return $ Nothing
 
     earlyDataExt
         | rtt0 = return $ Just $ toExtensionRaw (EarlyDataIndication Nothing)
@@ -506,9 +511,13 @@
     step2 (sniExtI@(ExtensionRaw EID_ServerName _) : exts) =
         (sniExtO : os, sniExtI : is)
       where
-        sniExtO = toExtensionRaw $ ServerName [ServerNameHostName host]
         (os, is) = step3 exts id
-    step2 _ = error "step2"
+    -- No server_name for an empty name: only the outer one names the
+    -- public name.
+    step2 exts = (sniExtO : os, is)
+      where
+        (os, is) = step3 exts id
+    sniExtO = toExtensionRaw $ ServerName [ServerNameHostName host]
     step3 [] build = ([], [echOuterExt])
       where
         echOuterExt = toExtensionRaw $ EchOuterExtensions $ build []
diff --git a/Network/TLS/Handshake/Client/Common.hs b/Network/TLS/Handshake/Client/Common.hs
--- a/Network/TLS/Handshake/Client/Common.hs
+++ b/Network/TLS/Handshake/Client/Common.hs
@@ -13,9 +13,10 @@
     clientSessions,
 ) where
 
-import Control.Exception (SomeException)
+import qualified Control.Exception as E
 import Control.Monad.State.Strict
-import Data.X509 (ExtKeyUsageFlag (..))
+import qualified Data.ByteString as B
+import Data.X509 (ExtKeyUsageFlag (..), ExtKeyUsagePurpose (..))
 
 import Network.TLS.Cipher
 import Network.TLS.Context.Internal
@@ -38,7 +39,7 @@
 
 ----------------------------------------------------------------
 
-throwMiscErrorOnException :: String -> SomeException -> IO a
+throwMiscErrorOnException :: String -> E.SomeException -> IO a
 throwMiscErrorOnException msg e =
     throwCore $ Error_Misc $ msg ++ ": " ++ show e
 
@@ -124,7 +125,9 @@
     -- then run certificate validation
     usage <- catchException (wrapCertificateChecks <$> checkCert) rejectOnException
     case usage of
-        CertificateUsageAccept -> checkLeafCertificateKeyUsage
+        CertificateUsageAccept -> do
+            verifyLeafKeyUsagePurpose KeyUsagePurpose_ServerAuth certs
+            checkLeafCertificateKeyUsage
         CertificateUsageReject reason -> certificateRejected reason
   where
     shared = clientShared cparams
@@ -342,14 +345,28 @@
         (return ())
         setAlpn
   where
-    setAlpn (ApplicationLayerProtocolNegotiation [proto]) = usingState_ ctx $ do
-        mprotos <- getClientALPNSuggest
+    setAlpn (ApplicationLayerProtocolNegotiation [proto]) = do
+        mprotos <- usingState_ ctx getClientALPNSuggest
         case mprotos of
-            Just protos -> when (proto `elem` protos) $ do
-                setExtensionALPN True
-                setNegotiatedProtocol proto
-            _ -> return ()
-    setAlpn _ = return ()
+            Nothing ->
+                throwCore $
+                    Error_Protocol
+                        "server sent ALPN without a client offer"
+                        UnsupportedExtension
+            Just protos
+                | not (B.null proto) && proto `elem` protos -> usingState_ ctx $ do
+                    setExtensionALPN True
+                    setNegotiatedProtocol proto
+                | otherwise ->
+                    throwCore $
+                        Error_Protocol
+                            "server selected an ALPN protocol not offered by the client"
+                            IllegalParameter
+    setAlpn _ =
+        throwCore $
+            Error_Protocol
+                "server ALPN response did not contain exactly one protocol"
+                IllegalParameter
 
 ----------------------------------------------------------------
 
diff --git a/Network/TLS/Handshake/Client/ServerHello.hs b/Network/TLS/Handshake/Client/ServerHello.hs
--- a/Network/TLS/Handshake/Client/ServerHello.hs
+++ b/Network/TLS/Handshake/Client/ServerHello.hs
@@ -138,6 +138,12 @@
 
     ver <- usingState_ ctx getVersion
 
+    unless (cipherAllowedForVersion ver usedCipher) $
+        throwCore $
+            Error_Protocol
+                "server selected a cipher invalid for the negotiated version"
+                IllegalParameter
+
     when (ver == TLS12) $
         setServerHelloParameters12 ctx shVersion shRandom usedCipher compressAlg
 
diff --git a/Network/TLS/Handshake/Client/TLS13.hs b/Network/TLS/Handshake/Client/TLS13.hs
--- a/Network/TLS/Handshake/Client/TLS13.hs
+++ b/Network/TLS/Handshake/Client/TLS13.hs
@@ -7,7 +7,7 @@
     postHandshakeAuthClientWith,
 ) where
 
-import Control.Exception (bracket)
+import qualified Control.Exception as E
 import Control.Monad.State.Strict
 import qualified Data.ByteArray as BA
 import Data.IORef
@@ -377,10 +377,13 @@
 ----------------------------------------------------------------
 
 postHandshakeAuthClientWith
-    :: ClientParams -> Context -> Handshake13 -> IO ()
-postHandshakeAuthClientWith cparams ctx (CertRequest13 certReqCtx exts) =
-    bracket (saveHState ctx) (restoreHState ctx) $ \_ -> do
-        --        updateTranscriptHash13 ctx h b
+    :: ClientParams -> Context -> Handshake13R -> IO ()
+postHandshakeAuthClientWith cparams ctx hb@(CertRequest13 certReqCtx exts, _) =
+    E.bracket (saveHState ctx) (restoreHState ctx) $ \_ -> do
+        -- RFC 8446 Section 4.4: the handshake context of
+        -- post-handshake authentication is ClientHello ... client
+        -- Finished + CertificateRequest.
+        updateTranscriptHash13 ctx hb
         processCertRequest13 ctx certReqCtx exts
         (usedHash, _, level, applicationSecretN) <- getTxRecordState ctx
         unless (level == CryptApplicationSecret) $
diff --git a/Network/TLS/Handshake/Common.hs b/Network/TLS/Handshake/Common.hs
--- a/Network/TLS/Handshake/Common.hs
+++ b/Network/TLS/Handshake/Common.hs
@@ -40,7 +40,7 @@
 ) where
 
 import Control.Concurrent.MVar
-import Control.Exception (IOException, fromException, handle, throwIO)
+import qualified Control.Exception as E
 import Control.Monad.State.Strict
 import Data.ByteArray (convert)
 import qualified Data.ByteString as B
@@ -69,7 +69,7 @@
 import Network.TLS.X509
 
 handshakeFailed :: TLSError -> IO ()
-handshakeFailed err = throwIO $ HandshakeFailed err
+handshakeFailed err = E.throwIO $ HandshakeFailed err
 
 handleException :: Context -> IO () -> IO ()
 handleException ctx f = catchException f $ \exception -> do
@@ -77,12 +77,12 @@
     -- If the error was an Uncontextualized TLSException, we replace the
     -- context with HandshakeFailed. If it's anything else, we convert
     -- it to a string and wrap it with Error_Misc and HandshakeFailed.
-    let tlserror = case fromException exception of
+    let tlserror = case E.fromException exception of
             Just e | Uncontextualized e' <- e -> e'
             _ -> Error_Misc (show exception)
     established <- ctxEstablished ctx
     setEstablished ctx NotEstablished
-    handle ignoreIOErr $ do
+    E.handle ignoreIOErr $ do
         tls13 <- tls13orLater ctx
         if tls13
             then do
@@ -93,7 +93,7 @@
             else sendPacket12 ctx $ Alert [errorToAlert tlserror]
     handshakeFailed tlserror
   where
-    ignoreIOErr :: IOException -> IO ()
+    ignoreIOErr :: E.IOException -> IO ()
     ignoreIOErr _ = return ()
 
 errorToAlert :: TLSError -> (AlertLevel, AlertDescription)
@@ -103,6 +103,9 @@
 errorToAlert (Error_Packet_Parsing msg)
     | "invalid version" `isInfixOf` msg = (AlertLevel_Fatal, ProtocolVersion)
     | "request_update" `isInfixOf` msg = (AlertLevel_Fatal, IllegalParameter)
+    | "cannot be decompressed" `isInfixOf` msg = (AlertLevel_Fatal, BadCertificate)
+    | "unsupported certificate compression algorithm" `isInfixOf` msg =
+        (AlertLevel_Fatal, IllegalParameter)
     | otherwise = (AlertLevel_Fatal, DecodeError)
 errorToAlert _ = (AlertLevel_Fatal, InternalError)
 
diff --git a/Network/TLS/Handshake/Common13.hs b/Network/TLS/Handshake/Common13.hs
--- a/Network/TLS/Handshake/Common13.hs
+++ b/Network/TLS/Handshake/Common13.hs
@@ -175,18 +175,25 @@
     -> Signature
     -> ByteString
     -> m Bool
-checkCertVerify ctx pub hs signature hashValue
-    | pub `signatureCompatible13` hs = liftIO $ do
-        role <- usingState_ ctx getRole
-        let ctxStr
-                | role == ClientRole = serverContextString -- opposite context
-                | otherwise = clientContextString
-            target = makeTarget ctxStr hashValue
-            sigParams = signatureParams pub hs
-        checkHashSignatureValid13 hs
-        checkSupportedHashSignature ctx hs
-        verifyPublic ctx sigParams target signature
-    | otherwise = return False
+-- RFC 8446 Section 6.2: an algorithm that may not be used -- one TLS 1.3
+-- does not allow, one not offered, or one that does not fit the key -- is a
+-- field that is incorrect, an illegal_parameter.  Only a signature that does
+-- not verify is a decrypt_error, which False leads to.
+checkCertVerify ctx pub hs signature hashValue = liftIO $ do
+    checkHashSignatureValid13 hs
+    checkSupportedHashSignature ctx hs
+    unless (pub `signatureCompatible13` hs) $
+        throwCore $
+            Error_Protocol
+                ("signature algorithm " ++ show hs ++ " does not fit the public key")
+                IllegalParameter
+    role <- usingState_ ctx getRole
+    let ctxStr
+            | role == ClientRole = serverContextString -- opposite context
+            | otherwise = clientContextString
+        target = makeTarget ctxStr hashValue
+        sigParams = signatureParams pub hs
+    verifyPublic ctx sigParams target signature
 
 makeTarget :: ByteString -> ByteString -> ByteString
 makeTarget contextString hashValue = runPut $ do
diff --git a/Network/TLS/Handshake/Control.hs b/Network/TLS/Handshake/Control.hs
--- a/Network/TLS/Handshake/Control.hs
+++ b/Network/TLS/Handshake/Control.hs
@@ -7,6 +7,7 @@
     NegotiatedProtocol,
 ) where
 
+import Crypto.Debug (DebugShow (..))
 import Network.TLS.Cipher
 import Network.TLS.Imports
 import Network.TLS.Struct
@@ -19,17 +20,40 @@
 type NegotiatedProtocol = ByteString
 
 -- | Handshake information generated for traffic at 0-RTT level.
+--
+-- 'Show' renders the cipher and @\<secret\>@ for the key material; a trace
+-- of what 'Network.TLS.QUIC.quicInstallKeys' is handed does not write the
+-- traffic secrets to a log.  'Crypto.Debug.debugShow' renders them.
 data EarlySecretInfo = EarlySecretInfo Cipher (ClientTrafficSecret EarlySecret)
     deriving (Show)
 
+instance DebugShow EarlySecretInfo where
+    debugShow (EarlySecretInfo c s) =
+        "EarlySecretInfo " ++ show c ++ " " ++ debugShow s
+
 -- | Handshake information generated for traffic at handshake level.
+--
+-- The secrets are not shown; see 'EarlySecretInfo'.
 data HandshakeSecretInfo
     = HandshakeSecretInfo Cipher (TrafficSecrets HandshakeSecret)
     deriving (Show)
 
+instance DebugShow HandshakeSecretInfo where
+    debugShow (HandshakeSecretInfo c ts) =
+        "HandshakeSecretInfo " ++ show c ++ " " ++ debugShowPair ts
+
 -- | Handshake information generated for traffic at application level.
+--
+-- The secrets are not shown; see 'EarlySecretInfo'.
 newtype ApplicationSecretInfo = ApplicationSecretInfo (TrafficSecrets ApplicationSecret)
     deriving (Show)
+
+instance DebugShow ApplicationSecretInfo where
+    debugShow (ApplicationSecretInfo ts) =
+        "ApplicationSecretInfo " ++ debugShowPair ts
+
+debugShowPair :: TrafficSecrets a -> String
+debugShowPair (c, s) = "(" ++ debugShow c ++ "," ++ debugShow s ++ ")"
 
 ----------------------------------------------------------------
 
diff --git a/Network/TLS/Handshake/Server.hs b/Network/TLS/Handshake/Server.hs
--- a/Network/TLS/Handshake/Server.hs
+++ b/Network/TLS/Handshake/Server.hs
@@ -45,9 +45,9 @@
 -- | Put the server context in handshake mode.
 --
 -- Expect a client hello message as parameter.
--- This is useful when the client hello has been already poped from the recv layer to inspect the packet.
+-- This is useful when the client hello has been already popped from the recv layer to inspect the packet.
 --
--- When the function returns, a new handshake has been succesfully negociated.
+-- When the function returns, a new handshake has been successfully negotiated.
 -- On any error, a HandshakeFailed exception is raised.
 handshake :: ServerParams -> Context -> HandshakeR -> IO ()
 handshake sparams ctx chb@(ClientHello ch, bs) = do
@@ -66,7 +66,7 @@
                 SelectKeyShareHRR g -> do
                     sendHRR ctx g r0 chI $ isJust mcrnd
                     -- Don't reset ctxEstablished since 0-RTT data
-                    -- would be comming, which should be ignored.
+                    -- would be coming, which should be ignored.
                     handshakeServer sparams ctx
                 SelectKeyShareFound cliKeyShare -> do
                     unless (checkClientKeyShareKeyLength cliKeyShare) $
@@ -86,4 +86,4 @@
             resumeSessionData <-
                 sendServerHello12 sparams ctx r chI
             recvClientSecondFlight12 sparams ctx resumeSessionData
-handshake _ _ _ = throwCore $ Error_Protocol "client Hello is expected" HandshakeFailure
+handshake _ _ (hs, _) = unexpected (show hs) (Just "client hello")
diff --git a/Network/TLS/Handshake/Server/ClientHello.hs b/Network/TLS/Handshake/Server/ClientHello.hs
--- a/Network/TLS/Handshake/Server/ClientHello.hs
+++ b/Network/TLS/Handshake/Server/ClientHello.hs
@@ -57,7 +57,11 @@
         (throwCore $ Error_HandshakePolicy "server: handshake denied")
     updateMeasure ctx incrementNbHandshakes
 
-    when (chVersion /= TLS12) $
+    -- A legacy_version below TLS 1.2 is refused.  One above it is not: a
+    -- server negotiates the highest version it supports (RFC 5246 Appendix
+    -- E.1), and with supported_versions present does not use legacy_version
+    -- at all (RFC 8446 Section 4.2.1).  Both are decided below.
+    when (chVersion < TLS12) $
         throwCore $
             Error_Protocol (show chVersion ++ " is not supported") ProtocolVersion
 
@@ -170,9 +174,20 @@
         Nothing
         extractServerName
   where
-    extractServerName (ServerName ns) = listToMaybe (mapMaybe toHostName ns)
+    extractServerName (ServerName ns) = case mapMaybe toHostName ns of
+        [] -> Nothing
+        [hostName]
+            | all validChar hostName -> Just hostName
+            | otherwise -> illegal "invalid host_name in SNI"
+        _ -> illegal "multiple host_names in SNI"
     toHostName (ServerNameHostName hostName) = Just hostName
     toHostName (ServerNameOther _) = Nothing
+    -- RFC 6066 Section 3: the server_name_list MUST NOT contain more
+    -- than one name of the same name_type, and HostName is an ASCII
+    -- DNS host name, so it has no control characters, spaces nor
+    -- non-ASCII bytes.
+    validChar c = c > ' ' && c < '\DEL'
+    illegal msg = E.throw $ Uncontextualized $ Error_Protocol msg IllegalParameter
 
 findHighestVersionFrom :: Version -> [Version] -> Maybe Version
 findHighestVersionFrom clientVersion allowedVersions =
diff --git a/Network/TLS/Handshake/Server/ClientHello12.hs b/Network/TLS/Handshake/Server/ClientHello12.hs
--- a/Network/TLS/Handshake/Server/ClientHello12.hs
+++ b/Network/TLS/Handshake/Server/ClientHello12.hs
@@ -11,10 +11,12 @@
 import Network.TLS.Crypto
 import Network.TLS.ErrT
 import Network.TLS.Extension
+import Network.TLS.Handshake.Common (ticketOrSessionID12)
 import Network.TLS.Handshake.Server.Common
 import Network.TLS.Handshake.Signature
 import Network.TLS.Imports
 import Network.TLS.Parameters
+import Network.TLS.Session (SessionManager (..))
 import Network.TLS.State
 import Network.TLS.Struct
 import Network.TLS.Types (CipherId (..), Role (..))
@@ -32,6 +34,7 @@
 processClientHello12 sparams ctx ch = do
     let secureRenegotiation = supportedSecureRenegotiation $ serverSupported sparams
     when secureRenegotiation $ checkSecureRenegotiation ctx ch
+    checkEcPointFormats ch
     serverName <- usingState_ ctx getClientSNI
     let hooks = serverHooks sparams
     extraCreds <- onServerNameIndication hooks serverName
@@ -40,18 +43,58 @@
     -- The shared cipherlist can become empty after filtering for compatible
     -- creds, check now before calling onCipherChoosing, which does not handle
     -- empty lists.
-    when (null ciphersFilteredVersion) $
+    when (null ciphersFilteredVersion) $ do
+        checkResumedCipherOffered ctx ch
         throwCore $
             Error_Protocol "no cipher in common with the TLS 1.2 client" HandshakeFailure
-    let usedCipher = onCipherChoosing hooks TLS12 ciphersFilteredVersion
+    usedCipher <- chooseCipher hooks TLS12 ciphersFilteredVersion
     mcred <- chooseCreds usedCipher creds signatureCreds
     return (usedCipher, mcred)
 
+-- RFC 5246 Section 7.4.1.2: a client resuming a session MUST offer the
+-- cipher suite of that session.  validateSession reports its absence with
+-- illegal_parameter once a cipher has been chosen; when none can be chosen,
+-- the same is reported here before the missing common cipher is.
+checkResumedCipherOffered :: Context -> ClientHello -> IO ()
+checkResumedCipherOffered ctx CH{..} = do
+    let mticket =
+            lookupAndDecode
+                EID_SessionTicket
+                MsgTClientHello
+                chExtensions
+                Nothing
+                (\(SessionTicket ticket) -> Just ticket)
+    case ticketOrSessionID12 mticket chSession of
+        Nothing -> return ()
+        Just identity -> do
+            msd <- sessionResume (sharedSessionManager $ ctxShared ctx) identity
+            case msd of
+                Just sd
+                    | sessionVersion sd <= TLS12
+                    , CipherId (sessionCipher sd) `notElem` chCiphers ->
+                        throwCore $
+                            Error_Protocol "new cipher is different from the old one" IllegalParameter
+                _ -> return ()
+
 checkSecureRenegotiation :: Context -> ClientHello -> IO ()
 checkSecureRenegotiation ctx CH{..} = do
     -- RFC 5746: secure renegotiation
     -- TLS_EMPTY_RENEGOTIATION_INFO_SCSV: {0x00, 0xFF}
-    when (CipherId 0xff `elem` chCiphers) $
+    let hasSCSV = CipherId 0xff `elem` chCiphers
+        hasExt = isJust $ extensionLookup EID_SecureRenegotiation chExtensions
+    established <- ctxEstablished ctx
+    secure <- usingState_ ctx getSecureRenegotiation
+    -- RFC 5746 Section 3.7: when renegotiating a connection whose
+    -- secure_renegotiation flag is set, ClientHello MUST NOT contain
+    -- the SCSV and MUST contain the renegotiation_info extension.
+    when (established == Established && secure) $ do
+        when hasSCSV $
+            throwCore $
+                Error_Protocol "SCSV in renegotiation" HandshakeFailure
+        unless hasExt $
+            throwCore $
+                Error_Protocol "no renegotiation_info in renegotiation" HandshakeFailure
+    when hasSCSV $
         usingState_ ctx $
             setSecureRenegotiation True
     case extensionLookup EID_SecureRenegotiation chExtensions of
@@ -101,7 +144,7 @@
 
     -- Cipher selection is performed in two steps: first server credentials
     -- are flagged as not suitable for signature if not compatible with
-    -- negotiated signature parameters.  Then ciphers are evalutated from
+    -- negotiated signature parameters.  Then ciphers are evaluated from
     -- the resulting credentials.
 
     supported = serverSupported sparams
@@ -170,6 +213,34 @@
             Error_Protocol "key exchange algorithm not implemented" HandshakeFailure
 
 ----------------------------------------------------------------
+
+-- RFC 8422 Section 5.1.2: a client that names a curve of RFC 8422 in
+-- supported_groups and sends ec_point_formats without the uncompressed
+-- format is refused with illegal_parameter.  An empty list is refused
+-- with decode_error when decoding it.
+checkEcPointFormats :: ClientHello -> IO ()
+checkEcPointFormats CH{..} =
+    lookupAndDecodeAndDo
+        EID_EcPointFormats
+        MsgTClientHello
+        chExtensions
+        (return ())
+        $ \(EcPointFormatsSupported formats) ->
+            when
+                ( EcPointFormat_Uncompressed `notElem` formats
+                    && any (`elem` rfc8422Groups) groups
+                )
+                $ throwCore
+                $ Error_Protocol "uncompressed point format missing" IllegalParameter
+  where
+    groups =
+        lookupAndDecode
+            EID_SupportedGroups
+            MsgTClientHello
+            chExtensions
+            []
+            (\(SupportedGroups gs) -> gs)
+    rfc8422Groups = [P256, P384, P521, X25519, X448]
 
 negotiatedGroupsInCommon :: [Group] -> [ExtensionRaw] -> [Group]
 negotiatedGroupsInCommon serverGroups exts =
diff --git a/Network/TLS/Handshake/Server/ClientHello13.hs b/Network/TLS/Handshake/Server/ClientHello13.hs
--- a/Network/TLS/Handshake/Server/ClientHello13.hs
+++ b/Network/TLS/Handshake/Server/ClientHello13.hs
@@ -13,6 +13,7 @@
 import Network.TLS.Crypto
 import Network.TLS.Extension
 import Network.TLS.Handshake.Common13
+import Network.TLS.Handshake.Server.Common
 import Network.TLS.Handshake.Signature
 import Network.TLS.Handshake.State
 import Network.TLS.IO.Encode
@@ -34,7 +35,8 @@
     -> IO
         ( SelectKeyShareResult
         , (Cipher, Hash, Bool) -- rtt0
-        , (SecretPair EarlySecret, [ExtensionRaw], Bool, Bool) -- authenticated, is0RTTvalid
+        , (SecretPair EarlySecret, [ExtensionRaw], Bool, Bool, Maybe ByteString)
+          -- authenticated, is0RTTvalid, ticket ALPN
         )
 processClientHello13 sparams ctx ch@CH{..} = do
     when
@@ -48,8 +50,8 @@
     when (null ciphersFilteredVersion) $
         throwCore $
             Error_Protocol "no cipher in common with the TLS 1.3 client" HandshakeFailure
-    let usedCipher = onCipherChoosing (serverHooks sparams) TLS13 ciphersFilteredVersion
-        usedHash = cipherHash usedCipher
+    usedCipher <- chooseCipher (serverHooks sparams) TLS13 ciphersFilteredVersion
+    let usedHash = cipherHash usedCipher
         rtt0 =
             lookupAndDecode
                 EID_EarlyData
@@ -126,14 +128,15 @@
     -> Context
     -> (Cipher, Hash, Bool) -- rtt0
     -> ClientHello
-    -> IO (SecretPair EarlySecret, [ExtensionRaw], Bool, Bool) -- authenticated, is0RTTvalid
+    -> IO (SecretPair EarlySecret, [ExtensionRaw], Bool, Bool, Maybe ByteString)
+    -- authenticated, is0RTTvalid, ticket ALPN
 pskAndEarlySecret sparams ctx (usedCipher, usedHash, rtt0) CH{..} = do
-    (psk, binderInfo, is0RTTvalid) <- choosePSK
+    (psk, binderInfo, is0RTTvalid, ticketALPN) <- choosePSK
     earlyKey <- calculateEarlySecret ctx choice (Left psk)
     let earlySecret = pairBase earlyKey
         authenticated = isJust binderInfo
     preSharedKeyExt <- checkBinder earlySecret binderInfo
-    return (earlyKey, preSharedKeyExt, authenticated, is0RTTvalid)
+    return (earlyKey, preSharedKeyExt, authenticated, is0RTTvalid, ticketALPN)
   where
     choice = makeCipherChoice TLS13 usedCipher
 
@@ -142,7 +145,7 @@
             EID_PreSharedKey
             MsgTClientHello
             chExtensions
-            (return (zero, Nothing, False))
+            (return (zero, Nothing, False, Nothing))
             selectPSK
 
     selectPSK (PreSharedKeyClientHello (PskIdentity identity obfAge : _) bnds@(bnd : _)) = do
@@ -161,18 +164,29 @@
                         then sessionResumeOnlyOnce mgr identity
                         else sessionResume mgr identity
                 case msdata of
-                    Just sdata -> do
-                        let tinfo = fromJust $ sessionTicketInfo sdata
-                            psk = sessionSecret sdata
-                        isFresh <- checkFreshness tinfo obfAge
-                        (isPSKvalid, is0RTTvalid) <- checkSessionEquality sdata
-                        if isPSKvalid && isFresh
-                            then return (psk, Just (bnd, 0 :: Int, len), is0RTTvalid)
-                            else -- fall back to full handshake
-                                return (zero, Nothing, False)
-                    _ -> return (zero, Nothing, False)
-            else return (zero, Nothing, False)
-    selectPSK _ = return (zero, Nothing, False)
+                    -- RFC 8446 Section 4.6.1: only a TLS 1.3 session, which
+                    -- has its ticket information, is resumed with a PSK.  A
+                    -- TLS 1.2 one found under the same identity falls back
+                    -- to a full handshake.
+                    Just sdata
+                        | sessionVersion sdata == TLS13
+                        , Just tinfo <- sessionTicketInfo sdata -> do
+                            let psk = sessionSecret sdata
+                            isFresh <- checkFreshness tinfo obfAge
+                            (isPSKvalid, is0RTTvalid) <- checkSessionEquality sdata
+                            if isPSKvalid && isFresh
+                                then
+                                    return
+                                        ( psk
+                                        , Just (bnd, 0 :: Int, len)
+                                        , is0RTTvalid
+                                        , sessionALPN sdata
+                                        )
+                                else -- fall back to full handshake
+                                    return (zero, Nothing, False, Nothing)
+                    _ -> return (zero, Nothing, False, Nothing)
+            else return (zero, Nothing, False, Nothing)
+    selectPSK _ = return (zero, Nothing, False, Nothing)
 
     checkBinder _ Nothing = return []
     checkBinder earlySecret (Just (binder, n, tlen)) = do
@@ -184,9 +198,6 @@
 
     checkSessionEquality sdata = do
         msni <- usingState_ ctx getClientSNI
-        -- ALPN should be checked.
-        -- But it's an extension in EE, sigh.
-        --        malpn <- usingState_ ctx getNegotiatedProtocol
         let isSameSNI = sessionClientSNI sdata == msni
             isSameCipher = sessionCipher sdata == cipherID usedCipher
             ciphers = supportedCiphers $ serverSupported sparams
@@ -195,9 +206,8 @@
                 Nothing -> False
                 Just c -> cipherHash c == cipherHash usedCipher
             isSameVersion = TLS13 == sessionVersion sdata
-            --            isSameALPN = sessionALPN sdata == malpn
             isPSKvalid = isSameKDF && isSameSNI -- fixme: SNI is not required
-            is0RTTvalid = isSameVersion && isSameCipher -- && isSameALPN
+            is0RTTvalid = isSameVersion && isSameCipher
         return (isPSKvalid, is0RTTvalid)
 
     dhModes =
diff --git a/Network/TLS/Handshake/Server/Common.hs b/Network/TLS/Handshake/Server/Common.hs
--- a/Network/TLS/Handshake/Server/Common.hs
+++ b/Network/TLS/Handshake/Server/Common.hs
@@ -2,6 +2,7 @@
 
 module Network.TLS.Handshake.Server.Common (
     applicationProtocol,
+    chooseCipher,
     checkValidClientCertChain,
     clientCertificate,
     credentialDigitalSignatureKey,
@@ -15,8 +16,9 @@
 ) where
 
 import Control.Monad.State.Strict
-import Data.X509 (ExtKeyUsageFlag (..))
+import Data.X509 (ExtKeyUsageFlag (..), ExtKeyUsagePurpose (..))
 
+import Network.TLS.Cipher
 import Network.TLS.Context.Internal
 import Network.TLS.Credentials
 import Network.TLS.Crypto
@@ -32,6 +34,18 @@
 import Network.TLS.Util (catchException)
 import Network.TLS.X509
 
+chooseCipher :: ServerHooks -> Version -> [Cipher] -> IO Cipher
+chooseCipher hooks ver candidates =
+    case find ((== cipherID selected) . cipherID) candidates of
+        Just cipher -> return cipher
+        Nothing ->
+            throwCore $
+                Error_Protocol
+                    "onCipherChoosing selected a cipher outside the candidate list"
+                    InternalError
+  where
+    selected = onCipherChoosing hooks ver candidates
+
 checkValidClientCertChain
     :: MonadIO m => Context -> String -> m CertificateChain
 checkValidClientCertChain ctx errmsg = do
@@ -132,6 +146,11 @@
         when (proto == "") $
             throwCore $
                 Error_Protocol "no supported application protocols" NoApplicationProtocol
+        unless (proto `elem` protos) $
+            throwCore $
+                Error_Protocol
+                    "ALPN callback selected a protocol not offered by the client"
+                    NoApplicationProtocol
         usingState_ ctx $ do
             setExtensionALPN True
             setNegotiatedProtocol proto
@@ -151,7 +170,9 @@
                 (onClientCertificate (serverHooks sparams) certs)
                 rejectOnException
     case usage of
-        CertificateUsageAccept -> verifyLeafKeyUsage [KeyUsage_digitalSignature] certs
+        CertificateUsageAccept -> do
+            verifyLeafKeyUsage [KeyUsage_digitalSignature] certs
+            verifyLeafKeyUsagePurpose KeyUsagePurpose_ClientAuth certs
         CertificateUsageReject reason -> certificateRejected reason
 
     -- Remember cert chain for later use.
diff --git a/Network/TLS/Handshake/Server/ServerHello12.hs b/Network/TLS/Handshake/Server/ServerHello12.hs
--- a/Network/TLS/Handshake/Server/ServerHello12.hs
+++ b/Network/TLS/Handshake/Server/ServerHello12.hs
@@ -93,7 +93,7 @@
     | TLS12 < sessionVersion sd = return Nothing -- fixme
     | CipherId (sessionCipher sd) `notElem` ciphers =
         throwCore $
-            Error_Protocol "new cipher is diffrent from the old one" IllegalParameter
+            Error_Protocol "new cipher is different from the old one" IllegalParameter
     | isJust sni && sessionClientSNI sd /= sni = do
         usingState_ ctx clearClientSNI
         return Nothing
@@ -143,7 +143,7 @@
         then do
             let (certTypes, hashSigs) =
                     let as = supportedHashSignatures serverSupported
-                     in (nub $ mapMaybe hashSigToCertType as, as)
+                     in (nub $ mapMaybe (fmap certTypeOnWire . hashSigToCertType) as, as)
                 creq =
                     CertRequest
                         certTypes
@@ -153,6 +153,12 @@
             return $ b2 . (creq :)
         else return b2
   where
+    -- RFC 8422 Section 3.1: in TLS 1.2, ecdsa_sign asks for a certificate
+    -- with an ECDSA- or EdDSA-capable public key.  The Ed25519 and Ed448
+    -- certificate types are synthetic values with no code point.
+    certTypeOnWire CertificateType_Ed25519_Sign = CertificateType_ECDSA_Sign
+    certTypeOnWire CertificateType_Ed448_Sign = CertificateType_ECDSA_Sign
+    certTypeOnWire t = t
     commonGroups = negotiatedGroupsInCommon (supportedGroups serverSupported) chExts
     commonHashSigs = hashAndSignaturesInCommon (supportedHashSignatures serverSupported) chExts
     setup_DHE = do
@@ -267,9 +273,13 @@
             | ems = Just $ toExtensionRaw ExtendedMainSecret
             | otherwise = Nothing
 
+    -- RFC 5077 Section 3.2: the extension is sent only to a client that
+    -- sent it.
     let useTicket = sessionUseTicket $ sharedSessionManager $ serverShared sparams
+        clientTicket = isJust $ extensionLookup EID_SessionTicket chExts
         sessionTicketExt
-            | not resuming && useTicket = Just $ toExtensionRaw $ SessionTicket ""
+            | not resuming && useTicket && clientTicket =
+                Just $ toExtensionRaw $ SessionTicket ""
             | otherwise = Nothing
 
     -- in TLS12, we need to check as well the certificates we are sending if they have in the extension
diff --git a/Network/TLS/Handshake/Server/ServerHello13.hs b/Network/TLS/Handshake/Server/ServerHello13.hs
--- a/Network/TLS/Handshake/Server/ServerHello13.hs
+++ b/Network/TLS/Handshake/Server/ServerHello13.hs
@@ -37,7 +37,8 @@
     -> Context
     -> KeyShareEntry
     -> (Cipher, Hash, Bool) -- rtt0
-    -> (SecretPair EarlySecret, [ExtensionRaw], Bool, Bool) -- authenticated, is0RTTvalid
+    -> (SecretPair EarlySecret, [ExtensionRaw], Bool, Bool, Maybe ByteString)
+    -- authenticated, is0RTTvalid, ticket ALPN
     -> ClientHello
     -> Maybe ClientRandom
     -> IO
@@ -46,7 +47,7 @@
         , Bool -- authenticated
         , Bool -- rtt0OK
         )
-sendServerHello13 sparams ctx clientKeyShare (usedCipher, usedHash, rtt0) (earlyKey, preSharedKeyExt, authenticated, is0RTTvalid) CH{..} mOuterClientRandom = do
+sendServerHello13 sparams ctx clientKeyShare (usedCipher, usedHash, rtt0) (earlyKey, preSharedKeyExt, authenticated, is0RTTvalid, ticketALPN) CH{..} mOuterClientRandom = do
     let clientEarlySecret = pairClient earlyKey
         earlySecret = pairBase earlyKey
     -- parse CompressCertificate to check if it is broken here
@@ -69,8 +70,15 @@
         setOuterClientRandom mOuterClientRandom
     hrr <- usingState_ ctx getTLS13HRR
     alpnExt <- applicationProtocol ctx chExtensions sparams
+    negotiatedALPN <- usingState_ ctx getNegotiatedProtocol
     setServerParameter
-    let rtt0OK = authenticated && not hrr && rtt0 && rtt0accept && is0RTTvalid
+    let rtt0OK =
+            authenticated
+                && not hrr
+                && rtt0
+                && rtt0accept
+                && is0RTTvalid
+                && ticketALPN == negotiatedALPN
     extraCreds <-
         usingState_ ctx getClientSNI >>= onServerNameIndication (serverHooks sparams)
     let p = makeCredentialPredicate TLS13 chExtensions
diff --git a/Network/TLS/Handshake/Server/TLS12.hs b/Network/TLS/Handshake/Server/TLS12.hs
--- a/Network/TLS/Handshake/Server/TLS12.hs
+++ b/Network/TLS/Handshake/Server/TLS12.hs
@@ -10,6 +10,7 @@
 
 import Network.TLS.Context.Internal
 import Network.TLS.Crypto
+import Network.TLS.Extension
 import Network.TLS.Handshake.Common
 import Network.TLS.Handshake.Key
 import Network.TLS.Handshake.Server.Common
@@ -37,11 +38,17 @@
         Nothing -> do
             recvClientCCC sparams ctx
             mticket <- sessionEstablished ctx
+            -- RFC 5077 Section 3.3: NewSessionTicket is sent only after
+            -- the session_ticket extension in ServerHello, which only a
+            -- client that sent it gets.
+            clientTicket <-
+                maybe False (isJust . extensionLookup EID_SessionTicket . chExtensions . fst)
+                    <$> usingHState ctx getClientHello
             case mticket of
-                Nothing -> return ()
-                Just ticket -> do
+                Just ticket | clientTicket -> do
                     let life = adjustLifetime $ serverTicketLifetime sparams
                     sendPacket12 ctx $ Handshake [NewSessionTicket life ticket] []
+                _ -> return ()
             sendCCSandFinished ctx ServerRole
         Just _ -> do
             _ <- sessionEstablished ctx
@@ -92,7 +99,11 @@
         -- matches our request and that we support
         -- verifying with that certificate.
 
-        return $ RecvStateHandshake $ expectClientKeyExchange True
+        -- RFC 5246 Section 7.4.8: CertificateVerify follows only a
+        -- certificate with signing capability, so not an empty one,
+        -- which the hook may have accepted.
+        let followedCertVerify = not $ isNullCertificateChain certs
+        return $ RecvStateHandshake $ expectClientKeyExchange followedCertVerify
     expectClientCertificate p = expectClientKeyExchange False p
 
     -- cannot use RecvStateHandshake, as the next message could be a ChangeCipher,
diff --git a/Network/TLS/Handshake/Server/TLS13.hs b/Network/TLS/Handshake/Server/TLS13.hs
--- a/Network/TLS/Handshake/Server/TLS13.hs
+++ b/Network/TLS/Handshake/Server/TLS13.hs
@@ -9,7 +9,7 @@
     KeyUpdateRequest (..),
 ) where
 
-import Control.Exception
+import qualified Control.Exception as E
 import Control.Monad.State.Strict
 import Data.IORef
 
@@ -28,6 +28,7 @@
 import Network.TLS.IO
 import Network.TLS.Imports
 import Network.TLS.KeySchedule
+import Network.TLS.Packet13 (encodeHandshake13)
 import Network.TLS.Parameters
 import Network.TLS.Session
 import Network.TLS.State
@@ -239,12 +240,16 @@
         origCertReqCtx <- newCertReqContext ctx
         let certReq13 = makeCertRequest sparams ctx origCertReqCtx False
         _ <- withWriteLock ctx $ do
-            bracket (saveHState ctx) (restoreHState ctx) $ \_ -> do
+            E.bracket (saveHState ctx) (restoreHState ctx) $ \_ -> do
                 sendPacket13 ctx $ Handshake13 [certReq13] []
         withReadLock ctx $ do
+            baseHState <- saveHState ctx
+            -- RFC 8446 Section 4.4: the handshake context of
+            -- post-handshake authentication is ClientHello ... client
+            -- Finished + CertificateRequest.
+            updateTranscriptHash13 ctx (certReq13, [encodeHandshake13 certReq13])
             (clientCert13, bClientCert13) <- getHandshake ctx ref
             emptyCert <- expectClientCertificate sparams ctx origCertReqCtx clientCert13
-            baseHState <- saveHState ctx
             updateTranscriptHash13 ctx (clientCert13, bClientCert13)
             th <- transcriptHash ctx "CH..Cert"
             unless emptyCert $ do
@@ -274,6 +279,13 @@
             Error_Protocol "post handshake authenticated" UnexpectedMessage
     chk [] = getHandshake ctx ref
     chk ((KeyUpdate13 mode, _) : hbs) = do
+        case limitKeyUpdate $ sharedLimit $ ctxShared ctx of
+            Just limit | limit > 0 -> do
+                count <- incrementTLS13KeyUpdateCount ctx
+                when (count > limit) $
+                    terminate ctx $
+                        Error_Protocol "too many consecutive KeyUpdate messages" UnexpectedMessage
+            _ -> return ()
         keyUpdate ctx getRxRecordState setRxRecordState
         -- Write lock wraps both actions because we don't want another
         -- packet to be sent by another thread before the Tx state is
@@ -328,18 +340,18 @@
         send = sendPacket13 ctx . Alert13
     catchException (send [(level, desc)]) (\_ -> return ())
     setEOF ctx
-    throwIO $ Terminated False reason err
+    E.throwIO $ Terminated False reason err
 
 handleEx :: Context -> IO Bool -> IO Bool
 handleEx ctx f = catchException f $ \exception -> do
     -- If the error was an Uncontextualized TLSException, we replace the
     -- context with HandshakeFailed. If it's anything else, we convert
     -- it to a string and wrap it with Error_Misc and HandshakeFailed.
-    let tlserror = case fromException exception of
+    let tlserror = case E.fromException exception of
             Just e | Uncontextualized e' <- e -> e'
             _ -> Error_Misc (show exception)
     sendPacket13 ctx $ Alert13 [errorToAlert tlserror]
-    void $ throwIO $ PostHandshake tlserror
+    void $ E.throwIO $ PostHandshake tlserror
     return False
 
 ----------------------------------------------------------------
@@ -369,7 +381,7 @@
       TwoWay
     deriving (Eq, Show)
 
--- | Updating appication traffic secrets for TLS 1.3.
+-- | Updating application traffic secrets for TLS 1.3.
 --   If this API is called for TLS 1.3, 'True' is returned.
 --   Otherwise, 'False' is returned.
 updateKey :: MonadIO m => Context -> KeyUpdateRequest -> m Bool
diff --git a/Network/TLS/Handshake/Signature.hs b/Network/TLS/Handshake/Signature.hs
--- a/Network/TLS/Handshake/Signature.hs
+++ b/Network/TLS/Handshake/Signature.hs
@@ -60,6 +60,25 @@
 signatureCompatible (PubKeyEd448 _) (_, SignatureEd448) = True
 signatureCompatible _ (_, _) = False
 
+-- Whether the signature algorithm is for the type of the key, whatever
+-- its other parameters.
+keyTypeFits :: PubKey -> SignatureAlgorithm -> Bool
+keyTypeFits (PubKeyRSA _) s =
+    s
+        `elem` [ SignatureRSA
+               , SignatureRSApssRSAeSHA256
+               , SignatureRSApssRSAeSHA384
+               , SignatureRSApssRSAeSHA512
+               , SignatureRSApsspssSHA256
+               , SignatureRSApsspssSHA384
+               , SignatureRSApsspssSHA512
+               ]
+keyTypeFits (PubKeyDSA _) s = s == SignatureDSA
+keyTypeFits (PubKeyEC _) s = s == SignatureECDSA
+keyTypeFits (PubKeyEd25519 _) s = s == SignatureEd25519
+keyTypeFits (PubKeyEd448 _) s = s == SignatureEd448
+keyTypeFits _ _ = False
+
 -- Same as 'signatureCompatible' but for TLS13: for ECDSA this also checks the
 -- relation between hash in the HashAndSignatureAlgorithm and elliptic curve
 signatureCompatible13 :: PubKey -> HashAndSignatureAlgorithm -> Bool
@@ -118,9 +137,21 @@
     -> ByteString
     -> DigitallySigned
     -> IO Bool
-checkCertificateVerify ctx usedVersion pubKey msgs digSig@(DigitallySigned hashSigAlg _)
-    | pubKey `signatureCompatible` hashSigAlg = doVerify
-    | otherwise = return False
+-- An algorithm not offered in CertificateRequest (RFC 5246 Section
+-- 7.4.8), or one for another type of key, is a field that is incorrect,
+-- an illegal_parameter.  One for the right type of key that still does
+-- not fit it, an RSASSA-PSS one for an rsaEncryption key say, is a
+-- signature that does not verify, a decrypt_error, which False leads to.
+checkCertificateVerify ctx usedVersion pubKey msgs digSig@(DigitallySigned hashSigAlg@(_, sigAlg) _) = do
+    checkSupportedHashSignature ctx hashSigAlg
+    unless (pubKey `keyTypeFits` sigAlg) $
+        throwCore $
+            Error_Protocol
+                ("signature algorithm " ++ show hashSigAlg ++ " is for another type of key")
+                IllegalParameter
+    if pubKey `signatureCompatible` hashSigAlg
+        then doVerify
+        else return False
   where
     doVerify =
         prepareCertificateVerifySignatureData ctx usedVersion pubKey hashSigAlg msgs
diff --git a/Network/TLS/IO.hs b/Network/TLS/IO.hs
--- a/Network/TLS/IO.hs
+++ b/Network/TLS/IO.hs
@@ -16,7 +16,7 @@
     loadPacket13,
 ) where
 
-import Control.Exception (finally, throwIO)
+import qualified Control.Exception as E
 import Control.Monad.Reader
 import Control.Monad.State.Strict
 import qualified Data.ByteString as B
@@ -102,8 +102,15 @@
             Left err -> do
                 logPacket ctx $ show err
                 return $ Left err
-            Right record
-                | hrr && isCCS record -> loop (count + 1)
+            Right record@(Record _ _ fragment)
+                -- the ChangeCipherSpec after a HelloRetryRequest is skipped,
+                -- but checked like any other
+                | hrr && isCCS record ->
+                    case checkChangeCipherSpec fragment of
+                        Left err -> do
+                            logPacket ctx $ show err
+                            return $ Left err
+                        Right _ -> loop (count + 1)
                 | otherwise -> do
                     pktRecv <- decodePacket12 ctx record
                     if isEmptyHandshake pktRecv
@@ -181,6 +188,9 @@
                                     return $ Right $ Handshake13 hss' bss
                                 logPacket ctx $ show pkt
                                 return pktRecv'
+                            Right pkt@(AppData13 _) -> do
+                                logPacket ctx $ show pkt
+                                checkNotInterleaved ctx pktRecv
                             Right pkt -> do
                                 logPacket ctx $ show pkt
                                 return pktRecv
@@ -194,6 +204,24 @@
 
 ----------------------------------------------------------------
 
+-- RFC 8446 Section 5.1: handshake messages MUST NOT be interleaved with
+-- other record types.  Application data that arrives while a handshake
+-- message is still incomplete is refused with unexpected_message rather
+-- than delivered.  TLS 1.3 only: RFC 5246 Section 6.2.1 lets TLS 1.2
+-- interleave data of different content types.
+checkNotInterleaved :: Context -> Either TLSError a -> IO (Either TLSError a)
+checkNotInterleaved ctx pktRecv = do
+    complete <- isRecvComplete ctx
+    if complete
+        then return pktRecv
+        else do
+            let err =
+                    Error_Packet_unexpected
+                        "application data"
+                        " expected: the rest of a handshake message"
+            logPacket ctx $ show err
+            return $ Left err
+
 isRecvComplete :: Context -> IO Bool
 isRecvComplete ctx = usingState_ ctx $ do
     cont12 <- gets stHandshakeRecordCont12
@@ -203,9 +231,9 @@
 checkValid :: Context -> IO ()
 checkValid ctx = do
     established <- ctxEstablished ctx
-    when (established == NotEstablished) $ throwIO ConnectionNotEstablished
+    when (established == NotEstablished) $ E.throwIO ConnectionNotEstablished
     eofed <- ctxEOF ctx
-    when eofed $ throwIO $ PostHandshake Error_EOF
+    when eofed $ E.throwIO $ PostHandshake Error_EOF
 
 ----------------------------------------------------------------
 
@@ -223,7 +251,8 @@
 runPacketFlight :: Context -> (forall b. Monoid b => PacketFlightM b a) -> IO a
 runPacketFlight ctx@Context{ctxRecordLayer = recordLayer} (PacketFlightM f) = do
     ref <- newIORef id
-    runReaderT f (recordLayer, ref) `finally` sendPendingFlight ctx recordLayer ref
+    runReaderT f (recordLayer, ref)
+        `E.finally` sendPendingFlight ctx recordLayer ref
 
 sendPendingFlight
     :: Monoid b => Context -> RecordLayer b -> IORef (Builder b) -> IO ()
diff --git a/Network/TLS/IO/Decode.hs b/Network/TLS/IO/Decode.hs
--- a/Network/TLS/IO/Decode.hs
+++ b/Network/TLS/IO/Decode.hs
@@ -3,6 +3,7 @@
 module Network.TLS.IO.Decode (
     decodePacket12,
     decodePacket13,
+    checkChangeCipherSpec,
 ) where
 
 import Control.Concurrent.MVar
@@ -20,6 +21,7 @@
 import Network.TLS.State
 import Network.TLS.Struct
 import Network.TLS.Struct13
+import Network.TLS.Types (Role (..))
 import Network.TLS.Util
 import Network.TLS.Wire
 
@@ -27,46 +29,69 @@
 decodePacket12 _ (Record ProtocolType_AppData _ fragment) = return $ Right $ AppData $ fragmentGetBytes fragment
 decodePacket12 _ (Record ProtocolType_Alert _ fragment) = return (Alert `fmapEither` decodeAlerts (fragmentGetBytes fragment))
 decodePacket12 ctx (Record ProtocolType_ChangeCipherSpec _ fragment) =
-    case decodeChangeCipherSpec $ fragmentGetBytes fragment of
+    case checkChangeCipherSpec fragment of
         Left err -> return $ Left err
         Right _ -> do
-            switchRxEncryption ctx
-            return $ Right ChangeCipherSpec
+            -- ChangeCipherSpec comes between complete handshake messages
+            -- (RFC 5246 Section 7.1), so not before the first ClientHello
+            -- of a server, which has no handshake state yet, nor in the
+            -- middle of a fragmented handshake message (CVE-2004-0079).
+            mhs <- getHState ctx
+            (mCont, _) <- usingState_ ctx $ gets stHandshakeRecordCont12
+            if isNothing mhs || isJust mCont
+                then
+                    return $
+                        Left $
+                            Error_Packet_unexpected "ChangeCipherSpec" " expected: handshake"
+                else do
+                    switchRxEncryption ctx
+                    return $ Right ChangeCipherSpec
 decodePacket12 ctx (Record ProtocolType_Handshake ver fragment) = do
-    keyxchg <-
-        getHState ctx >>= \hs -> return (hs >>= hstPendingCipher >>= Just . cipherKeyExchange)
+    mhs <- getHState ctx
+    let keyxchg = mhs >>= hstPendingCipher >>= Just . cipherKeyExchange
     usingState ctx $ do
+        role <- getRole
         let currentParams =
                 CurrentParams
                     { cParamsVersion = ver
                     , cParamsKeyXchgType = keyxchg
                     }
+            -- A server has no handshake state until its first ClientHello,
+            -- and that is the only message it may receive then (RFC 8446
+            -- Section 4).  Any other is answered with unexpected_message
+            -- before its body is decoded, rather than with whatever decoding
+            -- the body as that type gives.
+            expectClientHello = role == ServerRole && isNothing mhs
+            decode ty content
+                | expectClientHello && ty /= HandshakeType_ClientHello =
+                    Left $ Error_Packet_unexpected (show ty) " expected: client hello"
+                | otherwise = decodeHandshake currentParams ty content
         -- get back the optional continuation, and parse as many handshake record as possible.
         (mCont, wirebytes) <- gets stHandshakeRecordCont12
         modify' (\st -> st{stHandshakeRecordCont12 = (Nothing, [])})
         (hss, bss) <-
-            unzip <$> parseMany currentParams mCont wirebytes (fragmentGetBytes fragment)
+            unzip <$> parseMany decode mCont wirebytes (fragmentGetBytes fragment)
         return $ Handshake hss bss
   where
-    parseMany currentParams mCont wirebytes bs =
+    parseMany decode mCont wirebytes bs =
         case fromMaybe decodeHandshakeRecord mCont bs of
             GotError err -> throwError err
             GotPartial cont -> do
                 modify' (\st -> st{stHandshakeRecordCont12 = (Just cont, bs : wirebytes)})
                 return []
             GotSuccess (ty, content) ->
-                case decodeHandshake currentParams ty content of
+                case decode ty content of
                     Left err -> throwError err
                     Right h -> return [(h, reverse (bs : wirebytes))]
             GotSuccessRemaining (ty, content) left ->
-                case decodeHandshake currentParams ty content of
+                case decode ty content of
                     Left err -> throwError err
                     Right h -> do
-                        hbs <- parseMany currentParams Nothing [] left
+                        hbs <- parseMany decode Nothing [] left
                         let len = BS.length bs - BS.length left
                             bs' = BS.take len bs
                         return ((h, reverse (bs' : wirebytes)) : hbs)
-decodePacket12 _ _ = return $ Left (Error_Packet_Parsing "unknown protocol type")
+decodePacket12 _ (Record ty _ _) = return $ Left $ unknownProtocolType ty
 
 switchRxEncryption :: Context -> IO ()
 switchRxEncryption ctx =
@@ -77,7 +102,7 @@
 
 decodePacket13 :: Context -> Record Plaintext -> IO (Either TLSError Packet13)
 decodePacket13 _ (Record ProtocolType_ChangeCipherSpec _ fragment) =
-    case decodeChangeCipherSpec $ fragmentGetBytes fragment of
+    case checkChangeCipherSpec fragment of
         Left err -> return $ Left err
         Right _ -> return $ Right ChangeCipherSpec13
 decodePacket13 _ (Record ProtocolType_AppData _ fragment) = return $ Right $ AppData13 $ fragmentGetBytes fragment
@@ -106,4 +131,20 @@
                         let len = BS.length bs - BS.length left
                             bs' = BS.take len bs
                         return ((h, reverse (bs' : wirebytes)) : hbs)
-decodePacket13 _ _ = return $ Left (Error_Packet_Parsing "unknown protocol type")
+decodePacket13 _ (Record ty _ _) = return $ Left $ unknownProtocolType ty
+
+-- RFC 8446 Section 5: a record of an unexpected type, including the inner
+-- type of a TLS 1.3 record, is answered with unexpected_message.
+unknownProtocolType :: ProtocolType -> TLSError
+unknownProtocolType ty = Error_Packet_unexpected (show ty) " expected: TLS record type"
+
+-- | A ChangeCipherSpec is the single byte 1.  RFC 8446 Section 5 answers any
+-- other value with unexpected_message, and TLS 1.2, which does not say,
+-- is answered the same way: a record of two of them included.
+checkChangeCipherSpec :: Fragment a -> Either TLSError ()
+checkChangeCipherSpec fragment =
+    case decodeChangeCipherSpec $ fragmentGetBytes fragment of
+        Left _ ->
+            Left $
+                Error_Packet_unexpected "ChangeCipherSpec" " expected: the single byte 1"
+        Right _ -> Right ()
diff --git a/Network/TLS/Packet.hs b/Network/TLS/Packet.hs
--- a/Network/TLS/Packet.hs
+++ b/Network/TLS/Packet.hs
@@ -76,6 +76,7 @@
 import Network.TLS.Struct
 import Network.TLS.Types
 import Network.TLS.Util.ASN1
+import Network.TLS.Util.Serialization (os2ip)
 import Network.TLS.Wire
 
 ----------------------------------------------------------------
@@ -157,12 +158,33 @@
 decodeHandshakeRecord :: ByteString -> GetResult (HandshakeType, ByteString)
 decodeHandshakeRecord = runGet "handshake-record" $ do
     ty <- getHandshakeType
-    content <- getOpaque24
+    len <- getWord24
+    -- Before the bytes, not after: the length is in the first four octets, so
+    -- refusing here is refusing to hold anything.  Reassembly keeps every
+    -- fragment until the message is whole, and the peer picks the number it
+    -- announces.
+    when (len > maxHandshakeSize) $
+        fail $
+            "handshake message of "
+                ++ show len
+                ++ " octets exceeds the limit of "
+                ++ show maxHandshakeSize
+    content <- getBytes len
     return (ty, content)
 
 {- FOURMOLU_DISABLE -}
 decodeHandshake
     :: CurrentParams -> HandshakeType -> ByteString -> Either TLSError Handshake
+-- A ClientKeyExchange is only expected once a cipher, and with it a key
+-- exchange, has been negotiated; one that comes without -- after Finished,
+-- say -- is out of order rather than malformed.
+decodeHandshake cp HandshakeType_ClientKeyXchg
+    | isNothing (cParamsKeyXchgType cp) =
+        const $
+            Left $
+                Error_Packet_unexpected
+                    (show HandshakeType_ClientKeyXchg)
+                    " expected: no ClientKeyExchange before a key exchange is negotiated"
 decodeHandshake cp ty = runGetErr ("handshake[" ++ show ty ++ "]") $ case ty of
     HandshakeType_HelloRequest     -> decodeHelloRequest
     HandshakeType_ClientHello      -> decodeClientHello False
@@ -337,8 +359,19 @@
     parseCKE CipherKeyExchange_ECDHE_RSA = parseClientECDHPublic
     parseCKE CipherKeyExchange_ECDHE_ECDSA = parseClientECDHPublic
     parseCKE _ = fail "unsupported client key exchange type"
-    parseClientDHPublic = CKX_DH . dhPublic <$> getInteger16
-    parseClientECDHPublic = CKX_ECDH <$> getOpaque8
+    -- RFC 5246 Section 7.4.7.2: dh_Yc is <1..2^16-1>, so an empty one is
+    -- malformed, a decode_error, before it is a public value that is not
+    -- valid.
+    parseClientDHPublic = do
+        bs <- getOpaque16
+        when (B.null bs) $ fail "empty DH public key"
+        return $ CKX_DH $ dhPublic $ os2ip bs
+    -- RFC 8422 Section 5.7: ecdh_Yc is <1..2^8-1>, so an empty one is
+    -- malformed, a decode_error, before it is a point that does not decode.
+    parseClientECDHPublic = do
+        bs <- getOpaque8
+        when (B.null bs) $ fail "empty ECDH public key"
+        return $ CKX_ECDH bs
 
 decodeFinished :: Get Handshake
 decodeFinished = Finished . VerifyData <$> (remaining >>= getBytes)
diff --git a/Network/TLS/Packet13.hs b/Network/TLS/Packet13.hs
--- a/Network/TLS/Packet13.hs
+++ b/Network/TLS/Packet13.hs
@@ -116,7 +116,18 @@
 decodeHandshakeRecord13 :: ByteString -> GetResult (HandshakeType, ByteString)
 decodeHandshakeRecord13 = runGet "handshake-record" $ do
     ty <- getHandshakeType
-    content <- getOpaque24
+    len <- getWord24
+    -- Before the bytes, not after: the length is in the first four octets, so
+    -- refusing here is refusing to hold anything.  Reassembly keeps every
+    -- fragment until the message is whole, and the peer picks the number it
+    -- announces.
+    when (len > maxHandshakeSize) $
+        fail $
+            "handshake message of "
+                ++ show len
+                ++ " octets exceeds the limit of "
+                ++ show maxHandshakeSize
+    content <- getBytes len
     return (ty, content)
 
 {- FOURMOLU_DISABLE -}
@@ -209,25 +220,39 @@
         1 -> return $ KeyUpdate13 UpdateRequested
         x -> fail $ "Unknown request_update: " ++ show x
 
+-- RFC 8879 Section 4: a certificate that cannot be decompressed, or whose
+-- decompressed length is not the declared one, is answered with
+-- bad_certificate, and an algorithm that was not offered with
+-- illegal_parameter.  errorToAlert tells them apart by the messages below.
+-- A message that is malformed as a whole -- an empty
+-- compressed_certificate_message, which its <1..2^24-1> bound forbids, or
+-- bytes beyond the declared length -- is a decode_error, and is found
+-- before anything is decompressed.
 decodeCompressedCertificate13 :: Get Handshake13
 decodeCompressedCertificate13 = do
     algo <- getWord16
-    when (algo /= 1) $ fail "comp algo is not supported" -- fixme
+    when (algo /= 1) $ fail "unsupported certificate compression algorithm" -- fixme
     len <- getWord24
     bs <- getOpaque24
+    left <- remaining
+    when (left /= 0) $ fail "bytes after compressed certificate"
     if bs == ""
         then fail "empty compressed certificate"
-        else case decompressIt bs of
-            Left e -> fail (show e)
+        else case decompressIt len bs of
+            Left e -> fail $ "certificate cannot be decompressed: " ++ show e
             Right bs' -> do
-                when (B.length bs' /= len) $ fail "plain length is wrong"
+                when (B.length bs' /= len) $
+                    fail "certificate cannot be decompressed: wrong uncompressed_length"
                 case runGetMaybe decodeCertificate13 bs' of
                     Just (Certificate13 reqctx certs ess) -> return $ CompressedCertificate13 reqctx certs ess
                     --                    _ -> fail "compressed certificate cannot be parsed"
                     _ -> fail $ "invalid compressed certificate: len = " ++ show len
 
-decompressIt :: ByteString -> Either DecompressError ByteString
-decompressIt inp = unsafePerformIO $ E.handle handler $ do
-    Right . BL.toStrict <$> E.evaluate (decompress (BL.fromStrict inp))
+decompressIt :: Int -> ByteString -> Either DecompressError ByteString
+decompressIt limit inp = unsafePerformIO $ E.handle handler $ do
+    -- One extra byte distinguishes exact-length output from oversized output.
+    let output = BL.take (fromIntegral limit + 1) $ decompress $ BL.fromStrict inp
+    Right <$> E.evaluate (BL.toStrict output)
   where
-    handler e = return $ Left (e :: DecompressError)
+    handler :: DecompressError -> IO (Either DecompressError ByteString)
+    handler e = return $ Left e
diff --git a/Network/TLS/Parameters.hs b/Network/TLS/Parameters.hs
--- a/Network/TLS/Parameters.hs
+++ b/Network/TLS/Parameters.hs
@@ -65,7 +65,7 @@
     --
     -- Default: 'Nothing'
     , debugPrintSeed :: Seed -> IO ()
-    -- ^ Add a way to print the seed that was randomly generated. re-using the same seed
+    -- ^ Add a way to print the seed that was randomly generated. reusing the same seed
     -- will reproduce the same randomness with 'debugSeed'
     --
     -- Default: no printing
@@ -153,6 +153,16 @@
     -- specified for TLS 1.2 but only the first entry is used.
     --
     -- Default: '[]'
+    , clientWantTicket :: Bool
+    -- ^ Whether to solicit TLS 1.2 session tickets (or TLS 1.3
+    -- resumption PSKs) from servers.  With a 'False' setting,
+    -- stateless clients that never do resumption can avoid
+    -- wasting server and client resources used to generate,
+    -- transmit and process tickets that will never be used.
+    --
+    -- Default: 'True'
+    --
+    -- @since 2.4.1
     , clientShared :: Shared
     -- ^ See the default value of 'Shared'.
     , clientHooks :: ClientHooks
@@ -192,6 +202,7 @@
         , clientUseServerNameIndication = True
         , clientWantSessionResume = Nothing
         , clientWantSessionResumeList = []
+        , clientWantTicket = True
         , clientShared = def
         , clientHooks = def
         , clientSupported = def
@@ -401,8 +412,9 @@
     --
     --   Default: @[X25519,P256,P384,X448,P521,FFDHE3072,FFDHE4096,FFDHE6144,FFDHE8192,X25519MLKEM768,P256MLKEM768,P384MLKEM1024,MLKEM768,MLKEM1024]@
     , supportedGroupsTLS13 :: [[Group]]
-    -- ^ @[Group]@ contains @Group@s of the same level in preferred order.
-    -- @[Group]@ is also listed in preferred order.
+    -- ^ The inside @[Group]@ is the list of @Group@ at the same level
+    -- in preferred order.  The inside @[Group]@s are also listed in
+    -- preferred order.
     --
     -- TLS 1.3 server: this is used as the 1st argument to
     -- 'onSelectKeyShare'.
@@ -628,10 +640,11 @@
     -- "Data.X509.Validation".  This can be replaced with a custom
     -- validation function using different settings.
     --
-    -- The function is not expected to verify the key-usage extension
-    -- of the end-entity certificate, as this depends on the
-    -- dynamically-selected cipher and this part should not be cached.
-    -- Key-usage verification is performed by the library internally.
+    -- The function is not expected to verify the key-usage or
+    -- extended-key-usage extensions of the end-entity certificate.
+    -- Key usage depends on the dynamically-selected cipher and this
+    -- part should not be cached.  Both checks are performed by the
+    -- library internally after this function accepts the chain.
     --
     -- Default: 'validateDefault'
     , onSuggestALPN :: IO (Maybe [ByteString])
@@ -657,7 +670,7 @@
     --   (3) rejecting unless 1 < dh_p && pub < dh_p - 1
     --   (4) rejecting if dh_size < 1024 (to prevent Logjam attack)
     --
-    --   See RFC 7919 section 3.1 for recommandations.
+    --   See RFC 7919 section 3.1 for recommendations.
     , onServerFinished :: Information -> IO ()
     -- ^ When a handshake is done, this hook can check `Information`.
     , onSelectKeyShareGroups :: [Group] -> [Group]
@@ -722,7 +735,7 @@
     -- of the certificate.  This verification is performed by the
     -- library internally.
     --
-    -- Default: returns the followings:
+    -- Default: returns the following:
     --
     -- @
     -- CertificateUsageReject (CertificateRejectOther "no client certificates expected")
@@ -738,7 +751,7 @@
     -- client version and the client list of ciphers.
     --
     -- This could be useful with old clients and as a workaround to
-    -- the BEAST (where RC4 is sometimes prefered with TLS < 1.1)
+    -- the BEAST (where RC4 is sometimes preferred with TLS < 1.1)
     --
     -- The client cipher list cannot be empty.
     --
@@ -875,6 +888,14 @@
     -- certificate.
     --
     -- Default: 32
+    , limitKeyUpdate :: Maybe Int
+    -- ^ Maximum number of consecutive TLS 1.3 KeyUpdate messages accepted
+    -- without intervening non-empty application data.  This bounds the CPU
+    -- work and response amplification a peer can trigger while application
+    -- code is blocked inside 'recvData'.  'Nothing' and non-positive values
+    -- disable the limit; they do not disable KeyUpdate processing.
+    --
+    -- Default: @Just 32@
     }
     deriving (Eq, Show)
 
@@ -884,4 +905,5 @@
     Limit
         { limitRecordSize = Nothing
         , limitHandshakeFragment = 32
+        , limitKeyUpdate = Just 32
         }
diff --git a/Network/TLS/PostHandshake.hs b/Network/TLS/PostHandshake.hs
--- a/Network/TLS/PostHandshake.hs
+++ b/Network/TLS/PostHandshake.hs
@@ -26,8 +26,8 @@
 -- | Handle a post-handshake authentication flight with TLS 1.3.  This
 -- is called automatically by 'recvData', in a context where the read
 -- lock is already taken. Client only.
-postHandshakeAuthWith :: Context -> Handshake13 -> IO ()
-postHandshakeAuthWith ctx hs =
+postHandshakeAuthWith :: Context -> Handshake13R -> IO ()
+postHandshakeAuthWith ctx hb =
     withWriteLock ctx $
         handleException ctx $
-            doPostHandshakeAuthWith_ (ctxRoleParams ctx) ctx hs
+            doPostHandshakeAuthWith_ (ctxRoleParams ctx) ctx hb
diff --git a/Network/TLS/QUIC.hs b/Network/TLS/QUIC.hs
--- a/Network/TLS/QUIC.hs
+++ b/Network/TLS/QUIC.hs
@@ -116,7 +116,7 @@
     { quicSend :: [(CryptLevel, ByteString)] -> IO ()
     -- ^ Called by TLS so that QUIC sends one or more handshake fragments. The
     -- content transiting on this API is the plaintext of the fragments and
-    -- QUIC responsability is to encrypt this payload with the key material
+    -- QUIC responsibility is to encrypt this payload with the key material
     -- given for the specified level and an appropriate encryption scheme.
     --
     -- The size of the fragments may exceed QUIC datagram limits so QUIC may
@@ -194,7 +194,7 @@
         let qexts = filterQTP exts
         when (null qexts) $ do
             throwCore $
-                Error_Protocol "QUIC transport parameters are mssing" MissingExtension
+                Error_Protocol "QUIC transport parameters are missing" MissingExtension
         quicNotifyExtensions callbacks ctx qexts
         quicInstallKeys callbacks ctx (InstallApplicationKeys appSecInfo)
 
@@ -222,7 +222,7 @@
         let qexts = filterQTP exts
         when (null qexts) $ do
             throwCore $
-                Error_Protocol "QUIC transport parameters are mssing" MissingExtension
+                Error_Protocol "QUIC transport parameters are missing" MissingExtension
         quicNotifyExtensions callbacks ctx qexts
         quicInstallKeys callbacks ctx (InstallEarlyKeys mEarlySecInfo)
         quicInstallKeys callbacks ctx (InstallHandshakeKeys handSecInfo)
diff --git a/Network/TLS/Record/Decrypt.hs b/Network/TLS/Record/Decrypt.hs
--- a/Network/TLS/Record/Decrypt.hs
+++ b/Network/TLS/Record/Decrypt.hs
@@ -59,25 +59,40 @@
     nonEmptyContentTypes = [ProtocolType_Handshake, ProtocolType_Alert]
     unknownContentType13 c = "unknown TLS 1.3 content type: " ++ show c
 
-getCipherData :: Record a -> CipherData -> RecordM ByteString
-getCipherData (Record pt ver _) cdata = do
+-- | Check a decrypted record.
+--
+-- The first 'Bool' is what the lengths already said: 'False' when the padding
+-- length the record claims cannot be one.  It is carried in rather than
+-- answered where it was found, so that the MAC is computed either way -- see
+-- 'decryptData'.
+--
+-- Everything is computed before anything is decided, and the verdicts are
+-- combined with '&&!', which does not short-circuit.
+getCipherData :: Record a -> Bool -> CipherData -> RecordM ByteString
+getCipherData (Record pt ver _) lengthValid cdata = do
     -- check if the MAC is valid.
     macValid <- case cipherDataMAC cdata of
         Nothing -> return True
         Just digest -> do
             let new_hdr = Header pt ver (fromIntegral $ B.length $ cipherDataContent cdata)
             expected_digest <- makeDigest new_hdr $ cipherDataContent cdata
-            return (expected_digest == digest)
+            -- constEq rather than (==): (==) on ByteString is memcmp, which
+            -- returns as soon as two octets differ, and how soon is a
+            -- measurement of how much of the MAC was guessed correctly.
+            return (expected_digest `BA.constEq` digest)
 
     -- check if the padding is filled with the correct pattern if it exists
     -- (before TLS10 this checks instead that the padding length is minimal)
     paddingValid <- case cipherDataPadding cdata of
         Nothing -> return True
         Just (pad, _blksz) -> do
-            let b = B.length pad - 1
-            return $ B.replicate (B.length pad) (fromIntegral b) == pad
+            let b = fromIntegral (B.length pad - 1)
+            -- Every octet, and no allocation of a pattern to compare against:
+            -- B.all stops at the first wrong octet, and replicating the
+            -- pattern costs time in proportion to a length the peer chose.
+            return $ B.foldl' (\acc w -> acc .|. (w `xor` b)) 0 pad == 0
 
-    unless (macValid &&! paddingValid) $
+    unless (lengthValid &&! macValid &&! paddingValid) $
         throwError $
             Error_Protocol "bad record mac Stream/Block" BadRecordMac
 
@@ -115,9 +130,13 @@
     blockSize = bulkBlockSize bulk
     econtentLen = B.length econtent
 
+    -- A record too short for the cipher cannot be deprotected: RFC 5246
+    -- Section 7.2.2 and RFC 8446 Section 5.2 answer it with bad_record_mac.
     sanityCheckError =
-        throwError
-            (Error_Packet "encrypted content too small for encryption parameters")
+        throwError $
+            Error_Protocol
+                "encrypted content too small for encryption parameters"
+                BadRecordMac
 
     decryptOf :: BulkState -> RecordM ByteString
     decryptOf (BulkStateBlock decryptF) = do
@@ -134,11 +153,25 @@
         let (content', iv') = decryptF iv econtent'
         modify' $ \txs -> txs{stCryptState = cst{cstIV = iv'}}
 
-        let paddinglength = fromIntegral (B.last content') + 1
-        let contentlen = B.length content' - paddinglength - macSize
+        -- The last octet of the plaintext says how much padding there is.
+        -- It may say more than the record can hold, and that already settles
+        -- the record -- but answering it here, by splitting the record and
+        -- failing, would answer it *without computing the MAC*.  How long a
+        -- record takes to reject would then say whether the padding length
+        -- was plausible, which is the question the attacker is asking.
+        --
+        -- So carry the verdict instead and go on with a length that fits.
+        -- getCipherData folds it in with the MAC, and the answer is the same
+        -- BadRecordMac either way.
+        let plainlen = B.length content'
+            claimed = fromIntegral (B.last content') + 1
+            lengthValid = claimed + macSize <= plainlen
+            paddinglength = if lengthValid then claimed else 1
+            contentlen = plainlen - paddinglength - macSize
         (content, mac, padding) <- get3i content' (contentlen, macSize, paddinglength)
         getCipherData
             record
+            lengthValid
             CipherData
                 { cipherDataContent = content
                 , cipherDataMAC = Just mac
@@ -155,6 +188,7 @@
         modify' $ \txs -> txs{stCryptState = cst{cstKey = BulkStateStream bulkStream'}}
         getCipherData
             record
+            True
             CipherData
                 { cipherDataContent = content
                 , cipherDataMAC = Just mac
@@ -194,9 +228,11 @@
     decryptOf BulkStateUninitialized =
         throwError $ Error_Protocol "decrypt state uninitialized" InternalError
 
-    -- handling of outer format can report errors with Error_Packet
+    -- the outer format of a record that cannot be deprotected is reported
+    -- as an integrity failure too, i.e. BadRecordMac
     get3o s ls =
-        maybe (throwError $ Error_Packet "record bad format") return $ partition3 s ls
+        maybe (throwError $ Error_Protocol "record bad format" BadRecordMac) return $
+            partition3 s ls
     get2o s (d1, d2) = get3o s (d1, d2, 0) >>= \(r1, r2, _) -> return (r1, r2)
 
     -- all format errors related to decrypted content are reported
diff --git a/Network/TLS/Record/Recv.hs b/Network/TLS/Record/Recv.hs
--- a/Network/TLS/Record/Recv.hs
+++ b/Network/TLS/Record/Recv.hs
@@ -50,7 +50,8 @@
     -- ^ TLS context
     -> IO (Either TLSError (Record Plaintext))
 recvRecord12 ctx =
-    readExactBytes ctx 5 >>= either (return . Left) (recvLengthE . decodeHeader)
+    readExactBytes ctx 5
+        >>= either (return . Left) (recvLengthE . (decodeHeader >=> checkType))
   where
     recvLengthE = either (return . Left) recvLength
 
@@ -69,7 +70,9 @@
                     >>= either (return . Left) (getRecord ctx header)
 
 recvRecord13 :: Context -> IO (Either TLSError (Record Plaintext))
-recvRecord13 ctx = readExactBytes ctx 5 >>= either (return . Left) (recvLengthE . decodeHeader)
+recvRecord13 ctx =
+    readExactBytes ctx 5
+        >>= either (return . Left) (recvLengthE . (decodeHeader >=> checkType))
   where
     recvLengthE = either (return . Left) recvLength
     recvLength header@(Header _ _ readlen) = do
@@ -90,6 +93,24 @@
 
 maximumSizeExceeded :: TLSError
 maximumSizeExceeded = Error_Protocol "record exceeding maximum size" RecordOverflow
+
+-- RFC 8446 Section 5: a record of an unexpected type is answered with
+-- unexpected_message.  Checked on the header, before the body is read, so
+-- that what is no TLS record at all -- an SSLv2 ClientHello, say, whose first
+-- byte reads as type 0x80 and whose length can ask for bytes that never
+-- come -- is answered at once rather than when the peer gives up.
+checkType :: Header -> Either TLSError Header
+checkType header@(Header ty _ _)
+    | ty `elem` known = Right header
+    | otherwise =
+        Left $ Error_Packet_unexpected (show ty) " expected: TLS record type"
+  where
+    known =
+        [ ProtocolType_ChangeCipherSpec
+        , ProtocolType_Alert
+        , ProtocolType_Handshake
+        , ProtocolType_AppData
+        ]
 
 ----------------------------------------------------------------
 
diff --git a/Network/TLS/State.hs b/Network/TLS/State.hs
--- a/Network/TLS/State.hs
+++ b/Network/TLS/State.hs
@@ -25,6 +25,7 @@
     setVersion,
     setVersionIfUnset,
     getVersion,
+    getVersionMaybe,
     getVersionWithDefault,
     setSecureRenegotiation,
     getSecureRenegotiation,
@@ -231,6 +232,14 @@
 getVersion =
     fromMaybe (error "internal error: version hasn't been set yet")
         <$> gets stVersion
+
+-- | The negotiated version, or 'Nothing' before there is one.
+--
+-- 'getVersion' calls 'error' in that case, which is the right answer inside
+-- the handshake -- reaching it there would be a bug -- and the wrong one for
+-- anything a user of the library can call before the handshake has run.
+getVersionMaybe :: TLSSt (Maybe Version)
+getVersionMaybe = gets stVersion
 
 getVersionWithDefault :: Version -> TLSSt Version
 getVersionWithDefault defaultVer = fromMaybe defaultVer <$> gets stVersion
diff --git a/Network/TLS/Types.hs b/Network/TLS/Types.hs
--- a/Network/TLS/Types.hs
+++ b/Network/TLS/Types.hs
@@ -11,6 +11,7 @@
     bigNumToInteger,
     bigNumFromInteger,
     defaultRecordSizeLimit,
+    maxHandshakeSize,
     TranscriptHash (..),
     WireBytes,
 ) where
@@ -58,6 +59,22 @@
 -- 2^14 + 1 for TLS 1.3
 defaultRecordSizeLimit :: Int
 defaultRecordSizeLimit = 16384
+
+----------------------------------------------------------------
+
+-- | The largest handshake message we will reassemble.
+--
+-- A handshake message carries a 24-bit length, so a peer may announce close
+-- to 16MB and then feed it a record at a time.  Records are bounded, but the
+-- message they are reassembled into was not, and the fragments are held until
+-- it is complete -- before anything has authenticated the peer.
+--
+-- The largest legitimate one is a Certificate message.  A long chain of
+-- post-quantum certificates runs to tens of kilobytes, so this leaves an
+-- order of magnitude over anything real while taking two orders of magnitude
+-- off what a peer can ask us to hold.
+maxHandshakeSize :: Int
+maxHandshakeSize = 262144
 
 ----------------------------------------------------------------
 
diff --git a/Network/TLS/Types/Secret.hs b/Network/TLS/Types/Secret.hs
--- a/Network/TLS/Types/Secret.hs
+++ b/Network/TLS/Types/Secret.hs
@@ -1,5 +1,14 @@
+-- | The secret types of the TLS key schedule.
+--
+-- None of these prints its key material: 'Show' renders @\<secret\>@, since
+-- these values reach a QUIC implementation through
+-- "Network.TLS.QUIC" and are the kind of thing a handshake trace prints
+-- without meaning to.  'Crypto.Debug.debugShow' returns the hexadecimal that
+-- 'Show' used to, for a debugging session that wants it.  @SSLKEYLOGFILE@
+-- does not go through either: it uses 'Network.TLS.Handshake.Key.LogLabel'.
 module Network.TLS.Types.Secret where
 
+import Crypto.Debug (DebugShow (..))
 import Data.ByteArray (convert)
 import Network.TLS.Imports
 import Network.TLS.Types.Cipher
@@ -18,27 +27,39 @@
 newtype BaseSecret a = BaseSecret Secret
 
 instance Show (BaseSecret a) where
-    show (BaseSecret bs) = showBytesHex $ convert bs
+    show _ = "<secret>"
 
+instance DebugShow (BaseSecret a) where
+    debugShow (BaseSecret bs) = showBytesHex $ convert bs
+
 newtype AnyTrafficSecret a = AnyTrafficSecret Secret
 
 instance Show (AnyTrafficSecret a) where
-    show (AnyTrafficSecret bs) = showBytesHex $ convert bs
+    show _ = "<secret>"
 
+instance DebugShow (AnyTrafficSecret a) where
+    debugShow (AnyTrafficSecret bs) = showBytesHex $ convert bs
+
 -- | A client traffic secret, typed with a parameter indicating a step in the
 -- TLS key schedule.
 newtype ClientTrafficSecret a = ClientTrafficSecret Secret
 
 instance Show (ClientTrafficSecret a) where
-    show (ClientTrafficSecret bs) = showBytesHex $ convert bs
+    show _ = "<secret>"
 
+instance DebugShow (ClientTrafficSecret a) where
+    debugShow (ClientTrafficSecret bs) = showBytesHex $ convert bs
+
 -- | A server traffic secret, typed with a parameter indicating a step in the
 -- TLS key schedule.
 newtype ServerTrafficSecret a = ServerTrafficSecret Secret
 
 instance Show (ServerTrafficSecret a) where
-    show (ServerTrafficSecret bs) = showBytesHex $ convert bs
+    show _ = "<secret>"
 
+instance DebugShow (ServerTrafficSecret a) where
+    debugShow (ServerTrafficSecret bs) = showBytesHex $ convert bs
+
 data SecretTriple a = SecretTriple
     { triBase :: BaseSecret a
     , triClient :: ClientTrafficSecret a
@@ -59,4 +80,7 @@
 newtype MainSecret = MainSecret Secret
 
 instance Show MainSecret where
-    show (MainSecret bs) = showBytesHex $ convert bs
+    show _ = "<secret>"
+
+instance DebugShow MainSecret where
+    debugShow (MainSecret bs) = showBytesHex $ convert bs
diff --git a/Network/TLS/Types/Session.hs b/Network/TLS/Types/Session.hs
--- a/Network/TLS/Types/Session.hs
+++ b/Network/TLS/Types/Session.hs
@@ -3,6 +3,7 @@
 module Network.TLS.Types.Session where
 
 import Codec.Serialise
+import Crypto.Debug (DebugShow (..))
 import qualified Data.ByteString as B
 import GHC.Generics
 import Network.Socket (HostName)
@@ -46,7 +47,42 @@
     , sessionMaxEarlyDataSize :: Int
     , sessionFlags :: [SessionFlag]
     } -- sessionFromTicket :: Bool
-    deriving (Show, Eq, Generic)
+    deriving (Eq, Generic)
+
+-- | Everything but @sessionSecret@, which renders as @\<secret\>@: whoever
+-- has it can resume the session.  'Crypto.Debug.debugShow' renders it.
+instance Show SessionData where
+    showsPrec = showsSessionData (showString "<secret>")
+
+instance DebugShow SessionData where
+    debugShow sd = showsSessionData (shows $ sessionSecret sd) 0 sd ""
+
+-- | What the two instances above share, so that a field added to
+-- 'SessionData' cannot reach one of them and not the other.
+showsSessionData :: ShowS -> Int -> SessionData -> ShowS
+showsSessionData secret d sd =
+    showParen (d > 10) $
+        showString "SessionData {sessionVersion = "
+            . shows (sessionVersion sd)
+            . showString ", sessionCipher = "
+            . shows (sessionCipher sd)
+            . showString ", sessionCompression = "
+            . shows (sessionCompression sd)
+            . showString ", sessionClientSNI = "
+            . shows (sessionClientSNI sd)
+            . showString ", sessionSecret = "
+            . secret
+            . showString ", sessionGroup = "
+            . shows (sessionGroup sd)
+            . showString ", sessionTicketInfo = "
+            . shows (sessionTicketInfo sd)
+            . showString ", sessionALPN = "
+            . shows (sessionALPN sd)
+            . showString ", sessionMaxEarlyDataSize = "
+            . shows (sessionMaxEarlyDataSize sd)
+            . showString ", sessionFlags = "
+            . shows (sessionFlags sd)
+            . showChar '}'
 
 is0RTTPossible :: SessionData -> Bool
 is0RTTPossible sd = sessionMaxEarlyDataSize sd > 0
diff --git a/Network/TLS/Util.hs b/Network/TLS/Util.hs
--- a/Network/TLS/Util.hs
+++ b/Network/TLS/Util.hs
@@ -18,7 +18,6 @@
 ) where
 
 import Control.Concurrent.MVar
-import Control.Exception (SomeAsyncException (..))
 import qualified Control.Exception as E
 import Data.ByteArray (ScrubbedBytes)
 import qualified Data.ByteArray as BA
@@ -83,7 +82,7 @@
   where
     filterExn :: E.SomeException -> Maybe E.SomeException
     filterExn e = case E.fromException (E.toException e) of
-        Just (SomeAsyncException _) -> Nothing
+        Just (E.SomeAsyncException _) -> Nothing
         Nothing -> Just e
 
 forEitherM :: Monad m => [a] -> (a -> m (Either l b)) -> m (Either l [b])
diff --git a/test/Certificate.hs b/test/Certificate.hs
--- a/test/Certificate.hs
+++ b/test/Certificate.hs
@@ -6,6 +6,7 @@
     arbitraryX509,
     arbitraryX509WithKey,
     arbitraryX509WithKeyAndUsage,
+    arbitraryRSACredentialWithPurpose,
     arbitraryDN,
     simpleCertificate,
     simpleX509,
@@ -117,6 +118,25 @@
     let sigalg = getSignatureALG pubKey
     let (signedExact, ()) = objectToSignedExact (\_ -> (B.pack sig, sigalg, ())) cert
     return signedExact
+
+arbitraryRSACredentialWithPurpose
+    :: ExtKeyUsagePurpose -> Gen (CertificateChain, PrivKey)
+arbitraryRSACredentialWithPurpose purpose = do
+    let (pubKey, privKey) = getGlobalRSAPair
+    cert <- arbitraryCertificate knownKeyUsage $ PubKeyRSA pubKey
+    sig <- resize 40 $ listOf1 arbitrary
+    let cert' =
+            cert
+                { certExtensions =
+                    Extensions $
+                        Just
+                            [ extensionEncode True $ ExtKeyUsage knownKeyUsage
+                            , extensionEncode False $ ExtExtendedKeyUsage [purpose]
+                            ]
+                }
+        sigalg = getSignatureALG $ PubKeyRSA pubKey
+        (signedExact, ()) = objectToSignedExact (\_ -> (B.pack sig, sigalg, ())) cert'
+    return (CertificateChain [signedExact], PrivKeyRSA privKey)
 
 arbitraryX509 :: Gen SignedCertificate
 arbitraryX509 = do
diff --git a/test/EncodeSpec.hs b/test/EncodeSpec.hs
--- a/test/EncodeSpec.hs
+++ b/test/EncodeSpec.hs
@@ -1,8 +1,18 @@
 module EncodeSpec where
 
+import Codec.Compression.Zlib (compress)
+import Control.Exception (bracket_, evaluate)
+import Control.Monad (forM_, void)
 import Data.ByteString (ByteString)
+import qualified Data.ByteString as B
+import qualified Data.ByteString.Lazy as BL
+import Data.Either (isLeft)
+import Data.Int (Int64)
+import Data.Word (Word16)
+import GHC.Conc (disableAllocationLimit, enableAllocationLimit, setAllocationCounter)
 import Network.TLS
 import Network.TLS.Internal
+import Network.TLS.QUIC (errorToAlertDescription)
 import Test.Hspec
 import Test.Hspec.QuickCheck
 
@@ -10,6 +20,36 @@
 
 spec :: Spec
 spec = do
+    describe "extension decoding" $ do
+        prop "yields Nothing rather than throwing, for any message type" $
+            \ws -> forM_ extensionDecoders $ \(name, decode) ->
+                forM_ [minBound .. maxBound] $ \mt ->
+                    decode mt (B.pack ws) `shouldReturn` name
+    describe "handshake record length" $ do
+        -- A handshake message carries a 24-bit length, and the fragments are
+        -- held until the message is whole.  Refusing at the header means
+        -- refusing to hold anything: the length arrives in the first four
+        -- octets, before any of the body.
+        it "refuses a length past the limit, on its header alone" $ do
+            let tooBig = maxHandshakeSize + 1
+            isGotError (decodeHandshakeRecord (handshakeHeader tooBig)) `shouldBe` True
+            isGotError (decodeHandshakeRecord13 (handshakeHeader tooBig)) `shouldBe` True
+        it "refuses the largest a 24-bit length can say" $ do
+            let header = handshakeHeader 0xffffff
+            isGotError (decodeHandshakeRecord header) `shouldBe` True
+            isGotError (decodeHandshakeRecord13 header) `shouldBe` True
+        -- Still waiting for the body rather than refusing it: at the limit
+        -- the header alone is not enough to decide anything is wrong.
+        it "asks for more at the limit itself" $ do
+            let header = handshakeHeader maxHandshakeSize
+            isGotPartial (decodeHandshakeRecord header) `shouldBe` True
+            isGotPartial (decodeHandshakeRecord13 header) `shouldBe` True
+        it "still decodes a message of an ordinary size" $ do
+            let body = B.replicate 1000 0
+                record = handshakeHeader (B.length body) `B.append` body
+            gotThisMuch (B.length body) (decodeHandshakeRecord record) `shouldBe` True
+            gotThisMuch (B.length body) (decodeHandshakeRecord13 record) `shouldBe` True
+
     describe "encoder/decoder" $ do
         prop "can encode/decode Header" $ \x -> do
             decodeHeader (encodeHeader x) `shouldBe` Right x
@@ -17,7 +57,151 @@
             decodeHs (encodeHandshake x) `shouldBe` Right x
         prop "can encode/decode Handshake13" $ \x -> do
             decodeHs13 (encodeHandshake13 x) `shouldBe` Right x
+        it "round trips a valid TLS 1.3 compressed certificate" $ do
+            let certificate =
+                    CompressedCertificate13
+                        B.empty
+                        (CertificateChain_ $ CertificateChain [])
+                        []
+            decodeHs13 (encodeHandshake13 certificate) `shouldBe` Right certificate
+        it "rejects decompressed output shorter than its declared size" $ do
+            let plain = encodeCertificate13 B.empty (CertificateChain []) []
+                compressed = BL.toStrict $ compress $ BL.fromStrict plain
+                encoded = runPut $ do
+                    putWord16 1
+                    putWord24 (B.length plain + 1)
+                    putOpaque24 compressed
+            decodeHandshake13 HandshakeType_CompressedCertificate encoded
+                `shouldSatisfy` isLeft
+        -- RFC 8879 Section 4: a CompressedCertificate that cannot be
+        -- decompressed, or whose decompressed length is not the one
+        -- declared, is answered with bad_certificate; one with an algorithm
+        -- that was not offered breaks no decoding rule but a field value,
+        -- and is answered with illegal_parameter.  One malformed as a whole
+        -- stays a decode_error.
+        it "answers a decompressed length mismatch with bad_certificate" $ do
+            let plain = encodeCertificate13 B.empty (CertificateChain []) []
+                compressed = BL.toStrict $ compress $ BL.fromStrict plain
+            compressedCertificateAlert 1 (B.length plain + 1) compressed
+                `shouldBe` Just BadCertificate
+        it "answers data that is not zlib with bad_certificate" $
+            compressedCertificateAlert 1 16 (B.replicate 16 0xff)
+                `shouldBe` Just BadCertificate
+        it "answers an empty compressed certificate with decode_error" $
+            compressedCertificateAlert 1 16 B.empty
+                `shouldBe` Just DecodeError
+        it "answers bytes after a compressed certificate with decode_error" $ do
+            let plain = encodeCertificate13 B.empty (CertificateChain []) []
+                compressed = BL.toStrict $ compress $ BL.fromStrict plain
+            either (Just . errorToAlertDescription) (const Nothing)
+                ( decodeHandshake13 HandshakeType_CompressedCertificate $
+                    runPut $ do
+                        putWord16 1
+                        putWord24 (B.length plain)
+                        putOpaque24 (B.drop 2 compressed)
+                        putBytes (B.take 2 compressed)
+                )
+                `shouldBe` Just DecodeError
+        it "answers an unsupported compression algorithm with illegal_parameter" $ do
+            let plain = encodeCertificate13 B.empty (CertificateChain []) []
+                compressed = BL.toStrict $ compress $ BL.fromStrict plain
+            compressedCertificateAlert 2 (B.length plain) compressed
+                `shouldBe` Just IllegalParameter
+        -- A ClientKeyExchange is only expected once a cipher, and with it a
+        -- key exchange, has been negotiated -- not, say, after Finished.
+        -- One that comes without is out of order: unexpected_message.
+        it "answers a ClientKeyExchange before a key exchange with unexpected_message" $
+            either (Just . errorToAlertDescription) (const Nothing)
+                ( decodeHandshake
+                    CurrentParams{cParamsVersion = TLS12, cParamsKeyXchgType = Nothing}
+                    HandshakeType_ClientKeyXchg
+                    (B.replicate 130 1)
+                )
+                `shouldBe` Just UnexpectedMessage
+        -- RFC 7301 Section 3.1: protocol_name_list<2..2^16-1> of
+        -- ProtocolName<1..2^8-1>.
+        it "refuses a malformed application_layer_protocol_negotiation" $
+            forM_
+                [ B.empty -- empty extension
+                , B.pack [0, 0] -- empty list
+                , B.pack [0, 1, 0] -- empty ProtocolName
+                , B.pack [0, 2, 1, 104, 2, 104, 50] -- trailing data
+                ]
+                $ \bs ->
+                    ( extensionDecode MsgTClientHello bs
+                        :: Maybe ApplicationLayerProtocolNegotiation
+                    )
+                        `shouldBe` Nothing
+        it "decodes an application_layer_protocol_negotiation" $
+            ( extensionDecode MsgTClientHello (B.pack [0, 3, 2, 104, 50])
+                :: Maybe ApplicationLayerProtocolNegotiation
+            )
+                `shouldBe` Just (ApplicationLayerProtocolNegotiation [B.pack [104, 50]])
+        -- RFC 8422 Section 5.7: ecdh_Yc is <1..2^8-1>, so an empty one is
+        -- malformed -- a decode_error -- rather than a point that does not
+        -- decode, which is an illegal_parameter.
+        it "answers an empty ECDH public key with decode_error" $
+            either (Just . errorToAlertDescription) (const Nothing)
+                ( decodeHandshake
+                    CurrentParams
+                        { cParamsVersion = TLS12
+                        , cParamsKeyXchgType = Just CipherKeyExchange_ECDHE_RSA
+                        }
+                    HandshakeType_ClientKeyXchg
+                    (B.singleton 0)
+                )
+                `shouldBe` Just DecodeError
+        -- RFC 5246 Section 7.4.7.2: dh_Yc is <1..2^16-1>, so an empty one is
+        -- malformed -- a decode_error -- rather than a public value that is
+        -- not valid, which is an illegal_parameter.
+        -- RFC 6066 Section 3: server_name_list<1..2^16-1> of
+        -- HostName<1..2^16-1>.
+        it "refuses a malformed server_name in ClientHello" $
+            forM_
+                [ B.empty -- empty extension
+                , B.pack [0, 0] -- empty list
+                , B.pack [0, 3, 0, 0, 0] -- empty host_name
+                , B.pack [0, 4, 0, 0, 1, 101, 120] -- trailing data
+                ]
+                $ \bs ->
+                    (extensionDecode MsgTClientHello bs :: Maybe ServerName)
+                        `shouldBe` Nothing
+        it "decodes a server_name in ClientHello" $
+            (extensionDecode MsgTClientHello (B.pack [0, 4, 0, 0, 1, 101]) :: Maybe ServerName)
+                `shouldBe` Just (ServerName [ServerNameHostName "e"])
+        it "answers an empty DH public key with decode_error" $
+            either (Just . errorToAlertDescription) (const Nothing)
+                ( decodeHandshake
+                    CurrentParams
+                        { cParamsVersion = TLS12
+                        , cParamsKeyXchgType = Just CipherKeyExchange_DHE_RSA
+                        }
+                    HandshakeType_ClientKeyXchg
+                    (B.pack [0, 0])
+                )
+                `shouldBe` Just DecodeError
+        it "bounds TLS 1.3 certificate decompression by the declared size" $ do
+            let compressed = BL.toStrict $ compress $ BL.replicate (32 * 1024 * 1024) 0
+                encoded = runPut $ do
+                    putWord16 1
+                    putWord24 1
+                    putOpaque24 compressed
+            _ <- evaluate $ B.length encoded
+            decoded <-
+                withinAllocationLimit (8 * 1024 * 1024) $
+                    evaluate $
+                        decodeHandshake13 HandshakeType_CompressedCertificate encoded
+            decoded `shouldSatisfy` isLeft
 
+compressedCertificateAlert :: Word16 -> Int -> ByteString -> Maybe AlertDescription
+compressedCertificateAlert algo len compressed =
+    either (Just . errorToAlertDescription) (const Nothing) $
+        decodeHandshake13 HandshakeType_CompressedCertificate $
+            runPut $ do
+                putWord16 algo
+                putWord24 len
+                putOpaque24 compressed
+
 decodeHs :: ByteString -> Either TLSError Handshake
 decodeHs b = verifyResult (decodeHandshake cp) $ decodeHandshakeRecord b
   where
@@ -30,6 +214,28 @@
 decodeHs13 :: ByteString -> Either TLSError Handshake13
 decodeHs13 b = verifyResult decodeHandshake13 $ decodeHandshakeRecord13 b
 
+-- | A handshake record header: a type octet then a 24-bit length.
+handshakeHeader :: Int -> ByteString
+handshakeHeader len =
+    B.pack
+        [ 1 -- ClientHello
+        , fromIntegral (len `div` 65536)
+        , fromIntegral ((len `div` 256) `mod` 256)
+        , fromIntegral (len `mod` 256)
+        ]
+
+isGotError :: GetResult a -> Bool
+isGotError (GotError _) = True
+isGotError _ = False
+
+isGotPartial :: GetResult a -> Bool
+isGotPartial (GotPartial _) = True
+isGotPartial _ = False
+
+gotThisMuch :: Int -> GetResult (a, ByteString) -> Bool
+gotThisMuch n (GotSuccess (_, content)) = B.length content == n
+gotThisMuch _ _ = False
+
 verifyResult :: (f -> r -> a) -> GetResult (f, r) -> a
 verifyResult fn result =
     case result of
@@ -37,3 +243,48 @@
         GotError e -> error ("got error: " ++ show e)
         GotSuccessRemaining _ _ -> error "got remaining byte left"
         GotSuccess (ty, content) -> fn ty content
+
+withinAllocationLimit :: Int64 -> IO a -> IO a
+withinAllocationLimit limit =
+    bracket_
+        (setAllocationCounter limit >> enableAllocationLimit)
+        disableAllocationLimit
+
+-- | Every 'Extension' instance, each wrapped so that the decoded value is
+-- forced inside IO.  A partial 'extensionDecode' therefore surfaces as a
+-- thrown exception the test can see, rather than as a thunk nobody looks at.
+--
+-- The name is threaded through as the return value only so that a failure
+-- report says which instance it was.
+type Decoder a = MessageType -> ByteString -> Maybe a
+
+extensionDecoders :: [(String, MessageType -> ByteString -> IO String)]
+extensionDecoders =
+    [
+      entry "ServerName" (extensionDecode :: Decoder ServerName),
+      entry "MaxFragmentLength" (extensionDecode :: Decoder MaxFragmentLength),
+      entry "SecureRenegotiation" (extensionDecode :: Decoder SecureRenegotiation),
+      entry "ApplicationLayerProtocolNegotiation" (extensionDecode :: Decoder ApplicationLayerProtocolNegotiation),
+      entry "ExtendedMainSecret" (extensionDecode :: Decoder ExtendedMainSecret),
+      entry "CompressCertificate" (extensionDecode :: Decoder CompressCertificate),
+      entry "SupportedGroups" (extensionDecode :: Decoder SupportedGroups),
+      entry "EcPointFormatsSupported" (extensionDecode :: Decoder EcPointFormatsSupported),
+      entry "RecordSizeLimit" (extensionDecode :: Decoder RecordSizeLimit),
+      entry "SessionTicket" (extensionDecode :: Decoder SessionTicket),
+      entry "HeartBeat" (extensionDecode :: Decoder HeartBeat),
+      entry "SignatureAlgorithms" (extensionDecode :: Decoder SignatureAlgorithms),
+      entry "SignatureAlgorithmsCert" (extensionDecode :: Decoder SignatureAlgorithmsCert),
+      entry "SupportedVersions" (extensionDecode :: Decoder SupportedVersions),
+      entry "KeyShare" (extensionDecode :: Decoder KeyShare),
+      entry "PostHandshakeAuth" (extensionDecode :: Decoder PostHandshakeAuth),
+      entry "PskKeyExchangeModes" (extensionDecode :: Decoder PskKeyExchangeModes),
+      entry "PreSharedKey" (extensionDecode :: Decoder PreSharedKey),
+      entry "EarlyDataIndication" (extensionDecode :: Decoder EarlyDataIndication),
+      entry "Cookie" (extensionDecode :: Decoder Cookie),
+      entry "CertificateAuthorities" (extensionDecode :: Decoder CertificateAuthorities),
+      entry "EchOuterExtensions" (extensionDecode :: Decoder EchOuterExtensions),
+      entry "EncryptedClientHello" (extensionDecode :: Decoder EncryptedClientHello)
+    ]
+  where
+    entry name decode = (name, \mt bs -> name <$ evaluate (length (show (decode mt bs))))
+
diff --git a/test/HandshakeSpec.hs b/test/HandshakeSpec.hs
--- a/test/HandshakeSpec.hs
+++ b/test/HandshakeSpec.hs
@@ -2,1041 +2,2298 @@
 
 module HandshakeSpec where
 
-import Control.Monad
-import qualified Data.ByteString as B
-import qualified Data.ByteString.Lazy as L
-import Data.IORef
-import Data.List
-import Data.Maybe
-import Data.X509 (ExtKeyUsageFlag (..))
-import Network.TLS
-import Network.TLS.Extra.Cipher
-import Network.TLS.Internal
-import Test.Hspec
-import Test.Hspec.QuickCheck
-import Test.QuickCheck
-
-import API
-import Arbitrary
-import PipeChan
-import Run
-import Session
-
-spec :: Spec
-spec = do
-    describe "pipe" $ do
-        it "can setup a channel" pipe_work
-    describe "handshake" $ do
-        prop "can run TLS 1.2" handshake_simple
-        prop "can run TLS 1.3" handshake13_simple
-        prop "can update key for TLS 1.3" handshake_update_key
-        prop "can prevent downgrade attack" handshake13_downgrade
-        prop "can negotiate hash and signature" handshake_hashsignatures
-        prop "can negotiate cipher suite" handshake_ciphersuites
-        prop "can negotiate group" handshake_groups
-        prop "can negotiate elliptic curve" handshake_ec
-        prop "can fallback for certificate with cipher" handshake_cert_fallback_cipher
-        prop
-            "can fallback for certificate with hash and signature"
-            handshake_cert_fallback_hs
-        prop "can handle server key usage" handshake_server_key_usage
-        prop "can handle client key usage" handshake_client_key_usage
-        prop "can authenticate client" handshake_client_auth
-        prop "can receive client authentication failure" handshake_client_auth_fail
-        prop "can handle extended main secret" handshake_ems
-        prop "can resume with extended main secret" handshake_resumption_ems
-        prop "can handle ALPN" handshake_alpn
-        prop "can handle SNI" handshake_sni
-        prop "can re-negotiate with TLS 1.2" handshake12_renegotiation
-        prop "can resume session with TLS 1.2" handshake12_session_resumption
-        prop "can resume session ticket with TLS 1.2" handshake12_session_ticket
-        prop "can handshake with TLS 1.3 Full" handshake13_full
-        prop "can handshake with TLS 1.3 HRR" handshake13_hrr
-        prop "can handshake with TLS 1.3 PSK" handshake13_psk
-        prop "can handshake with TLS 1.3 PSK ticket" handshake13_psk_ticket
-        prop "can handshake with TLS 1.3 PSK -> HRR" handshake13_psk_fallback
-        prop "can handshake with TLS 1.3 0RTT" handshake13_0rtt
-        prop "can handshake with TLS 1.3 0RTT -> PSK" handshake13_0rtt_fallback
-        prop "can handshake with TLS 1.3 EE" handshake13_ee_groups
-        prop "can handshake with TLS 1.3 EC groups" handshake13_ec
-        prop "can handshake with TLS 1.3 FFDHE groups" handshake13_ffdhe
-        prop "can handshake with TLS 1.3 Post-handshake auth" post_handshake_auth
-
---------------------------------------------------------------
-
-pipe_work :: IO ()
-pipe_work = do
-    pipe <- newPipe
-    _ <- runPipe pipe
-
-    let bSize = 16
-    n <- generate (choose (1, 32))
-
-    let d1 = B.replicate (bSize * n) 40
-    let d2 = B.replicate (bSize * n) 45
-
-    d1' <- writePipeC pipe d1 >> readPipeS pipe (B.length d1)
-    d1' `shouldBe` d1
-
-    d2' <- writePipeS pipe d2 >> readPipeC pipe (B.length d2)
-    d2' `shouldBe` d2
-
---------------------------------------------------------------
-
-handshake_simple :: (ClientParams, ServerParams) -> IO ()
-handshake_simple = runTLSSimple
-
---------------------------------------------------------------
-
-newtype CSP13 = CSP13 (ClientParams, ServerParams) deriving (Show)
-
-instance Arbitrary CSP13 where
-    arbitrary = CSP13 <$> arbitraryPairParams13
-
-handshake13_simple :: CSP13 -> IO ()
-handshake13_simple (CSP13 params) = runTLSSimple13 params hs
-  where
-    cgrps = supportedGroups $ clientSupported $ fst params
-    sgrps = supportedGroups $ serverSupported $ snd params
-    hs = if unsafeHead cgrps `elem` sgrps then FullHandshake else HelloRetryRequest
-
---------------------------------------------------------------
-
-handshake13_downgrade :: (ClientParams, ServerParams) -> IO ()
-handshake13_downgrade (cparam, sparam) = do
-    versionForced <-
-        generate $ elements (supportedVersions $ clientSupported cparam)
-    let debug' = (serverDebug sparam){debugVersionForced = Just versionForced}
-        sparam' = sparam{serverDebug = debug'}
-        params = (cparam, sparam')
-        downgraded =
-            (isVersionEnabled TLS13 params && versionForced < TLS13)
-                || (isVersionEnabled TLS12 params && versionForced < TLS12)
-    if downgraded
-        then runTLSFailure params handshake handshake
-        else runTLSSimple params
-
-handshake_update_key :: (ClientParams, ServerParams) -> IO ()
-handshake_update_key = runTLSSimpleKeyUpdate
-
---------------------------------------------------------------
-
-handshake_hashsignatures
-    :: ([HashAndSignatureAlgorithm], [HashAndSignatureAlgorithm]) -> IO ()
-handshake_hashsignatures (clientHashSigs, serverHashSigs) = do
-    tls13 <- generate arbitrary
-    let version = if tls13 then TLS13 else TLS12
-        ciphers =
-            [ cipher_ECDHE_RSA_WITH_AES_256_GCM_SHA384
-            , cipher_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
-            , cipher13_AES_128_GCM_SHA256
-            ]
-    (clientParam, serverParam) <-
-        generate $
-            arbitraryPairParamsWithVersionsAndCiphers
-                ([version], [version])
-                (ciphers, ciphers)
-    let clientParam' =
-            clientParam
-                { clientSupported =
-                    (clientSupported clientParam)
-                        { supportedHashSignatures = clientHashSigs
-                        }
-                }
-        serverParam' =
-            serverParam
-                { serverSupported =
-                    (serverSupported serverParam)
-                        { supportedHashSignatures = serverHashSigs
-                        }
-                }
-        commonHashSigs = clientHashSigs `intersect` serverHashSigs
-        shouldFail
-            | tls13 = all incompatibleWithDefaultCurve commonHashSigs
-            | otherwise = null commonHashSigs
-    if shouldFail
-        then runTLSFailure (clientParam', serverParam') handshake handshake
-        else runTLSSimple (clientParam', serverParam')
-  where
-    incompatibleWithDefaultCurve (h, SignatureECDSA) = h /= HashSHA256
-    incompatibleWithDefaultCurve _ = False
-
-handshake_ciphersuites :: ([Cipher], [Cipher]) -> IO ()
-handshake_ciphersuites (clientCiphers, serverCiphers) = do
-    tls13 <- generate arbitrary
-    let version = if tls13 then TLS13 else TLS12
-    (clientParam, serverParam) <-
-        generate $
-            arbitraryPairParamsWithVersionsAndCiphers
-                ([version], [version])
-                (clientCiphers, serverCiphers)
-    let adequate = cipherAllowedForVersion version
-        shouldSucceed = any adequate (clientCiphers `intersect` serverCiphers)
-    if shouldSucceed
-        then runTLSSimple (clientParam, serverParam)
-        else runTLSFailure (clientParam, serverParam) handshake handshake
-
---------------------------------------------------------------
-
-handshake_groups :: GGP -> IO ()
-handshake_groups (GGP clientGroups serverGroups) = do
-    tls13 <- generate arbitrary
-    let versions = if tls13 then [TLS13] else [TLS12]
-        ciphers = ciphersuite_strong
-    (clientParam, serverParam) <-
-        generate $
-            arbitraryPairParamsWithVersionsAndCiphers
-                (versions, versions)
-                (ciphers, ciphers)
-    denyCustom <- generate arbitrary
-    let groupUsage =
-            if denyCustom
-                then GroupUsageUnsupported "custom group denied"
-                else GroupUsageValid
-        clientParam' =
-            clientParam
-                { clientSupported =
-                    (clientSupported clientParam)
-                        { supportedGroups = clientGroups
-                        }
-                , clientHooks =
-                    (clientHooks clientParam)
-                        { onCustomFFDHEGroup = \_ _ -> return groupUsage
-                        }
-                }
-        serverParam' =
-            serverParam
-                { serverSupported =
-                    (serverSupported serverParam)
-                        { supportedGroups = serverGroups
-                        , supportedGroupsTLS13 = [serverGroups]
-                        }
-                }
-        commonGroups = clientGroups `intersect` serverGroups
-        shouldFail = null commonGroups
-        p minfo = isNothing (minfo >>= infoSupportedGroup) == null commonGroups
-    if shouldFail
-        then runTLSFailure (clientParam', serverParam') handshake handshake
-        else runTLSPredicate (clientParam', serverParam') p
-
---------------------------------------------------------------
-
-newtype SG = SG [Group] deriving (Show)
-
-instance Arbitrary SG where
-    arbitrary = SG <$> shuffle sigGroups
-      where
-        sigGroups = [P256, P521]
-
-handshake_ec :: SG -> IO ()
-handshake_ec (SG sigGroups) = do
-    let versions = [TLS12]
-        ciphers =
-            [ cipher_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
-            ]
-        hashSignatures =
-            [ (HashSHA256, SignatureECDSA)
-            ]
-    (clientParam, serverParam) <-
-        generate $
-            arbitraryPairParamsWithVersionsAndCiphers
-                (versions, versions)
-                (ciphers, ciphers)
-    clientGroups <- generate $ shuffle sigGroups
-    clientHashSignatures <- generate $ sublistOf hashSignatures
-    serverHashSignatures <- generate $ sublistOf hashSignatures
-    credentials <- generate arbitraryCredentialsOfEachCurve
-    let clientParam' =
-            clientParam
-                { clientSupported =
-                    (clientSupported clientParam)
-                        { supportedGroups = clientGroups
-                        , supportedHashSignatures = clientHashSignatures
-                        }
-                }
-        serverParam' =
-            serverParam
-                { serverSupported =
-                    (serverSupported serverParam)
-                        { supportedGroups = sigGroups
-                        , supportedGroupsTLS13 = [sigGroups]
-                        , supportedHashSignatures = serverHashSignatures
-                        }
-                , serverShared =
-                    (serverShared serverParam)
-                        { sharedCredentials = Credentials credentials
-                        }
-                }
-        sigAlgs = map snd (clientHashSignatures `intersect` serverHashSignatures)
-        ecdsaDenied = SignatureECDSA `notElem` sigAlgs
-    if ecdsaDenied
-        then runTLSFailure (clientParam', serverParam') handshake handshake
-        else runTLSSimple (clientParam', serverParam')
-
--- Tests ability to use or ignore client "signature_algorithms" extension when
--- choosing a server certificate.  Here peers allow DHE_RSA_AES128_SHA1 but
--- the server RSA certificate has a SHA-1 signature that the client does not
--- support.  Server may choose the DSA certificate only when cipher
--- DHE_DSA_AES128_SHA1 is allowed.  Otherwise it must fallback to the RSA
--- certificate.
-
-data OC = OC [Cipher] [Cipher] deriving (Show)
-
-instance Arbitrary OC where
-    arbitrary = OC <$> sublistOf otherCiphers <*> sublistOf otherCiphers
-      where
-        otherCiphers =
-            [ cipher_ECDHE_RSA_WITH_AES_256_GCM_SHA384
-            , cipher_ECDHE_RSA_WITH_AES_128_GCM_SHA256
-            ]
-
-handshake_cert_fallback_cipher :: OC -> IO ()
-handshake_cert_fallback_cipher (OC clientCiphers serverCiphers) = do
-    let clientVersions = [TLS12]
-        serverVersions = [TLS12]
-        commonCiphers = [cipher_ECDHE_RSA_WITH_AES_128_GCM_SHA256]
-        hashSignatures = [(HashSHA256, SignatureRSA), (HashSHA1, SignatureDSA)]
-    chainRef <- newIORef Nothing
-    (clientParam, serverParam) <-
-        generate $
-            arbitraryPairParamsWithVersionsAndCiphers
-                (clientVersions, serverVersions)
-                (clientCiphers ++ commonCiphers, serverCiphers ++ commonCiphers)
-    let clientParam' =
-            clientParam
-                { clientSupported =
-                    (clientSupported clientParam)
-                        { supportedHashSignatures = hashSignatures
-                        }
-                , clientHooks =
-                    (clientHooks clientParam)
-                        { onServerCertificate = \_ _ _ chain ->
-                            writeIORef chainRef (Just chain) >> return []
-                        }
-                }
-    runTLSSimple (clientParam', serverParam)
-    serverChain <- readIORef chainRef
-    isLeafRSA serverChain `shouldBe` True
-
--- Same as above but testing with supportedHashSignatures directly instead of
--- ciphers, and thus allowing TLS13.  Peers accept RSA with SHA-256 but the
--- server RSA certificate has a SHA-1 signature.  When Ed25519 is allowed by
--- both client and server, the Ed25519 certificate is selected.  Otherwise the
--- server fallbacks to RSA.
---
--- Note: SHA-1 is supposed to be disallowed in X.509 signatures with TLS13
--- unless client advertises explicit support.  Currently this is not enforced by
--- the library, which is useful to test this scenario.  SHA-1 could be replaced
--- by another algorithm.
-
-data OHS = OHS [HashAndSignatureAlgorithm] [HashAndSignatureAlgorithm]
-    deriving (Show)
-
-instance Arbitrary OHS where
-    arbitrary = OHS <$> sublistOf otherHS <*> sublistOf otherHS
-      where
-        otherHS = [(HashIntrinsic, SignatureEd25519)]
-
-handshake_cert_fallback_hs :: OHS -> IO ()
-handshake_cert_fallback_hs (OHS clientHS serverHS) = do
-    tls13 <- generate arbitrary
-    let versions = if tls13 then [TLS13] else [TLS12]
-        ciphers =
-            [ cipher_ECDHE_RSA_WITH_AES_128_GCM_SHA256
-            , cipher_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
-            , cipher13_AES_128_GCM_SHA256
-            ]
-        commonHS =
-            [ (HashSHA256, SignatureRSA)
-            , (HashIntrinsic, SignatureRSApssRSAeSHA256)
-            ]
-    chainRef <- newIORef Nothing
-    (clientParam, serverParam) <-
-        generate $
-            arbitraryPairParamsWithVersionsAndCiphers
-                (versions, versions)
-                (ciphers, ciphers)
-    let clientParam' =
-            clientParam
-                { clientSupported =
-                    (clientSupported clientParam)
-                        { supportedHashSignatures = commonHS ++ clientHS
-                        }
-                , clientHooks =
-                    (clientHooks clientParam)
-                        { onServerCertificate = \_ _ _ chain ->
-                            writeIORef chainRef (Just chain) >> return []
-                        }
-                }
-        serverParam' =
-            serverParam
-                { serverSupported =
-                    (serverSupported serverParam)
-                        { supportedHashSignatures = commonHS ++ serverHS
-                        }
-                }
-        eddsaDisallowed =
-            (HashIntrinsic, SignatureEd25519) `notElem` clientHS
-                || (HashIntrinsic, SignatureEd25519) `notElem` serverHS
-    runTLSSimple (clientParam', serverParam')
-    serverChain <- readIORef chainRef
-    isLeafRSA serverChain `shouldBe` eddsaDisallowed
-
---------------------------------------------------------------
-
-handshake_server_key_usage :: [ExtKeyUsageFlag] -> IO ()
-handshake_server_key_usage usageFlags = do
-    tls13 <- generate arbitrary
-    let versions = if tls13 then [TLS13] else [TLS12]
-        ciphers = ciphersuite_all
-    (clientParam, serverParam) <-
-        generate $
-            arbitraryPairParamsWithVersionsAndCiphers
-                (versions, versions)
-                (ciphers, ciphers)
-    cred <- generate $ arbitraryRSACredentialWithUsage usageFlags
-    let serverParam' =
-            serverParam
-                { serverShared =
-                    (serverShared serverParam)
-                        { sharedCredentials = Credentials [cred]
-                        }
-                }
-        shouldSucceed = KeyUsage_digitalSignature `elem` usageFlags
-    if shouldSucceed
-        then runTLSSimple (clientParam, serverParam')
-        else runTLSFailure (clientParam, serverParam') handshake handshake
-
-handshake_client_key_usage :: [ExtKeyUsageFlag] -> IO ()
-handshake_client_key_usage usageFlags = do
-    (clientParam, serverParam) <- generate arbitrary
-    cred <- generate $ arbitraryRSACredentialWithUsage usageFlags
-    let clientParam' =
-            clientParam
-                { clientHooks =
-                    (clientHooks clientParam)
-                        { onCertificateRequest = \_ -> return $ Just cred
-                        }
-                }
-        serverParam' =
-            serverParam
-                { serverWantClientCert = True
-                , serverHooks =
-                    (serverHooks serverParam)
-                        { onClientCertificate = \_ -> return CertificateUsageAccept
-                        }
-                }
-        shouldSucceed = KeyUsage_digitalSignature `elem` usageFlags
-    if shouldSucceed
-        then runTLSSimple (clientParam', serverParam')
-        else runTLSFailure (clientParam', serverParam') handshake handshake
-
---------------------------------------------------------------
-
-handshake_client_auth :: (ClientParams, ServerParams) -> IO ()
-handshake_client_auth (clientParam, serverParam) = do
-    let clientVersions = supportedVersions $ clientSupported clientParam
-        serverVersions = supportedVersions $ serverSupported serverParam
-        version = maximum (clientVersions `intersect` serverVersions)
-    cred <- generate (arbitraryClientCredential version)
-    let clientParam' =
-            clientParam
-                { clientHooks =
-                    (clientHooks clientParam)
-                        { onCertificateRequest = \_ -> return $ Just cred
-                        }
-                }
-        serverParam' =
-            serverParam
-                { serverWantClientCert = True
-                , serverHooks =
-                    (serverHooks serverParam)
-                        { onClientCertificate = validateChain cred
-                        }
-                }
-    runTLSSimple (clientParam', serverParam')
-  where
-    validateChain cred chain
-        | chain == fst cred = return CertificateUsageAccept
-        | otherwise = return (CertificateUsageReject CertificateRejectUnknownCA)
-
-handshake_client_auth_fail :: (ClientParams, ServerParams) -> IO ()
-handshake_client_auth_fail (clientParam, serverParam) = do
-    let clientVersions = supportedVersions $ clientSupported clientParam
-        serverVersions = supportedVersions $ serverSupported serverParam
-        version = maximum (clientVersions `intersect` serverVersions)
-    cred <- generate (arbitraryClientCredential version)
-    let clientParam' =
-            clientParam
-                { clientHooks =
-                    (clientHooks clientParam)
-                        { onCertificateRequest = \_ -> return $ Just cred
-                        }
-                }
-        serverParam' =
-            serverParam
-                { serverWantClientCert = True
-                , serverHooks =
-                    (serverHooks serverParam)
-                        { onClientCertificate = validateChain cred
-                        }
-                }
-    runTLSFailure (clientParam', serverParam') handshake handshake
-  where
-    validateChain _ _ = return (CertificateUsageReject CertificateRejectUnknownCA)
-
---------------------------------------------------------------
-
-handshake_ems :: (EMSMode, EMSMode) -> IO ()
-handshake_ems (cems, sems) = do
-    params <- generate arbitrary
-    let params' = setEMSMode (cems, sems) params
-        version = getConnectVersion params'
-        emsVersion = version >= TLS10 && version <= TLS12
-        use = cems /= NoEMS && sems /= NoEMS
-        require = cems == RequireEMS || sems == RequireEMS
-        p info = infoExtendedMainSecret info == (emsVersion && use)
-    if emsVersion && require && not use
-        then runTLSFailure params' handshake handshake
-        else runTLSPredicate params' (maybe False p)
-
-newtype CompatEMS = CompatEMS (EMSMode, EMSMode) deriving (Show)
-
-instance Arbitrary CompatEMS where
-    arbitrary = CompatEMS <$> (arbitrary `suchThat` compatible)
-      where
-        compatible (NoEMS, RequireEMS) = False
-        compatible (RequireEMS, NoEMS) = False
-        compatible _ = True
-
-handshake_resumption_ems :: (CompatEMS, CompatEMS) -> IO ()
-handshake_resumption_ems (CompatEMS ems, CompatEMS ems2) = do
-    sessionRefs <- twoSessionRefs
-    let sessionManagers = twoSessionManagers sessionRefs
-
-    plainParams <- generate arbitrary
-    let params =
-            setEMSMode ems $
-                setPairParamsSessionManagers sessionManagers plainParams
-
-    runTLSSimple params
-
-    -- and resume
-    sessionParams <- readClientSessionRef sessionRefs
-    expectJust "session param should be Just" sessionParams
-    let params2 =
-            setEMSMode ems2 $
-                setPairParamsSessionResuming (fromJust sessionParams) params
-
-    let version = getConnectVersion params2
-        emsVersion = version >= TLS10 && version <= TLS12
-
-    if emsVersion && use ems && not (use ems2)
-        then runTLSFailure params2 handshake handshake
-        else do
-            runTLSSimple params2
-            mSessionParams2 <- readClientSessionRef sessionRefs
-            let sameSession = sessionParams == mSessionParams2
-                sameUse = use ems == use ems2
-            when emsVersion (sameSession `shouldBe` sameUse)
-  where
-    use (NoEMS, _) = False
-    use (_, NoEMS) = False
-    use _ = True
-
---------------------------------------------------------------
-
-handshake_alpn :: (ClientParams, ServerParams) -> IO ()
-handshake_alpn (clientParam, serverParam) = do
-    let clientParam' =
-            clientParam
-                { clientHooks =
-                    (clientHooks clientParam)
-                        { onSuggestALPN = return $ Just ["h2", "http/1.1"]
-                        }
-                }
-        serverParam' =
-            serverParam
-                { serverHooks =
-                    (serverHooks serverParam)
-                        { onALPNClientSuggest = Just alpn
-                        }
-                }
-        params' = (clientParam', serverParam')
-    runTLSSuccess params' hsClient hsServer
-  where
-    hsClient ctx = do
-        handshake ctx
-        proto <- getNegotiatedProtocol ctx
-        proto `shouldBe` Just "h2"
-    hsServer ctx = do
-        handshake ctx
-        proto <- getNegotiatedProtocol ctx
-        proto `shouldBe` Just "h2"
-    alpn xs
-        | "h2" `elem` xs = return "h2"
-        | otherwise = return "http/1.1"
-
-handshake_sni :: (ClientParams, ServerParams) -> IO ()
-handshake_sni (clientParam, serverParam) = do
-    ref <- newIORef Nothing
-    let clientParam' =
-            clientParam
-                { clientServerIdentification = (serverName, "")
-                }
-        serverParam' =
-            serverParam
-                { serverHooks =
-                    (serverHooks serverParam)
-                        { onServerNameIndication = onSNI ref
-                        }
-                }
-        params' = (clientParam', serverParam')
-    runTLSSuccess params' hsClient hsServer
-    receivedName <- readIORef ref
-    receivedName `shouldBe` Just (Just serverName)
-  where
-    hsClient ctx = do
-        handshake ctx
-        msni <- getClientSNI ctx
-        expectMaybe "C: SNI should be Just" serverName msni
-    hsServer ctx = do
-        handshake ctx
-        msni <- getClientSNI ctx
-        expectMaybe "S: SNI should be Just" serverName msni
-    onSNI ref name = do
-        mx <- readIORef ref
-        mx `shouldBe` Nothing
-        writeIORef ref (Just name)
-        return (Credentials [])
-    serverName = "haskell.org"
-
---------------------------------------------------------------
-
-newtype CSP12 = CSP12 (ClientParams, ServerParams) deriving (Show)
-
-instance Arbitrary CSP12 where
-    arbitrary = CSP12 <$> arbitraryPairParams12
-
-handshake12_renegotiation :: CSP12 -> IO ()
-handshake12_renegotiation (CSP12 (cparams, sparams)) = do
-    renegDisabled <- generate arbitrary
-    let sparams' =
-            sparams
-                { serverSupported =
-                    (serverSupported sparams)
-                        { supportedClientInitiatedRenegotiation = not renegDisabled
-                        }
-                }
-    if renegDisabled
-        then runTLSFailure (cparams, sparams') hsClient hsServer
-        else runTLSSimple (cparams, sparams')
-  where
-    hsClient ctx = handshake ctx >> handshake ctx
-    -- recvData receives the alert from the second handshake
-    hsServer ctx = handshake ctx >> void (recvData ctx)
-
-handshake12_session_resumption :: CSP12 -> IO ()
-handshake12_session_resumption (CSP12 plainParams) = do
-    sessionRefs <- twoSessionRefs
-    let sessionManagers = twoSessionManagers sessionRefs
-
-    let params = setPairParamsSessionManagers sessionManagers plainParams
-
-    runTLSSimple params
-
-    -- and resume
-    sessionParams <- readClientSessionRef sessionRefs
-    expectJust "session param should be Just" sessionParams
-    let params2 = setPairParamsSessionResuming (fromJust sessionParams) params
-
-    runTLSPredicate params2 (maybe False infoTLS12Resumption)
-
-handshake12_session_ticket :: CSP12 -> IO ()
-handshake12_session_ticket (CSP12 plainParams) = do
-    sessionRefs <- twoSessionRefs
-    let sessionManagers0 = twoSessionManagers sessionRefs
-        sessionManagers = (fst sessionManagers0, oneSessionTicket)
-
-    let params = setPairParamsSessionManagers sessionManagers plainParams
-
-    runTLSSimple params
-
-    -- and resume
-    sessionParams <- readClientSessionRef sessionRefs
-    expectJust "session param should be Just" sessionParams
-    let params2 = setPairParamsSessionResuming (fromJust sessionParams) params
-
-    runTLSPredicate params2 (maybe False infoTLS12Resumption)
-
---------------------------------------------------------------
-
-handshake13_full :: CSP13 -> IO ()
-handshake13_full (CSP13 (cli, srv)) = do
-    let cliSupported =
-            defaultSupported
-                { supportedCiphers = [cipher13_AES_128_GCM_SHA256]
-                , supportedGroups = [X25519]
-                }
-        svrSupported =
-            defaultSupported
-                { supportedCiphers = [cipher13_AES_128_GCM_SHA256]
-                , supportedGroups = [X25519]
-                , supportedGroupsTLS13 = [[X25519]]
-                }
-        params =
-            ( cli{clientSupported = cliSupported}
-            , srv{serverSupported = svrSupported}
-            )
-    runTLSSimple13 params FullHandshake
-
-handshake13_hrr :: CSP13 -> IO ()
-handshake13_hrr (CSP13 (cli, srv)) = do
-    let cliSupported =
-            defaultSupported
-                { supportedCiphers = [cipher13_AES_128_GCM_SHA256]
-                , supportedGroups = [P256, X25519]
-                }
-        svrSupported =
-            defaultSupported
-                { supportedCiphers = [cipher13_AES_128_GCM_SHA256]
-                , supportedGroups = [X25519]
-                , supportedGroupsTLS13 = [[X25519]]
-                }
-        params =
-            ( cli{clientSupported = cliSupported}
-            , srv{serverSupported = svrSupported}
-            )
-    runTLSSimple13 params HelloRetryRequest
-
-handshake13_psk :: CSP13 -> IO ()
-handshake13_psk (CSP13 (cli, srv)) = do
-    let cliSupported =
-            defaultSupported
-                { supportedCiphers = [cipher13_AES_128_GCM_SHA256]
-                , supportedGroups = [P256, X25519]
-                }
-        svrSupported =
-            defaultSupported
-                { supportedCiphers = [cipher13_AES_128_GCM_SHA256]
-                , supportedGroups = [X25519]
-                , supportedGroupsTLS13 = [[X25519]]
-                }
-        params0 =
-            ( cli{clientSupported = cliSupported}
-            , srv{serverSupported = svrSupported}
-            )
-
-    sessionRefs <- twoSessionRefs
-    let sessionManagers = twoSessionManagers sessionRefs
-
-    let params = setPairParamsSessionManagers sessionManagers params0
-
-    runTLSSimple13 params HelloRetryRequest
-
-    -- and resume
-    sessionParams <- readClientSessionRef sessionRefs
-    expectJust "session param should be Just" sessionParams
-    let params2 = setPairParamsSessionResuming (fromJust sessionParams) params
-
-    runTLSSimple13 params2 PreSharedKey
-
-handshake13_psk_ticket :: CSP13 -> IO ()
-handshake13_psk_ticket (CSP13 (cli, srv)) = do
-    let cliSupported =
-            defaultSupported
-                { supportedCiphers = [cipher13_AES_128_GCM_SHA256]
-                , supportedGroups = [P256, X25519]
-                }
-        svrSupported =
-            defaultSupported
-                { supportedCiphers = [cipher13_AES_128_GCM_SHA256]
-                , supportedGroups = [X25519]
-                , supportedGroupsTLS13 = [[X25519]]
-                }
-        params0 =
-            ( cli{clientSupported = cliSupported}
-            , srv{serverSupported = svrSupported}
-            )
-
-    sessionRefs <- twoSessionRefs
-    let sessionManagers0 = twoSessionManagers sessionRefs
-        sessionManagers = (fst sessionManagers0, oneSessionTicket)
-
-    let params = setPairParamsSessionManagers sessionManagers params0
-
-    runTLSSimple13 params HelloRetryRequest
-
-    -- and resume
-    sessionParams <- readClientSessionRef sessionRefs
-    expectJust "session param should be Just" sessionParams
-    let params2 = setPairParamsSessionResuming (fromJust sessionParams) params
-
-    runTLSSimple13 params2 PreSharedKey
-
-handshake13_psk_fallback :: CSP13 -> IO ()
-handshake13_psk_fallback (CSP13 (cli, srv)) = do
-    let cliSupported =
-            defaultSupported
-                { supportedCiphers =
-                    [ cipher13_AES_128_GCM_SHA256
-                    , cipher13_AES_128_CCM_SHA256
-                    ]
-                , supportedGroups = [P256, X25519]
-                }
-        svrSupported =
-            defaultSupported
-                { supportedCiphers = [cipher13_AES_128_GCM_SHA256]
-                , supportedGroups = [X25519]
-                , supportedGroupsTLS13 = [[X25519]]
-                }
-        params0 =
-            ( cli{clientSupported = cliSupported}
-            , srv{serverSupported = svrSupported}
-            )
-
-    sessionRefs <- twoSessionRefs
-    let sessionManagers = twoSessionManagers sessionRefs
-
-    let params = setPairParamsSessionManagers sessionManagers params0
-
-    runTLSSimple13 params HelloRetryRequest
-
-    -- resumption fails because GCM cipher is not supported anymore, full
-    -- handshake is not possible because X25519 has been removed, so we are
-    -- back with P256 after hello retry
-    sessionParams <- readClientSessionRef sessionRefs
-    expectJust "session param should be Just" sessionParams
-    let (cli2, srv2) = setPairParamsSessionResuming (fromJust sessionParams) params
-        srv2' =
-            srv2{serverSupported = svrSupported'}
-        svrSupported' =
-            defaultSupported
-                { supportedCiphers = [cipher13_AES_128_CCM_SHA256]
-                , supportedGroups = [P256]
-                , supportedGroupsTLS13 = [[P256]]
-                }
-
-    runTLSSimple13 (cli2, srv2') HelloRetryRequest
-
-handshake13_0rtt :: CSP13 -> IO ()
-handshake13_0rtt (CSP13 (cli, srv)) = do
-    let cliSupported =
-            defaultSupported
-                { supportedCiphers = [cipher13_AES_128_GCM_SHA256]
-                , supportedGroups = [P256, X25519]
-                }
-        svrSupported =
-            defaultSupported
-                { supportedCiphers = [cipher13_AES_128_GCM_SHA256]
-                , supportedGroups = [X25519]
-                , supportedGroupsTLS13 = [[X25519]]
-                }
-        cliHooks =
-            defaultClientHooks
-                { onSuggestALPN = return $ Just ["h2"]
-                }
-        svrHooks =
-            defaultServerHooks
-                { onALPNClientSuggest = Just (return . unsafeHead)
-                }
-        params0 =
-            ( cli
-                { clientSupported = cliSupported
-                , clientHooks = cliHooks
-                }
-            , srv
-                { serverSupported = svrSupported
-                , serverHooks = svrHooks
-                , serverEarlyDataSize = 2048
-                }
-            )
-
-    sessionRefs <- twoSessionRefs
-    let sessionManagers = twoSessionManagers sessionRefs
-
-    let params = setPairParamsSessionManagers sessionManagers params0
-
-    runTLSSimple13 params HelloRetryRequest
-    runTLS0rtt params sessionRefs
-    runTLS0rtt params sessionRefs
-  where
-    runTLS0rtt params sessionRefs = do
-        -- and resume
-        sessionParams <- readClientSessionRef sessionRefs
-        expectJust "session param should be Just" sessionParams
-        clearClientSessionRef sessionRefs
-        earlyData <- B.pack <$> generate (someWords8 256)
-        let (pc, ps) = setPairParamsSessionResuming (fromJust sessionParams) params
-            params2 = (pc{clientUseEarlyData = True}, ps)
-
-        runTLS0RTT params2 RTT0 earlyData
-
-handshake13_0rtt_fallback :: CSP13 -> IO ()
-handshake13_0rtt_fallback (CSP13 (cli, srv)) = do
-    group0 <- generate $ elements [P256, X25519]
-    let cliSupported =
-            defaultSupported
-                { supportedCiphers = [cipher13_AES_128_GCM_SHA256]
-                , supportedGroups = [P256, X25519]
-                }
-        svrSupported =
-            defaultSupported
-                { supportedCiphers = [cipher13_AES_128_GCM_SHA256]
-                , supportedGroups = [group0]
-                , supportedGroupsTLS13 = [[group0]]
-                }
-        params =
-            ( cli{clientSupported = cliSupported}
-            , srv
-                { serverSupported = svrSupported
-                , serverEarlyDataSize = 1024
-                }
-            )
-
-    sessionRefs <- twoSessionRefs
-    let sessionManagers = twoSessionManagers sessionRefs
-
-    let params0 = setPairParamsSessionManagers sessionManagers params
-
-    let mode = if group0 == P256 then FullHandshake else HelloRetryRequest
-    runTLSSimple13 params0 mode
-
-    -- and resume
-    mSessionParams <- readClientSessionRef sessionRefs
-    case mSessionParams of
-        Nothing -> expectationFailure "session params: Just is expected"
-        Just sessionParams -> do
-            earlyData <- B.pack <$> generate (someWords8 256)
-            group1 <- generate $ elements [P256, X25519]
-            let (pc, ps) = setPairParamsSessionResuming sessionParams params0
-                svrSupported1 =
-                    defaultSupported
-                        { supportedCiphers = [cipher13_AES_128_GCM_SHA256]
-                        , supportedGroups = [group1]
-                        , supportedGroupsTLS13 = [[group1]]
-                        }
-                params1 =
-                    ( pc{clientUseEarlyData = True}
-                    , ps
-                        { serverEarlyDataSize = 0
-                        , serverSupported = svrSupported1
-                        }
-                    )
-            -- C: [P256, X25519]
-            -- S: [group0]
-            -- C: [P256, X25519]
-            -- S: [group1]
-            if group0 == group1
-                -- 0-RTT is not allowed, so fallback to PreSharedKey
-                then runTLS0RTT params1 PreSharedKey earlyData
-                -- HRR but not allowed for 0-RTT
-                else runTLSFailure params1 (tlsClient earlyData) tlsServer
-  where
-    tlsClient earlyData ctx = do
-        handshake ctx
-        sendData ctx $ L.fromStrict earlyData
-        _ <- recvData ctx
-        bye ctx
-    tlsServer ctx = do
-        handshake ctx
-        _ <- recvData ctx
-        bye ctx
-
-handshake13_ee_groups :: CSP13 -> IO ()
-handshake13_ee_groups (CSP13 (cli, srv)) = do
-    let -- The client prefers P256
-        cliSupported = (clientSupported cli){supportedGroups = [P256, X25519]}
-        -- The server prefers X25519
-        svrSupported =
-            (serverSupported srv)
-                { supportedGroups = [X25519, P256]
-                , supportedGroupsTLS13 = [[X25519, P256]]
-                }
-        params =
-            ( cli{clientSupported = cliSupported}
-            , srv{serverSupported = svrSupported}
-            )
-    (_, serverMessages) <- runTLSCapture13 params
-    -- The server should tell X25519 in supported_groups in EE to clinet
-    let isSupportedGroups (ExtensionRaw eid _) = eid == EID_SupportedGroups
-        eeMessagesHaveExt =
-            [ any isSupportedGroups exts
-            | EncryptedExtensions13 exts <- serverMessages
-            ]
-    eeMessagesHaveExt `shouldBe` [True]
-
-handshake13_ec :: CSP13 -> IO ()
-handshake13_ec (CSP13 (cli, srv)) = do
-    EC cgrps <- generate arbitrary
-    EC sgrps <- generate arbitrary
-    let cliSupported = (clientSupported cli){supportedGroups = cgrps}
-        svrSupported =
-            (serverSupported srv)
-                { supportedGroups = sgrps
-                , supportedGroupsTLS13 = [sgrps]
-                }
-        params =
-            ( cli{clientSupported = cliSupported}
-            , srv{serverSupported = svrSupported}
-            )
-    runTLSSimple13 params FullHandshake
-
-handshake13_ffdhe :: CSP13 -> IO ()
-handshake13_ffdhe (CSP13 (cli, srv)) = do
-    FFDHE cgrps <- generate arbitrary
-    FFDHE sgrps <- generate arbitrary
-    let cliSupported = (clientSupported cli){supportedGroups = cgrps}
-        svrSupported =
-            (serverSupported srv)
-                { supportedGroups = sgrps
-                , supportedGroupsTLS13 = [sgrps]
-                }
-        params =
-            ( cli{clientSupported = cliSupported}
-            , srv{serverSupported = svrSupported}
-            )
-    runTLSSimple13 params FullHandshake
-
-post_handshake_auth :: CSP13 -> IO ()
-post_handshake_auth (CSP13 (clientParam, serverParam)) = do
-    cred <- generate (arbitraryClientCredential TLS13)
-    let clientParam' =
-            clientParam
-                { clientHooks =
-                    (clientHooks clientParam)
-                        { onCertificateRequest = \_ -> return $ Just cred
-                        }
-                }
-        serverParam' =
-            serverParam
-                { serverHooks =
-                    (serverHooks serverParam)
-                        { onClientCertificate = validateChain cred
-                        }
-                }
-    if isCredentialDSA cred
-        then runTLSFailure (clientParam', serverParam') hsClient hsServer
-        else runTLSSuccess (clientParam', serverParam') hsClient hsServer
-  where
-    validateChain cred chain
-        | chain == fst cred = return CertificateUsageAccept
-        | otherwise = return (CertificateUsageReject CertificateRejectUnknownCA)
-    hsClient ctx = do
-        handshake ctx
-        sendData ctx "request 1"
-        recvDataAssert ctx "response 1"
-        sendData ctx "request 2"
-        recvDataAssert ctx "response 2"
-    hsServer ctx = do
-        handshake ctx
-        recvDataAssert ctx "request 1"
-        _ <- requestCertificate ctx -- single request
-        sendData ctx "response 1"
-        recvDataAssert ctx "request 2"
-        _ <- requestCertificate ctx
-        _ <- requestCertificate ctx -- two simultaneously
-        sendData ctx "response 2"
+import Control.Concurrent (threadDelay)
+import Control.Concurrent.Async (concurrently_)
+import qualified Control.Exception as E
+import Control.Monad
+import Crypto.Cipher.AES (AES128)
+import Crypto.Cipher.Types (AEADMode (..), AuthTag (..), aeadInit, aeadSimpleEncrypt, cipherInit)
+import Crypto.Error (throwCryptoError)
+import Data.Bits (shiftR, xor)
+import qualified Data.ByteArray as BA
+import qualified Data.ByteString as B
+import qualified Data.ByteString.Lazy as L
+import Data.IORef
+import Data.List
+import Data.Maybe
+import Data.Word (Word16, Word64, Word8)
+import Data.X509 (ExtKeyUsageFlag (..), ExtKeyUsagePurpose (..))
+import Network.TLS
+import Network.TLS.Extra.Cipher
+import Network.TLS.Extra.CipherCBC
+import Network.TLS.Internal
+import Network.TLS.QUIC (hkdfExpandLabel)
+import Test.Hspec
+import Test.Hspec.QuickCheck
+import System.Timeout (timeout)
+import Test.QuickCheck
+
+import API
+import Arbitrary
+import Certificate (arbitraryRSACredentialWithPurpose)
+import PipeChan
+import Run
+import Session
+
+spec :: Spec
+spec = do
+    describe "pipe" $ do
+        it "can setup a channel" pipe_work
+    describe "channel binding" $ do
+        prop "is unavailable before the handshake" binding_before_handshake
+    describe "handshake" $ do
+        prop "can run TLS 1.2" handshake_simple
+        prop "can run TLS 1.3" handshake13_simple
+        prop "can update key for TLS 1.3" handshake_update_key
+        it
+            "rejects more than 32 consecutive TLS 1.3 KeyUpdates"
+            handshake_key_update_flood
+        it
+            "can disable the consecutive TLS 1.3 KeyUpdate limit"
+            handshake_key_update_unlimited
+        it
+            "does not disable TLS 1.3 KeyUpdates with non-positive limits"
+            handshake_key_update_non_positive
+        prop "can prevent downgrade attack" handshake13_downgrade
+        it "negotiates TLS 1.2 with a ClientHello naming a higher version" $
+            handshake_high_legacy_version TLS12 (Version 0x0309)
+        it "ignores legacy_version when supported_versions is present" $
+            handshake_high_legacy_version TLS13 TLS13
+        it "rejects ec_point_formats without uncompressed" $
+            handshake12_ec_point_formats
+                (B.pack [1, 1])
+                rejectedAsIllegalParameter
+        it "rejects an empty ec_point_formats" $
+            handshake12_ec_point_formats (B.pack [0]) rejectedAsDecodeError
+        prop "can negotiate hash and signature" handshake_hashsignatures
+        prop "can negotiate cipher suite" handshake_ciphersuites
+        it "rejects a cipher outside the server callback candidates" $
+            handshake_rejects_server_cipher_callback_escape
+        it "rejects a TLS 1.2-only cipher selected for TLS 1.3" $
+            handshake_rejects_legacy_cipher_in_tls13
+        prop "can negotiate group" handshake_groups
+        prop "can negotiate elliptic curve" handshake_ec
+        prop "can fallback for certificate with cipher" handshake_cert_fallback_cipher
+        prop
+            "can fallback for certificate with hash and signature"
+            handshake_cert_fallback_hs
+        prop "can handle server key usage" handshake_server_key_usage
+        it "accepts a TLS 1.2 server certificate permitting server auth" $
+            handshake_server_key_purpose TLS12 KeyUsagePurpose_ServerAuth True
+        it "accepts a TLS 1.3 server certificate permitting server auth" $
+            handshake_server_key_purpose TLS13 KeyUsagePurpose_ServerAuth True
+        it "rejects a TLS 1.2 server certificate restricted to client auth" $
+            handshake_server_key_purpose TLS12 KeyUsagePurpose_ClientAuth False
+        it "rejects a TLS 1.3 server certificate restricted to client auth" $
+            handshake_server_key_purpose TLS13 KeyUsagePurpose_ClientAuth False
+        prop "can handle client key usage" handshake_client_key_usage
+        prop "can authenticate client" handshake_client_auth
+        it "rejects a TLS 1.3 CertificateVerify algorithm unfit for the key" $
+            handshake13_client_cert_verify_unfit_sigalg
+        it "rejects a TLS 1.2 CertificateVerify algorithm for another key type" $
+            handshake12_client_cert_verify_sigalg
+                (HashSHA256, SignatureECDSA)
+                rejectedAsIllegalParameter
+        it "rejects a TLS 1.2 CertificateVerify that does not fit the RSA key" $
+            handshake12_client_cert_verify_sigalg
+                (HashIntrinsic, SignatureRSApsspssSHA256)
+                rejectedAsDecryptError
+        prop "can receive client authentication failure" handshake_client_auth_fail
+        it "accepts an empty TLS 1.2 client certificate when the hook does" $
+            handshake_client_auth_empty TLS12
+        it "accepts an empty TLS 1.3 client certificate when the hook does" $
+            handshake_client_auth_empty TLS13
+        it "requests only defined certificate types in TLS 1.2" $
+            handshake12_cert_request_types
+        prop "can handle extended main secret" handshake_ems
+        prop "can resume with extended main secret" handshake_resumption_ems
+        prop "can handle ALPN" handshake_alpn
+        it "rejects an unoffered ALPN selection from the server hook" $
+            handshake_alpn_rejects_unoffered_server_selection
+        it "rejects an unoffered ALPN selection received by the client" $
+            handshake_alpn_rejects_unoffered_client_selection
+        prop "can handle SNI" handshake_sni
+        it "sends no SNI for an empty server name" handshake_sni_empty
+        it "rejects multiple host_names in SNI" $
+            handshake_sni_illegal ["example.com", "example.org"]
+        it "rejects a host_name with a control character in SNI" $
+            handshake_sni_illegal ["example\0.com"]
+        it "rejects a non-ASCII host_name in SNI" $
+            handshake_sni_illegal ["ex\xc4\x85mple.com"]
+        prop "can handshake with TLS 1.2 CBC" handshake_cbc
+        prop "can re-negotiate with TLS 1.2" handshake12_renegotiation
+        it "rejects SCSV in a secure renegotiation" $
+            handshake12_renegotiation_tampered $ \ch ->
+                ch{chCiphers = chCiphers ch ++ [CipherId 0xff]}
+        it "rejects a secure renegotiation without renegotiation_info" $
+            handshake12_renegotiation_tampered $ \ch ->
+                ch
+                    { chExtensions =
+                        filter
+                            (\(ExtensionRaw eid _) -> eid /= EID_SecureRenegotiation)
+                            (chExtensions ch)
+                    }
+        prop "can resume session with TLS 1.2" handshake12_session_resumption
+        prop
+            "rejects resuming a TLS 1.2 session without its cipher"
+            handshake12_session_resumption_cipher_missing
+        prop "can resume session ticket with TLS 1.2" handshake12_session_ticket
+        it "sends no session ticket to a TLS 1.2 client that did not ask" $
+            handshake12_session_ticket_unoffered
+        prop "can handshake with TLS 1.3 Full" handshake13_full
+        prop "can handshake with TLS 1.3 HRR" handshake13_hrr
+        prop "can handshake with TLS 1.3 PSK" handshake13_psk
+        it "does not resume a TLS 1.2 session with a TLS 1.3 PSK" $
+            handshake13_psk_tls12_session
+        prop "can handshake with TLS 1.3 PSK ticket" handshake13_psk_ticket
+        prop "can handshake with TLS 1.3 PSK -> HRR" handshake13_psk_fallback
+        prop "can handshake with TLS 1.3 0RTT" handshake13_0rtt
+        it "rejects TLS 1.3 early data when ALPN changes" $
+            handshake13_0rtt_alpn
+        prop "can handshake with TLS 1.3 0RTT -> PSK" handshake13_0rtt_fallback
+        prop "can handshake with TLS 1.3 EE" handshake13_ee_groups
+        prop "can handshake with TLS 1.3 EC groups" handshake13_ec
+        prop "can handshake with TLS 1.3 FFDHE groups" handshake13_ffdhe
+        it "rejects an X25519MLKEM768 key share with a zero X25519 part" $
+            handshake13_x25519mlkem768_zero_x25519
+        prop "can handshake with TLS 1.3 Post-handshake auth" post_handshake_auth
+        it
+            "keeps record alignment when a slow record follows client auth"
+            handshake13_client_auth_slow_record
+        it "rejects a too short TLS 1.2 AEAD record with bad_record_mac" $
+            short_record_bad_record_mac TLS12 cipher_ECDHE_RSA_WITH_AES_128_GCM_SHA256
+        it "rejects a too short TLS 1.2 CBC record with bad_record_mac" $
+            short_record_bad_record_mac TLS12 cipher_ECDHE_RSA_AES128CBC_SHA256
+        it "rejects a too short TLS 1.3 record with bad_record_mac" $
+            short_record_bad_record_mac TLS13 cipher13_AES_128_GCM_SHA256
+        it "rejects a ServerHello as the first client message" $
+            server_first_message_unexpected 2
+        it "rejects a Finished as the first client message" $
+            server_first_message_unexpected 20
+        it "rejects an unknown handshake type as the first client message" $
+            server_first_message_unexpected 254
+        it "rejects a ChangeCipherSpec before the first ClientHello" $
+            server_ccs_interleaved 0
+        it "rejects a ChangeCipherSpec inside a fragmented ClientHello" $
+            server_ccs_interleaved 2
+        it "rejects an SSLv2-style record header at once" $
+            server_first_record_type_unexpected 0x80 0x3fff
+        it "rejects an unknown record type" $
+            server_first_record_type_unexpected 24 0
+        it "rejects application data inside a TLS 1.3 handshake message" $
+            handshake13_interleaved_app_data
+        it "rejects a two-byte TLS 1.2 ChangeCipherSpec" $
+            malformed_ccs_unexpected
+                TLS12
+                cipher_ECDHE_RSA_WITH_AES_128_GCM_SHA256
+                [P256]
+                [P256]
+        it "rejects a two-byte TLS 1.3 ChangeCipherSpec" $
+            malformed_ccs_unexpected TLS13 cipher13_AES_128_GCM_SHA256 [X25519] [X25519]
+        it "rejects a two-byte TLS 1.3 ChangeCipherSpec after HelloRetryRequest" $
+            malformed_ccs_unexpected
+                TLS13
+                cipher13_AES_128_GCM_SHA256
+                [P256, X25519]
+                [X25519]
+
+--------------------------------------------------------------
+
+-- | Both channel bindings already answer with 'Maybe', and a caller may
+-- reasonably ask for one on a context whose handshake has not run -- or has
+-- failed.  The answer is that there is no binding, not a crash.
+binding_before_handshake :: (ClientParams, ServerParams) -> IO ()
+binding_before_handshake params = withPairContext params $ \(cCtx, sCtx) ->
+    forM_ [cCtx, sCtx] $ \ctx -> do
+        getTLSUnique ctx `shouldReturn` Nothing
+        getTLSExporter ctx `shouldReturn` Nothing
+
+pipe_work :: IO ()
+pipe_work = do
+    pipe <- newPipe
+    _ <- runPipe pipe
+
+    let bSize = 16
+    n <- generate (choose (1, 32))
+
+    let d1 = B.replicate (bSize * n) 40
+    let d2 = B.replicate (bSize * n) 45
+
+    d1' <- writePipeC pipe d1 >> readPipeS pipe (B.length d1)
+    d1' `shouldBe` d1
+
+    d2' <- writePipeS pipe d2 >> readPipeC pipe (B.length d2)
+    d2' `shouldBe` d2
+
+--------------------------------------------------------------
+
+handshake_simple :: (ClientParams, ServerParams) -> IO ()
+handshake_simple = runTLSSimple
+
+--------------------------------------------------------------
+
+newtype CSP13 = CSP13 (ClientParams, ServerParams) deriving (Show)
+
+instance Arbitrary CSP13 where
+    arbitrary = CSP13 <$> arbitraryPairParams13
+
+handshake13_simple :: CSP13 -> IO ()
+handshake13_simple (CSP13 params) = runTLSSimple13 params hs
+  where
+    cgrps = supportedGroups $ clientSupported $ fst params
+    sgrps = supportedGroups $ serverSupported $ snd params
+    hs = if unsafeHead cgrps `elem` sgrps then FullHandshake else HelloRetryRequest
+
+handshake_rejects_server_cipher_callback_escape :: IO ()
+handshake_rejects_server_cipher_callback_escape = do
+    (clientParam, serverParam) <- generate arbitraryPairParams13
+    let params = cipherSelectionParams clientParam serverParam selectLegacy
+    withPairContextWith (id, id) params $ \(cctx, sctx) ->
+        concurrently_
+            (handshake sctx `shouldThrow` serverRejectedCipherEscape)
+            (handshake cctx `shouldThrow` anyTLSException)
+  where
+    selectLegacy _ _ = cipher_ECDHE_RSA_AES128CBC_SHA256
+
+handshake_rejects_legacy_cipher_in_tls13 :: IO ()
+handshake_rejects_legacy_cipher_in_tls13 = do
+    (clientParam, serverParam) <- generate arbitraryPairParams13
+    let params = cipherSelectionParams clientParam serverParam defaultSelection
+    withPairContextWith (id, id) params $ \(cctx, sctx) -> do
+        contextHookSetHandshakeRecv cctx tamperCipher
+        concurrently_
+            (handshake sctx `shouldThrow` anyTLSException)
+            (handshake cctx `shouldThrow` clientRejectedLegacyCipher)
+  where
+    defaultSelection _ = unsafeHead
+    tamperCipher (ServerHello sh) =
+        pure $
+            ServerHello
+                sh
+                    { shCipher =
+                        CipherId $ cipherID cipher_ECDHE_RSA_AES128CBC_SHA256
+                    }
+    tamperCipher hs = pure hs
+
+cipherSelectionParams
+    :: ClientParams
+    -> ServerParams
+    -> (Version -> [Cipher] -> Cipher)
+    -> (ClientParams, ServerParams)
+cipherSelectionParams clientParam serverParam select =
+    ( clientParam{clientSupported = supported}
+    , serverParam
+        { serverSupported = supported
+        , serverHooks =
+            (serverHooks serverParam)
+                { onCipherChoosing = select
+                }
+        }
+    )
+  where
+    supported =
+        defaultSupported
+            { supportedVersions = [TLS13]
+            , supportedCiphers =
+                [ cipher13_AES_128_GCM_SHA256
+                , cipher_ECDHE_RSA_AES128CBC_SHA256
+                ]
+            }
+
+serverRejectedCipherEscape :: TLSException -> Bool
+serverRejectedCipherEscape (HandshakeFailed (Error_Protocol msg alert)) =
+    msg == "onCipherChoosing selected a cipher outside the candidate list"
+        && alert == InternalError
+serverRejectedCipherEscape _ = False
+
+clientRejectedLegacyCipher :: TLSException -> Bool
+clientRejectedLegacyCipher (HandshakeFailed (Error_Protocol msg alert)) =
+    msg == "server selected a cipher invalid for the negotiated version"
+        && alert == IllegalParameter
+clientRejectedLegacyCipher _ = False
+
+anyTLSException :: TLSException -> Bool
+anyTLSException = const True
+
+handshake_key_update_flood :: IO ()
+handshake_key_update_flood = do
+    params <- generate arbitraryPairParams13
+    withPairContextWith (id, id) params $ \(cctx, sctx) ->
+        concurrently_
+            ( do
+                handshake sctx
+                recvData sctx `shouldReturn` "after 32 key updates"
+                recvData sctx `shouldThrow` excessiveKeyUpdate
+            )
+            ( do
+                handshake cctx
+                replicateM_ 32 $ void $ updateKey cctx OneWay
+                sendData cctx "after 32 key updates"
+                replicateM_ 33 $ void $ updateKey cctx OneWay
+                sendData cctx "after 33 key updates"
+            )
+  where
+    excessiveKeyUpdate
+        (Terminated _ _ (Error_Misc "too many consecutive KeyUpdate messages")) = True
+    excessiveKeyUpdate _ = False
+
+handshake_key_update_unlimited :: IO ()
+handshake_key_update_unlimited = do
+    (cparams, sparams0) <- generate arbitraryPairParams13
+    let shared0 = serverShared sparams0
+        limits = (sharedLimit shared0){limitKeyUpdate = Nothing}
+        sparams = sparams0{serverShared = shared0{sharedLimit = limits}}
+    withPairContextWith (id, id) (cparams, sparams) $ \(cctx, sctx) ->
+        concurrently_
+            ( do
+                handshake sctx
+                recvData sctx `shouldReturn` "after 33 key updates"
+            )
+            ( do
+                handshake cctx
+                replicateM_ 33 $ void $ updateKey cctx OneWay
+                sendData cctx "after 33 key updates"
+            )
+
+handshake_key_update_non_positive :: IO ()
+handshake_key_update_non_positive =
+    forM_ [0, -1] $ \limit -> do
+        (cparams, sparams0) <- generate arbitraryPairParams13
+        let shared0 = serverShared sparams0
+            limits = (sharedLimit shared0){limitKeyUpdate = Just limit}
+            sparams = sparams0{serverShared = shared0{sharedLimit = limits}}
+        withPairContextWith (id, id) (cparams, sparams) $ \(cctx, sctx) ->
+            concurrently_
+                ( do
+                    handshake sctx
+                    recvData sctx `shouldReturn` "after key update"
+                )
+                ( do
+                    handshake cctx
+                    void $ updateKey cctx OneWay
+                    sendData cctx "after key update"
+                )
+
+--------------------------------------------------------------
+
+handshake_cbc :: IO ()
+handshake_cbc = do
+    clientCiphers <- generate $ cipherGen >>= shuffle
+    serverCiphers <- generate $ cipherGen >>= shuffle
+    clientGroups <- generate $ groupGen >>= shuffle
+    serverGroups <- generate $ groupGen >>= shuffle
+    (clientParam, serverParam) <- generate $
+        arbitraryPairParamsWithVersionsAndCiphers
+            ([TLS12], [TLS12])
+            (clientCiphers, serverCiphers)
+    let clientParam' = clientParam {
+            clientSupported = (clientSupported clientParam)
+                { supportedGroups = clientGroups } }
+        serverParam' = serverParam {
+            serverSupported = (serverSupported serverParam)
+                { supportedGroups = serverGroups } }
+    let ciphers = clientCiphers `intersect` serverCiphers
+        groups = clientGroups `intersect` serverGroups
+     in if compat ciphers groups
+        then runTLSSimple (clientParam', serverParam')
+        else runTLSFailure (clientParam', serverParam') handshake handshake
+  where
+    groupGen :: Gen [Group]
+    groupGen = sublistOf grps `suchThat` (not . null)
+      where
+        grps = [X25519, P256, P384, FFDHE2048, FFDHE3072, FFDHE4096]
+
+    cipherGen :: Gen [Cipher]
+    cipherGen = sublistOf ciphersuite_pfs_sha2_cbc `suchThat` (not . null)
+
+    compat :: [Cipher] -> [Group] -> Bool
+    compat [] _ = False
+    compat _ [] = False
+    compat ciphers groups =
+        let mustdh = all (== CipherKeyExchange_DHE_RSA) $ map cipherKeyExchange ciphers
+            mustec = all (/= CipherKeyExchange_DHE_RSA) $ map cipherKeyExchange ciphers
+            havedh = any (`elem` [FFDHE2048, FFDHE3072, FFDHE4096]) groups
+            haveec = any (`elem` [X25519, P256, P384]) groups
+         in ((not mustdh || havedh) && (not mustec || haveec))
+
+--------------------------------------------------------------
+
+handshake13_downgrade :: (ClientParams, ServerParams) -> IO ()
+handshake13_downgrade (cparam, sparam) = do
+    versionForced <-
+        generate $ elements (supportedVersions $ clientSupported cparam)
+    let debug' = (serverDebug sparam){debugVersionForced = Just versionForced}
+        sparam' = sparam{serverDebug = debug'}
+        params = (cparam, sparam')
+        downgraded =
+            (isVersionEnabled TLS13 params && versionForced < TLS13)
+                || (isVersionEnabled TLS12 params && versionForced < TLS12)
+    if downgraded
+        then runTLSFailure params handshake handshake
+        else runTLSSimple params
+
+handshake_update_key :: (ClientParams, ServerParams) -> IO ()
+handshake_update_key = runTLSSimpleKeyUpdate
+
+--------------------------------------------------------------
+
+handshake_hashsignatures
+    :: ([HashAndSignatureAlgorithm], [HashAndSignatureAlgorithm]) -> IO ()
+handshake_hashsignatures (clientHashSigs, serverHashSigs) = do
+    tls13 <- generate arbitrary
+    let version = if tls13 then TLS13 else TLS12
+        ciphers =
+            [ cipher_ECDHE_RSA_WITH_AES_256_GCM_SHA384
+            , cipher_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
+            , cipher13_AES_128_GCM_SHA256
+            ]
+    (clientParam, serverParam) <-
+        generate $
+            arbitraryPairParamsWithVersionsAndCiphers
+                ([version], [version])
+                (ciphers, ciphers)
+    let clientParam' =
+            clientParam
+                { clientSupported =
+                    (clientSupported clientParam)
+                        { supportedHashSignatures = clientHashSigs
+                        }
+                }
+        serverParam' =
+            serverParam
+                { serverSupported =
+                    (serverSupported serverParam)
+                        { supportedHashSignatures = serverHashSigs
+                        }
+                }
+        commonHashSigs = clientHashSigs `intersect` serverHashSigs
+        shouldFail
+            | tls13 = all incompatibleWithDefaultCurve commonHashSigs
+            | otherwise = null commonHashSigs
+    if shouldFail
+        then runTLSFailure (clientParam', serverParam') handshake handshake
+        else runTLSSimple (clientParam', serverParam')
+  where
+    incompatibleWithDefaultCurve (h, SignatureECDSA) = h /= HashSHA256
+    incompatibleWithDefaultCurve _ = False
+
+handshake_ciphersuites :: ([Cipher], [Cipher]) -> IO ()
+handshake_ciphersuites (clientCiphers, serverCiphers) = do
+    tls13 <- generate arbitrary
+    let version = if tls13 then TLS13 else TLS12
+    (clientParam, serverParam) <-
+        generate $
+            arbitraryPairParamsWithVersionsAndCiphers
+                ([version], [version])
+                (clientCiphers, serverCiphers)
+    let adequate = cipherAllowedForVersion version
+        shouldSucceed = any adequate (clientCiphers `intersect` serverCiphers)
+    if shouldSucceed
+        then runTLSSimple (clientParam, serverParam)
+        else runTLSFailure (clientParam, serverParam) handshake handshake
+
+--------------------------------------------------------------
+
+handshake_groups :: GGP -> IO ()
+handshake_groups (GGP clientGroups serverGroups) = do
+    tls13 <- generate arbitrary
+    let versions = if tls13 then [TLS13] else [TLS12]
+        ciphers = ciphersuite_strong
+    (clientParam, serverParam) <-
+        generate $
+            arbitraryPairParamsWithVersionsAndCiphers
+                (versions, versions)
+                (ciphers, ciphers)
+    denyCustom <- generate arbitrary
+    let groupUsage =
+            if denyCustom
+                then GroupUsageUnsupported "custom group denied"
+                else GroupUsageValid
+        clientParam' =
+            clientParam
+                { clientSupported =
+                    (clientSupported clientParam)
+                        { supportedGroups = clientGroups
+                        }
+                , clientHooks =
+                    (clientHooks clientParam)
+                        { onCustomFFDHEGroup = \_ _ -> return groupUsage
+                        }
+                }
+        serverParam' =
+            serverParam
+                { serverSupported =
+                    (serverSupported serverParam)
+                        { supportedGroups = serverGroups
+                        , supportedGroupsTLS13 = [serverGroups]
+                        }
+                }
+        commonGroups = clientGroups `intersect` serverGroups
+        shouldFail = null commonGroups
+        p minfo = isNothing (minfo >>= infoSupportedGroup) == null commonGroups
+    if shouldFail
+        then runTLSFailure (clientParam', serverParam') handshake handshake
+        else runTLSPredicate (clientParam', serverParam') p
+
+--------------------------------------------------------------
+
+newtype SG = SG [Group] deriving (Show)
+
+instance Arbitrary SG where
+    arbitrary = SG <$> shuffle sigGroups
+      where
+        sigGroups = [P256, P521]
+
+handshake_ec :: SG -> IO ()
+handshake_ec (SG sigGroups) = do
+    let versions = [TLS12]
+        ciphers =
+            [ cipher_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
+            ]
+        hashSignatures =
+            [ (HashSHA256, SignatureECDSA)
+            ]
+    (clientParam, serverParam) <-
+        generate $
+            arbitraryPairParamsWithVersionsAndCiphers
+                (versions, versions)
+                (ciphers, ciphers)
+    clientGroups <- generate $ shuffle sigGroups
+    clientHashSignatures <- generate $ sublistOf hashSignatures
+    serverHashSignatures <- generate $ sublistOf hashSignatures
+    credentials <- generate arbitraryCredentialsOfEachCurve
+    let clientParam' =
+            clientParam
+                { clientSupported =
+                    (clientSupported clientParam)
+                        { supportedGroups = clientGroups
+                        , supportedHashSignatures = clientHashSignatures
+                        }
+                }
+        serverParam' =
+            serverParam
+                { serverSupported =
+                    (serverSupported serverParam)
+                        { supportedGroups = sigGroups
+                        , supportedGroupsTLS13 = [sigGroups]
+                        , supportedHashSignatures = serverHashSignatures
+                        }
+                , serverShared =
+                    (serverShared serverParam)
+                        { sharedCredentials = Credentials credentials
+                        }
+                }
+        sigAlgs = map snd (clientHashSignatures `intersect` serverHashSignatures)
+        ecdsaDenied = SignatureECDSA `notElem` sigAlgs
+    if ecdsaDenied
+        then runTLSFailure (clientParam', serverParam') handshake handshake
+        else runTLSSimple (clientParam', serverParam')
+
+-- Tests ability to use or ignore client "signature_algorithms" extension when
+-- choosing a server certificate.  Here peers allow DHE_RSA_AES128_SHA1 but
+-- the server RSA certificate has a SHA-1 signature that the client does not
+-- support.  Server may choose the DSA certificate only when cipher
+-- DHE_DSA_AES128_SHA1 is allowed.  Otherwise it must fallback to the RSA
+-- certificate.
+
+data OC = OC [Cipher] [Cipher] deriving (Show)
+
+instance Arbitrary OC where
+    arbitrary = OC <$> sublistOf otherCiphers <*> sublistOf otherCiphers
+      where
+        otherCiphers =
+            [ cipher_ECDHE_RSA_WITH_AES_256_GCM_SHA384
+            , cipher_ECDHE_RSA_WITH_AES_128_GCM_SHA256
+            ]
+
+handshake_cert_fallback_cipher :: OC -> IO ()
+handshake_cert_fallback_cipher (OC clientCiphers serverCiphers) = do
+    let clientVersions = [TLS12]
+        serverVersions = [TLS12]
+        commonCiphers = [cipher_ECDHE_RSA_WITH_AES_128_GCM_SHA256]
+        hashSignatures = [(HashSHA256, SignatureRSA), (HashSHA1, SignatureDSA)]
+    chainRef <- newIORef Nothing
+    (clientParam, serverParam) <-
+        generate $
+            arbitraryPairParamsWithVersionsAndCiphers
+                (clientVersions, serverVersions)
+                (clientCiphers ++ commonCiphers, serverCiphers ++ commonCiphers)
+    let clientParam' =
+            clientParam
+                { clientSupported =
+                    (clientSupported clientParam)
+                        { supportedHashSignatures = hashSignatures
+                        }
+                , clientHooks =
+                    (clientHooks clientParam)
+                        { onServerCertificate = \_ _ _ chain ->
+                            writeIORef chainRef (Just chain) >> return []
+                        }
+                }
+    runTLSSimple (clientParam', serverParam)
+    serverChain <- readIORef chainRef
+    isLeafRSA serverChain `shouldBe` True
+
+-- Same as above but testing with supportedHashSignatures directly instead of
+-- ciphers, and thus allowing TLS13.  Peers accept RSA with SHA-256 but the
+-- server RSA certificate has a SHA-1 signature.  When Ed25519 is allowed by
+-- both client and server, the Ed25519 certificate is selected.  Otherwise the
+-- server fallbacks to RSA.
+--
+-- Note: SHA-1 is supposed to be disallowed in X.509 signatures with TLS13
+-- unless client advertises explicit support.  Currently this is not enforced by
+-- the library, which is useful to test this scenario.  SHA-1 could be replaced
+-- by another algorithm.
+
+data OHS = OHS [HashAndSignatureAlgorithm] [HashAndSignatureAlgorithm]
+    deriving (Show)
+
+instance Arbitrary OHS where
+    arbitrary = OHS <$> sublistOf otherHS <*> sublistOf otherHS
+      where
+        otherHS = [(HashIntrinsic, SignatureEd25519)]
+
+handshake_cert_fallback_hs :: OHS -> IO ()
+handshake_cert_fallback_hs (OHS clientHS serverHS) = do
+    tls13 <- generate arbitrary
+    let versions = if tls13 then [TLS13] else [TLS12]
+        ciphers =
+            [ cipher_ECDHE_RSA_WITH_AES_128_GCM_SHA256
+            , cipher_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
+            , cipher13_AES_128_GCM_SHA256
+            ]
+        commonHS =
+            [ (HashSHA256, SignatureRSA)
+            , (HashIntrinsic, SignatureRSApssRSAeSHA256)
+            ]
+    chainRef <- newIORef Nothing
+    (clientParam, serverParam) <-
+        generate $
+            arbitraryPairParamsWithVersionsAndCiphers
+                (versions, versions)
+                (ciphers, ciphers)
+    let clientParam' =
+            clientParam
+                { clientSupported =
+                    (clientSupported clientParam)
+                        { supportedHashSignatures = commonHS ++ clientHS
+                        }
+                , clientHooks =
+                    (clientHooks clientParam)
+                        { onServerCertificate = \_ _ _ chain ->
+                            writeIORef chainRef (Just chain) >> return []
+                        }
+                }
+        serverParam' =
+            serverParam
+                { serverSupported =
+                    (serverSupported serverParam)
+                        { supportedHashSignatures = commonHS ++ serverHS
+                        }
+                }
+        eddsaDisallowed =
+            (HashIntrinsic, SignatureEd25519) `notElem` clientHS
+                || (HashIntrinsic, SignatureEd25519) `notElem` serverHS
+    runTLSSimple (clientParam', serverParam')
+    serverChain <- readIORef chainRef
+    isLeafRSA serverChain `shouldBe` eddsaDisallowed
+
+--------------------------------------------------------------
+
+handshake_server_key_usage :: [ExtKeyUsageFlag] -> IO ()
+handshake_server_key_usage usageFlags = do
+    tls13 <- generate arbitrary
+    let versions = if tls13 then [TLS13] else [TLS12]
+        ciphers = ciphersuite_all
+    (clientParam, serverParam) <-
+        generate $
+            arbitraryPairParamsWithVersionsAndCiphers
+                (versions, versions)
+                (ciphers, ciphers)
+    cred <- generate $ arbitraryRSACredentialWithUsage usageFlags
+    let serverParam' =
+            serverParam
+                { serverShared =
+                    (serverShared serverParam)
+                        { sharedCredentials = Credentials [cred]
+                        }
+                }
+        shouldSucceed = KeyUsage_digitalSignature `elem` usageFlags
+    if shouldSucceed
+        then runTLSSimple (clientParam, serverParam')
+        else runTLSFailure (clientParam, serverParam') handshake handshake
+
+handshake_server_key_purpose :: Version -> ExtKeyUsagePurpose -> Bool -> IO ()
+handshake_server_key_purpose version purpose shouldSucceed = do
+    let cipher
+            | version == TLS13 = cipher13_AES_128_GCM_SHA256
+            | otherwise = cipher_ECDHE_RSA_WITH_AES_128_GCM_SHA256
+    (clientParam, serverParam) <-
+        generate $
+            arbitraryPairParamsWithVersionsAndCiphers
+                ([version], [version])
+                ([cipher], [cipher])
+    cred <- generate $ arbitraryRSACredentialWithPurpose purpose
+    let clientParam' =
+            clientParam
+                { clientHooks =
+                    (clientHooks clientParam)
+                        { onServerCertificate = \_ _ _ _ -> return []
+                        }
+                }
+        serverParam' =
+            serverParam
+                { serverShared =
+                    (serverShared serverParam)
+                        { sharedCredentials = Credentials [cred]
+                        }
+                }
+    if shouldSucceed
+        then runTLSSimple (clientParam', serverParam')
+        else runTLSFailure (clientParam', serverParam') handshake handshake
+
+handshake_client_key_usage :: [ExtKeyUsageFlag] -> IO ()
+handshake_client_key_usage usageFlags = do
+    (clientParam, serverParam) <- generate arbitrary
+    cred <- generate $ arbitraryRSACredentialWithUsage usageFlags
+    let clientParam' =
+            clientParam
+                { clientHooks =
+                    (clientHooks clientParam)
+                        { onCertificateRequest = \_ -> return $ Just cred
+                        }
+                }
+        serverParam' =
+            serverParam
+                { serverWantClientCert = True
+                , serverHooks =
+                    (serverHooks serverParam)
+                        { onClientCertificate = \_ -> return CertificateUsageAccept
+                        }
+                }
+        shouldSucceed = KeyUsage_digitalSignature `elem` usageFlags
+    if shouldSucceed
+        then runTLSSimple (clientParam', serverParam')
+        else runTLSFailure (clientParam', serverParam') handshake handshake
+
+--------------------------------------------------------------
+
+handshake_client_auth :: (ClientParams, ServerParams) -> IO ()
+handshake_client_auth (clientParam, serverParam) = do
+    let clientVersions = supportedVersions $ clientSupported clientParam
+        serverVersions = supportedVersions $ serverSupported serverParam
+        version = maximum (clientVersions `intersect` serverVersions)
+    cred <- generate (arbitraryClientCredential version)
+    let clientParam' =
+            clientParam
+                { clientHooks =
+                    (clientHooks clientParam)
+                        { onCertificateRequest = \_ -> return $ Just cred
+                        }
+                }
+        serverParam' =
+            serverParam
+                { serverWantClientCert = True
+                , serverHooks =
+                    (serverHooks serverParam)
+                        { onClientCertificate = validateChain cred
+                        }
+                }
+    runTLSSimple (clientParam', serverParam')
+  where
+    validateChain cred chain
+        | chain == fst cred = return CertificateUsageAccept
+        | otherwise = return (CertificateUsageReject CertificateRejectUnknownCA)
+
+-- A client without a certificate answers CertificateRequest with an empty
+-- Certificate and, in TLS 1.2, sends no CertificateVerify.  A server whose
+-- hook accepts that must go on to ChangeCipherSpec rather than wait for a
+-- CertificateVerify.
+handshake_client_auth_empty :: Version -> IO ()
+handshake_client_auth_empty version = do
+    let cipher
+            | version == TLS13 = cipher13_AES_128_GCM_SHA256
+            | otherwise = cipher_ECDHE_RSA_WITH_AES_128_GCM_SHA256
+    (clientParam, serverParam) <-
+        generate $
+            arbitraryPairParamsWithVersionsAndCiphers
+                ([version], [version])
+                ([cipher], [cipher])
+    let clientParam' =
+            clientParam
+                { clientHooks =
+                    (clientHooks clientParam)
+                        { onCertificateRequest = \_ -> return Nothing
+                        }
+                }
+        serverParam' =
+            serverParam
+                { serverWantClientCert = True
+                , serverHooks =
+                    (serverHooks serverParam)
+                        { onClientCertificate = acceptEmpty
+                        }
+                }
+    runTLSSimple (clientParam', serverParam')
+  where
+    acceptEmpty chain
+        | isNullCertificateChain chain = return CertificateUsageAccept
+        | otherwise = return (CertificateUsageReject CertificateRejectUnknownCA)
+
+-- In TLS 1.2 a client certificate with an Ed25519 or Ed448 key is asked
+-- for with ecdsa_sign (RFC 8422 Section 3.1).  The Ed25519 and Ed448
+-- certificate types of this library are synthetic and have no code point,
+-- so they must not reach the wire.
+handshake12_cert_request_types :: IO ()
+handshake12_cert_request_types = do
+    let cipher = cipher_ECDHE_RSA_WITH_AES_128_GCM_SHA256
+    (clientParam, serverParam) <-
+        generate $
+            arbitraryPairParamsWithVersionsAndCiphers
+                ([TLS12], [TLS12])
+                ([cipher], [cipher])
+    ref <- newIORef Nothing
+    let clientParam' =
+            clientParam
+                { clientHooks =
+                    (clientHooks clientParam)
+                        { onCertificateRequest = \_ -> return Nothing
+                        }
+                }
+        serverParam' =
+            serverParam
+                { serverWantClientCert = True
+                , serverSupported =
+                    (serverSupported serverParam)
+                        { supportedHashSignatures =
+                            supportedHashSignatures defaultSupported
+                        }
+                , serverHooks =
+                    (serverHooks serverParam)
+                        { onClientCertificate = \_ -> return CertificateUsageAccept
+                        }
+                }
+        record hs@(CertRequest certTypes _ _) = writeIORef ref (Just certTypes) >> return hs
+        record hs = return hs
+    withPairContextWith (id, id) (clientParam', serverParam') $ \(cctx, sctx) -> do
+        contextHookSetHandshakeRecv cctx record
+        concurrently_ (handshake sctx) (handshake cctx)
+    mtypes <- readIORef ref
+    case mtypes of
+        Nothing -> expectationFailure "no CertificateRequest received"
+        Just certTypes -> do
+            certTypes `shouldSatisfy` all (`elem` defined)
+            certTypes `shouldContain` [CertificateType_ECDSA_Sign]
+  where
+    defined =
+        [ CertificateType_RSA_Sign
+        , CertificateType_DSA_Sign
+        , CertificateType_ECDSA_Sign
+        ]
+
+-- RFC 5246 Section 7.4.8: a TLS 1.2 CertificateVerify algorithm for
+-- another type of key than the certificate's has a field that is
+-- incorrect, an illegal_parameter, even when onUnverifiedClientCert would
+-- accept a signature that does not verify.  One for an RSA key that still
+-- does not fit it, RSASSA-PSS for an rsaEncryption key, is a signature
+-- that does not verify, a decrypt_error.  The client signs with an
+-- rsaEncryption key and the server reads the given algorithm.
+handshake12_client_cert_verify_sigalg
+    :: HashAndSignatureAlgorithm -> Selector TLSException -> IO ()
+handshake12_client_cert_verify_sigalg alg rejected = do
+    let cipher = cipher_ECDHE_RSA_WITH_AES_128_GCM_SHA256
+    (clientParam, serverParam) <-
+        generate $
+            arbitraryPairParamsWithVersionsAndCiphers
+                ([TLS12], [TLS12])
+                ([cipher], [cipher])
+    cred <- generate $ arbitraryRSACredentialWithPurpose KeyUsagePurpose_ClientAuth
+    let clientParam' =
+            clientParam
+                { clientHooks =
+                    (clientHooks clientParam)
+                        { onCertificateRequest = \_ -> return $ Just cred
+                        }
+                }
+        acceptUnverified = alg == (HashSHA256, SignatureECDSA)
+        serverParam' =
+            serverParam
+                { serverWantClientCert = True
+                , serverHooks =
+                    (serverHooks serverParam)
+                        { onClientCertificate = \_ -> return CertificateUsageAccept
+                        , onUnverifiedClientCert = return acceptUnverified
+                        }
+                }
+        unfit (CertVerify (DigitallySigned _ sig)) =
+            pure $ CertVerify (DigitallySigned alg sig)
+        unfit hs = pure hs
+    r <- timeout 10000000 $
+        withPairContextWith (id, id) (clientParam', serverParam') $ \(cctx, sctx) -> do
+            contextHookSetHandshakeRecv sctx unfit
+            concurrently_
+                (handshake sctx `shouldThrow` rejected)
+                (void (E.try (handshake cctx) :: IO (Either TLSException ())))
+    r `shouldSatisfy` isJust
+
+rejectedAsDecryptError :: TLSException -> Bool
+rejectedAsDecryptError (HandshakeFailed (Error_Protocol _ DecryptError)) = True
+rejectedAsDecryptError _ = False
+
+-- RFC 8446 Section 6.2: a CertificateVerify whose algorithm may not be used
+-- with the certificate's key has a field that is incorrect, which is an
+-- illegal_parameter; decrypt_error is for a signature that does not verify.
+-- The client signs with RSA-PSS for an rsaEncryption key and the server
+-- reads the algorithm as rsa_pss_pss_sha256, which needs an RSASSA-PSS key.
+handshake13_client_cert_verify_unfit_sigalg :: IO ()
+handshake13_client_cert_verify_unfit_sigalg = do
+    let cipher = cipher13_AES_128_GCM_SHA256
+    (clientParam, serverParam) <-
+        generate $
+            arbitraryPairParamsWithVersionsAndCiphers
+                ([TLS13], [TLS13])
+                ([cipher], [cipher])
+    cred <- generate $ arbitraryRSACredentialWithPurpose KeyUsagePurpose_ClientAuth
+    let clientParam' =
+            clientParam
+                { clientHooks =
+                    (clientHooks clientParam)
+                        { onCertificateRequest = \_ -> return $ Just cred
+                        }
+                }
+        serverParam' =
+            serverParam
+                { serverWantClientCert = True
+                , serverHooks =
+                    (serverHooks serverParam)
+                        { onClientCertificate = \_ -> return CertificateUsageAccept
+                        }
+                }
+        unfit (CertVerify13 (DigitallySigned _ sig)) =
+            pure $
+                CertVerify13 (DigitallySigned (HashIntrinsic, SignatureRSApsspssSHA256) sig)
+        unfit hs = pure hs
+    r <- timeout 10000000 $
+        withPairContextWith (id, id) (clientParam', serverParam') $ \(cctx, sctx) -> do
+            contextHookSetHandshake13Recv sctx unfit
+            concurrently_
+                ((handshake sctx >> recvData sctx) `shouldThrow` rejectedAsIllegalParameter)
+                ( void
+                    (E.try (handshake cctx >> recvData cctx) :: IO (Either TLSException B.ByteString))
+                )
+    r `shouldSatisfy` isJust
+
+rejectedAsIllegalParameter :: TLSException -> Bool
+rejectedAsIllegalParameter (HandshakeFailed (Error_Protocol _ IllegalParameter)) = True
+rejectedAsIllegalParameter (Terminated _ _ (Error_Protocol _ IllegalParameter)) = True
+rejectedAsIllegalParameter _ = False
+
+-- A server that receives a ClientHello whose legacy_version is higher than
+-- its own negotiates the highest version it supports (RFC 5246 Appendix
+-- E.1); with supported_versions present it does not use legacy_version at
+-- all (RFC 8446 Section 4.2.1).  The ClientHello's legacy_version is raised
+-- on its way to the server, and the handshake must still reach the version
+-- both sides support.
+handshake_high_legacy_version :: Version -> Version -> IO ()
+handshake_high_legacy_version version legacy = do
+    let cipher
+            | version == TLS13 = cipher13_AES_128_GCM_SHA256
+            | otherwise = cipher_ECDHE_RSA_WITH_AES_128_GCM_SHA256
+    (clientParam, serverParam) <-
+        generate $
+            arbitraryPairParamsWithVersionsAndCiphers
+                ([version], [version])
+                ([cipher], [cipher])
+    let raise (ClientHello ch) = pure $ ClientHello ch{chVersion = legacy}
+        raise hs = pure hs
+    withPairContextWith (id, id) (clientParam, serverParam) $ \(cctx, sctx) -> do
+        contextHookSetHandshakeRecv sctx raise
+        concurrently_ (handshake sctx) (handshake cctx)
+        info <- contextGetInformation sctx
+        (infoVersion <$> info) `shouldBe` Just version
+
+-- RFC 8422 Section 5.1.2: ec_point_format_list is <1..2^8-1>, and a
+-- client naming one of its curves in supported_groups must offer the
+-- uncompressed format.  The ClientHello's ec_point_formats is replaced
+-- on its way to the server, which must refuse it.
+handshake12_ec_point_formats
+    :: B.ByteString -> Selector TLSException -> IO ()
+handshake12_ec_point_formats formats rejected = do
+    CSP12 (cparams, sparams) <- generate arbitrary
+    let cparams' =
+            cparams
+                { clientSupported =
+                    (clientSupported cparams){supportedGroups = [X25519]}
+                }
+        sparams' =
+            sparams
+                { serverSupported =
+                    (serverSupported sparams){supportedGroups = [X25519]}
+                }
+        replace (ClientHello ch) =
+            pure $
+                ClientHello
+                    ch
+                        { chExtensions =
+                            filter
+                                (\(ExtensionRaw eid _) -> eid /= EID_EcPointFormats)
+                                (chExtensions ch)
+                                ++ [ExtensionRaw EID_EcPointFormats formats]
+                        }
+        replace hs = pure hs
+    r <- timeout 10000000 $
+        withPairContextWith (id, id) (cparams', sparams') $ \(cctx, sctx) -> do
+            contextHookSetHandshakeRecv sctx replace
+            concurrently_
+                (handshake sctx `shouldThrow` rejected)
+                (void (E.try (handshake cctx) :: IO (Either TLSException ())))
+    r `shouldSatisfy` isJust
+
+rejectedAsDecodeError :: TLSException -> Bool
+rejectedAsDecodeError (HandshakeFailed (Error_Protocol _ DecodeError)) = True
+rejectedAsDecodeError _ = False
+
+handshake_client_auth_fail :: (ClientParams, ServerParams) -> IO ()
+handshake_client_auth_fail (clientParam, serverParam) = do
+    let clientVersions = supportedVersions $ clientSupported clientParam
+        serverVersions = supportedVersions $ serverSupported serverParam
+        version = maximum (clientVersions `intersect` serverVersions)
+    cred <- generate (arbitraryClientCredential version)
+    let clientParam' =
+            clientParam
+                { clientHooks =
+                    (clientHooks clientParam)
+                        { onCertificateRequest = \_ -> return $ Just cred
+                        }
+                }
+        serverParam' =
+            serverParam
+                { serverWantClientCert = True
+                , serverHooks =
+                    (serverHooks serverParam)
+                        { onClientCertificate = validateChain cred
+                        }
+                }
+    runTLSFailure (clientParam', serverParam') handshake handshake
+  where
+    validateChain _ _ = return (CertificateUsageReject CertificateRejectUnknownCA)
+
+--------------------------------------------------------------
+
+handshake_ems :: (EMSMode, EMSMode) -> IO ()
+handshake_ems (cems, sems) = do
+    params <- generate arbitrary
+    let params' = setEMSMode (cems, sems) params
+        version = getConnectVersion params'
+        emsVersion = version >= TLS10 && version <= TLS12
+        use = cems /= NoEMS && sems /= NoEMS
+        require = cems == RequireEMS || sems == RequireEMS
+        p info = infoExtendedMainSecret info == (emsVersion && use)
+    if emsVersion && require && not use
+        then runTLSFailure params' handshake handshake
+        else runTLSPredicate params' (maybe False p)
+
+newtype CompatEMS = CompatEMS (EMSMode, EMSMode) deriving (Show)
+
+instance Arbitrary CompatEMS where
+    arbitrary = CompatEMS <$> (arbitrary `suchThat` compatible)
+      where
+        compatible (NoEMS, RequireEMS) = False
+        compatible (RequireEMS, NoEMS) = False
+        compatible _ = True
+
+handshake_resumption_ems :: (CompatEMS, CompatEMS) -> IO ()
+handshake_resumption_ems (CompatEMS ems, CompatEMS ems2) = do
+    sessionRefs <- twoSessionRefs
+    let sessionManagers = twoSessionManagers sessionRefs
+
+    plainParams <- generate arbitrary
+    let params =
+            setEMSMode ems $
+                setPairParamsSessionManagers sessionManagers plainParams
+
+    runTLSSimple params
+
+    -- and resume
+    sessionParams <- readClientSessionRef sessionRefs
+    expectJust "session param should be Just" sessionParams
+    let params2 =
+            setEMSMode ems2 $
+                setPairParamsSessionResuming (fromJust sessionParams) params
+
+    let version = getConnectVersion params2
+        emsVersion = version >= TLS10 && version <= TLS12
+
+    if emsVersion && use ems && not (use ems2)
+        then runTLSFailure params2 handshake handshake
+        else do
+            runTLSSimple params2
+            mSessionParams2 <- readClientSessionRef sessionRefs
+            let sameSession = sessionParams == mSessionParams2
+                sameUse = use ems == use ems2
+            when emsVersion (sameSession `shouldBe` sameUse)
+  where
+    use (NoEMS, _) = False
+    use (_, NoEMS) = False
+    use _ = True
+
+--------------------------------------------------------------
+
+handshake_alpn :: (ClientParams, ServerParams) -> IO ()
+handshake_alpn (clientParam, serverParam) = do
+    let clientParam' =
+            clientParam
+                { clientHooks =
+                    (clientHooks clientParam)
+                        { onSuggestALPN = return $ Just ["h2", "http/1.1"]
+                        }
+                }
+        serverParam' =
+            serverParam
+                { serverHooks =
+                    (serverHooks serverParam)
+                        { onALPNClientSuggest = Just alpn
+                        }
+                }
+        params' = (clientParam', serverParam')
+    runTLSSuccess params' hsClient hsServer
+  where
+    hsClient ctx = do
+        handshake ctx
+        proto <- getNegotiatedProtocol ctx
+        proto `shouldBe` Just "h2"
+    hsServer ctx = do
+        handshake ctx
+        proto <- getNegotiatedProtocol ctx
+        proto `shouldBe` Just "h2"
+    alpn xs
+        | "h2" `elem` xs = return "h2"
+        | otherwise = return "http/1.1"
+
+handshake_alpn_rejects_unoffered_server_selection :: IO ()
+handshake_alpn_rejects_unoffered_server_selection = do
+    (clientParam, serverParam) <- generate arbitraryPairParams13
+    let params = alpnParams clientParam serverParam (const $ pure "h2")
+    withPairContextWith (id, id) params $ \(cctx, sctx) ->
+        concurrently_
+            (handshake sctx `shouldThrow` serverRejectedUnofferedALPN)
+            (handshake cctx `shouldThrow` anyTLSException)
+
+handshake_alpn_rejects_unoffered_client_selection :: IO ()
+handshake_alpn_rejects_unoffered_client_selection = do
+    (clientParam, serverParam) <- generate arbitraryPairParams13
+    let params = alpnParams clientParam serverParam (pure . unsafeHead)
+    withPairContextWith (id, id) params $ \(cctx, sctx) -> do
+        contextHookSetHandshake13Recv cctx tamperALPN
+        concurrently_
+            (handshake sctx `shouldThrow` anyTLSException)
+            (handshake cctx `shouldThrow` clientRejectedUnofferedALPN)
+  where
+    tamperALPN (EncryptedExtensions13 exts) =
+        pure $ EncryptedExtensions13 $ map replaceALPN exts
+    tamperALPN hs = pure hs
+    replaceALPN ext@(ExtensionRaw eid _)
+        | eid == EID_ApplicationLayerProtocolNegotiation =
+            toExtensionRaw $ ApplicationLayerProtocolNegotiation ["h2"]
+        | otherwise = ext
+
+alpnParams
+    :: ClientParams
+    -> ServerParams
+    -> ([B.ByteString] -> IO B.ByteString)
+    -> (ClientParams, ServerParams)
+alpnParams clientParam serverParam select =
+    ( clientParam
+        { clientHooks =
+            (clientHooks clientParam)
+                { onSuggestALPN = pure $ Just ["http/1.1"]
+                }
+        }
+    , serverParam
+        { serverHooks =
+            (serverHooks serverParam)
+                { onALPNClientSuggest = Just select
+                }
+        }
+    )
+
+serverRejectedUnofferedALPN :: TLSException -> Bool
+serverRejectedUnofferedALPN (HandshakeFailed (Error_Protocol msg alert)) =
+    msg == "ALPN callback selected a protocol not offered by the client"
+        && alert == NoApplicationProtocol
+serverRejectedUnofferedALPN _ = False
+
+clientRejectedUnofferedALPN :: TLSException -> Bool
+clientRejectedUnofferedALPN (HandshakeFailed (Error_Protocol msg alert)) =
+    msg == "server selected an ALPN protocol not offered by the client"
+        && alert == IllegalParameter
+clientRejectedUnofferedALPN _ = False
+
+-- RFC 6066 Section 3: HostName is <1..2^16-1>, so a client with an
+-- empty server name sends no server_name, and the server sees none.
+handshake_sni_empty :: IO ()
+handshake_sni_empty = do
+    CSP12 (clientParam, serverParam) <- generate arbitrary
+    let clientParam' = clientParam{clientServerIdentification = ("", "")}
+    runTLSSuccess (clientParam', serverParam) hs hs
+  where
+    hs ctx = do
+        handshake ctx
+        msni <- getClientSNI ctx
+        msni `shouldBe` Nothing
+
+-- RFC 6066 Section 3: the server_name_list MUST NOT contain more than one
+-- name of the same name_type, and HostName is an ASCII DNS host name.  The
+-- ClientHello's server_name is replaced on its way to the server, which
+-- must refuse it with illegal_parameter.
+handshake_sni_illegal :: [B.ByteString] -> IO ()
+handshake_sni_illegal names = do
+    CSP12 (clientParam, serverParam) <- generate arbitrary
+    let entry name =
+            B.concat [B.pack [0, len `shiftR` 8, len], name]
+          where
+            len = fromIntegral $ B.length name
+        list = B.concat $ map entry names
+        listLen = fromIntegral $ B.length list
+        sni = B.concat [B.pack [listLen `shiftR` 8, listLen], list]
+        replace (ClientHello ch) =
+            pure $
+                ClientHello
+                    ch
+                        { chExtensions =
+                            ExtensionRaw EID_ServerName sni
+                                : filter
+                                    (\(ExtensionRaw eid _) -> eid /= EID_ServerName)
+                                    (chExtensions ch)
+                        }
+        replace hs = pure hs
+    r <- timeout 10000000 $
+        withPairContextWith (id, id) (clientParam, serverParam) $ \(cctx, sctx) -> do
+            contextHookSetHandshakeRecv sctx replace
+            concurrently_
+                (handshake sctx `shouldThrow` rejectedAsIllegalParameter)
+                (void (E.try (handshake cctx) :: IO (Either TLSException ())))
+    r `shouldSatisfy` isJust
+
+handshake_sni :: (ClientParams, ServerParams) -> IO ()
+handshake_sni (clientParam, serverParam) = do
+    ref <- newIORef Nothing
+    let clientParam' =
+            clientParam
+                { clientServerIdentification = (serverName, "")
+                }
+        serverParam' =
+            serverParam
+                { serverHooks =
+                    (serverHooks serverParam)
+                        { onServerNameIndication = onSNI ref
+                        }
+                }
+        params' = (clientParam', serverParam')
+    runTLSSuccess params' hsClient hsServer
+    receivedName <- readIORef ref
+    receivedName `shouldBe` Just (Just serverName)
+  where
+    hsClient ctx = do
+        handshake ctx
+        msni <- getClientSNI ctx
+        expectMaybe "C: SNI should be Just" serverName msni
+    hsServer ctx = do
+        handshake ctx
+        msni <- getClientSNI ctx
+        expectMaybe "S: SNI should be Just" serverName msni
+    onSNI ref name = do
+        mx <- readIORef ref
+        mx `shouldBe` Nothing
+        writeIORef ref (Just name)
+        return (Credentials [])
+    serverName = "haskell.org"
+
+--------------------------------------------------------------
+
+newtype CSP12 = CSP12 (ClientParams, ServerParams) deriving (Show)
+
+instance Arbitrary CSP12 where
+    arbitrary = CSP12 <$> arbitraryPairParams12
+
+handshake12_renegotiation :: CSP12 -> IO ()
+handshake12_renegotiation (CSP12 (cparams, sparams)) = do
+    renegDisabled <- generate arbitrary
+    let sparams' =
+            sparams
+                { serverSupported =
+                    (serverSupported sparams)
+                        { supportedClientInitiatedRenegotiation = not renegDisabled
+                        }
+                }
+    if renegDisabled
+        then runTLSFailure (cparams, sparams') hsClient hsServer
+        else runTLSSimple (cparams, sparams')
+  where
+    hsClient ctx = handshake ctx >> handshake ctx
+    -- recvData receives the alert from the second handshake
+    hsServer ctx = handshake ctx >> void (recvData ctx)
+
+-- RFC 5746 Section 3.7: when a connection with secure renegotiation is
+-- renegotiated, ClientHello must not contain the SCSV and must contain
+-- the renegotiation_info extension.  The second ClientHello is tampered
+-- with on its way to the server, which must abort with handshake_failure.
+handshake12_renegotiation_tampered :: (ClientHello -> ClientHello) -> IO ()
+handshake12_renegotiation_tampered tamper = do
+    CSP12 (cparams, sparams) <- generate arbitrary
+    let cparams' =
+            cparams
+                { clientSupported =
+                    (clientSupported cparams)
+                        { supportedSecureRenegotiation = True
+                        }
+                }
+        sparams' =
+            sparams
+                { serverSupported =
+                    (serverSupported sparams)
+                        { supportedSecureRenegotiation = True
+                        , supportedClientInitiatedRenegotiation = True
+                        }
+                }
+    count <- newIORef (0 :: Int)
+    let tamperSecond (ClientHello ch) = do
+            n <- atomicModifyIORef' count $ \i -> (i + 1, i)
+            pure $ ClientHello $ if n == 0 then ch else tamper ch
+        tamperSecond hs = pure hs
+    r <- timeout 10000000 $
+        withPairContextWith (id, id) (cparams', sparams') $ \(cctx, sctx) -> do
+            contextHookSetHandshakeRecv sctx tamperSecond
+            concurrently_ (handshake sctx) (handshake cctx)
+            concurrently_
+                (recvData sctx `shouldThrow` rejectedAsHandshakeFailure)
+                ( void
+                    (E.try (handshake cctx >> recvData cctx) :: IO (Either TLSException B.ByteString))
+                )
+    r `shouldSatisfy` isJust
+
+rejectedAsHandshakeFailure :: TLSException -> Bool
+rejectedAsHandshakeFailure (HandshakeFailed (Error_Protocol _ HandshakeFailure)) = True
+rejectedAsHandshakeFailure (Terminated _ _ (Error_Protocol _ HandshakeFailure)) = True
+rejectedAsHandshakeFailure _ = False
+
+handshake12_session_resumption :: CSP12 -> IO ()
+handshake12_session_resumption (CSP12 plainParams) = do
+    sessionRefs <- twoSessionRefs
+    let sessionManagers = twoSessionManagers sessionRefs
+
+    let params = setPairParamsSessionManagers sessionManagers plainParams
+
+    runTLSSimple params
+
+    -- and resume
+    sessionParams <- readClientSessionRef sessionRefs
+    expectJust "session param should be Just" sessionParams
+    let params2 = setPairParamsSessionResuming (fromJust sessionParams) params
+
+    runTLSPredicate params2 (maybe False infoTLS12Resumption)
+
+-- RFC 5246 Section 7.4.1.2: a client resuming a session MUST offer its
+-- cipher suite.  When it asks to resume one and offers none the server can
+-- use, that is what is reported -- illegal_parameter, as when a cipher could
+-- be chosen -- rather than the missing common cipher.
+handshake12_session_resumption_cipher_missing :: CSP12 -> IO ()
+handshake12_session_resumption_cipher_missing (CSP12 plainParams) = do
+    sessionRefs <- twoSessionRefs
+    let sessionManagers = twoSessionManagers sessionRefs
+        params = setPairParamsSessionManagers sessionManagers plainParams
+    runTLSSimple params
+    sessionParams <- readClientSessionRef sessionRefs
+    expectJust "session param should be Just" sessionParams
+    let params2 = setPairParamsSessionResuming (fromJust sessionParams) params
+        -- TLS_RSA_WITH_NULL_MD5, which the server does not support
+        nullCiphers (ClientHello ch) = pure $ ClientHello ch{chCiphers = [CipherId 0x0001]}
+        nullCiphers hs = pure hs
+    withPairContextWith (id, id) params2 $ \(cctx, sctx) -> do
+        contextHookSetHandshakeRecv sctx nullCiphers
+        concurrently_
+            (handshake sctx `shouldThrow` serverRejectedMissingCipher)
+            (handshake cctx `shouldThrow` anyTLSException)
+
+serverRejectedMissingCipher :: TLSException -> Bool
+serverRejectedMissingCipher (HandshakeFailed (Error_Protocol _ IllegalParameter)) = True
+serverRejectedMissingCipher _ = False
+
+-- RFC 5077 Sections 3.2 and 3.3: a server sends the session_ticket
+-- extension and NewSessionTicket only to a client that sent the
+-- extension.  The client's session_ticket is removed on its way to a
+-- server whose session manager uses tickets, and the client must then
+-- receive neither.
+handshake12_session_ticket_unoffered :: IO ()
+handshake12_session_ticket_unoffered = do
+    CSP12 (cparams, sparams) <- generate arbitrary
+    let sparams' =
+            sparams
+                { serverShared =
+                    (serverShared sparams){sharedSessionManager = oneSessionTicket}
+                }
+        unoffer (ClientHello ch) =
+            pure $
+                ClientHello
+                    ch
+                        { chExtensions =
+                            filter
+                                (\(ExtensionRaw eid _) -> eid /= EID_SessionTicket)
+                                (chExtensions ch)
+                        }
+        unoffer hs = pure hs
+    received <- newIORef []
+    let record hs = modifyIORef received (hs :) >> pure hs
+    withPairContextWith (id, id) (cparams, sparams') $ \(cctx, sctx) -> do
+        contextHookSetHandshakeRecv sctx unoffer
+        contextHookSetHandshakeRecv cctx record
+        concurrently_ (handshake sctx) (handshake cctx)
+    hss <- readIORef received
+    let ticketExt (ServerHello sh) =
+            any (\(ExtensionRaw eid _) -> eid == EID_SessionTicket) (shExtensions sh)
+        ticketExt _ = False
+        newTicket NewSessionTicket{} = True
+        newTicket _ = False
+    filter (\h -> ticketExt h || newTicket h) hss `shouldBe` []
+
+handshake12_session_ticket :: CSP12 -> IO ()
+handshake12_session_ticket (CSP12 plainParams) = do
+    sessionRefs <- twoSessionRefs
+    let sessionManagers0 = twoSessionManagers sessionRefs
+        sessionManagers = (fst sessionManagers0, oneSessionTicket)
+
+    let params = setPairParamsSessionManagers sessionManagers plainParams
+
+    runTLSSimple params
+
+    -- and resume
+    sessionParams <- readClientSessionRef sessionRefs
+    expectJust "session param should be Just" sessionParams
+    let params2 = setPairParamsSessionResuming (fromJust sessionParams) params
+
+    runTLSPredicate params2 (maybe False infoTLS12Resumption)
+
+--------------------------------------------------------------
+
+handshake13_full :: CSP13 -> IO ()
+handshake13_full (CSP13 (cli, srv)) = do
+    let cliSupported =
+            defaultSupported
+                { supportedCiphers = [cipher13_AES_128_GCM_SHA256]
+                , supportedGroups = [X25519]
+                }
+        svrSupported =
+            defaultSupported
+                { supportedCiphers = [cipher13_AES_128_GCM_SHA256]
+                , supportedGroups = [X25519]
+                , supportedGroupsTLS13 = [[X25519]]
+                }
+        params =
+            ( cli{clientSupported = cliSupported}
+            , srv{serverSupported = svrSupported}
+            )
+    runTLSSimple13 params FullHandshake
+
+handshake13_hrr :: CSP13 -> IO ()
+handshake13_hrr (CSP13 (cli, srv)) = do
+    let cliSupported =
+            defaultSupported
+                { supportedCiphers = [cipher13_AES_128_GCM_SHA256]
+                , supportedGroups = [P256, X25519]
+                }
+        svrSupported =
+            defaultSupported
+                { supportedCiphers = [cipher13_AES_128_GCM_SHA256]
+                , supportedGroups = [X25519]
+                , supportedGroupsTLS13 = [[X25519]]
+                }
+        params =
+            ( cli{clientSupported = cliSupported}
+            , srv{serverSupported = svrSupported}
+            )
+    runTLSSimple13 params HelloRetryRequest
+
+handshake13_psk :: CSP13 -> IO ()
+handshake13_psk (CSP13 (cli, srv)) = do
+    let cliSupported =
+            defaultSupported
+                { supportedCiphers = [cipher13_AES_128_GCM_SHA256]
+                , supportedGroups = [P256, X25519]
+                }
+        svrSupported =
+            defaultSupported
+                { supportedCiphers = [cipher13_AES_128_GCM_SHA256]
+                , supportedGroups = [X25519]
+                , supportedGroupsTLS13 = [[X25519]]
+                }
+        params0 =
+            ( cli{clientSupported = cliSupported}
+            , srv{serverSupported = svrSupported}
+            )
+
+    sessionRefs <- twoSessionRefs
+    let sessionManagers = twoSessionManagers sessionRefs
+
+    let params = setPairParamsSessionManagers sessionManagers params0
+
+    runTLSSimple13 params HelloRetryRequest
+
+    -- and resume
+    sessionParams <- readClientSessionRef sessionRefs
+    expectJust "session param should be Just" sessionParams
+    let params2 = setPairParamsSessionResuming (fromJust sessionParams) params
+
+    runTLSSimple13 params2 PreSharedKey
+
+-- RFC 8446 Section 4.6.1: a TLS 1.3 PSK resumes only a TLS 1.3 session.
+-- The server's session manager answers the client's PSK identity with a
+-- TLS 1.2 session, which has no ticket information; the server must
+-- fall back to a full handshake rather than fail.
+handshake13_psk_tls12_session :: IO ()
+handshake13_psk_tls12_session = do
+    CSP12 params12 <- generate arbitrary
+    refs12 <- twoSessionRefs
+    runTLSSimple $ setPairParamsSessionManagers (twoSessionManagers refs12) params12
+    Just (_, sdata12) <- readIORef (snd refs12)
+    sessionVersion sdata12 `shouldBe` TLS12
+
+    CSP13 (cli, srv) <- generate arbitrary
+    let cliSupported =
+            defaultSupported
+                { supportedCiphers = [cipher13_AES_128_GCM_SHA256]
+                , supportedGroups = [P256, X25519]
+                }
+        svrSupported =
+            defaultSupported
+                { supportedCiphers = [cipher13_AES_128_GCM_SHA256]
+                , supportedGroups = [X25519]
+                , supportedGroupsTLS13 = [[X25519]]
+                }
+        params0 =
+            ( cli{clientSupported = cliSupported}
+            , srv{serverSupported = svrSupported}
+            )
+    refs13 <- twoSessionRefs
+    let params = setPairParamsSessionManagers (twoSessionManagers refs13) params0
+    runTLSSimple13 params HelloRetryRequest
+    Just sessionParams <- readClientSessionRef refs13
+
+    let tls12Manager =
+            noSessionManager
+                { sessionResume = \_ -> return $ Just sdata12
+                , sessionResumeOnlyOnce = \_ -> return $ Just sdata12
+                }
+        params2 =
+            setPairParamsSessionResuming sessionParams $
+                setPairParamsSessionManagers (fst (twoSessionManagers refs13), tls12Manager) params0
+    -- The key share is for the group of the earlier session, so no
+    -- HelloRetryRequest, and the PSK is not used.
+    runTLSSimple13 params2 FullHandshake
+
+handshake13_psk_ticket :: CSP13 -> IO ()
+handshake13_psk_ticket (CSP13 (cli, srv)) = do
+    let cliSupported =
+            defaultSupported
+                { supportedCiphers = [cipher13_AES_128_GCM_SHA256]
+                , supportedGroups = [P256, X25519]
+                }
+        svrSupported =
+            defaultSupported
+                { supportedCiphers = [cipher13_AES_128_GCM_SHA256]
+                , supportedGroups = [X25519]
+                , supportedGroupsTLS13 = [[X25519]]
+                }
+        params0 =
+            ( cli{clientSupported = cliSupported}
+            , srv{serverSupported = svrSupported}
+            )
+
+    sessionRefs <- twoSessionRefs
+    let sessionManagers0 = twoSessionManagers sessionRefs
+        sessionManagers = (fst sessionManagers0, oneSessionTicket)
+
+    let params = setPairParamsSessionManagers sessionManagers params0
+
+    runTLSSimple13 params HelloRetryRequest
+
+    -- and resume
+    sessionParams <- readClientSessionRef sessionRefs
+    expectJust "session param should be Just" sessionParams
+    let params2 = setPairParamsSessionResuming (fromJust sessionParams) params
+
+    runTLSSimple13 params2 PreSharedKey
+
+handshake13_psk_fallback :: CSP13 -> IO ()
+handshake13_psk_fallback (CSP13 (cli, srv)) = do
+    let cliSupported =
+            defaultSupported
+                { supportedCiphers =
+                    [ cipher13_AES_128_GCM_SHA256
+                    , cipher13_AES_128_CCM_SHA256
+                    ]
+                , supportedGroups = [P256, X25519]
+                }
+        svrSupported =
+            defaultSupported
+                { supportedCiphers = [cipher13_AES_128_GCM_SHA256]
+                , supportedGroups = [X25519]
+                , supportedGroupsTLS13 = [[X25519]]
+                }
+        params0 =
+            ( cli{clientSupported = cliSupported}
+            , srv{serverSupported = svrSupported}
+            )
+
+    sessionRefs <- twoSessionRefs
+    let sessionManagers = twoSessionManagers sessionRefs
+
+    let params = setPairParamsSessionManagers sessionManagers params0
+
+    runTLSSimple13 params HelloRetryRequest
+
+    -- resumption fails because GCM cipher is not supported anymore, full
+    -- handshake is not possible because X25519 has been removed, so we are
+    -- back with P256 after hello retry
+    sessionParams <- readClientSessionRef sessionRefs
+    expectJust "session param should be Just" sessionParams
+    let (cli2, srv2) = setPairParamsSessionResuming (fromJust sessionParams) params
+        srv2' =
+            srv2{serverSupported = svrSupported'}
+        svrSupported' =
+            defaultSupported
+                { supportedCiphers = [cipher13_AES_128_CCM_SHA256]
+                , supportedGroups = [P256]
+                , supportedGroupsTLS13 = [[P256]]
+                }
+
+    runTLSSimple13 (cli2, srv2') HelloRetryRequest
+
+handshake13_0rtt :: CSP13 -> IO ()
+handshake13_0rtt (CSP13 (cli, srv)) = do
+    let cliSupported =
+            defaultSupported
+                { supportedCiphers = [cipher13_AES_128_GCM_SHA256]
+                , supportedGroups = [P256, X25519]
+                }
+        svrSupported =
+            defaultSupported
+                { supportedCiphers = [cipher13_AES_128_GCM_SHA256]
+                , supportedGroups = [X25519]
+                , supportedGroupsTLS13 = [[X25519]]
+                }
+        cliHooks =
+            defaultClientHooks
+                { onSuggestALPN = return $ Just ["h2"]
+                }
+        svrHooks =
+            defaultServerHooks
+                { onALPNClientSuggest = Just (return . unsafeHead)
+                }
+        params0 =
+            ( cli
+                { clientSupported = cliSupported
+                , clientHooks = cliHooks
+                }
+            , srv
+                { serverSupported = svrSupported
+                , serverHooks = svrHooks
+                , serverEarlyDataSize = 2048
+                }
+            )
+
+    sessionRefs <- twoSessionRefs
+    let sessionManagers = twoSessionManagers sessionRefs
+
+    let params = setPairParamsSessionManagers sessionManagers params0
+
+    runTLSSimple13 params HelloRetryRequest
+    runTLS0rtt params sessionRefs
+    runTLS0rtt params sessionRefs
+  where
+    runTLS0rtt params sessionRefs = do
+        -- and resume
+        sessionParams <- readClientSessionRef sessionRefs
+        expectJust "session param should be Just" sessionParams
+        clearClientSessionRef sessionRefs
+        earlyData <- B.pack <$> generate (someWords8 256)
+        let (pc, ps) = setPairParamsSessionResuming (fromJust sessionParams) params
+            params2 = (pc{clientUseEarlyData = True}, ps)
+
+        runTLS0RTT params2 RTT0 earlyData
+
+handshake13_0rtt_alpn :: IO ()
+handshake13_0rtt_alpn = do
+    (cli, srv) <- generate arbitraryPairParams13
+    let cliSupported =
+            defaultSupported
+                { supportedCiphers = [cipher13_AES_128_GCM_SHA256]
+                , supportedGroups = [X25519]
+                }
+        svrSupported =
+            defaultSupported
+                { supportedCiphers = [cipher13_AES_128_GCM_SHA256]
+                , supportedGroups = [X25519]
+                , supportedGroupsTLS13 = [[X25519]]
+                }
+        cliHooks =
+            defaultClientHooks
+                { onSuggestALPN = return $ Just ["h2"]
+                }
+        svrHooks =
+            defaultServerHooks
+                { onALPNClientSuggest = Just (return . unsafeHead)
+                }
+        params0 =
+            ( cli
+                { clientSupported = cliSupported
+                , clientHooks = cliHooks
+                }
+            , srv
+                { serverSupported = svrSupported
+                , serverHooks = svrHooks
+                , serverEarlyDataSize = 2048
+                }
+            )
+    sessionRefs <- twoSessionRefs
+    let params =
+            setPairParamsSessionManagers
+                (twoSessionManagers sessionRefs)
+                params0
+    runTLSSimple13 params FullHandshake
+
+    sessionParams <- readClientSessionRef sessionRefs
+    expectJust "session param should be Just" sessionParams
+    sessionALPN (snd $ fromJust sessionParams) `shouldBe` Just "h2"
+    let (pc, ps) = setPairParamsSessionResuming (fromJust sessionParams) params
+        pc' =
+            pc
+                { clientUseEarlyData = True
+                , clientHooks =
+                    (clientHooks pc)
+                        { onSuggestALPN = return $ Just ["http/1.1"]
+                        }
+                }
+        ps' =
+            ps
+                { serverHooks =
+                    (serverHooks ps)
+                        { onALPNClientSuggest = Just (return . unsafeHead)
+                        }
+                }
+    runTLS0RTT (pc', ps') PreSharedKey "GET /admin HTTP/1.1\r\n\r\n"
+
+handshake13_0rtt_fallback :: CSP13 -> IO ()
+handshake13_0rtt_fallback (CSP13 (cli, srv)) = do
+    group0 <- generate $ elements [P256, X25519]
+    let cliSupported =
+            defaultSupported
+                { supportedCiphers = [cipher13_AES_128_GCM_SHA256]
+                , supportedGroups = [P256, X25519]
+                }
+        svrSupported =
+            defaultSupported
+                { supportedCiphers = [cipher13_AES_128_GCM_SHA256]
+                , supportedGroups = [group0]
+                , supportedGroupsTLS13 = [[group0]]
+                }
+        params =
+            ( cli{clientSupported = cliSupported}
+            , srv
+                { serverSupported = svrSupported
+                , serverEarlyDataSize = 1024
+                }
+            )
+
+    sessionRefs <- twoSessionRefs
+    let sessionManagers = twoSessionManagers sessionRefs
+
+    let params0 = setPairParamsSessionManagers sessionManagers params
+
+    let mode = if group0 == P256 then FullHandshake else HelloRetryRequest
+    runTLSSimple13 params0 mode
+
+    -- and resume
+    mSessionParams <- readClientSessionRef sessionRefs
+    case mSessionParams of
+        Nothing -> expectationFailure "session params: Just is expected"
+        Just sessionParams -> do
+            earlyData <- B.pack <$> generate (someWords8 256)
+            group1 <- generate $ elements [P256, X25519]
+            let (pc, ps) = setPairParamsSessionResuming sessionParams params0
+                svrSupported1 =
+                    defaultSupported
+                        { supportedCiphers = [cipher13_AES_128_GCM_SHA256]
+                        , supportedGroups = [group1]
+                        , supportedGroupsTLS13 = [[group1]]
+                        }
+                params1 =
+                    ( pc{clientUseEarlyData = True}
+                    , ps
+                        { serverEarlyDataSize = 0
+                        , serverSupported = svrSupported1
+                        }
+                    )
+            -- C: [P256, X25519]
+            -- S: [group0]
+            -- C: [P256, X25519]
+            -- S: [group1]
+            if group0 == group1
+                -- 0-RTT is not allowed, so fallback to PreSharedKey
+                then runTLS0RTT params1 PreSharedKey earlyData
+                -- HRR but not allowed for 0-RTT
+                else runTLSFailure params1 (tlsClient earlyData) tlsServer
+  where
+    tlsClient earlyData ctx = do
+        handshake ctx
+        sendData ctx $ L.fromStrict earlyData
+        _ <- recvData ctx
+        bye ctx
+    tlsServer ctx = do
+        handshake ctx
+        _ <- recvData ctx
+        bye ctx
+
+handshake13_ee_groups :: CSP13 -> IO ()
+handshake13_ee_groups (CSP13 (cli, srv)) = do
+    let -- The client prefers P256
+        cliSupported = (clientSupported cli){supportedGroups = [P256, X25519]}
+        -- The server prefers X25519
+        svrSupported =
+            (serverSupported srv)
+                { supportedGroups = [X25519, P256]
+                , supportedGroupsTLS13 = [[X25519, P256]]
+                }
+        params =
+            ( cli{clientSupported = cliSupported}
+            , srv{serverSupported = svrSupported}
+            )
+    (_, serverMessages) <- runTLSCapture13 params
+    -- The server should tell X25519 in supported_groups in EE to client
+    let isSupportedGroups (ExtensionRaw eid _) = eid == EID_SupportedGroups
+        eeMessagesHaveExt =
+            [ any isSupportedGroups exts
+            | EncryptedExtensions13 exts <- serverMessages
+            ]
+    eeMessagesHaveExt `shouldBe` [True]
+
+handshake13_ec :: CSP13 -> IO ()
+handshake13_ec (CSP13 (cli, srv)) = do
+    EC cgrps <- generate arbitrary
+    EC sgrps <- generate arbitrary
+    let cliSupported = (clientSupported cli){supportedGroups = cgrps}
+        svrSupported =
+            (serverSupported srv)
+                { supportedGroups = sgrps
+                , supportedGroupsTLS13 = [sgrps]
+                }
+        params =
+            ( cli{clientSupported = cliSupported}
+            , srv{serverSupported = svrSupported}
+            )
+    runTLSSimple13 params FullHandshake
+
+handshake13_ffdhe :: CSP13 -> IO ()
+handshake13_ffdhe (CSP13 (cli, srv)) = do
+    FFDHE cgrps <- generate arbitrary
+    FFDHE sgrps <- generate arbitrary
+    let cliSupported = (clientSupported cli){supportedGroups = cgrps}
+        svrSupported =
+            (serverSupported srv)
+                { supportedGroups = sgrps
+                , supportedGroupsTLS13 = [sgrps]
+                }
+        params =
+            ( cli{clientSupported = cliSupported}
+            , srv{serverSupported = svrSupported}
+            )
+    runTLSSimple13 params FullHandshake
+
+-- An all-zero X25519 public key decodes, but the shared secret computed
+-- from it is all zero and is rejected.  The server must answer with
+-- illegal_parameter, as it does for X25519 alone, rather than crash.
+handshake13_x25519mlkem768_zero_x25519 :: IO ()
+handshake13_x25519mlkem768_zero_x25519 = do
+    CSP13 (cli, srv) <- generate arbitrary
+    let cliSupported =
+            (clientSupported cli){supportedGroups = [X25519MLKEM768]}
+        svrSupported =
+            (serverSupported srv)
+                { supportedGroups = [X25519MLKEM768]
+                , supportedGroupsTLS13 = [[X25519MLKEM768]]
+                }
+        params =
+            ( cli{clientSupported = cliSupported}
+            , srv{serverSupported = svrSupported}
+            )
+    withPairContextWith (id, id) params $ \(cctx, sctx) -> do
+        contextHookSetHandshakeRecv sctx zeroX25519
+        concurrently_
+            (handshake sctx `shouldThrow` serverRejectedZeroX25519)
+            (handshake cctx `shouldThrow` anyTLSException)
+  where
+    zeroX25519 (ClientHello ch) =
+        pure $ ClientHello ch{chExtensions = map zeroKeyShare $ chExtensions ch}
+    zeroX25519 hs = pure hs
+    zeroKeyShare ext@(ExtensionRaw eid bs)
+        | eid == EID_KeyShare
+        , Just (KeyShareClientHello kses) <- extensionDecode MsgTClientHello bs =
+            toExtensionRaw $ KeyShareClientHello $ map zeroEntry kses
+        | otherwise = ext
+    -- The ML-KEM-768 encapsulation key (1184 bytes) is followed by the
+    -- X25519 public key (32 bytes).
+    zeroEntry (KeyShareEntry grp key)
+        | grp == X25519MLKEM768 =
+            KeyShareEntry grp $ B.take 1184 key <> B.replicate 32 0
+    zeroEntry kse = kse
+
+serverRejectedZeroX25519 :: TLSException -> Bool
+serverRejectedZeroX25519 (HandshakeFailed (Error_Protocol msg alert)) =
+    msg == "invalid client X25519MLKEM768 public key"
+        && alert == IllegalParameter
+serverRejectedZeroX25519 _ = False
+
+post_handshake_auth :: CSP13 -> IO ()
+post_handshake_auth (CSP13 (clientParam, serverParam)) = do
+    cred <- generate (arbitraryClientCredential TLS13)
+    let clientParam' =
+            clientParam
+                { clientHooks =
+                    (clientHooks clientParam)
+                        { onCertificateRequest = \_ -> return $ Just cred
+                        }
+                }
+        serverParam' =
+            serverParam
+                { serverHooks =
+                    (serverHooks serverParam)
+                        { onClientCertificate = validateChain cred
+                        }
+                }
+    if isCredentialDSA cred
+        then runTLSFailure (clientParam', serverParam') hsClient hsServer
+        else runTLSSuccess (clientParam', serverParam') hsClient hsServer
+  where
+    validateChain cred chain
+        | chain == fst cred = return CertificateUsageAccept
+        | otherwise = return (CertificateUsageReject CertificateRejectUnknownCA)
+    hsClient ctx = do
+        handshake ctx
+        sendData ctx "request 1"
+        recvDataAssert ctx "response 1"
+        sendData ctx "request 2"
+        recvDataAssert ctx "response 2"
+    hsServer ctx = do
+        handshake ctx
+        recvDataAssert ctx "request 1"
+        _ <- requestCertificate ctx -- single request
+        sendData ctx "response 1"
+        recvDataAssert ctx "request 2"
+        _ <- requestCertificate ctx
+        _ <- requestCertificate ctx -- two simultaneously
+        sendData ctx "response 2"
+
+-- | After sending a Certificate message, a TLS 1.3 client peeks for a
+-- client-authentication alert with a deadline of a few RTTs.  That peek must
+-- not abandon a record it has already started reading: the record layer has no
+-- receive buffer, so the bytes consumed for the record header would be lost and
+-- the caller's next 'recvData' would decode part of a record body as a header.
+--
+-- Here the server's first write is one full-size record whose body is made to
+-- arrive late, so the peek does hit its deadline with the header already
+-- consumed.  Before the fix this failed with
+-- @Error_Protocol "record exceeding maximum size" RecordOverflow@.
+handshake13_client_auth_slow_record :: IO ()
+handshake13_client_auth_slow_record = do
+    (clientParam, serverParam) <- generate arbitraryPairParams13
+    cred <- generate (arbitraryClientCredential TLS13)
+    let clientParam' =
+            clientParam
+                { clientHooks =
+                    (clientHooks clientParam)
+                        { onCertificateRequest = \_ -> return $ Just cred
+                        }
+                }
+        serverParam' =
+            serverParam
+                { serverWantClientCert = True
+                , serverHooks =
+                    (serverHooks serverParam)
+                        { onClientCertificate = \_ -> return CertificateUsageAccept
+                        }
+                }
+        payload = B.replicate 16384 65
+    withPairContextWith (delayBigReads, id) (clientParam', serverParam') $
+        \(cCtx, sCtx) ->
+            concurrently_
+                ( do
+                    handshake sCtx
+                    sendData sCtx $ L.fromStrict payload
+                )
+                ( do
+                    handshake cCtx
+                    recvData cCtx `shouldReturn` payload
+                )
+  where
+    -- Only the body of the big record is held back; every handshake record is
+    -- far smaller than this threshold and so arrives immediately.
+    delayBigReads be =
+        be
+            { backendRecv = \n -> do
+                when (n > 4096) $ threadDelay 300000
+                backendRecv be n
+            }
+
+-- A protected record too short for the cipher cannot be deprotected, which
+-- RFC 5246 Section 7.2.2 and RFC 8446 Section 5.2 answer with
+-- bad_record_mac.  The client's first application data record is cut down
+-- to a single byte of ciphertext on its way to the server.
+short_record_bad_record_mac :: Version -> Cipher -> IO ()
+short_record_bad_record_mac version cipher = do
+    (clientParam, serverParam) <-
+        generate $
+            arbitraryPairParamsWithVersionsAndCiphers
+                ([version], [version])
+                ([cipher], [cipher])
+    armed <- newIORef False
+    let truncateRecord be =
+            be
+                { backendSend = \bs -> do
+                    cut <- readIORef armed
+                    if cut && B.length bs > 6 && B.head bs == 23
+                        then do
+                            writeIORef armed False
+                            backendSend be $ B.take 3 bs <> B.pack [0, 1] <> B.take 1 (B.drop 5 bs)
+                        else backendSend be bs
+                }
+    withPairContextWith (truncateRecord, id) (clientParam, serverParam) $
+        \(cctx, sctx) ->
+            concurrently_
+                ( do
+                    handshake sctx
+                    recvData sctx `shouldThrow` serverRejectedShortRecord
+                )
+                ( do
+                    handshake cctx
+                    writeIORef armed True
+                    sendData cctx "hello"
+                    void (E.try (recvData cctx) :: IO (Either E.SomeException B.ByteString))
+                )
+
+serverRejectedShortRecord :: TLSException -> Bool
+serverRejectedShortRecord (Terminated _ _ (Error_Protocol _ BadRecordMac)) = True
+serverRejectedShortRecord _ = False
+
+-- The first handshake message a server receives must be a ClientHello; any
+-- other is out of order and answered with unexpected_message (RFC 8446
+-- Section 4), whatever its body would decode to.  The handshake type of the
+-- client's first record is replaced on its way to the server.
+server_first_message_unexpected :: Word8 -> IO ()
+server_first_message_unexpected ty = do
+    (clientParam, serverParam) <- generate arbitrary
+    armed <- newIORef True
+    let retype be =
+            be
+                { backendSend = \bs -> do
+                    first <- atomicModifyIORef' armed (\a -> (False, a))
+                    if first && B.length bs > 5 && B.head bs == 22
+                        then backendSend be $ B.take 5 bs <> B.singleton ty <> B.drop 6 bs
+                        else backendSend be bs
+                }
+    withPairContextWith (retype, id) (clientParam, serverParam) $ \(cctx, sctx) ->
+        concurrently_
+            (handshake sctx `shouldThrow` serverRejectedUnexpectedFirst)
+            (handshake cctx `shouldThrow` anyTLSException)
+
+-- ChangeCipherSpec comes between complete handshake messages (RFC 5246
+-- Section 7.1), and a server has no handshake state before its first
+-- ClientHello (CVE-2004-0079).  The client's first record is split after
+-- the given number of bytes of its body, and a ChangeCipherSpec record is
+-- sent in between; with 0 it is sent before the whole ClientHello.
+server_ccs_interleaved :: Int -> IO ()
+server_ccs_interleaved n = do
+    (clientParam, serverParam) <- generate arbitrary
+    armed <- newIORef True
+    let interleave be =
+            be
+                { backendSend = \bs -> do
+                    first <- atomicModifyIORef' armed (\a -> (False, a))
+                    if first && B.length bs > 5 + n && B.head bs == 22
+                        then do
+                            let (hdr, body) = B.splitAt 5 bs
+                                (body1, body2) = B.splitAt n body
+                                record b = B.take 3 hdr <> encodeWord16 (fromIntegral $ B.length b) <> b
+                                ccs = B.pack [20, 3, 3, 0, 1, 1]
+                            if n == 0
+                                then backendSend be $ ccs <> bs
+                                else backendSend be $ record body1 <> ccs <> record body2
+                        else backendSend be bs
+                }
+    withPairContextWith (interleave, id) (clientParam, serverParam) $ \(cctx, sctx) ->
+        concurrently_
+            (handshake sctx `shouldThrow` serverRejectedUnexpectedFirst)
+            (handshake cctx `shouldThrow` anyTLSException)
+
+-- An unknown record type is answered with unexpected_message (RFC 8446
+-- Section 5).  The type of the client's first record is replaced; with a
+-- length larger than what follows, as an SSLv2 ClientHello reads when taken
+-- for a TLS record header, the server must answer from the header alone
+-- rather than wait for a body that never comes.  A length of 0 keeps the
+-- original one.
+server_first_record_type_unexpected :: Word8 -> Word16 -> IO ()
+server_first_record_type_unexpected ty len = do
+    (clientParam, serverParam) <- generate arbitrary
+    armed <- newIORef True
+    let retype be =
+            be
+                { backendSend = \bs -> do
+                    first <- atomicModifyIORef' armed (\a -> (False, a))
+                    if first && B.length bs > 5 && B.head bs == 22
+                        then do
+                            let lenBytes
+                                    | len == 0 = B.take 2 (B.drop 3 bs)
+                                    | otherwise =
+                                        B.pack [fromIntegral (len `div` 256), fromIntegral (len `mod` 256)]
+                            backendSend be $
+                                B.singleton ty <> B.take 2 (B.drop 1 bs) <> lenBytes <> B.drop 5 bs
+                        else backendSend be bs
+                }
+    r <- timeout 5000000 $
+        withPairContextWith (retype, id) (clientParam, serverParam) $ \(cctx, sctx) ->
+            concurrently_
+                (handshake sctx `shouldThrow` serverRejectedUnexpectedFirst)
+                (handshake cctx `shouldThrow` anyTLSException)
+    r `shouldSatisfy` isJust
+
+-- RFC 8446 Section 5.1: handshake messages MUST NOT be interleaved with
+-- other record types.  After the handshake the client sends, protected under
+-- its application traffic secret, a record holding only the first two bytes
+-- of a KeyUpdate and then a record of application data.  The server must not
+-- deliver the data and must answer with unexpected_message.
+handshake13_interleaved_app_data :: IO ()
+handshake13_interleaved_app_data = do
+    let cipher = cipher13_AES_128_GCM_SHA256
+    (clientParam, serverParam) <-
+        generate $
+            arbitraryPairParamsWithVersionsAndCiphers
+                ([TLS13], [TLS13])
+                ([cipher], [cipher])
+    secretRef <- newIORef Nothing
+    sendRef <- newIORef (\_ -> return ())
+    let logKey line = case words line of
+            ["CLIENT_TRAFFIC_SECRET_0", _, h] -> writeIORef secretRef (Just h)
+            _ -> return ()
+        clientParam' =
+            clientParam
+                { clientDebug = (clientDebug clientParam){debugKeyLogger = logKey}
+                }
+        -- remember the raw sender so that records can be written by hand
+        capture be =
+            be
+                { backendSend = \bs -> do
+                    writeIORef sendRef (backendSend be)
+                    backendSend be bs
+                }
+    withPairContextWith (capture, id) (clientParam', serverParam) $ \(cctx, sctx) ->
+        concurrently_
+            ( do
+                handshake sctx
+                r <- E.try (recvData sctx) :: IO (Either TLSException B.ByteString)
+                case r of
+                    Right d -> expectationFailure $ "server delivered " ++ show d
+                    Left _ -> return ()
+            )
+            ( do
+                handshake cctx
+                Just h <- readIORef secretRef
+                send <- readIORef sendRef
+                let secret = BA.convert (unhex h) :: BA.ScrubbedBytes
+                    key = hkdfExpandLabel SHA256 secret "key" "" 16 :: B.ByteString
+                    iv = hkdfExpandLabel SHA256 secret "iv" "" 12 :: B.ByteString
+                    -- the first two bytes of KeyUpdate(update_not_requested)
+                    partialKeyUpdate = B.pack [24, 0]
+                send $ protect13 key iv 0 22 partialKeyUpdate
+                send $ protect13 key iv 1 23 "hello"
+                recvData cctx `shouldThrow` peerSentUnexpected
+            )
+  where
+    unhex [] = B.empty
+    unhex (a : b : rest) = B.cons (read ['0', 'x', a, b]) (unhex rest)
+    unhex _ = error "unhex"
+
+-- A ChangeCipherSpec is the single byte 1; any other value is answered with
+-- unexpected_message (RFC 8446 Section 5), and so is one carrying two of
+-- them.  The client's ChangeCipherSpec record is made two bytes long.  The
+-- groups are fixed so that a TLS 1.3 handshake goes through a
+-- HelloRetryRequest -- after which the client sends its ChangeCipherSpec
+-- before the second ClientHello -- or not, as the test asks.
+malformed_ccs_unexpected :: Version -> Cipher -> [Group] -> [Group] -> IO ()
+malformed_ccs_unexpected version cipher cgroups sgroups = do
+    (clientParam0, serverParam0) <-
+        generate $
+            arbitraryPairParamsWithVersionsAndCiphers
+                ([version], [version])
+                ([cipher], [cipher])
+    let clientParam =
+            clientParam0
+                { clientSupported = (clientSupported clientParam0){supportedGroups = cgroups}
+                }
+        serverParam =
+            serverParam0
+                { serverSupported =
+                    (serverSupported serverParam0)
+                        { supportedGroups = sgroups
+                        , supportedGroupsTLS13 = [sgroups]
+                        }
+                }
+    seen <- newIORef False
+    let ccs = B.pack [20, 3, 3, 0, 1, 1]
+        doubled = B.pack [20, 3, 3, 0, 2, 1, 1]
+        double be =
+            be
+                { backendSend = \bs ->
+                    if ccs `B.isPrefixOf` bs
+                        then do
+                            writeIORef seen True
+                            backendSend be $ doubled <> B.drop 6 bs
+                        else backendSend be bs
+                }
+    -- a TLS 1.3 server takes the client's ChangeCipherSpec and Finished in
+    -- its first recvData, a TLS 1.2 one in handshake
+    r <- timeout 10000000 $
+        withPairContextWith (double, id) (clientParam, serverParam) $ \(cctx, sctx) ->
+            concurrently_
+                ((handshake sctx >> recvData sctx) `shouldThrow` rejectedAsUnexpected)
+                ( void
+                    (E.try (handshake cctx >> recvData cctx) :: IO (Either TLSException B.ByteString))
+                )
+    r `shouldSatisfy` isJust
+    readIORef seen `shouldReturn` True
+
+rejectedAsUnexpected :: TLSException -> Bool
+rejectedAsUnexpected (HandshakeFailed (Error_Packet_unexpected _ _)) = True
+rejectedAsUnexpected (Terminated _ _ (Error_Packet_unexpected _ _)) = True
+rejectedAsUnexpected _ = False
+
+-- An AES-128-GCM TLS 1.3 record: content and inner type, protected with the
+-- record header as additional data and the sequence number in the nonce.
+protect13 :: B.ByteString -> B.ByteString -> Word64 -> Word8 -> B.ByteString -> B.ByteString
+protect13 key iv sqn innerType content = hdr <> ct <> BA.convert tag
+  where
+    len = B.length content + 1 + 16
+    hdr = B.pack [23, 3, 3, fromIntegral (len `div` 256), fromIntegral (len `mod` 256)]
+    sqnBytes = B.pack [fromIntegral (sqn `shiftR` (8 * i)) | i <- [7, 6 .. 0]]
+    nonce = B.pack $ B.zipWith xor iv (B.replicate 4 0 <> sqnBytes)
+    aes = throwCryptoError (cipherInit key) :: AES128
+    aead = throwCryptoError (aeadInit AEAD_GCM aes nonce)
+    (AuthTag tag, ct) = aeadSimpleEncrypt aead hdr (content <> B.singleton innerType) 16
+
+peerSentUnexpected :: TLSException -> Bool
+peerSentUnexpected (Terminated True _ (Error_Protocol _ UnexpectedMessage)) = True
+peerSentUnexpected _ = False
+
+serverRejectedUnexpectedFirst :: TLSException -> Bool
+serverRejectedUnexpectedFirst (HandshakeFailed (Error_Packet_unexpected _ _)) = True
+serverRejectedUnexpectedFirst _ = False
 
 expectJust :: String -> Maybe a -> Expectation
 expectJust tag mx = case mx of
diff --git a/test/Run.hs b/test/Run.hs
--- a/test/Run.hs
+++ b/test/Run.hs
@@ -16,6 +16,9 @@
     runTLSFailure,
     expectMaybe,
     newPairContext,
+    newPairContextWith,
+    withPairContext,
+    withPairContextWith,
     withDataPipe,
     byeBye,
 ) where
@@ -156,6 +159,16 @@
         handshake ctx
         sendData ctx $ L.fromStrict earlyData
         _ <- recvData ctx
+        -- One more exchange, and this one the client starts.  Our Finished is
+        -- not sent by 'handshake' here: 0-RTT defers it, and the receive loop
+        -- above is what puts it on the wire.  The server emits the
+        -- NewSessionTicket when it reads that Finished, which is after it sent
+        -- the echo -- so reading the echo is not enough to have seen the
+        -- ticket, and neither is a byte the server sends straight after it.
+        -- The server cannot answer this without having read past the Finished
+        -- first, and records arrive in order.
+        sendData ctx "x"
+        recvDataAssert ctx "x"
         bye ctx
         mmode <- (>>= infoTLS13HandshakeMode) <$> contextGetInformation ctx
         expectMaybe "C: mode should be Just" mode mmode
@@ -165,6 +178,8 @@
         chunks <- replicateM (length ls) $ recvData ctx
         (map B.length chunks, B.concat chunks) `shouldBe` (ls, earlyData)
         sendData ctx $ L.fromStrict earlyData
+        recvDataAssert ctx "x"
+        sendData ctx "x"
         bye ctx
         mmode <- (>>= infoTLS13HandshakeMode) <$> contextGetInformation ctx
         expectMaybe "S: mode should be Just" mode mmode
@@ -187,6 +202,16 @@
         handshake ctx
         sendData ctx $ L.fromStrict earlyData
         _ <- recvData ctx
+        -- One more exchange, and this one the client starts.  Our Finished is
+        -- not sent by 'handshake' here: 0-RTT defers it, and the receive loop
+        -- above is what puts it on the wire.  The server emits the
+        -- NewSessionTicket when it reads that Finished, which is after it sent
+        -- the echo -- so reading the echo is not enough to have seen the
+        -- ticket, and neither is a byte the server sends straight after it.
+        -- The server cannot answer this without having read past the Finished
+        -- first, and records arrive in order.
+        sendData ctx "x"
+        recvDataAssert ctx "x"
         bye ctx
         minfo <- contextGetInformation ctx
         let mmode = minfo >>= infoTLS13HandshakeMode
@@ -199,6 +224,8 @@
         chunks <- replicateM (length ls) $ recvData ctx
         (map B.length chunks, B.concat chunks) `shouldBe` (ls, earlyData)
         sendData ctx $ L.fromStrict earlyData
+        recvDataAssert ctx "x"
+        sendData ctx "x"
         bye ctx
         mmode <- (>>= infoTLS13HandshakeMode) <$> contextGetInformation ctx
         expectMaybe "S: mode should be Just" mode mmode
@@ -295,11 +322,15 @@
         hsClient ctx
         d <- readChan queue
         sendData ctx (L.fromChunks [d])
+        -- The server writes after any TLS 1.3 NewSessionTicket, so waiting for
+        -- this byte ensures the client session manager received the ticket.
+        recvDataAssert ctx "x"
         checkCtxFinished ctx
         bye ctx
     tlsServer ctx queue = do
         hsServer ctx
         d <- recvData ctx
+        sendData ctx "x"
         writeChan queue [d]
         checkCtxFinished ctx
         bye ctx
@@ -326,16 +357,32 @@
 
 withPairContext
     :: (ClientParams, ServerParams) -> ((Context, Context) -> IO ()) -> IO ()
-withPairContext params body =
+withPairContext = withPairContextWith (id, id)
+
+withPairContextWith
+    :: (Backend -> Backend, Backend -> Backend)
+    -> (ClientParams, ServerParams)
+    -> ((Context, Context) -> IO ())
+    -> IO ()
+withPairContextWith wrapBackends params body =
     E.bracket
-        (newPairContext params)
+        (newPairContextWith wrapBackends params)
         (\((t1, t2), _) -> killThread t1 >> killThread t2)
         (\(_, ctxs) -> body ctxs)
 
 newPairContext
     :: (ClientParams, ServerParams)
     -> IO ((ThreadId, ThreadId), (Context, Context))
-newPairContext (cParams, sParams) = do
+newPairContext = newPairContextWith (id, id)
+
+-- | 'newPairContext' with a hook on each side's 'Backend' -- client first, as
+-- with the parameters -- so that a test can control how bytes arrive (delay
+-- them, split them).  Pass 'id' for a side to leave it alone.
+newPairContextWith
+    :: (Backend -> Backend, Backend -> Backend)
+    -> (ClientParams, ServerParams)
+    -> IO ((ThreadId, ThreadId), (Context, Context))
+newPairContextWith (wrapCBackend, wrapSBackend) (cParams, sParams) = do
     pipe <- newPipe
     tids <- runPipe pipe
     let noFlush = return ()
@@ -343,8 +390,8 @@
 
     let cBackend = Backend noFlush noClose (writePipeC pipe) (readPipeC pipe)
     let sBackend = Backend noFlush noClose (writePipeS pipe) (readPipeS pipe)
-    cCtx' <- contextNew cBackend cParams
-    sCtx' <- contextNew sBackend sParams
+    cCtx' <- contextNew (wrapCBackend cBackend) cParams
+    sCtx' <- contextNew (wrapSBackend sBackend) sParams
 
     contextHookSetLogging cCtx' (logging "client: ")
     contextHookSetLogging sCtx' (logging "server: ")
diff --git a/test/SecretSpec.hs b/test/SecretSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/SecretSpec.hs
@@ -0,0 +1,63 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module SecretSpec where
+
+import Crypto.Debug (debugShow)
+import qualified Data.ByteArray as BA
+import qualified Data.ByteString as B
+import Data.List (isInfixOf)
+import Network.TLS (Version (TLS13))
+import Network.TLS.Extra.Cipher (ciphersuite_default)
+import Network.TLS.Internal (SessionData (..))
+import Network.TLS.QUIC
+import Test.Hspec
+
+-- | The traffic secrets reach a QUIC implementation through
+-- 'quicInstallKeys', so a trace of what it is handed must not write them to a
+-- log.  'debugShow' is how a debugging session asks for them on purpose.
+spec :: Spec
+spec = do
+    describe "Show of the QUIC secret types" $ do
+        it "does not print an early traffic secret" $
+            check $
+                EarlySecretInfo cipher clientSecret
+        it "does not print the handshake traffic secrets" $
+            check $
+                HandshakeSecretInfo cipher (clientSecret, serverSecret)
+        it "does not print the application traffic secrets" $
+            check $
+                ApplicationSecretInfo (clientSecret, serverSecret)
+    describe "Show of a resumable session" $
+        it "does not print the session secret" $ do
+            let shown = show sessionData
+            show (B.replicate 32 0xa5) `isInfixOf` shown `shouldBe` False
+            "<secret>" `isInfixOf` shown `shouldBe` True
+            show (B.replicate 32 0xa5)
+                `isInfixOf` debugShow sessionData
+                `shouldBe` True
+  where
+    sessionData =
+        SessionData
+            { sessionVersion = TLS13
+            , sessionCipher = 0x1301
+            , sessionCompression = 0
+            , sessionClientSNI = Just "example.com"
+            , sessionSecret = B.replicate 32 0xa5
+            , sessionGroup = Nothing
+            , sessionTicketInfo = Nothing
+            , sessionALPN = Nothing
+            , sessionMaxEarlyDataSize = 0
+            , sessionFlags = []
+            }
+    cipher = case ciphersuite_default of
+        c : _ -> c
+        [] -> error "ciphersuite_default is empty"
+    clientSecret = ClientTrafficSecret $ BA.convert $ B.replicate 32 0xa5
+    serverSecret = ServerTrafficSecret $ BA.convert $ B.replicate 32 0x5a
+    hexOf w = concat $ replicate 32 w
+    check x = do
+        let shown = show x
+        hexOf "a5" `isInfixOf` shown `shouldBe` False
+        hexOf "5a" `isInfixOf` shown `shouldBe` False
+        "<secret>" `isInfixOf` shown `shouldBe` True
+        hexOf "a5" `isInfixOf` debugShow x `shouldBe` True
diff --git a/tls.cabal b/tls.cabal
--- a/tls.cabal
+++ b/tls.cabal
@@ -1,6 +1,6 @@
-cabal-version:      >=1.10
+cabal-version:      2.0
 name:               tls
-version:            2.3.1
+version:            2.4.9
 license:            BSD3
 license-file:       LICENSE
 copyright:          Vincent Hanquez <vincent@snarc.org>
@@ -34,6 +34,7 @@
         Network.TLS.Internal
         Network.TLS.Extra
         Network.TLS.Extra.Cipher
+        Network.TLS.Extra.CipherCBC
         Network.TLS.Extra.FFDHE
         Network.TLS.QUIC
 
@@ -121,7 +122,7 @@
         base16-bytestring,
         bytestring >=0.10 && <0.13,
         cereal >=0.5.3 && <0.6,
-        crypton >=1.1.2 && <1.2,
+        crypton >=2.1.1 && <2.2,
         crypton-asn1-encoding >= 0.10.0 && < 0.11,
         crypton-asn1-types >= 0.4.1 && < 0.5,
         crypton-x509 >=1.9 && <1.10,
@@ -129,7 +130,7 @@
         crypton-x509-validation >=1.9 && <1.10,
         data-default,
         ech-config,
-        hpke >=0.1.0 && <0.2,
+        hpke >=0.1.0 && <0.3,
         mlkem >= 0.2.0 && <0.3,
         mtl >=2.2 && <2.4,
         network >=3.1,
@@ -137,7 +138,7 @@
         random >=1.2 && <1.4,
         serialise >=0.2 && <0.3,
         transformers >=0.5 && <0.7,
-        unix-time >=0.4.11 && <0.5,
+        unix-time >=0.4.11 && <0.6,
         zlib >=0.7 && <0.8
 
 executable tls-server
@@ -189,7 +190,7 @@
         crypton-x509-system,
         ech-config,
         network,
-        network-run >=0.5,
+        network-run >=0.6.0 && < 0.7,
         tls
 
     if flag(devel)
@@ -213,6 +214,7 @@
         PipeChan
         PubKey
         Run
+        SecretSpec
         Session
         ThreadSpec
 
@@ -234,7 +236,8 @@
         ram,
         serialise,
         time-hourglass,
-        tls
+        tls,
+        zlib
 
 benchmark tls-bench
     type:             exitcode-stdio-1.0
@@ -273,6 +276,7 @@
         hspec,
         network,
         network-run,
+        ram,
         serialise,
         tasty-bench,
         time-hourglass,
diff --git a/util/Common.hs b/util/Common.hs
--- a/util/Common.hs
+++ b/util/Common.hs
@@ -111,7 +111,7 @@
     minfo <- contextGetInformation ctx
     case minfo of
         Nothing -> do
-            putStrLn "Erro: information cannot be obtained"
+            putStrLn "Error: information cannot be obtained"
             exitFailure
         Just info -> return info
 
diff --git a/util/Server.hs b/util/Server.hs
--- a/util/Server.hs
+++ b/util/Server.hs
@@ -28,15 +28,18 @@
   where
     body = "<html><<body>Hello world!</body></html>"
 
+-- An HTTP request is answered with HTML, anything else is echoed.
+-- tlsfuzzer's test-lengths.py sends "A..A\n", or just "\n", and expects
+-- it back.
 server :: Context -> Bool -> IO ()
 server ctx showRequest = do
     bs <- recvData ctx
     case C8.uncons bs of
         Nothing -> return ()
-        Just ('A', _) -> do
+        Just ('G', _) -> handleHTML ctx showRequest bs
+        Just _ -> do
             sendData ctx $ CL8.fromStrict bs
             echo ctx
-        Just _ -> handleHTML ctx showRequest bs
 
 echo :: Context -> IO ()
 echo ctx = loop
diff --git a/util/tls-server.hs b/util/tls-server.hs
--- a/util/tls-server.hs
+++ b/util/tls-server.hs
@@ -4,12 +4,16 @@
 
 module Main where
 
+import qualified Control.Exception as E
 import Data.IORef
 import qualified Data.Map.Strict as M
 import Data.X509.CertificateStore
 import Network.Run.TCP
 import Network.TLS
 import Network.TLS.ECH.Config
+import Network.TLS.Extra.Cipher
+import Network.TLS.Extra.CipherCBC
+import Network.TLS.Extra.FFDHE
 import Network.TLS.Internal
 import System.Console.GetOpt
 import System.Environment (getArgs)
@@ -27,12 +31,14 @@
     , optShow :: Bool
     , optKeyLogFile :: Maybe FilePath
     , optTrustedAnchor :: Maybe FilePath
-    , optGroups :: [Group]
+    , optGroups :: Maybe [Group]
     , optCertFile :: FilePath
     , optKeyFile :: FilePath
     , optECHConfigFile :: Maybe FilePath
     , optECHKeyFile :: Maybe FilePath
     , optTraceKey :: Bool
+    , optUseWeakCiphers :: Bool
+    , optServerName :: Maybe HostName
     }
     deriving (Show)
 
@@ -44,13 +50,14 @@
         , optShow = False
         , optKeyLogFile = Nothing
         , optTrustedAnchor = Nothing
-        , -- excluding FFDHE8192 for retry
-          optGroups = FFDHE8192 `delete` supportedGroups defaultSupported
+        , optGroups = Nothing
         , optCertFile = "servercert.pem"
         , optKeyFile = "serverkey.pem"
         , optECHConfigFile = Nothing
         , optECHKeyFile = Nothing
         , optTraceKey = False
+        , optUseWeakCiphers = False
+        , optServerName = Nothing
         }
 
 options :: [OptDescr (Options -> Options)]
@@ -78,7 +85,7 @@
     , Option
         ['g']
         ["groups"]
-        (ReqArg (\gs o -> o{optGroups = readGroups gs}) "<groups>")
+        (ReqArg (\gs o -> o{optGroups = Just $ readGroups gs}) "<groups>")
         "groups for key exchange"
     , Option
         ['c']
@@ -110,6 +117,16 @@
         ["trace-key"]
         (NoArg (\o -> o{optTraceKey = True}))
         "Trace transcript hash"
+    , Option
+        []
+        ["use-weak-ciphers"]
+        (NoArg (\o -> o{optUseWeakCiphers = True}))
+        "accept deprecated ciphers and relax checks (for tlsfuzzer)"
+    , Option
+        []
+        ["server-name"]
+        (ReqArg (\n o -> o{optServerName = Just n}) "<name>")
+        "refuse other names in SNI with unrecognized_name"
     ]
 
 usage :: String
@@ -135,7 +152,12 @@
     (host, port) <- case ips of
         [h, p] -> return (h, p)
         _ -> showUsageAndExit "cannot recognize <addr> and <port>\n"
-    when (null optGroups) $ do
+    let groups = fromMaybe defaultGroups optGroups
+        defaultGroups
+            | optUseWeakCiphers = supportedGroups defaultSupported
+            -- excluding FFDHE8192 for retry
+            | otherwise = FFDHE8192 `delete` supportedGroups defaultSupported
+    when (null groups) $ do
         putStrLn "Error: unsupported groups"
         exitFailure
     smgr <- newSessionManager
@@ -169,7 +191,8 @@
         let sparams =
                 getServerParams
                     creds
-                    optGroups
+                    optUseWeakCiphers
+                    groups
                     smgr
                     keyLog
                     optClientAuth
@@ -177,6 +200,7 @@
                     ech
                     printError
                     traceKey
+                    optServerName
         ctx <- contextNew sock sparams
         when optDebugLog $
             contextHookSetLogging
@@ -196,6 +220,7 @@
 
 getServerParams
     :: Credentials
+    -> Bool
     -> [Group]
     -> SessionManager
     -> (String -> IO ())
@@ -204,8 +229,9 @@
     -> ([(Word8, ByteString)], ECHConfigList)
     -> (String -> IO ())
     -> (String -> IO ())
+    -> Maybe HostName
     -> ServerParams
-getServerParams creds groups sm keyLog clientAuth mstore (ekey, ecnf) printError traceKey =
+getServerParams creds weak groups sm keyLog clientAuth mstore (ekey, ecnf) printError traceKey mname =
     defaultParamsServer
         { serverSupported = supported
         , serverShared = shared
@@ -214,6 +240,7 @@
         , serverEarlyDataSize = 2048
         , serverWantClientCert = clientAuth
         , serverECHKey = ekey
+        , serverDHEParams = if weak then Just ffdhe2048 else Nothing
         }
   where
     shared =
@@ -231,15 +258,26 @@
             }
     supported =
         defaultSupported
-            { supportedGroups = groups
+            { supportedCiphers = ciphers
+            , supportedGroups = groups
+            , supportedExtendedMainSecret =
+                if weak then AllowEMS else supportedExtendedMainSecret defaultSupported
+            , supportedClientInitiatedRenegotiation =
+                weak || supportedClientInitiatedRenegotiation defaultSupported
             }
+    ciphers
+        | weak = ciphersuite_default ++ ciphersForFuzzer
+        | otherwise = ciphersuite_default
     hooks =
         defaultServerHooks
-            { onALPNClientSuggest = Just chooseALPN
+            { onALPNClientSuggest = Just $ chooseALPN weak
             , onClientCertificate = case mstore of
                 Nothing -> onClientCertificate defaultServerHooks
-                Just _ ->
-                    validateClientCertificate (sharedCAStore shared) (sharedValidationCache shared)
+                Just _
+                    | weak -> acceptEmptyCertificate
+                    | otherwise ->
+                        validateClientCertificate (sharedCAStore shared) (sharedValidationCache shared)
+            , onServerNameIndication = checkServerName mname
             }
     debug =
         defaultDebugParams
@@ -247,12 +285,122 @@
             , debugError = printError
             , debugTraceKey = traceKey
             }
+    acceptEmptyCertificate cc
+        | isNullCertificateChain cc = return CertificateUsageAccept
+        | otherwise =
+            validateClientCertificate
+                (sharedCAStore shared)
+                (sharedValidationCache shared)
+                cc
 
-chooseALPN :: [ByteString] -> IO ByteString
-chooseALPN protos
-    | "http/1.1" `elem` protos = return "http/1.1"
-    | otherwise = return ""
+----------------------------------------------------------------
+-- Deprecated ciphers, accepted only with --use-weak-ciphers.
+-- tlsfuzzer uses them in its TLS 1.2 tests.
 
+ciphersForFuzzer :: [Cipher]
+ciphersForFuzzer =
+    [ cipher_ECDHE_RSA_WITH_AES_128_CBC_SHA
+    , cipher_DHE_RSA_WITH_AES_128_CBC_SHA
+    , cipher_RSA_WITH_AES_256_CBC_SHA
+    , cipher_RSA_WITH_AES_128_CBC_SHA
+    , cipher_RSA_WITH_AES_128_CBC_SHA256
+    , cipher_DHE_RSA_WITH_AES_128_GCM_SHA256
+    , cipher_RSA_WITH_AES_128_GCM_SHA256
+    , cipher_RSA_WITH_AES_256_GCM_SHA384
+    , cipher_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256
+    , cipher13_AES_128_CCM_8_SHA256
+    ]
+        ++ ciphersuite_pfs_sha2_cbc
+
+-- CBC with HMAC-SHA1, derived from the SHA-2 ones in CipherCBC.
+cipher_RSA_WITH_AES_128_CBC_SHA :: Cipher
+cipher_RSA_WITH_AES_128_CBC_SHA =
+    cipher_DHE_RSA_AES128_SHA256
+        { cipherID = 0x002F
+        , cipherName = "TLS_RSA_WITH_AES_128_CBC_SHA"
+        , cipherHash = SHA1
+        , cipherPRFHash = Nothing
+        , cipherKeyExchange = CipherKeyExchange_RSA
+        , cipherMinVer = Just SSL3
+        }
+
+cipher_RSA_WITH_AES_256_CBC_SHA :: Cipher
+cipher_RSA_WITH_AES_256_CBC_SHA =
+    cipher_DHE_RSA_AES256_SHA256
+        { cipherID = 0x0035
+        , cipherName = "TLS_RSA_WITH_AES_256_CBC_SHA"
+        , cipherHash = SHA1
+        , cipherPRFHash = Nothing
+        , cipherKeyExchange = CipherKeyExchange_RSA
+        , cipherMinVer = Just SSL3
+        }
+
+-- tlsfuzzer's test-atypical-padding.py and test-lengths.py use this one
+-- for an HMAC-SHA256 record.
+cipher_RSA_WITH_AES_128_CBC_SHA256 :: Cipher
+cipher_RSA_WITH_AES_128_CBC_SHA256 =
+    cipher_DHE_RSA_AES128_SHA256
+        { cipherID = 0x003C
+        , cipherName = "TLS_RSA_WITH_AES_128_CBC_SHA256"
+        , cipherKeyExchange = CipherKeyExchange_RSA
+        }
+
+cipher_DHE_RSA_WITH_AES_128_CBC_SHA :: Cipher
+cipher_DHE_RSA_WITH_AES_128_CBC_SHA =
+    cipher_RSA_WITH_AES_128_CBC_SHA
+        { cipherID = 0x0033
+        , cipherName = "TLS_DHE_RSA_WITH_AES_128_CBC_SHA"
+        , cipherKeyExchange = CipherKeyExchange_DHE_RSA
+        , cipherMinVer = Nothing
+        }
+
+cipher_ECDHE_RSA_WITH_AES_128_CBC_SHA :: Cipher
+cipher_ECDHE_RSA_WITH_AES_128_CBC_SHA =
+    cipher_RSA_WITH_AES_128_CBC_SHA
+        { cipherID = 0xC013
+        , cipherName = "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA"
+        , cipherKeyExchange = CipherKeyExchange_ECDHE_RSA
+        , cipherMinVer = Just TLS10
+        }
+
+-- AES-GCM with RSA key exchange, derived from the DHE ones.
+cipher_RSA_WITH_AES_128_GCM_SHA256 :: Cipher
+cipher_RSA_WITH_AES_128_GCM_SHA256 =
+    cipher_DHE_RSA_WITH_AES_128_GCM_SHA256
+        { cipherID = 0x009C
+        , cipherName = "TLS_RSA_WITH_AES_128_GCM_SHA256"
+        , cipherKeyExchange = CipherKeyExchange_RSA
+        }
+
+cipher_RSA_WITH_AES_256_GCM_SHA384 :: Cipher
+cipher_RSA_WITH_AES_256_GCM_SHA384 =
+    cipher_DHE_RSA_WITH_AES_256_GCM_SHA384
+        { cipherID = 0x009D
+        , cipherName = "TLS_RSA_WITH_AES_256_GCM_SHA384"
+        , cipherKeyExchange = CipherKeyExchange_RSA
+        }
+
+-- Only HTTP/1.1 is spoken.  With --use-weak-ciphers, the names
+-- tlsfuzzer's test-alpn-negotiation.py switches to on renegotiation and
+-- resumption are accepted too, in the client's order.
+chooseALPN :: Bool -> [ByteString] -> IO ByteString
+chooseALPN weak protos = return $ fromMaybe "" $ find (`elem` known) protos
+  where
+    known
+        | weak = ["http/1.1", "h2", "http/2"]
+        | otherwise = ["http/1.1"]
+
+-- RFC 6066 Section 3: a server that does not recognize the name may
+-- abort with a fatal unrecognized_name, a warning one being NOT
+-- RECOMMENDED.
+checkServerName :: Maybe HostName -> Maybe HostName -> IO Credentials
+checkServerName (Just name) (Just sni)
+    | sni /= name =
+        E.throwIO $
+            Uncontextualized $
+                Error_Protocol ("unrecognized name: " ++ sni) UnrecognizedName
+checkServerName _ _ = return mempty
+
 newSessionManager :: IO SessionManager
 newSessionManager = do
     ref <- newIORef M.empty
@@ -262,9 +410,11 @@
                 M.lookup key <$> readIORef ref
             , sessionResumeOnlyOnce = \key -> do
                 M.lookup key <$> readIORef ref
-            , sessionEstablish = \key val -> do
-                atomicModifyIORef' ref $ \m -> (M.insert key val m, Nothing)
+            , -- The session ID doubles as the ticket, so the table
+              -- serves resumption by either.
+              sessionEstablish = \key val -> do
+                atomicModifyIORef' ref $ \m -> (M.insert key val m, Just key)
             , sessionInvalidate = \key -> do
                 atomicModifyIORef' ref $ \m -> (M.delete key m, ())
-            , sessionUseTicket = False
+            , sessionUseTicket = True
             }
