{-# 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