summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Cady <d@jerkface.net>2016-04-11 01:39:16 -0400
committerAndrew Cady <d@jerkface.net>2016-04-11 01:39:16 -0400
commit5978351c55544b0ab72630ba70fec25825cad7b4 (patch)
treea82d2de4525b7b618ff39e6fe5d9be0fcc7885ef
parent74451bfa239515ed419e47e587a2c0009808525c (diff)
Check existing certificate expiration
This disables renewing the cert unless the existing cert will expire within 20 days. It doesn't check if the existing cert has the same names as the new cert would! This needs to be done.
-rw-r--r--acme-certify.cabal2
-rw-r--r--acme-certify.hs77
2 files changed, 57 insertions, 22 deletions
diff --git a/acme-certify.cabal b/acme-certify.cabal
index ac6427a..9a67cb9 100644
--- a/acme-certify.cabal
+++ b/acme-certify.cabal
@@ -30,7 +30,7 @@ executable acme
30 text, HsOpenSSL, optparse-applicative, time, 30 text, HsOpenSSL, optparse-applicative, time,
31 email-validate, network-uri, directory, yaml-config, 31 email-validate, network-uri, directory, yaml-config,
32 yaml, unordered-containers, lens, lens-aeson, process, 32 yaml, unordered-containers, lens, lens-aeson, process,
33 posix-escape, transformers, resourcet 33 posix-escape, transformers, resourcet, mtl
34 default-language: Haskell2010 34 default-language: Haskell2010
35 35
36-- test-suite acme-certify-test 36-- test-suite acme-certify-test
diff --git a/acme-certify.hs b/acme-certify.hs
index 1ae9dbf..ecddc99 100644
--- a/acme-certify.hs
+++ b/acme-certify.hs
@@ -1,5 +1,6 @@
1{-# LANGUAGE FlexibleContexts #-} 1{-# LANGUAGE FlexibleContexts #-}
2{-# LANGUAGE FlexibleInstances #-} 2{-# LANGUAGE FlexibleInstances #-}
3{-# LANGUAGE MultiWayIf #-}
3{-# LANGUAGE NamedFieldPuns #-} 4{-# LANGUAGE NamedFieldPuns #-}
4{-# LANGUAGE NoImplicitPrelude #-} 5{-# LANGUAGE NoImplicitPrelude #-}
5{-# LANGUAGE OverloadedStrings #-} 6{-# LANGUAGE OverloadedStrings #-}
@@ -16,13 +17,16 @@
16module Main where 17module Main where
17 18
18import BasePrelude 19import BasePrelude
19import Control.Lens hiding ((&), argument) 20import Control.Lens hiding (argument, (&))
20import Control.Monad.IO.Class 21import Control.Monad.IO.Class
22import Control.Monad.Trans.Except
23import Control.Monad.Except
21import Control.Monad.Trans.Resource 24import Control.Monad.Trans.Resource
22import Data.Aeson.Lens 25import Data.Aeson.Lens
23import qualified Data.HashMap.Strict as HashMap 26import qualified Data.HashMap.Strict as HashMap
24import Data.Text (Text, unpack, pack) 27import Data.Text (Text, pack, unpack)
25import Data.Text.Encoding (decodeUtf8, encodeUtf8) 28import Data.Text.Encoding (decodeUtf8, encodeUtf8)
29import Data.Time.Clock
26import Data.Yaml (Object) 30import Data.Yaml (Object)
27import qualified Data.Yaml.Config as Config 31import qualified Data.Yaml.Config as Config
28import Data.Yaml.Config.Internal (Config (..)) 32import Data.Yaml.Config.Internal (Config (..))
@@ -37,7 +41,7 @@ import OpenSSL
37import OpenSSL.DH 41import OpenSSL.DH
38import OpenSSL.PEM 42import OpenSSL.PEM
39import OpenSSL.RSA 43import OpenSSL.RSA
40import OpenSSL.X509 (X509) 44import OpenSSL.X509 (X509, getNotAfter)
41import Options.Applicative hiding (header) 45import Options.Applicative hiding (header)
42import qualified Options.Applicative as Opt 46import qualified Options.Applicative as Opt
43import System.Directory 47import System.Directory
@@ -89,12 +93,12 @@ data CertifyOpts = CertifyOpts {
89} 93}
90 94
91data UpdateOpts = UpdateOpts { 95data UpdateOpts = UpdateOpts {
92 updateConfigFile :: Maybe FilePath, 96 updateConfigFile :: Maybe FilePath,
93 updateHosts :: [String], 97 updateHosts :: [String],
94 updateStaging :: Bool, 98 updateStaging :: Bool,
95 updateDryRun :: Bool, 99 updateDryRun :: Bool,
96 updateDoPrivisionCheck :: Bool, 100 updateDoPrivisionCheck :: Bool,
97 updateTryVHosts :: [String] 101 updateTryVHosts :: [String]
98} 102}
99 103
100updateOpts :: Parser Command 104updateOpts :: Parser Command
@@ -183,6 +187,26 @@ data CertSpec = CertSpec {
183 csUserKeys :: Keys 187 csUserKeys :: Keys
184} deriving Show 188} deriving Show
185 189
190data NeedCertReason = Expired | NearExpiration | SubDomainsAdded | NoExistingCert deriving Show
191fetchNeeded :: CertSpec -> IO (Either NeedCertReason ())
192fetchNeeded (domainCertFile -> certFile) = runExceptT $ do
193 exists <- liftIO $ doesFileExist certFile
194 unless exists $ throwError NoExistingCert
195
196 cert <- liftIO $ readFile certFile >>= readX509
197 expiration <- liftIO $ getNotAfter cert
198 now <- liftIO getCurrentTime
199
200 -- TODO: check X509v3 subjectAltName list within certificate
201 -- Need to patch HsOpenSSL. Or use cryptonite.
202
203 if | expiration < now -> throwError Expired
204 | expiration < addUTCTime graceTime now -> throwError NearExpiration
205 | otherwise -> return ()
206 where
207 graceTime = days 20
208 days = (*) (24 * 60 * 60)
209
186runUpdate :: UpdateOpts -> IO () 210runUpdate :: UpdateOpts -> IO ()
187runUpdate UpdateOpts { .. } = do 211runUpdate UpdateOpts { .. } = do
188 issuerCert <- readX509 letsEncryptX1CrossSigned 212 issuerCert <- readX509 letsEncryptX1CrossSigned
@@ -194,7 +218,7 @@ runUpdate UpdateOpts { .. } = do
194 return $ flip map (HashMap.keys hostParts) $ \domain -> 218 return $ flip map (HashMap.keys hostParts) $ \domain ->
195 (unpack host, domainName' $ unpack domain, combineSubdomains domain hostParts) 219 (unpack host, domainName' $ unpack domain, combineSubdomains domain hostParts)
196 220
197 globalCertificateDir <- getHomeDirectory <&> (</> if updateStaging then ".acme/test-certs" else ".acme/certs") 221 globalCertificateDir <- getHomeDirectory <&> (</> if updateStaging then ".acme/fake-certs" else ".acme/certs")
198 createDirectoryIfMissing True globalCertificateDir 222 createDirectoryIfMissing True globalCertificateDir
199 223
200 Just keys <- getOrCreateKeys $ globalCertificateDir </> "rsa.key" 224 Just keys <- getOrCreateKeys $ globalCertificateDir </> "rsa.key"
@@ -217,15 +241,17 @@ runUpdate UpdateOpts { .. } = do
217 can <- uncurry canProvision csd 241 can <- uncurry canProvision csd
218 unless can $ error "Error: cannot provision files to web server" 242 unless can $ error "Error: cannot provision files to web server"
219 243
220 when (null updateTryVHosts) $ forM_ (view _3 <$> wantedCertSpecs) $ \spec -> do 244 when (null updateTryVHosts) $ forM_ wantedCertSpecs $ \(_, domain, spec) -> do
221 245
222 let terms = defaultTerms 246 let terms = defaultTerms
223 directoryUrl = if updateStaging then stagingDirectoryUrl else liveDirectoryUrl 247 directoryUrl = if updateStaging then stagingDirectoryUrl else liveDirectoryUrl
224 email = emailAddress $ encodeUtf8 . pack $ "root@" ++ (domainToString . fst . head) (csDomains spec) 248 email = emailAddress $ encodeUtf8 . pack $ "root@" ++ (domainToString . fst . head) (csDomains spec)
225 249
226 if updateDryRun 250 (fetchNeeded spec >>=) $ leftMapM_ $ \reason -> do
227 then putStrLn $ "Dry run; would have fetched certificate: " ++ show spec 251 putStrLn $ concat ["New certificate needed (for domain ", domainToString domain, "): ", show reason]
228 else print =<< fetchCertificate directoryUrl terms email issuerCert spec 252 if updateDryRun
253 then putStrLn "Dry run: nothing fetched, nothing saved."
254 else print =<< fetchCertificate directoryUrl terms email issuerCert spec
229 255
230 where 256 where
231 extractObject :: Config -> Object 257 extractObject :: Config -> Object
@@ -349,18 +375,24 @@ fetchCertificate directoryUrl terms email issuerCert cs@CertSpec{..} = do
349 for certificate $ saveCertificate issuerCert dh domainKeys cs 375 for certificate $ saveCertificate issuerCert dh domainKeys cs
350 376
351saveDhParams :: CertSpec -> IO (Maybe DHP) 377saveDhParams :: CertSpec -> IO (Maybe DHP)
352saveDhParams CertSpec{csSkipDH, csCertificateDir} = do 378saveDhParams cs@CertSpec{csSkipDH} =
353 let domainDhFile = csCertificateDir </> "dhparams.pem" 379 if csSkipDH then return Nothing else Just <$> getOrCreateDH (domainDhFile cs)
354 if csSkipDH then return Nothing else Just <$> getOrCreateDH domainDhFile
355 380
356saveCertificate :: X509 -> Maybe DHP -> Keys -> CertSpec -> X509 -> IO () 381saveCertificate :: X509 -> Maybe DHP -> Keys -> CertSpec -> X509 -> IO ()
357saveCertificate issuerCert dh domainKeys CertSpec{csCertificateDir} = saveBoth 382saveCertificate issuerCert dh domainKeys cs = saveBoth
358 where 383 where
359 saveBoth x509 = savePEM x509 >> saveCombined x509 384 saveBoth x509 = savePEM x509 >> saveCombined x509
360 saveCombined = combinedCert issuerCert dh domainKeys >=> writeFile domainCombinedFile 385 saveCombined = combinedCert issuerCert dh domainKeys >=> writeFile (domainCombinedFile cs)
361 savePEM = writeX509 >=> writeFile domainCertFile 386 savePEM = writeX509 >=> writeFile (domainCertFile cs)
362 domainCombinedFile = csCertificateDir </> "cert.combined.pem" 387
363 domainCertFile = csCertificateDir </> "cert.pem" 388domainDhFile :: CertSpec -> FilePath
389domainDhFile CertSpec{..} = csCertificateDir </> "dhparams.pem"
390
391domainCombinedFile :: CertSpec -> FilePath
392domainCombinedFile CertSpec{..} = csCertificateDir </> "cert.combined.pem"
393
394domainCertFile :: CertSpec -> FilePath
395domainCertFile CertSpec{..} = csCertificateDir </> "cert.pem"
364 396
365genKey :: IO String 397genKey :: IO String
366genKey = withOpenSSL $ do 398genKey = withOpenSSL $ do
@@ -407,3 +439,6 @@ infixl 0 `otherwiseM`
407(<..>) :: String -> String -> String 439(<..>) :: String -> String -> String
408"." <..> dom = dom 440"." <..> dom = dom
409sub <..> dom = sub ++ "." ++ dom 441sub <..> dom = sub ++ "." ++ dom
442
443leftMapM_ :: Monad m => (a -> m ()) -> Either a b -> m ()
444leftMapM_ f = mapM_ f . either Right Left