diff options
author | Andrew Cady <d@jerkface.net> | 2016-01-26 14:12:13 -0500 |
---|---|---|
committer | Andrew Cady <d@jerkface.net> | 2016-01-26 14:22:16 -0500 |
commit | 64e8a8ef7833fb7a9325372c09bcb9a682e1ed30 (patch) | |
tree | 275bd316cf63d4406714b332234e7b27480b6342 /acme-certify.hs | |
parent | 7373a3ede2216048d2766f8f27e77d014b82dc43 (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.hs | 130 |
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 @@ | |||
12 | module Main where | 12 | module Main where |
13 | 13 | ||
14 | import BasePrelude | 14 | import BasePrelude |
15 | import qualified Data.ByteString.Lazy.Char8 as LC | 15 | import Network.ACME (canProvision, certify, |
16 | import Network.ACME (canProvision, certify, fileProvisioner, ensureWritableDir, (</>), genReq) | 16 | ensureWritableDir, fileProvisioner, |
17 | import Network.ACME.Encoding (Keys (..), readKeys) | 17 | genReq, (</>)) |
18 | import Network.ACME.Encoding (Keys (..), readKeys) | ||
18 | import Network.URI | 19 | import Network.URI |
19 | import OpenSSL | 20 | import OpenSSL |
20 | import OpenSSL.X509 (X509) | ||
21 | import OpenSSL.DH | 21 | import OpenSSL.DH |
22 | import OpenSSL.PEM | 22 | import OpenSSL.PEM |
23 | import OpenSSL.RSA | 23 | import OpenSSL.RSA |
24 | import Options.Applicative hiding (header) | 24 | import OpenSSL.X509 (X509) |
25 | import qualified Options.Applicative as Opt | 25 | import Options.Applicative hiding (header) |
26 | import qualified Options.Applicative as Opt | ||
26 | import System.Directory | 27 | import System.Directory |
27 | import Text.Domain.Validate hiding (validate) | 28 | import System.IO |
29 | import Text.Domain.Validate hiding (validate) | ||
28 | import Text.Email.Validate | 30 | import Text.Email.Validate |
29 | import System.IO | ||
30 | 31 | ||
31 | stagingDirectoryUrl, liveDirectoryUrl :: URI | 32 | stagingDirectoryUrl, liveDirectoryUrl :: URI |
32 | Just liveDirectoryUrl = parseAbsoluteURI "https://acme-v01.api.letsencrypt.org/directory" | 33 | Just liveDirectoryUrl = parseAbsoluteURI "https://acme-v01.api.letsencrypt.org/directory" |
@@ -42,13 +43,15 @@ main = execParser opts >>= go | |||
42 | ] | 43 | ] |
43 | 44 | ||
44 | data CmdOpts = CmdOpts { | 45 | data 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 | ||
54 | defaultTerms :: URI | 57 | defaultTerms :: URI |
@@ -56,71 +59,71 @@ Just defaultTerms = parseAbsoluteURI "https://letsencrypt.org/documents/LE-SA-v1 | |||
56 | 59 | ||
57 | cmdopts :: Parser CmdOpts | 60 | cmdopts :: Parser CmdOpts |
58 | cmdopts = CmdOpts <$> strOption (long "key" <> metavar "FILE" <> | 61 | cmdopts = 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 | |
92 | genKey :: FilePath -> IO String | 97 | (long "skip-provision-check" <> help |
93 | genKey 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 | |||
99 | getOrCreateKeys :: FilePath -> IO (Maybe Keys) | ||
100 | getOrCreateKeys file = do | ||
101 | exists <- doesFileExist file | ||
102 | readKeys =<< if exists then readFile file else genKey file | ||
103 | 102 | ||
104 | go :: CmdOpts -> IO () | 103 | go :: CmdOpts -> IO () |
105 | go CmdOpts { .. } = do | 104 | go 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 | |||
144 | genKey :: IO String | ||
145 | genKey = withOpenSSL $ do | ||
146 | kp <- generateRSAKey' 4096 65537 | ||
147 | writePKCS8PrivateKey kp Nothing | ||
148 | |||
149 | getOrCreate :: IO String -> (String -> IO a) -> FilePath -> IO a | ||
150 | getOrCreate 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 | ||
156 | getOrCreateKeys :: FilePath -> IO (Maybe Keys) | ||
157 | getOrCreateKeys = getOrCreate genKey readKeys | ||
158 | |||
159 | getOrCreateDH :: FilePath -> IO DHP | ||
160 | getOrCreateDH = getOrCreate (genDHParams' >>= writeDHParams) readDHParams | ||
161 | |||
162 | domainName' :: String -> DomainName | ||
163 | domainName' dom = fromMaybe (error $ "Error: invalid domain name: " ++ show dom) (domainName $ fromString dom) | ||
164 | |||
165 | genDHParams' :: IO DHP | ||
166 | genDHParams' = 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 | ||
145 | combinedCert :: X509 -> Maybe DHP -> Keys -> X509 -> IO String | 173 | combinedCert :: X509 -> Maybe DHP -> Keys -> X509 -> IO String |
146 | combinedCert issuerCert dh (Keys privKey _) cert = do | 174 | combinedCert issuerCert dh (Keys privKey _) cert = do |