packages feed

grapesy-1.0.0: src/Network/GRPC/Util/TLS.hs

{-# LANGUAGE OverloadedStrings #-}

-- | TLS utilities
--
-- Intended for qualified import.
--
-- > import Network.GRPC.Util.TLS (ServerValidation(..))
-- > import Network.GRPC.Util.TLS qualified as Util.TLS
module Network.GRPC.Util.TLS (
    -- * Certificate store
    CertificateStoreSpec(..)
  , certStoreFromSystem
  , certStoreFromCerts
  , certStoreFromPath
  , loadCertificateStore
    -- * Configuration
    -- ** Parameters
  , ServerValidation(..)
  , validationCAStore
    -- ** Common to server and client
  , SslKeyLog(..)
  , keyLogger
  ) where

import Control.Exception
import Data.Default
import Data.X509 qualified as X509
import Data.X509.CertificateStore qualified as X509
import GHC.Generics (Generic)
import System.Environment
import System.X509 qualified as X509

{-------------------------------------------------------------------------------
  Certificate store
-------------------------------------------------------------------------------}

-- | Certificate store specification (for certificate validation)
--
-- This is a deep embedding, describing how to construct a certificate store.
-- The actual construction happens in 'loadCertificateStore'.
--
-- There are three primitive ways to construct a 'CertificateStore':
-- 'certStoreFromSystem', 'certStoreFromCerts', and 'certStoreFromPath'; please
-- refer to the corresponding documentation.
--
-- You can also combine 'CertificateStore's through the 'Monoid' instance.
data CertificateStoreSpec =
    CertStoreEmpty
  | CertStoreAppend CertificateStoreSpec CertificateStoreSpec
  | CertStoreFromSystem
  | CertStoreFromCerts [X509.SignedCertificate]
  | CertStoreFromPath FilePath
  deriving (Show)

instance Semigroup CertificateStoreSpec where
  (<>) = CertStoreAppend

instance Monoid CertificateStoreSpec where
  mempty = CertStoreEmpty

-- | Use the system's certificate store
certStoreFromSystem :: CertificateStoreSpec
certStoreFromSystem = CertStoreFromSystem

-- | Construct a certificate store with the given certificates
certStoreFromCerts :: [X509.SignedCertificate] -> CertificateStoreSpec
certStoreFromCerts = CertStoreFromCerts

-- | Load certificate store from disk
--
-- The path may point to single file (multiple PEM formatted certificates
-- concanated) or directory (one certificate per file, file names are hashes
-- from certificate).
certStoreFromPath :: FilePath -> CertificateStoreSpec
certStoreFromPath = CertStoreFromPath

-- | Load the certificate store
loadCertificateStore :: CertificateStoreSpec -> IO X509.CertificateStore
loadCertificateStore = go
  where
    go :: CertificateStoreSpec -> IO X509.CertificateStore
    go  CertStoreEmpty         = return mempty
    go (CertStoreAppend c1 c2) = (<>) <$> go c1 <*> go c2
    go  CertStoreFromSystem    = X509.getSystemCertificateStore
    go (CertStoreFromCerts cs) = return $ X509.makeCertificateStore cs
    go (CertStoreFromPath fp)  = X509.readCertificateStore fp >>= \case
                                   Nothing -> throwIO $ NoCertificatesAtPath fp
                                   Just cs -> return cs

data LoadCertificateStoreException =
    NoCertificatesAtPath FilePath
  deriving stock (Show)
  deriving anyclass (Exception)

{-------------------------------------------------------------------------------
  Parameters
-------------------------------------------------------------------------------}

-- | How does the client want to validate the server?
data ServerValidation =
    -- | Validate the server
    --
    -- The 'CertificateStore' is a collection of trust anchors. If 'Nothing'
    -- is specified, the system certificate store will be used.
    ValidateServer CertificateStoreSpec

    -- | Skip server validation
    --
    -- WARNING: This is dangerous. Although communication with the server will
    -- still be encrypted, you cannot be sure that the server is who they claim
    -- to be.
  | NoServerValidation
  deriving (Show)

validationCAStore :: ServerValidation -> IO X509.CertificateStore
validationCAStore (ValidateServer storeSpec) = loadCertificateStore storeSpec
validationCAStore NoServerValidation         = return mempty

{-------------------------------------------------------------------------------
  Configuration common to server and client
-------------------------------------------------------------------------------}

-- | SSL key log file
--
-- An SSL key log file can be used by tools such as Wireshark to decode TLS
-- network traffic. It is used for debugging only.
data SslKeyLog =
    -- | Don't use a key log file
    SslKeyLogNone

    -- | Use the specified path
  | SslKeyLogPath FilePath

    -- | Use the @SSLKEYLOGFILE@ environment variable to determine the key log
    --
    -- This is the default.
  | SslKeyLogFromEnv
  deriving stock (Show, Eq, Generic)

instance Default SslKeyLog where
  def = SslKeyLogFromEnv

keyLogger :: SslKeyLog -> IO (String -> IO ())
keyLogger sslKeyLog = do
    keyLogFile <- case sslKeyLog of
                    SslKeyLogNone    -> return $ Nothing
                    SslKeyLogPath fp -> return $ Just fp
                    SslKeyLogFromEnv -> lookupEnv "SSLKEYLOGFILE"
    return $
      case keyLogFile of
        Nothing -> \_   -> return ()
        Just fp -> \str -> appendFile fp (str ++ "\n")