packages feed

curryer-rpc-0.5.0: test/Curryer/Test/TLS.hs

{-# LANGUAGE DerivingStrategies, DerivingVia, DeriveGeneric, ScopedTypeVariables #-}
module Curryer.Test.TLS where
import Test.Tasty
import Test.Tasty.HUnit
import Codec.Winery
import GHC.Generics
import Control.Concurrent.MVar
import Network.Socket (SockAddr(..))
import Control.Concurrent.Async
import Control.Monad
import System.Directory
import System.Process
import System.FilePath
import Control.Exception
import Network.TLS (TLSException)

import Network.RPC.Curryer.Server as S
import Network.RPC.Curryer.Client as C

{- Setup in test/Curryer/Test/:
mkdir ca ca/certs ca/private ca/newcerts client_certs server
echo 01 > ca/serial
touch ca/index.txt
cp /usr/lib/ssl/openssl.cnf .
#modify openssl.cnf
dir = /root/mtls
new_certs_dir = $dir/certs
certificate = $dir/certs/cacert.pem
countryName_default
stateOrProvinceName_default
localityName_default
0.organizationName_default
organizationalUnitName_default
# back to shell
# Generate the private key for the CA certificate
openssl genrsa -out ca/private/cakey.pem 4096
# Create the CA certificate
openssl req -new -x509 -days 3650 -config openssl.cnf -key ca/private/cakey.pem -out ca/certs/cacert.pem
# Generate the private key for the client.
openssl genrsa -out client_certs/client.key.pem 4096
# Generating a Certificate Signing Request (CSR) for the Client
# Remember to revise the Common Name (CN) to match the client's hostname, e.g., "client.yourdomain.com."
openssl req -new -key client_certs/client.key.pem -out ca/client.csr
# Creating the Client Certificate
openssl ca -config openssl.cnf -days 1650 -notext -batch -in ca/client.csr -out client_certs/client.cert.pem
# Generate the private key for the server
openssl genrsa -out server/server.key.pem 4096
# Generating a Certificate Signing Request (CSR) for the Server.
# Remember to revise the Common Name (CN) to match the server's hostname, e.g., "server.yourdomain.com."
openssl req -new -key server/server.key.pem -out server/server.csr
# Creating the Server Certificate
openssl ca -config openssl.cnf -days 1650 -notext -batch -in server/server.csr -out server.cert.pem

#validate tls connection
#open server tls socket
openssl s_server -accept 3000 -CAfile ca/certs/cacert.pem -cert server_certs/server.cert.pem -key /root/server_certs/server.key.pem -state
#connect client tls socket
openssl s_client -connect 127.0.0.1:3000 -key client_certs/client.key.pem -cert client_certs/client.cert.pem -CAfile ca/certs/cacert.pem -state

-}

testTree :: TestTree
testTree = withResource setupOpenSSL cleanupOpenSSL $ \_certdir ->
  testGroup "tls" [
  testCase "simple request and response" testSimpleCall,
  testCase "mutual TLS" testMutualTLS,
  testCase "test rejected anonymous client" testRejectedAnonymousClient
  ]

setupOpenSSL :: IO FilePath
setupOpenSSL = do
  let top = "test/Curryer/Test/pems"
  topExists <- doesPathExist top
  when topExists (removeDirectoryRecursive top)
  createDirectory top
  forM_ ["client", "server", "ca", "ca/private", "ca/newcerts", "ca/certs"] $ \d ->
    createDirectoryIfMissing True (top </> d)
  writeFile (top </> "ca/serial") "01"
  writeFile (top </> "ca/index.txt") ""
  --openssl.cnf is in the repo already
  -- CA
  -- generate the private key for the self-signed certificate authority
  let serverSubject = "/C=US/ST=DC/O=testo/CN=localhost"
      clientSubject = "/C=US/ST=DC/O=testo/OU=testou/CN=localclient"
      sslconfig = "test/Curryer/Test/openssl.cnf"
  callProcess "openssl" ["genrsa", "-out", top </> "ca/private/cakey.pem", "4096"]
  callProcess "openssl" ["req", "-new", "-x509", "-nodes", "-days", "3650",
                         "-config", sslconfig,
                         "-key", top </> "ca/private/cakey.pem",
                         "-out", top </> "ca/certs/cacert.pem",
                         "-subj", serverSubject]
  -- SERVER
  -- generate the server's private key
  callProcess "openssl" ["genrsa", "-out", top </> "server/server.key.pem", "4096"]
  -- generate CSR
  callProcess "openssl" ["req", "-new",
                         "-key", top </> "server/server.key.pem",
                         "-out", top </> "server/server.csr",
                         "-subj", serverSubject]
  -- generate server certificate
  callProcess "openssl" ["ca",
                         "-config", sslconfig,
                         "-days", "1650",
                         "-notext", "-batch", "-in", top </> "server/server.csr",
                         "-out", top </> "server/server.cert.pem"]

  -- CLIENT
  -- generate private key
  callProcess "openssl" ["genrsa", "-out", top </> "client/client.key.pem", "4096"]
  -- generate client CSR
  callProcess "openssl" ["req", "-new",
                         "-key", top </> "client/client.key.pem",
                         "-out", top </> "ca/client.csr", "-nodes", "-subj", clientSubject]
  -- generate client certificate
  callProcess "openssl" ["ca", "-config", sslconfig,
                         "-days", "1650",
                         "-notext", "-batch",
                         "-in", top </> "ca/client.csr",
                         "-out", top </> "client/client.cert.pem"]
  pure top

cleanupOpenSSL :: FilePath -> IO ()
cleanupOpenSSL certdir = 
  removeDirectoryRecursive certdir

data AddTwoNumbersReq = AddTwoNumbersReq Int Int
  deriving (Generic, Show)
  deriving Serialise via WineryVariant AddTwoNumbersReq

data GetRoleName = GetRoleName
  deriving (Generic, Show)
  deriving Serialise via WineryVariant GetRoleName

testServerRequestHandlers :: Maybe (MVar String) -> RequestHandlers ()
testServerRequestHandlers _mAsyncMVar =
    [ RequestHandler $ \_ (AddTwoNumbersReq x y) -> pure (x + y),
      RequestHandler $ \state GetRoleName -> do
        --print ("request roleName"::String, connectionRoleName state)
        pure (connectionRoleName state)]
  

serverConnectionConfig :: ServerConnectionConfig
serverConnectionConfig = S.EncryptedConnectionConfig
                         (ServerTLSConfig
                           {S.tlsCertInfo = certInfo,
                            S.tlsServerHostName = "localhost",
                            S.tlsServerServiceName = mempty}) AcceptAnonymousClient
  where
    certInfo = ServerTLSCertInfo {
      x509PublicFilePath = "./test/Curryer/Test/pems/server/server.cert.pem",
      S.x509CertFilePath = Just "./test/Curryer/Test/pems/ca/certs/cacert.pem",
      x509PrivateFilePath = "./test/Curryer/Test/pems/server/server.key.pem"
      }

mTLSServerConnectionConfig :: ServerConnectionConfig
mTLSServerConnectionConfig = S.EncryptedConnectionConfig
                         (ServerTLSConfig
                           {S.tlsCertInfo = certInfo,
                            S.tlsServerHostName = "localhost",
                            S.tlsServerServiceName = mempty}) ClientAuthRequired
  where
    certInfo = ServerTLSCertInfo {
      x509PublicFilePath = "./test/Curryer/Test/pems/server/server.cert.pem",
      S.x509CertFilePath = Just "./test/Curryer/Test/pems/ca/certs/cacert.pem",
      x509PrivateFilePath = "./test/Curryer/Test/pems/server/server.key.pem"
      }

mTLSClientConnectionConfig :: ClientConnectionConfig
mTLSClientConnectionConfig = C.EncryptedConnectionConfig
                         (ClientTLSConfig {C.tlsCertInfo = certData,
                                     C.tlsServerHostName = "localhost",
                                     C.tlsServerServiceName = mempty})
  where
    certData = ClientTLSCertInfo {
      x509PublicPrivateFilePaths = Just ("./test/Curryer/Test/pems/client/client.cert.pem",
                                    "./test/Curryer/Test/pems/client/client.key.pem"),
      C.x509CertFilePath = Just "./test/Curryer/Test/pems/ca/certs/cacert.pem"
      }

clientConnectionConfig :: ClientConnectionConfig
clientConnectionConfig = C.EncryptedConnectionConfig
                         (ClientTLSConfig {C.tlsCertInfo = certData,
                                     C.tlsServerHostName = "localhost",
                                     C.tlsServerServiceName = mempty})
  where
    certData = ClientTLSCertInfo {
      x509PublicPrivateFilePaths = Nothing,
      C.x509CertFilePath = Just "./test/Curryer/Test/pems/ca/certs/cacert.pem"
      }

-- test an anonymous client connecting to a self-signed cert server
testSimpleCall :: Assertion
testSimpleCall = do
  readyVar <- newEmptyMVar
  let emptyServerState = ()
  server <- async (serveIPv4 (testServerRequestHandlers Nothing) emptyServerState serverConnectionConfig localHostAddr 0 (Just readyVar))
  --wait for server to be ready
  (SockAddrInet port _) <- takeMVar readyVar
  conn <- connectIPv4 [] clientConnectionConfig localHostAddr port
  replicateM_ 5 $ do --make five AddTwo calls to shake out parallelism bugs
    x <- call conn (AddTwoNumbersReq 1 1)
    assertEqual "server request+response" (Right (2 :: Int)) x
  close conn
  cancel server
  
-- test a mutual TLS RPC call
testMutualTLS :: Assertion
testMutualTLS = do
  readyVar <- newEmptyMVar
  let emptyServerState = ()
  server <- async (serveIPv4 (testServerRequestHandlers Nothing) emptyServerState mTLSServerConnectionConfig localHostAddr 0 (Just readyVar))
  --wait for server to be ready
  (SockAddrInet port _) <- takeMVar readyVar
  conn <- connectIPv4 [] mTLSClientConnectionConfig localHostAddr port
  x <- call conn GetRoleName
  assertEqual "get role name" (Right (Just "testou" :: Maybe String)) x
  close conn
  cancel server

-- setup server expecting client certificate but the client does not provide one  
testRejectedAnonymousClient :: Assertion
testRejectedAnonymousClient = do
  readyVar <- newEmptyMVar
  let emptyServerState = ()
  server <- async (serveIPv4 (testServerRequestHandlers Nothing) emptyServerState mTLSServerConnectionConfig localHostAddr 0 (Just readyVar))
  --wait for server to be ready
  (SockAddrInet port _) <- takeMVar readyVar
  res <- try $ do
    connectIPv4 [] clientConnectionConfig localHostAddr port
  case res of
    Left (_exc :: TLSException) -> pure () --expected failure
    Right _ -> assertFailure "expected connection failure"
  cancel server