summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Cady <d@jerkface.net>2016-01-24 12:50:34 -0500
committerAndrew Cady <d@jerkface.net>2016-01-24 12:50:34 -0500
commit5921fc9e6876536178f903cd5c18be0308af89cf (patch)
tree8099057bdf9a8115a902196a804253a7079c1340
parent0a77e9cb30c12a516993013b7ab5f4272bea94f9 (diff)
validate domain names
-rw-r--r--acme.hs43
-rw-r--r--stack.yaml4
2 files changed, 27 insertions, 20 deletions
diff --git a/acme.hs b/acme.hs
index be5a319..3ebc911 100644
--- a/acme.hs
+++ b/acme.hs
@@ -44,8 +44,9 @@ import Options.Applicative hiding (header)
44import qualified Options.Applicative as Opt 44import qualified Options.Applicative as Opt
45import Pipes 45import Pipes
46import System.Directory 46import System.Directory
47import Text.Email.Validate 47import Text.Email.Validate
48import Network.URI 48import Text.Domain.Validate hiding (validate)
49import Network.URI
49 50
50stagingDirectoryUrl, liveDirectoryUrl :: URI 51stagingDirectoryUrl, liveDirectoryUrl :: URI
51Just liveDirectoryUrl = parseAbsoluteURI "https://acme-v01.api.letsencrypt.org/directory" 52Just liveDirectoryUrl = parseAbsoluteURI "https://acme-v01.api.letsencrypt.org/directory"
@@ -114,16 +115,16 @@ genKey privKeyFile = withOpenSSL $ do
114 pem <- writePKCS8PrivateKey kp Nothing 115 pem <- writePKCS8PrivateKey kp Nothing
115 writeFile privKeyFile pem 116 writeFile privKeyFile pem
116 117
117genReq :: FilePath -> [String] -> IO LC.ByteString 118genReq :: FilePath -> [DomainName] -> IO LC.ByteString
118genReq _ [] = error "genReq called with zero domains" 119genReq _ [] = error "genReq called with zero domains"
119genReq domainKeyFile domains@(domain:_) = withOpenSSL $ do 120genReq domainKeyFile domains@(domain:_) = withOpenSSL $ do
120 Just (Keys priv pub) <- readKeyFile domainKeyFile 121 Just (Keys priv pub) <- readKeyFile domainKeyFile
121 Just dig <- getDigestByName "SHA256" 122 Just dig <- getDigestByName "SHA256"
122 req <- newX509Req 123 req <- newX509Req
123 setSubjectName req [("CN", domain)] 124 setSubjectName req [("CN", show domain)]
124 setVersion req 0 125 setVersion req 0
125 setPublicKey req pub 126 setPublicKey req pub
126 void $ addExtensions req [(nidSubjectAltName, intercalate ", " (map ("DNS:" ++) domains))] 127 void $ addExtensions req [(nidSubjectAltName, intercalate ", " (map (("DNS:" ++) . show) domains))]
127 signX509Req req priv (Just dig) 128 signX509Req req priv (Just dig)
128 writeX509ReqDER req 129 writeX509ReqDER req
129 where 130 where
@@ -147,7 +148,7 @@ go CmdOpts { .. } = do
147 domainCertFile = domainDir </> "cert.der" 148 domainCertFile = domainDir </> "cert.der"
148 domainDir = fromMaybe (head optDomains) optDomainDir 149 domainDir = fromMaybe (head optDomains) optDomainDir
149 privKeyFile = optKeyFile 150 privKeyFile = optKeyFile
150 requestDomains = optDomains 151 requestDomains = fromMaybe (error "invalid domain name") $ sequence $ domainName . fromString <$> optDomains
151 152
152 doesFileExist privKeyFile `otherwiseM` genKey privKeyFile 153 doesFileExist privKeyFile `otherwiseM` genKey privKeyFile
153 154
@@ -156,20 +157,19 @@ go CmdOpts { .. } = do
156 157
157 Just keys <- readKeyFile privKeyFile 158 Just keys <- readKeyFile privKeyFile
158 159
159 ensureWritable optChallengeDir "challenge directory" 160 challengeDir <- ensureWritableDir optChallengeDir "challenge directory"
160 ensureWritable domainDir "domain directory" 161 void $ ensureWritableDir domainDir "domain directory"
161 162
162 forM_ requestDomains $ canProvision optChallengeDir >=> (`unless` error "Error: cannot provision files to web server via challenge directory") 163 forM_ requestDomains $ canProvision challengeDir >=> (`unless` error "Error: cannot provision files to web server via challenge directory")
163 164
164 csrData <- CSR . toStrict <$> genReq domainKeyFile requestDomains 165 csrData <- CSR . toStrict <$> genReq domainKeyFile requestDomains
165 B.writeFile domainCSRFile (coerce csrData) 166 B.writeFile domainCSRFile (coerce csrData)
166 167
167 let email = either (error . ("Error: invalid email address: " ++)) id . validate . fromString <$> optEmail 168 let email = either (error . ("Error: invalid email address: " ++)) id . validate . fromString <$> optEmail
168 169
169 certify directoryUrl keys email terms requestDomains optChallengeDir csrData domainCertFile 170 certify directoryUrl keys email terms requestDomains challengeDir csrData domainCertFile
170 171
171type DomainName = String -- TODO: use validated type 172certify :: URI -> Keys -> Maybe EmailAddress -> URI -> [DomainName] -> WritableDir -> CSR -> FilePath -> IO ()
172certify :: URI -> Keys -> Maybe EmailAddress -> URI -> [DomainName] -> String -> CSR -> FilePath -> IO ()
173certify directoryUrl keys optEmail terms requestDomains optChallengeDir csrData domainCertFile = 173certify directoryUrl keys optEmail terms requestDomains optChallengeDir csrData domainCertFile =
174 174
175 runACME directoryUrl keys $ do 175 runACME directoryUrl keys $ do
@@ -180,7 +180,7 @@ certify directoryUrl keys optEmail terms requestDomains optChallengeDir csrData
180 consumer :: Consumer ChallengeRequest ACME () 180 consumer :: Consumer ChallengeRequest ACME ()
181 consumer = forever $ await >>= consume1 181 consumer = forever $ await >>= consume1
182 consume1 (ChallengeRequest nextUri token thumbtoken) = do 182 consume1 (ChallengeRequest nextUri token thumbtoken) = do
183 lift $ liftIO $ BC.writeFile (optChallengeDir </> BC.unpack token) thumbtoken 183 lift $ liftIO $ BC.writeFile (coerce optChallengeDir </> BC.unpack token) thumbtoken
184 notifyChallenge nextUri thumbtoken >>= statusReport >>= ncErrorReport 184 notifyChallenge nextUri thumbtoken >>= statusReport >>= ncErrorReport
185 185
186 runEffect $ producer >-> consumer 186 runEffect $ producer >-> consumer
@@ -193,21 +193,24 @@ newtype CSR = CSR ByteString
193a </> b = a ++ "/" ++ b 193a </> b = a ++ "/" ++ b
194infixr 5 </> 194infixr 5 </>
195 195
196canProvision :: FilePath -> String -> IO Bool 196canProvision :: WritableDir -> DomainName -> IO Bool
197canProvision challengeDir domain = do 197canProvision challengeDir domain = do
198 randomish <- fromString . show <$> getPOSIXTime 198 randomish <- fromString . show <$> getPOSIXTime
199 199
200 let absFile = challengeDir </> relFile 200 let absFile = coerce challengeDir </> relFile
201 relFile = ".test." ++ show randomish 201 relFile = ".test." ++ show randomish
202 202
203 LC.writeFile absFile randomish 203 LC.writeFile absFile randomish
204 r <- W.get $ "http://" ++ domain </> ".well-known/acme-challenge" </> relFile 204 r <- W.get $ "http://" ++ show domain </> ".well-known/acme-challenge" </> relFile
205 removeFile absFile 205 removeFile absFile
206 return $ r ^. responseBody == randomish 206 return $ r ^. responseBody == randomish
207 207
208 208
209ensureWritable :: FilePath -> String -> IO () 209newtype WritableDir = WritableDir String
210ensureWritable file name = (writable <$> getPermissions file) >>= flip unless (err name) 210ensureWritableDir :: FilePath -> String -> IO WritableDir
211ensureWritableDir file name = do
212 (writable <$> getPermissions file) >>= flip unless (err name)
213 return $ WritableDir file
211 where err n = error $ "Error: " ++ n ++ " is not writable" 214 where err n = error $ "Error: " ++ n ++ " is not writable"
212 215
213extractCR :: MonadReader Env m => Response LC.ByteString -> m ChallengeRequest 216extractCR :: MonadReader Env m => Response LC.ByteString -> m ChallengeRequest
@@ -277,8 +280,8 @@ getDirectory sess url = do
277register :: URI -> EmailAddress -> ACME (Response LC.ByteString) 280register :: URI -> EmailAddress -> ACME (Response LC.ByteString)
278register terms email = sendPayload _newReg (registration email (show terms)) 281register terms email = sendPayload _newReg (registration email (show terms))
279 282
280challengeRequest :: (MonadIO m, MonadState Nonce m, MonadReader Env m) => String -> m (Response LC.ByteString) 283challengeRequest :: (MonadIO m, MonadState Nonce m, MonadReader Env m) => DomainName -> m (Response LC.ByteString)
281challengeRequest domain = sendPayload _newAuthz (authz domain) 284challengeRequest = sendPayload _newAuthz . authz . show
282 285
283statusLine :: Response body -> String 286statusLine :: Response body -> String
284statusLine r = (r ^. responseStatus . statusCode . to show) ++ " " ++ r ^. responseStatus . statusMessage . to (T.unpack . decodeUtf8) 287statusLine r = (r ^. responseStatus . statusCode . to show) ++ " " ++ r ^. responseStatus . statusMessage . to (T.unpack . decodeUtf8)
diff --git a/stack.yaml b/stack.yaml
index df9bc13..12e38e9 100644
--- a/stack.yaml
+++ b/stack.yaml
@@ -10,6 +10,10 @@ packages:
10 git: git@github.com:afcady/HSOpenSSL.git 10 git: git@github.com:afcady/HSOpenSSL.git
11 commit: 11f5c83fbe44d6c1c496be4cc3017fd925ba26e2 11 commit: 11f5c83fbe44d6c1c496be4cc3017fd925ba26e2
12 extra-dep: true 12 extra-dep: true
13- location:
14 git: git@github.com:afcady/email-validate-hs.git
15 commit: faf9c1bbb2051de314bce4c159d4dbdbc768b629
16 extra-dep: true
13 17
14# Packages to be pulled from upstream that are not in the resolver (e.g., acme-missiles-0.3) 18# Packages to be pulled from upstream that are not in the resolver (e.g., acme-missiles-0.3)
15extra-deps: [] 19extra-deps: []