test-certs (empty) → 0.1.0.0
raw patch · 7 files changed
+402/−0 lines, 7 filesdep +HsOpenSSLdep +QuickCheckdep +basesetup-changed
Dependencies added: HsOpenSSL, QuickCheck, base, bytestring, directory, filepath, hspec, temporary, test-certs, text, time, tls
Files
- ChangeLog.md +9/−0
- LICENSE +30/−0
- Setup.hs +4/−0
- src/Test/Certs/Temp.hs +210/−0
- test-certs.cabal +60/−0
- test/Certs/TempSpec.hs +71/−0
- test/Spec.hs +18/−0
+ ChangeLog.md view
@@ -0,0 +1,9 @@+# Revision history for test-certs++`test-certs` uses [PVP Versioning][1].++## 0.1.0.0 -- 2024-03-24++* Initial version.++[1]: https://pvp.haskell.org
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2023, Tim Emiola++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Tim Emiola nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,4 @@+import Distribution.Simple+++main = defaultMain
+ src/Test/Certs/Temp.hs view
@@ -0,0 +1,210 @@+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TupleSections #-}++{- |+Module : Test.Certs.Temp+Copyright : (c) 2023 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Enables configuration and generation of temporary certificates+-}+module Test.Certs.Temp+ ( -- * generate certificates+ withCertPaths+ , withCertFilenames+ , withCertPathsInTmp+ , withCertPathsInTmp'+ , generateAndStore++ -- * configuration+ , Config (..)+ , defaultConfig++ -- * certificate filenames+ , CertPaths (..)+ , keyPath+ , certificatePath+ )+where++import qualified Data.ByteString as BS+import Data.Maybe (catMaybes)+import Data.Text (Text)+import qualified Data.Text as Text+import Data.Time (UTCTime, addUTCTime, getCurrentTime, nominalDay)+import Numeric.Natural (Natural)+import qualified OpenSSL.PEM as SSL+import qualified OpenSSL.RSA as SSL+import qualified OpenSSL.Random as SSL+import qualified OpenSSL.X509 as SSL+import System.FilePath ((</>))+import System.IO.Temp (getCanonicalTemporaryDirectory, withTempDirectory)+++-- | Specifies the location to write the temporary certificates+data CertPaths = CertPaths+ { cpKey :: !FilePath+ -- ^ the basename of the private key file+ , cpCert :: !FilePath+ -- ^ the basename of the certificate file+ , cpDir :: !FilePath+ -- ^ the directory containing the certificate files+ }+ deriving (Eq, Show)+++-- | The path of the generated key file+keyPath :: CertPaths -> FilePath+keyPath cp = cpDir cp </> cpKey cp+++-- | The path of the generated certificate file+certificatePath :: CertPaths -> FilePath+certificatePath cp = cpDir cp </> cpCert cp+++{- | A @CertPaths using the default basenames for the certificate files+@cpKey@ is @key.pem@+@cpCert@ is @certificate.pem@+-}+defaultBasenames :: FilePath -> CertPaths+defaultBasenames cpDir =+ CertPaths+ { cpDir+ , cpKey = "key.pem"+ , cpCert = "certificate.pem"+ }+++-- | Configure some details of the generated certificates+data Config = Config+ { cCommonName :: !Text+ -- ^ the certificate common name+ , cDurationDays :: !Natural+ -- ^ the certificate's duration in days+ , cProvince :: !(Maybe Text)+ , cCity :: !(Maybe Text)+ , cOrganization :: !(Maybe Text)+ , cCountry :: !(Maybe Text)+ }+ deriving (Eq, Show)+++-- | A default value for @'Config'@: CN=localhost, duration is 365 days.+defaultConfig :: Config+defaultConfig =+ Config+ { cCountry = Nothing+ , cProvince = Nothing+ , cCity = Nothing+ , cOrganization = Nothing+ , cCommonName = "localhost"+ , cDurationDays = 365+ }+++asDistinguished :: Config -> [(String, String)]+asDistinguished c =+ let dnMaybe k f = (fmap ((k,) . Text.unpack) . f)+ in catMaybes+ [ dnMaybe "C" cCountry c+ , dnMaybe "ST" cProvince c+ , dnMaybe "L" cCity c+ , dnMaybe "O" cOrganization c+ , dnMaybe "CN" (Just . cCommonName) c+ ]+++validityNow :: Natural -> IO (UTCTime, UTCTime)+validityNow ndays = do+ start <- getCurrentTime+ let end = (nominalDay * fromIntegral ndays) `addUTCTime` start+ pure (start, end)+++testKeySize :: Int+testKeySize = 4096+++testExponent :: Integer+testExponent = 257+++genCerts :: Config -> IO (String, String)+genCerts config = do+ -- set up values to use in the certificate fields+ let mkSerialNum = BS.foldl (\a w -> a * 256 + fromIntegral w) 0+ distinguished = asDistinguished config+ serialNumber <- mkSerialNum <$> SSL.randBytes 8+ (start, end) <- validityNow $ cDurationDays config++ -- generate an RSA key pair+ kp <- SSL.generateRSAKey' testKeySize $ fromIntegral testExponent++ -- create and sign a certificate using the private key of the key pair+ cert <- SSL.newX509+ SSL.setVersion cert 2+ SSL.setSerialNumber cert serialNumber+ SSL.setIssuerName cert distinguished+ SSL.setSubjectName cert distinguished+ SSL.setNotBefore cert start+ SSL.setNotAfter cert end+ SSL.setPublicKey cert kp+ SSL.signX509 cert kp Nothing++ -- the PEM representation of the private key+ privString <- SSL.writePKCS8PrivateKey kp Nothing++ -- the PEM representation of the certificate+ certString <- SSL.writeX509 cert++ pure (certString, privString)+++storeCerts :: CertPaths -> String -> String -> IO ()+storeCerts cp rsaKey signedCert = do+ writeFile (keyPath cp) rsaKey+ writeFile (certificatePath cp) signedCert+++-- | Generate and store certificate files as specified as @'CertPaths'@+generateAndStore :: CertPaths -> Config -> IO ()+generateAndStore cp config = do+ (certificate, privKey) <- genCerts config+ storeCerts cp privKey certificate+++-- | Like 'withCertPaths', but allows the @CertPath@ filenames to be specified+withCertFilenames+ :: (FilePath -> CertPaths)+ -> FilePath+ -> Config+ -> (CertPaths -> IO a)+ -> IO a+withCertFilenames mkCertPath parentDir config useCerts =+ withTempDirectory parentDir "temp-certs" $ \tmpDir -> do+ let certPaths = mkCertPath tmpDir+ generateAndStore certPaths config+ useCerts certPaths+++{- | Create certificates in a temporary directory below @parentDir@, specify the+locations using @CertPaths@, use them, then delete them+-}+withCertPaths :: FilePath -> Config -> (CertPaths -> IO a) -> IO a+withCertPaths = withCertFilenames defaultBasenames+++-- | Like 'withCertPaths' with the system @TEMP@ dir as the @parentDir@+withCertPathsInTmp :: Config -> (CertPaths -> IO a) -> IO a+withCertPathsInTmp config action = do+ parentDir <- getCanonicalTemporaryDirectory+ withCertPaths parentDir config action+++-- | Like 'withCertPathsInTmp' using a default @'Config'@+withCertPathsInTmp' :: (CertPaths -> IO a) -> IO a+withCertPathsInTmp' = withCertPathsInTmp defaultConfig
+ test-certs.cabal view
@@ -0,0 +1,60 @@+cabal-version: 3.0+name: test-certs+version: 0.1.0.0+synopsis: create temporary SSL certificates in tests+description:+ Its functions generate the certificates as files in a temporary directory.++ * Note: this package depends on [HsOpenSSL](https://hackage.haskell.org/package/HsOpenSSL).+ * It expects the openssl system libraries to be available on your system, this+ is usually the case on most modern linux distributions.++ See the [README](https://github.com/adetokunbo/test-certs#readme) for a usage example.++license: BSD-3-Clause+license-file: LICENSE+author: Tim Emiola+maintainer: adetokunbo@emio.la+category: Testing+homepage: https://github.com/adetokunbo/test-certs#readme+bug-reports: https://github.com/adetokunbo/test-certs/issues+build-type: Simple+extra-source-files: ChangeLog.md++source-repository head+ type: git+ location: https://github.com/adetokunbo/test-certs.git++library+ exposed-modules: Test.Certs.Temp+ hs-source-dirs: src+ build-depends:+ , base >=4.10 && <5+ , bytestring >=0.10.8.2 && <0.11 || >=0.11.3 && <0.13+ , filepath >=1.4 && <1.4.3 || >=1.5.1 && <1.6+ , HsOpenSSL >=0.11.7 && <0.12+ , temporary >=1.2 && <1.5+ , text >=1.2.3 && <2.2+ , time >=1.10 && <1.13++ default-language: Haskell2010+ ghc-options:+ -Wall -Wincomplete-uni-patterns -Wpartial-fields -fwarn-tabs++test-suite test+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ hs-source-dirs: test+ other-modules: Certs.TempSpec+ default-language: Haskell2010+ ghc-options:+ -threaded -rtsopts -with-rtsopts=-N -Wall -fwarn-tabs+ -funbox-strict-fields -optc-O3 -optc-ffast-math++ build-depends:+ , base+ , directory >=1.3 && <1.4+ , hspec >=2.1+ , QuickCheck+ , test-certs+ , tls
+ test/Certs/TempSpec.hs view
@@ -0,0 +1,71 @@+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Certs.TempSpec+Copyright : (c) 2023 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3+-}+module Certs.TempSpec (spec) where++import Data.Either (isRight)+import Network.TLS (credentialLoadX509)+import System.Directory (getCurrentDirectory)+import Test.Certs.Temp+import Test.Hspec+++spec :: Spec+spec = describe "Test.Certs.Temp" $ do+ context "loading certificates generated via" $ do+ context "withCertsPathInTmp" $ do+ it "should succeed withCertsPathsInTmp" $ do+ shouldGenerateCerts $ withCertPathsInTmp testConfig++ context "withCertsPathInTmp'" $ do+ it "should succeed" $ do+ shouldGenerateCerts withCertPathsInTmp'++ context "withCertsPath" $ do+ it "should succeed" $ do+ cwd <- getCurrentDirectory+ shouldGenerateCerts $ withCertPaths cwd testConfig++ context "withCertFilenames" $ do+ it "should succeed" $ do+ cwd <- getCurrentDirectory+ shouldGenerateCerts $ withCertFilenames altBasenames cwd testConfig+++shouldGenerateCerts :: ((CertPaths -> IO Bool) -> IO Bool) -> IO ()+shouldGenerateCerts actionF = actionF hasCertsAt >>= (`shouldBe` True)+++hasCertsAt :: CertPaths -> IO Bool+hasCertsAt cp = do+ let cert = certificatePath cp+ key = keyPath cp+ isRight <$> credentialLoadX509 cert key+++-- | A default value for @'Config'@+testConfig :: Config+testConfig =+ Config+ { cCountry = Just "JP"+ , cProvince = Just "Fukuoka"+ , cCity = Just "Itoshima"+ , cOrganization = Just "haskell:test-certs"+ , cCommonName = "localhost"+ , cDurationDays = 365+ }+++altBasenames :: FilePath -> CertPaths+altBasenames cpDir =+ CertPaths+ { cpDir+ , cpKey = "privkey.pem"+ , cpCert = "cert.pem"+ }
+ test/Spec.hs view
@@ -0,0 +1,18 @@+module Main where++import qualified Certs.TempSpec as Temp+import System.IO+ ( BufferMode (..)+ , hSetBuffering+ , stderr+ , stdout+ )+import Test.Hspec+++main :: IO ()+main = do+ hSetBuffering stdout NoBuffering+ hSetBuffering stderr NoBuffering+ hspec $ do+ Temp.spec