packages feed

danecheck-1.0.0.0: Dane/Scanner/SMTP/TLS.hs

{-# LANGUAGE OverloadedStrings #-}

module Dane.Scanner.SMTP.TLS
  ( tlsSource
  , tlsSink
  , tlsParams
  , connTLS
  , hasTLS
  , endTLS
  , startTLS
  , tlsInfo
  ) where

import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as LB
import qualified Data.List as L
import qualified Data.X509.CertificateStore as X509
import qualified Network.TLS as TLS
import qualified Network.TLS.Extra as TLS
import qualified Network.TLS.Extra.CipherCBC as TLS
import qualified Streaming.ByteString as Q
import           Control.Applicative ((<|>))
import           Control.Exception (Handler(..), catches, toException)
import           Control.Monad ((>=>))
import           Control.Monad.IO.Class (liftIO)
import           Control.Monad.Trans.Class (lift)
import           Control.Monad.Trans.State.Strict (gets, modify')
import           Data.Default.Class
import           Data.Function ((&))
import           Data.IORef (IORef)
import           Data.Maybe (isJust)
import           Network.Socket (Socket)
import           Network.TLS (Version(TLS12, TLS13))
import           System.Timeout (timeout)

import           Dane.Scanner.Opts (SigAlgs(..), SigAlgGroup(..))
import           Dane.Scanner.SMTP.Certs
import           Dane.Scanner.SMTP.Internal

-- callback args: serviceid fingerprint certificate
nullCache :: TLS.ValidationCache
nullCache = TLS.ValidationCache
  { TLS.cacheQuery = \_ _ _ -> return TLS.ValidationCacheUnknown
  , TLS.cacheAdd   = \_ _ _ -> return ()
  }

tlsParams :: SigAlgs
          -> String
          -> IORef ChainInfo
          -> X509.CertificateStore
          -> TLS.ClientParams
tlsParams sigAlgs host cref store =
  (TLS.defaultParamsClient host "smtp")
      { TLS.clientUseServerNameIndication = True
      , TLS.clientHooks = def
          { TLS.onCertificateRequest = \ _ -> return Nothing
          , TLS.onServerCertificate  = genChainInfo cref
          , TLS.onSuggestALPN        = return Nothing
          , TLS.onSelectKeyShareGroups = chooseKS
          }
      , TLS.clientShared    = def
          { TLS.sharedCAStore            = store
          , TLS.sharedCredentials        = mempty
          , TLS.sharedValidationCache    = nullCache
          , TLS.sharedSessionManager     = TLS.noSessionManager
          }
      , TLS.clientSupported = def & \ d -> d
          { TLS.supportedCiphers = chooseCS
          , TLS.supportedVersions = [TLS13, TLS12]
          , TLS.supportedCompressions = [TLS.nullCompression]
          , TLS.supportedSecureRenegotiation = True
          , TLS.supportedSession = False
          , TLS.supportedFallbackScsv = False
          , TLS.supportedEmptyPacket = True
          , TLS.supportedGroups = chooseGS (TLS.supportedGroups d)
          , TLS.supportedHashSignatures = case sigAlgs of
                SigAlgs [] -> TLS.supportedHashSignatures d
                SigAlgs gs -> concatMap sigAlgsFor gs
          }
      , TLS.clientWantSessionResume = Nothing    -- no session to resume
      , TLS.clientDebug = def                    -- Can override DRBG seed
      , TLS.clientWantTicket = False
      }
  where
    -- Expand a named group to its (HashAlgorithm, SignatureAlgorithm)
    -- pairs.  RSA covers both TLS 1.3 (RSA-PSS-RSAE) and TLS 1.2
    -- (RSA-PKCS#1) variants; ECDSA covers the three NIST P-curves;
    -- EdDSA covers Ed25519 and Ed448.  Within each group the pairs
    -- are listed in the conventional SHA-256\/384\/512 order; the
    -- caller controls the group-level order via the @--sigalgs@
    -- option, and the TLS library honours that order when offering
    -- algorithms to the server.
    sigAlgsFor :: SigAlgGroup -> [(TLS.HashAlgorithm, TLS.SignatureAlgorithm)]
    sigAlgsFor SigEcdsa = (,) <$> [ TLS.HashSHA256
                                  , TLS.HashSHA384
                                  , TLS.HashSHA512 ]
                              <*> [ TLS.SignatureECDSA ]
    sigAlgsFor SigRsa   = -- TLS 1.3 RSA-PSS-RSAE
                          ((,) TLS.HashIntrinsic <$>
                              [ TLS.SignatureRSApssRSAeSHA256
                              , TLS.SignatureRSApssRSAeSHA384
                              , TLS.SignatureRSApssRSAeSHA512 ])
                          ++
                          -- TLS 1.2 RSA-PKCS#1
                          ((,) <$> [ TLS.HashSHA256
                                   , TLS.HashSHA384
                                   , TLS.HashSHA512 ]
                               <*> [ TLS.SignatureRSA ])
    sigAlgsFor SigEdDsa = (,) TLS.HashIntrinsic <$>
                              [ TLS.SignatureEd25519
                              , TLS.SignatureEd448 ]

    -- Add some DHE TLS 1.2 ciphers, with the AES128 choice first
    chooseCS :: [TLS.Cipher]
    chooseCS =
        TLS.ciphersuite_strong ++
        case L.uncons $ reverse TLS.ciphersuite_dhe_rsa of
            Just (l, cs) -> l : reverse cs
            Nothing       -> []
        ++ TLS.ciphersuite_pfs_sha2_cbc

    chooseGS :: [TLS.Group] -> [TLS.Group]
    chooseGS = go False
      where
        go _ [] = []
        go dheSeen (g@(TLS.Group n) : gs)
            -- No P521 or SecP384r1MLKEM1024
            | n == 25 || n == 4589 = go dheSeen gs
            -- All other EC curves
            | n < 256
            = g : go dheSeen gs
            -- Replace FFDHE list with just ffdhe2048 and ffdhe3072
            | n >= 256 && n < 512
            = if dheSeen
              then go dheSeen gs
              else TLS.FFDHE2048 : TLS.FFDHE3072 : go True gs
            | otherwise
            = g : go dheSeen gs

    chooseKS :: [TLS.Group] -> [TLS.Group]
    chooseKS = go Nothing Nothing
      where
        go g2 g3 (g@(TLS.Group n) : gs)
            -- EC, if possible
            | n < 256   = [g]
            -- else FFDHE, if possible
            | n < 512   = go (g2 <|> Just g) g3 gs
            -- else anything goes
            | otherwise = go g2 (g3 <|> Just g) gs
        go (Just g) _ _ = [g]
        go _ (Just g) _ = [g]
        go _ _ _        = []

-- | Read decrypted records off the TLS context and produce a
-- 'ByteStream' for downstream consumers.  Errors, EOF and
-- timeouts are recorded in 'smtpErr' and terminate the stream.
tlsSource :: Q.ByteStream SmtpM ()
tlsSource = lift (gets smtpConn) >>= \ case
    SmtpTLS ctx -> go ctx
    _           -> error "Non-TLS channel"
  where
    go ctx = do
        tm <- lift timeLeft
        liftIO (timeout tm (doRecv ctx)) >>= \ case
            Just (Right bs)
                | B.length bs > 0 -> Q.chunk bs >> go ctx
                | otherwise
                  -> lift $ modify' \s -> s { smtpErr = DataErr $ eofErr "TLS read" }
            Just (Left e)
                  -> lift $ modify' \s -> s { smtpErr = e }
            _     -> lift $ modify' \s -> s { smtpErr = DataErr $ timeErr "TLS read" }

    doRecv ctx = (Right <$> TLS.recvData ctx) `catches`
        [ Handler handleTLS, Handler handleIO ]

    handleTLS e = case e of
        TLS.Terminated _ _ _ -> return $ Left TlsRecvError
        _                    -> return $ Left $ OtherErr $ toException e

    handleIO e = return $ Left $ DataErr e

-- | Consume a 'ByteStream' and write each chunk through the TLS
-- context.  Errors and timeouts are recorded in 'smtpErr' and
-- stop the loop.
tlsSink :: Q.ByteStream SmtpM () -> SmtpM ()
tlsSink str = gets smtpConn >>= \ case
    SmtpTLS ctx -> go ctx str
    _           -> fail "Non-TLS channel"
  where
    go ctx = Q.unconsChunk >=> \ case
        Right (bs, bss) -> do
            tm <- timeLeft
            liftIO (timeout tm (doSend ctx bs)) >>= \ case
                Just (Right _) -> go ctx bss
                Just (Left e)  -> modify' \s -> s { smtpErr = e }
                _              -> modify' \s -> s { smtpErr = DataErr $ timeErr "TLS write" }
        Left () -> pure ()

    doSend ctx bs = (Right <$> TLS.sendData ctx (LB.fromStrict bs)) `catches`
        [ Handler handleTLS, Handler handleIO ]

    handleTLS e = case e of
        TLS.Terminated _ _ _ -> return $ Left TlsSendError
        _                    -> return $ Left $ OtherErr $ toException e

    handleIO e = return $ Left $ DataErr e

endTLS :: SmtpM ()
endTLS = gets smtpConn >>= \ case
    SmtpTLS ctx -> go ctx
    _           -> error "Non-TLS channel"
  where
    go ctx = do
        tm <- timeLeft
        liftIO (timeout tm (doBye ctx)) >>= \ case
            Just (Right _) -> pure ()
            Just (Left  e) -> modify' \s -> s { smtpErr = e }
            _              -> modify' \s -> s { smtpErr = DataErr $ timeErr "shutdown" }

    doBye ctx = (Right <$> TLS.bye ctx) `catches`
        [ Handler handleTLS, Handler handleIO ]

    handleTLS e = case e of
        TLS.Terminated _ _ _ -> return $ Left TlsSendError
        _                    -> return $ Left $ OtherErr $ toException e

    handleIO e = return $ Left $ DataErr e

connTLS :: SmtpConn -> Maybe TLS.Context
connTLS (SmtpTLS ctx) = Just ctx
connTLS _             = Nothing

hasTLS :: ProtoState -> Bool
hasTLS = isJust . connTLS . smtpConn

startTLS :: SmtpM ()
startTLS = gets smtpConn >>= \ case
    SmtpPlain sock -> go sock
    _              -> error "Non-plaintext channel"
  where
    go :: Socket -> SmtpM ()
    go sock = do
      servername <- gets serverName
      cref <- gets chainRef
      sigAlgs <- gets tlsSigAlgs
      store <- getStore Nothing
      ctx <- TLS.contextNew sock $ tlsParams sigAlgs servername cref store
      tmout <- gets smtpTimeout
      liftIO (timeout tmout (doHandshake ctx)) >>= \ case
          Just (Right _) -> do
              deadline <- liftIO $ timeLimit tmout
              modify' \s -> s { smtpConn = SmtpTLS ctx, ioDeadline = deadline }
          Just (Left e)  -> modify' \s -> s { smtpErr = e }
          _              -> modify' \s -> s { smtpErr = DataErr $ timeErr "handshake" }

    getStore :: Maybe FilePath -> SmtpM X509.CertificateStore
    getStore cafp =
        maybe (return Nothing)
              (\fp -> liftIO $ X509.readCertificateStore fp) cafp >>=
        return . maybe (X509.makeCertificateStore []) id

    doHandshake ctx = (Right <$> TLS.handshake ctx) `catches`
        [ Handler handleTLS, Handler handleIO ]

    handleTLS e = case e of
        TLS.HandshakeFailed t -> return $ Left $ TlsHandError t
        TLS.Terminated _ _ t  -> return $ Left $ TlsHandError t
        _                     -> return $ Left $ OtherErr $ toException e

    handleIO e = return $ Left $ DataErr e

tlsInfo :: TLS.Context -> IO (TLS.Version, TLS.Cipher, Maybe TLS.Group)
tlsInfo ctx = do
  ~(Just i) <- TLS.contextGetInformation ctx
  return $ (,,) <$> TLS.infoVersion
                <*> TLS.infoCipher
                <*> TLS.infoSupportedGroup
                 $! i