summaryrefslogtreecommitdiff
path: root/acme-certify.hs
diff options
context:
space:
mode:
authorAndrew Cady <d@jerkface.net>2016-01-26 14:12:13 -0500
committerAndrew Cady <d@jerkface.net>2016-01-26 14:22:16 -0500
commit64e8a8ef7833fb7a9325372c09bcb9a682e1ed30 (patch)
tree275bd316cf63d4406714b332234e7b27480b6342 /acme-certify.hs
parent7373a3ede2216048d2766f8f27e77d014b82dc43 (diff)
Pre-generate DH params
The program now outputs a combined PEM certificate. A new option allows DH-param generation to be disabled.
Diffstat (limited to 'acme-certify.hs')
-rw-r--r--acme-certify.hs130
1 files changed, 79 insertions, 51 deletions
diff --git a/acme-certify.hs b/acme-certify.hs
index b84a728..219b0c1 100644
--- a/acme-certify.hs
+++ b/acme-certify.hs
@@ -12,21 +12,22 @@
12module Main where 12module Main where
13 13
14import BasePrelude 14import BasePrelude
15import qualified Data.ByteString.Lazy.Char8 as LC 15import Network.ACME (canProvision, certify,
16import Network.ACME (canProvision, certify, fileProvisioner, ensureWritableDir, (</>), genReq) 16 ensureWritableDir, fileProvisioner,
17import Network.ACME.Encoding (Keys (..), readKeys) 17 genReq, (</>))
18import Network.ACME.Encoding (Keys (..), readKeys)
18import Network.URI 19import Network.URI
19import OpenSSL 20import OpenSSL
20import OpenSSL.X509 (X509)
21import OpenSSL.DH 21import OpenSSL.DH
22import OpenSSL.PEM 22import OpenSSL.PEM
23import OpenSSL.RSA 23import OpenSSL.RSA
24import Options.Applicative hiding (header) 24import OpenSSL.X509 (X509)
25import qualified Options.Applicative as Opt 25import Options.Applicative hiding (header)
26import qualified Options.Applicative as Opt
26import System.Directory 27import System.Directory
27import Text.Domain.Validate hiding (validate) 28import System.IO
29import Text.Domain.Validate hiding (validate)
28import Text.Email.Validate 30import Text.Email.Validate
29import System.IO
30 31
31stagingDirectoryUrl, liveDirectoryUrl :: URI 32stagingDirectoryUrl, liveDirectoryUrl :: URI
32Just liveDirectoryUrl = parseAbsoluteURI "https://acme-v01.api.letsencrypt.org/directory" 33Just liveDirectoryUrl = parseAbsoluteURI "https://acme-v01.api.letsencrypt.org/directory"
@@ -42,13 +43,15 @@ main = execParser opts >>= go
42 ] 43 ]
43 44
44data CmdOpts = CmdOpts { 45data CmdOpts = CmdOpts {
45 optKeyFile :: String, 46 optKeyFile :: String,
46 optDomains :: [String], 47 optDomains :: [String],
47 optChallengeDir :: String, 48 optChallengeDir :: String,
48 optDomainDir :: Maybe String, 49 optDomainDir :: Maybe String,
49 optEmail :: Maybe String, 50 optEmail :: Maybe String,
50 optTerms :: Maybe String, 51 optTerms :: Maybe String,
51 optStaging :: Bool 52 optSkipDH :: Bool,
53 optStaging :: Bool,
54 optSkipProvisionCheck :: Bool
52} 55}
53 56
54defaultTerms :: URI 57defaultTerms :: URI
@@ -56,71 +59,71 @@ Just defaultTerms = parseAbsoluteURI "https://letsencrypt.org/documents/LE-SA-v1
56 59
57cmdopts :: Parser CmdOpts 60cmdopts :: Parser CmdOpts
58cmdopts = CmdOpts <$> strOption (long "key" <> metavar "FILE" <> 61cmdopts = CmdOpts <$> strOption (long "key" <> metavar "FILE" <>
59 help "filename of your private RSA key") 62 help "Filename of your private RSA key")
60 <*> some 63 <*> some
61 (strOption 64 (strOption
62 (long "domain" <> 65 (long "domain" <>
63 metavar "DOMAIN" <> 66 metavar "DOMAIN" <>
64 help 67 help
65 (unwords 68 (unwords
66 [ "the domain name(s) to certify;" 69 [ "The domain name(s) to certify;"
67 , "specify more than once for a multi-domain certificate" 70 , "specify more than once for a multi-domain certificate"
68 ]))) 71 ])))
69 <*> strOption (long "challenge-dir" <> metavar "DIR" <> 72 <*> strOption (long "challenge-dir" <> metavar "DIR" <>
70 help "output directory for ACME challenges") 73 help "Output directory for ACME challenges")
71 <*> optional 74 <*> optional
72 (strOption 75 (strOption
73 (long "domain-dir" <> 76 (long "domain-dir" <>
74 metavar "DIR" <> 77 metavar "DIR" <>
75 help 78 help
76 (unwords 79 (unwords
77 [ "directory in which to domain certificates and keys are stored;" 80 [ "Directory in which to domain certificates and keys are stored;"
78 , "the default is to use the (first) domain name as a directory name" 81 , "the default is to use the (first) domain name as a directory name"
79 ]))) 82 ])))
80 <*> optional 83 <*> optional
81 (strOption (long "email" <> metavar "ADDRESS" <> 84 (strOption (long "email" <> metavar "ADDRESS" <>
82 help "an email address with which to register an account")) 85 help "An email address with which to register an account"))
83 <*> optional (strOption (long "terms" <> metavar "URL" <> 86 <*> optional (strOption (long "terms" <> metavar "URL" <>
84 help "the terms param of the registration request")) 87 help "The terms param of the registration request"))
88 <*> switch
89 (long "skip-dhparams" <> help "Don't generate DH params for combined cert")
85 <*> switch 90 <*> switch
86 (long "staging" <> help 91 (long "staging" <> help
87 (unwords 92 (unwords
88 [ "use staging servers instead of live servers" 93 [ "Use staging servers instead of live servers"
89 , "(generated certificates will not be trusted!)" 94 , "(generated certificates will not be trusted!)"
90 ])) 95 ]))
91 96 <*> switch
92genKey :: FilePath -> IO String 97 (long "skip-provision-check" <> help
93genKey privKeyFile = withOpenSSL $ do 98 (unwords
94 kp <- generateRSAKey' 4096 65537 99 [ "Don't test whether HTTP provisioning works before"
95 pem <- writePKCS8PrivateKey kp Nothing 100 , "making ACME requests; only useful for testing."
96 writeFile privKeyFile pem 101 ]))
97 return pem
98
99getOrCreateKeys :: FilePath -> IO (Maybe Keys)
100getOrCreateKeys file = do
101 exists <- doesFileExist file
102 readKeys =<< if exists then readFile file else genKey file
103 102
104go :: CmdOpts -> IO () 103go :: CmdOpts -> IO ()
105go CmdOpts { .. } = do 104go CmdOpts { .. } = do
106 let terms = fromMaybe defaultTerms (join $ parseAbsoluteURI <$> optTerms) 105 let terms = fromMaybe defaultTerms (join $ parseAbsoluteURI <$> optTerms)
107 directoryUrl = if optStaging then stagingDirectoryUrl else liveDirectoryUrl 106 directoryUrl = if optStaging then stagingDirectoryUrl else liveDirectoryUrl
108 domainKeyFile = domainDir </> "rsa.key" 107 domainKeyFile = domainDir </> "rsa.key"
109 domainCertFile = domainDir </> "cert.der" 108 domainCombinedFile = domainDir </> "cert.combined.pem"
110 domainDir = fromMaybe (head optDomains) optDomainDir 109 domainCertFile = domainDir </> "cert.pem"
111 privKeyFile = optKeyFile 110 domainDhFile = domainDir </> "dhparams.pem"
112 requestDomains = fromMaybe (error "invalid domain name") $ sequence $ domainName . fromString <$> optDomains 111 domainDir = fromMaybe (head optDomains) optDomainDir
112 privKeyFile = optKeyFile
113 requestDomains = map domainName' optDomains
113 114
114 doesDirectoryExist domainDir `otherwiseM` createDirectory domainDir 115 doesDirectoryExist domainDir `otherwiseM` createDirectory domainDir
115 116
117 let issuerCertFile = "lets-encrypt-x1-cross-signed.pem"
118 issuerCert <- readFile issuerCertFile >>= readX509
119
116 Just domainKeys <- getOrCreateKeys domainKeyFile 120 Just domainKeys <- getOrCreateKeys domainKeyFile
117 Just keys <- getOrCreateKeys privKeyFile 121 Just keys <- getOrCreateKeys privKeyFile
118 122
119 challengeDir <- ensureWritableDir optChallengeDir "challenge directory" 123 challengeDir <- ensureWritableDir optChallengeDir "challenge directory"
120 void $ ensureWritableDir domainDir "domain directory" 124 void $ ensureWritableDir domainDir "domain directory"
121 125
122 let skipProvisionCheck = True 126 unless optSkipProvisionCheck $
123 unless skipProvisionCheck $
124 forM_ requestDomains $ canProvision challengeDir >=> 127 forM_ requestDomains $ canProvision challengeDir >=>
125 (`unless` error "Error: cannot provision files to web server via challenge directory") 128 (`unless` error "Error: cannot provision files to web server via challenge directory")
126 129
@@ -128,19 +131,44 @@ go CmdOpts { .. } = do
128 131
129 let email = either (error . ("Error: invalid email address: " ++)) id . validate . fromString <$> optEmail 132 let email = either (error . ("Error: invalid email address: " ++)) id . validate . fromString <$> optEmail
130 133
131 let issuerCertFile = "lets-encrypt-x1-cross-signed.pem" 134 dh <- if optSkipDH then return Nothing else Just <$> getOrCreateDH domainDhFile
132 issuerCert <- readFile issuerCertFile >>= readX509 135
136 certificate <- certify directoryUrl keys ((,) terms <$> email) (fileProvisioner challengeDir) certReq
137
138 let saveCombined = combinedCert issuerCert dh domainKeys >=> writeFile domainCombinedFile
139 savePEM = writeX509 >=> writeFile domainCertFile
140 saveBoth x509 = savePEM x509 >> saveCombined x509
141
142 either (error . ("Error: " ++)) saveBoth certificate
143
144genKey :: IO String
145genKey = withOpenSSL $ do
146 kp <- generateRSAKey' 4096 65537
147 writePKCS8PrivateKey kp Nothing
148
149getOrCreate :: IO String -> (String -> IO a) -> FilePath -> IO a
150getOrCreate gen parse file = do
151 exists <- doesFileExist file
152 parse =<< if exists then readFile file else gen >>= save file
153 where
154 save f x = writeFile f x >> return x
133 155
156getOrCreateKeys :: FilePath -> IO (Maybe Keys)
157getOrCreateKeys = getOrCreate genKey readKeys
158
159getOrCreateDH :: FilePath -> IO DHP
160getOrCreateDH = getOrCreate (genDHParams' >>= writeDHParams) readDHParams
161
162domainName' :: String -> DomainName
163domainName' dom = fromMaybe (error $ "Error: invalid domain name: " ++ show dom) (domainName $ fromString dom)
164
165genDHParams' :: IO DHP
166genDHParams' = do
134 hSetBuffering stdout NoBuffering 167 hSetBuffering stdout NoBuffering
135 putStr "Generating DH Params..." 168 putStr "Generating DH Params..."
136 dh <- genDHParams DHGen2 2048 169 dh <- genDHParams DHGen2 2048
137 putStrLn " Done." 170 putStrLn " Done."
138 171 return dh
139 certificate <- certify directoryUrl keys ((,) terms <$> email) (fileProvisioner challengeDir) certReq
140
141 either (error . ("Error: " ++))
142 (combinedCert issuerCert (Just dh) domainKeys >=> writeFile domainCertFile)
143 certificate
144 172
145combinedCert :: X509 -> Maybe DHP -> Keys -> X509 -> IO String 173combinedCert :: X509 -> Maybe DHP -> Keys -> X509 -> IO String
146combinedCert issuerCert dh (Keys privKey _) cert = do 174combinedCert issuerCert dh (Keys privKey _) cert = do