{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

module ECHSpec (spec) where

import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import qualified Data.ByteString.Base64 as B64
import qualified Data.ByteString.Lazy as L
import Data.Maybe
import Network.TLS
import Network.TLS.ECH.Config
import Network.TLS.Extra.Cipher
import Network.TLS.Internal
import Test.Hspec
import Test.Hspec.QuickCheck
import Test.QuickCheck

import Arbitrary
import Run
import Session

spec :: Spec
spec = do
    describe "ECH" $ do
        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 EC groups" handshake13_ec
        prop "can handshake with TLS 1.3 FFDHE groups" handshake13_ffdhe
    describe "ECH greasing" $ do
        prop "sends greasing ECH" handshake13_greasing
        prop "sends greasing ECH HRR" handshake13_greasing_hrr

--------------------------------------------------------------

newtype CSP13 = CSP13 (ClientParams, ServerParams) deriving (Show)

instance Arbitrary CSP13 where
    arbitrary = CSP13 <$> arbitraryPairParams13

--------------------------------------------------------------

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]
                }
        params =
            setParams
                ( cli{clientSupported = cliSupported}
                , srv{serverSupported = svrSupported}
                )
    runTLSSimple13ECH 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]
                }
        params =
            setParams
                ( cli{clientSupported = cliSupported}
                , srv{serverSupported = svrSupported}
                )
    runTLSSimple13ECH 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]
                }
        params0 =
            setParams
                ( cli{clientSupported = cliSupported}
                , srv{serverSupported = svrSupported}
                )

    sessionRefs <- twoSessionRefs
    let sessionManagers = twoSessionManagers sessionRefs

    let params = setPairParamsSessionManagers sessionManagers params0

    runTLSSimple13ECH params HelloRetryRequest

    -- and resume
    sessionParams <- readClientSessionRef sessionRefs
    expectJust "session param should be Just" sessionParams
    let params2 = setPairParamsSessionResuming (fromJust sessionParams) params

    runTLSSimple13ECH 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]
                }
        params0 =
            setParams
                ( cli{clientSupported = cliSupported}
                , srv{serverSupported = svrSupported}
                )

    sessionRefs <- twoSessionRefs
    let sessionManagers0 = twoSessionManagers sessionRefs
        sessionManagers = (fst sessionManagers0, oneSessionTicket)

    let params = setPairParamsSessionManagers sessionManagers params0

    runTLSSimple13ECH params HelloRetryRequest

    -- and resume
    sessionParams <- readClientSessionRef sessionRefs
    expectJust "session param should be Just" sessionParams
    let params2 = setPairParamsSessionResuming (fromJust sessionParams) params

    runTLSSimple13ECH 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]
                }
        params0 =
            setParams
                ( cli{clientSupported = cliSupported}
                , srv{serverSupported = svrSupported}
                )

    sessionRefs <- twoSessionRefs
    let sessionManagers = twoSessionManagers sessionRefs

    let params = setPairParamsSessionManagers sessionManagers params0

    runTLSSimple13ECH 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]
                }

    runTLSSimple13ECH (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]
                }
        cliHooks =
            defaultClientHooks
                { onSuggestALPN = return $ Just ["h2"]
                }
        svrHooks =
            defaultServerHooks
                { onALPNClientSuggest = Just (return . unsafeHead)
                }
        params0 =
            setParams
                ( cli
                    { clientSupported = cliSupported
                    , clientHooks = cliHooks
                    }
                , srv
                    { serverSupported = svrSupported
                    , serverHooks = svrHooks
                    , serverEarlyDataSize = 2048
                    }
                )

    sessionRefs <- twoSessionRefs
    let sessionManagers = twoSessionManagers sessionRefs

    let params = setPairParamsSessionManagers sessionManagers params0

    runTLSSimple13ECH 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)

        runTLS0RTTech 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]
                }
        params =
            setParams
                ( 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
    runTLSSimple13ECH 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]
                        }
                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 runTLS0RTTech 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_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}
        params =
            setParams
                ( cli{clientSupported = cliSupported}
                , srv{serverSupported = svrSupported}
                )
    runTLSSimple13ECH 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}
        params =
            setParams
                ( cli{clientSupported = cliSupported}
                , srv{serverSupported = svrSupported}
                )
    runTLSSimple13ECH params FullHandshake

handshake13_greasing :: CSP13 -> IO ()
handshake13_greasing (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]
                }
        params =
            ( cli
                { clientSupported = cliSupported
                , clientUseECH = True
                , clientShared = (clientShared cli){sharedECHConfigList = echConfList}
                }
            , srv{serverSupported = svrSupported}
            )
    (clientMessages, _) <- runTLSCaptureFail params
    let isGreasing (ExtensionRaw eid _) = eid == EID_EncryptedClientHello
        eeMessagesHaveExt =
            [ any isGreasing chExtensions
            | ClientHello CH{..} <- clientMessages
            ]
    eeMessagesHaveExt `shouldBe` [True]

handshake13_greasing_hrr :: CSP13 -> IO ()
handshake13_greasing_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]
                }
        params =
            ( cli
                { clientSupported = cliSupported
                , clientUseECH = True
                , clientShared = (clientShared cli){sharedECHConfigList = echConfList}
                }
            , srv{serverSupported = svrSupported}
            )
    (clientMessages, _) <- runTLSCaptureFail params
    let isGreasing (ExtensionRaw eid _) = eid == EID_EncryptedClientHello
        eeMessagesHaveExt =
            [ any isGreasing chExtensions
            | ClientHello CH{..} <- clientMessages
            ]
    eeMessagesHaveExt `shouldBe` [True, True]

expectJust :: String -> Maybe a -> Expectation
expectJust tag mx = case mx of
    Nothing -> expectationFailure tag
    Just _ -> return ()

setParams :: (ClientParams, ServerParams) -> (ClientParams, ServerParams)
setParams (cli, srv) = (cli', srv')
  where
    cli' =
        cli
            { clientUseECH = True
            , clientShared = (clientShared cli){sharedECHConfigList = echConfList}
            }
    srv' =
        srv
            { serverECHKey = echKey
            , serverShared = (serverShared srv){sharedECHConfigList = echConfList}
            }

echKey :: [(ConfigId, ByteString)]
echKey = [(0, B64.decodeLenient "GAl/YqzDDnssODe5t+2xlQsbSv26kNlfJ0D+nZbK62I=")]

echConfList :: ECHConfigList
echConfList =
    fromJust $
        decodeECHConfigList $
            B64.decodeLenient
                "AEP+DQA/AAAgACDGNVZWrmqQfzAuYGJNa8+OEc6zaUfzd0ltyJQ2y1U2AwAEAAEAAQAQcHVibGljLWxvY2FsaG9zdAAA"
