summaryrefslogtreecommitdiff
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
parent7373a3ede2216048d2766f8f27e77d014b82dc43 (diff)
Pre-generate DH params
The program now outputs a combined PEM certificate. A new option allows DH-param generation to be disabled.
-rw-r--r--README.md52
-rw-r--r--acme-certify.hs130
2 files changed, 111 insertions, 71 deletions
diff --git a/README.md b/README.md
index 9db3b7b..b2d5422 100644
--- a/README.md
+++ b/README.md
@@ -1,31 +1,35 @@
1# Let's Encrypt ACME protocol 1# `acme-certify` -- A `Let's Encrypt!` ACME client
2 2
3``` 3```
4Let's Encrypt! ACME client 4Let's Encrypt! ACME client
5 5
6Usage: acme-certify --key FILE --domain DOMAIN --challenge-dir DIR 6Usage: acme-certify --key FILE --domain DOMAIN --challenge-dir DIR
7 [--domain-dir DIR] [--email ADDRESS] [--terms URL] 7 [--domain-dir DIR] [--email ADDRESS] [--terms URL]
8 [--staging] 8 [--skip-dhparams] [--staging] [--skip-provision-check]
9 This program will generate a signed TLS certificate using the ACME protocol 9 This program will generate a signed TLS certificate using the ACME protocol
10 and the free Let's Encrypt! CA. 10 and the free Let's Encrypt! CA.
11 11
12Available options: 12Available options:
13 -h,--help Show this help text 13 -h,--help Show this help text
14 --key FILE filename of your private RSA key 14 --key FILE Filename of your private RSA key
15 --domain DOMAIN the domain name(s) to certify; specify more than once 15 --domain DOMAIN The domain name(s) to certify; specify more than once
16 for a multi-domain certificate 16 for a multi-domain certificate
17 --challenge-dir DIR output directory for ACME challenges 17 --challenge-dir DIR Output directory for ACME challenges
18 --domain-dir DIR directory in which to domain certificates and keys 18 --domain-dir DIR Directory in which to domain certificates and keys
19 are stored; the default is to use the (first) domain 19 are stored; the default is to use the (first) domain
20 name as a directory name 20 name as a directory name
21 --email ADDRESS an email address with which to register an account 21 --email ADDRESS An email address with which to register an account
22 --terms URL the terms param of the registration request 22 --terms URL The terms param of the registration request
23 --staging use staging servers instead of live servers 23 --skip-dhparams Don't generate DH params for combined cert
24 --staging Use staging servers instead of live servers
24 (generated certificates will not be trusted!) 25 (generated certificates will not be trusted!)
26 --skip-provision-check Don't test whether HTTP provisioning works before
27 making ACME requests; only useful for testing.
25``` 28```
26 29
27This program can be used to obtain a certificate from 30This program can be used to obtain a certificate from the
28[Let's Encrypt](https://letsencrypt.org/) using their ACME protocol. 31[Let's Encrypt](https://letsencrypt.org/) certificate authority, using their
32ACME protocol.
29 33
30## Rate Limits 34## Rate Limits
31 35
@@ -63,27 +67,35 @@ openssl genrsa 4096 > ${DOMAIN}/rsa.key
63 67
64## Receive certificate 68## Receive certificate
65 69
66The signed certificate will be saved by this program in 70The signed certificate will be saved by this program in `./${DOMAIN}/cert.pem`.
67``./${DOMAIN}/cert.der``. You can copy that file to the place your TLS 71A combined certificate, containing the issuer certificate, the private key, and
68server is configured to read it. 72(possibly) DH parameters, will be saved in `./${DOMAIN}/cert.combined.pem`. You
73can copy that file to the place your TLS server is configured to read it.
69 74
70You can also view the certificate like so: 75You can also view the certificate like so:
71 76
72``` 77```
73openssl x509 -inform der -in ${DOMAIN}/cert.der -noout -text | less 78openssl x509 -in ${DOMAIN}/cert.pem -noout -text | less
74``` 79```
75 80
76## Create a certificate for HAProxy 81## Create a certificate for HAProxy
77 82
78Vo Minh Thu, the original author of this program, suggests to include explicit 83Vo Minh Thu, the original author of this program, suggests to include explicit
79DH key exchange parameters to prevent the [Logjam attack](https://weakdh.org/). 84DH key exchange parameters to prevent the [Logjam attack](https://weakdh.org/).
85This is now automatically performed by default.
86
87Note: generating DH params is CPU-intensive and takes a long time. For that
88reason, it is done once per domain, and the result is saved in
89`${DOMAIN}/dhparams.pem`.
90
91You can also disable DH generation it with `--skip-dhparams`.
92
93The certificate is generated by this program equivalently to this:
80 94
81``` 95```
82> openssl x509 -inform der -in ${DOMAIN}/cert.der \ 96openssl dhparam -out ${DOMAIN}/dhparams.pem 2048
83 -out ${DOMAIN}/cert.pem 97cat ${DOMAIN}/cert.pem \
84> openssl dhparam -out ${DOMAIN}/dhparams.pem 2048
85> cat ${DOMAIN}/cert.pem \
86 lets-encrypt-x1-cross-signed.pem \ 98 lets-encrypt-x1-cross-signed.pem \
87 ${DOMAIN}/rsa.key \ 99 ${DOMAIN}/rsa.key \
88 ${DOMAIN}/dhparams.pem > aaa.reesd.com-combined.pem 100 ${DOMAIN}/dhparams.pem > ${DOMAIN}/cert.combined.pem
89``` 101```
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