summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Cady <d@jerkface.net>2016-04-09 17:50:20 -0400
committerAndrew Cady <d@jerkface.net>2016-04-09 17:50:20 -0400
commit6224e4d1dc99244fbad13dc0613fa16e87c20396 (patch)
treeb68484de3c49d402532438b02b1c5b758c7aad8d
parent9bf06242513945fbadd6fcd20b41efd0f1b073c2 (diff)
generate stub CertSpec objects from config file
(CertSpec is the new name for AcmeCertRequest)
-rw-r--r--acme-certify.hs123
1 files changed, 81 insertions, 42 deletions
diff --git a/acme-certify.hs b/acme-certify.hs
index 48901c9..579622f 100644
--- a/acme-certify.hs
+++ b/acme-certify.hs
@@ -1,9 +1,11 @@
1{-# LANGUAGE FlexibleContexts #-} 1{-# LANGUAGE FlexibleContexts #-}
2{-# LANGUAGE NamedFieldPuns #-} 2{-# LANGUAGE FlexibleInstances #-}
3{-# LANGUAGE NoImplicitPrelude #-} 3{-# LANGUAGE NamedFieldPuns #-}
4{-# LANGUAGE OverloadedStrings #-} 4{-# LANGUAGE NoImplicitPrelude #-}
5{-# LANGUAGE RecordWildCards #-} 5{-# LANGUAGE OverloadedStrings #-}
6{-# LANGUAGE ScopedTypeVariables #-} 6{-# LANGUAGE RecordWildCards #-}
7{-# LANGUAGE ScopedTypeVariables #-}
8{-# LANGUAGE TypeSynonymInstances #-}
7 9
8-------------------------------------------------------------------------------- 10--------------------------------------------------------------------------------
9-- | Get a certificate from Let's Encrypt using the ACME protocol. 11-- | Get a certificate from Let's Encrypt using the ACME protocol.
@@ -16,7 +18,7 @@ import BasePrelude
16import Control.Lens hiding ((&)) 18import Control.Lens hiding ((&))
17import Data.Aeson.Lens 19import Data.Aeson.Lens
18import qualified Data.HashMap.Strict as HashMap 20import qualified Data.HashMap.Strict as HashMap
19import Data.Text (unpack) 21import Data.Text (Text, unpack)
20import Data.Yaml (Object) 22import Data.Yaml (Object)
21import qualified Data.Yaml.Config as Config 23import qualified Data.Yaml.Config as Config
22import Data.Yaml.Config.Internal (Config (..)) 24import Data.Yaml.Config.Internal (Config (..))
@@ -83,12 +85,17 @@ data UpdateOpts = UpdateOpts {
83 updateConfigFile :: Maybe FilePath 85 updateConfigFile :: Maybe FilePath
84} 86}
85 87
86data AcmeCertRequest = AcmeCertRequest { 88instance Show HttpProvisioner where
87 acrDomains :: [(DomainName, HttpProvisioner)], 89 show _ = "<code>"
88 acrSkipDH :: Bool, 90instance Show Keys where
89 acrCertificateDir :: FilePath, 91 show _ = "<keys>"
90 acrUserKeys :: Keys 92
91} 93data CertSpec = CertSpec {
94 csDomains :: [(DomainName, HttpProvisioner)],
95 csSkipDH :: Bool,
96 csCertificateDir :: FilePath,
97 csUserKeys :: Keys
98} deriving Show
92 99
93updateOpts :: Parser Command 100updateOpts :: Parser Command
94updateOpts = fmap Update $ 101updateOpts = fmap Update $
@@ -148,16 +155,48 @@ extractObject (Config _ o) = o
148runUpdate :: UpdateOpts -> IO () 155runUpdate :: UpdateOpts -> IO ()
149runUpdate UpdateOpts { .. } = do 156runUpdate UpdateOpts { .. } = do
150 config <- Config.load "config.yaml" 157 config <- Config.load "config.yaml"
151 hosts <- Config.subconfig "hosts" config 158 hostsConfig <- Config.subconfig "hosts" config
152 forM_ (Config.keys hosts) $ \host -> do 159 certReqDomains <- fmap concat <$> forM (Config.keys hostsConfig) $ \host ->
153 domains <- extractObject <$> (Config.subconfig host hosts >>= Config.subconfig "domains") 160 do
154 putStrLn $ unpack host ++ ": " 161 hostParts <- (Config.subconfig host hostsConfig >>= Config.subconfig
155 forM_ (HashMap.keys domains) $ \domain -> do 162 "domains") <&> extractObject
156 let subdomains = map (<..> unpack domain) $ sort $ concat $ HashMap.lookup domain domains & toListOf (_Just . _String . to (words . unpack)) 163 forM (HashMap.keys hostParts) $ \domain ->
157 putStrLn $ " " ++ unwords subdomains 164 return (unpack host, combineSubdomains domain hostParts)
158 putStrLn "" 165 forM_ certReqDomains print
166
167 globalCertificateDir <- getHomeDirectory <&> (</> ".acme/test")
168 createDirectoryIfMissing True globalCertificateDir
169
170 Just keys <- getOrCreateKeys $ globalCertificateDir </> "rsa.key"
171
172
173 certSpecs :: [CertSpec] <- forM certReqDomains $ \(host, domains) -> do
174 provisioners <- mapM (chooseProvisioner host) domains
175 return $ certSpec globalCertificateDir keys (host, provisioners)
176
177 mapM_ print certSpecs
159 error "Error: unimplemented" 178 error "Error: unimplemented"
160 179
180 where
181 chooseProvisioner :: String -> String -> IO (DomainName, HttpProvisioner)
182 chooseProvisioner host domain = do -- TODO: implement
183 let errmsg = "whatever"
184 dir <- ensureWritableDir "/var/www/html/.well-known/acme-challenge/" errmsg
185 return (domainName' domain, provisionViaFile dir)
186
187 certSpec :: FilePath -> Keys -> (String, [(DomainName, HttpProvisioner)]) -> CertSpec
188 certSpec baseDir keys (host, requestDomains) = CertSpec { .. }
189 where
190 csDomains = requestDomains
191 csSkipDH = True -- TODO: implement
192 csUserKeys = keys
193 csCertificateDir = baseDir </> host </> (show . fst) (head requestDomains)
194
195 combineSubdomains :: AsPrimitive v => Text -> HashMap.HashMap Text v -> [String]
196 combineSubdomains domain subs =
197 map (<..> unpack domain) $ sort -- relying on the fact that '.' sorts first
198 $ concat $ HashMap.lookup domain subs & toListOf (_Just . _String . to (words . unpack))
199
161runCertify :: CertifyOpts -> IO (Either String ()) 200runCertify :: CertifyOpts -> IO (Either String ())
162runCertify CertifyOpts{..} = do 201runCertify CertifyOpts{..} = do
163 let terms = fromMaybe defaultTerms (join $ parseAbsoluteURI <$> optTerms) 202 let terms = fromMaybe defaultTerms (join $ parseAbsoluteURI <$> optTerms)
@@ -170,45 +209,45 @@ runCertify CertifyOpts{..} = do
170 issuerCert <- readX509 letsEncryptX1CrossSigned 209 issuerCert <- readX509 letsEncryptX1CrossSigned
171 210
172 seq email (return ()) 211 seq email (return ())
173 doesDirectoryExist domainDir `otherwiseM` createDirectory domainDir 212 createDirectoryIfMissing False domainDir
174 challengeDir <- ensureWritableDir optChallengeDir "challenge directory" 213 challengeDir <- ensureWritableDir optChallengeDir "challenge directory"
175 void $ ensureWritableDir domainDir "domain directory" 214 void $ ensureWritableDir domainDir "domain directory"
176 215
177 Just keys <- getOrCreateKeys privKeyFile 216 Just keys <- getOrCreateKeys privKeyFile
178 217
179 let req = AcmeCertRequest {..} 218 let req = CertSpec {..}
180 acrDomains = map (flip (,) (provisionViaFile challengeDir)) requestDomains 219 csDomains = map (flip (,) (provisionViaFile challengeDir)) requestDomains
181 acrSkipDH = optSkipDH 220 csSkipDH = optSkipDH
182 acrUserKeys = keys 221 csUserKeys = keys
183 acrCertificateDir = domainDir 222 csCertificateDir = domainDir
184 223
185 unless optSkipProvisionCheck $ 224 unless optSkipProvisionCheck $
186 forM_ acrDomains $ uncurry canProvision >=> 225 forM_ csDomains $ uncurry canProvision >=>
187 (`unless` error "Error: cannot provision files to web server") 226 (`unless` error "Error: cannot provision files to web server")
188 227
189 go' directoryUrl terms email issuerCert req 228 go' directoryUrl terms email issuerCert req
190 229
191go' :: URI -> URI -> Maybe EmailAddress -> X509 -> AcmeCertRequest -> IO (Either String ()) 230go' :: URI -> URI -> Maybe EmailAddress -> X509 -> CertSpec -> IO (Either String ())
192go' directoryUrl terms email issuerCert acr@AcmeCertRequest{..} = do 231go' directoryUrl terms email issuerCert cs@CertSpec{..} = do
193 Just domainKeys <- getOrCreateKeys $ acrCertificateDir </> "rsa.key" 232 Just domainKeys <- getOrCreateKeys $ csCertificateDir </> "rsa.key"
194 dh <- saveDhParams acr 233 dh <- saveDhParams cs
195 234
196 certificate <- certify directoryUrl acrUserKeys ((,) terms <$> email) domainKeys acrDomains 235 certificate <- certify directoryUrl csUserKeys ((,) terms <$> email) domainKeys csDomains
197 for certificate $ saveCertificate issuerCert dh domainKeys acr 236 for certificate $ saveCertificate issuerCert dh domainKeys cs
198 237
199saveDhParams :: AcmeCertRequest -> IO (Maybe DHP) 238saveDhParams :: CertSpec -> IO (Maybe DHP)
200saveDhParams AcmeCertRequest{acrSkipDH, acrCertificateDir} = do 239saveDhParams CertSpec{csSkipDH, csCertificateDir} = do
201 let domainDhFile = acrCertificateDir </> "dhparams.pem" 240 let domainDhFile = csCertificateDir </> "dhparams.pem"
202 if acrSkipDH then return Nothing else Just <$> getOrCreateDH domainDhFile 241 if csSkipDH then return Nothing else Just <$> getOrCreateDH domainDhFile
203 242
204saveCertificate :: X509 -> Maybe DHP -> Keys -> AcmeCertRequest -> X509 -> IO () 243saveCertificate :: X509 -> Maybe DHP -> Keys -> CertSpec -> X509 -> IO ()
205saveCertificate issuerCert dh domainKeys AcmeCertRequest{acrCertificateDir} = saveBoth 244saveCertificate issuerCert dh domainKeys CertSpec{csCertificateDir} = saveBoth
206 where 245 where
207 saveBoth x509 = savePEM x509 >> saveCombined x509 246 saveBoth x509 = savePEM x509 >> saveCombined x509
208 saveCombined = combinedCert issuerCert dh domainKeys >=> writeFile domainCombinedFile 247 saveCombined = combinedCert issuerCert dh domainKeys >=> writeFile domainCombinedFile
209 savePEM = writeX509 >=> writeFile domainCertFile 248 savePEM = writeX509 >=> writeFile domainCertFile
210 domainCombinedFile = acrCertificateDir </> "cert.combined.pem" 249 domainCombinedFile = csCertificateDir </> "cert.combined.pem"
211 domainCertFile = acrCertificateDir </> "cert.pem" 250 domainCertFile = csCertificateDir </> "cert.pem"
212 251
213genKey :: IO String 252genKey :: IO String
214genKey = withOpenSSL $ do 253genKey = withOpenSSL $ do