summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Cady <d@jerkface.net>2016-01-27 20:42:05 -0500
committerAndrew Cady <d@jerkface.net>2016-01-27 20:42:40 -0500
commit0bb7b5f6884a617301c9ddb0927f5829476483b4 (patch)
tree59c978a3af6c40e6037908d845b63b559d8e72f9
parent53840cb3e183bebead084a1ed550728b69ed88f3 (diff)
Re-order some definitions (no semantic changes)
-rw-r--r--src/Network/ACME.hs220
1 files changed, 115 insertions, 105 deletions
diff --git a/src/Network/ACME.hs b/src/Network/ACME.hs
index 2734132..e2c0996 100644
--- a/src/Network/ACME.hs
+++ b/src/Network/ACME.hs
@@ -49,52 +49,7 @@ import System.Directory
49import Text.Domain.Validate hiding (validate) 49import Text.Domain.Validate hiding (validate)
50import Text.Email.Validate 50import Text.Email.Validate
51 51
52genReq :: Keys -> [DomainName] -> IO CSR 52-- The `certify` function
53genReq _ [] = error "genReq called with zero domains"
54genReq (Keys priv pub) domains@(domain:_) = withOpenSSL $ do
55 Just dig <- getDigestByName "SHA256"
56 req <- newX509Req
57 setSubjectName req [("CN", domainToString domain)]
58 setVersion req 0
59 setPublicKey req pub
60 void $ addExtensions req [(nidSubjectAltName, intercalate ", " (map (("DNS:" ++) . domainToString) domains))]
61 signX509Req req priv (Just dig)
62 CSR domains . toStrict <$> writeX509ReqDER req
63 where
64 nidSubjectAltName = 85
65
66data Keys = Keys RSAKeyPair RSAPubKey
67readKeys :: String -> IO (Maybe Keys)
68readKeys privKeyData = do
69 keypair :: SomeKeyPair <- readPrivateKey privKeyData PwTTY
70 let (priv :: Maybe RSAKeyPair) = toKeyPair keypair
71 pub <- maybe (return Nothing) (fmap Just . rsaCopyPublic) priv
72 return $ Keys <$> priv <*> pub
73
74signPayload :: Keys -> String -> ByteString -> IO LC.ByteString
75signPayload (Keys priv pub) = signPayload' sign pub
76 where
77 sign x = do
78 Just dig <- getDigestByName "SHA256"
79 signBS dig priv x
80
81type HttpProvisioner = URI -> ByteString -> ResIO ()
82fileProvisioner :: WritableDir -> HttpProvisioner
83fileProvisioner challengeDir uri thumbtoken = do
84 void $ allocate (return f) removeFile
85 liftIO $ BC.writeFile f thumbtoken
86
87 where
88 f = (coerce challengeDir </>) . takeWhileEnd (/= '/') . uriPath $ uri
89 takeWhileEnd s = reverse . takeWhile s . reverse
90
91acmeChallengeURI :: DomainName -> BC.ByteString -> URI
92acmeChallengeURI dom tok = URI
93 "http:"
94 (Just $ URIAuth "" (domainToString dom) "")
95 ("/.well-known/acme-challenge" </> BC.unpack tok)
96 ""
97 ""
98 53
99certify :: URI -> Keys -> Maybe (URI, EmailAddress) -> HttpProvisioner -> CSR -> IO (Either String X509) 54certify :: URI -> Keys -> Maybe (URI, EmailAddress) -> HttpProvisioner -> CSR -> IO (Either String X509)
100certify directoryUrl keys reg provision certReq = 55certify directoryUrl keys reg provision certReq =
@@ -126,9 +81,17 @@ pollResults (link:links) = do
126 "invalid" -> return . Left $ r ^. responseBody . JSON.key "error" . to extractAcmeError 81 "invalid" -> return . Left $ r ^. responseBody . JSON.key "error" . to extractAcmeError
127 _ -> return . Left $ "unexpected response from ACME server: " ++ show r 82 _ -> return . Left $ "unexpected response from ACME server: " ++ show r
128 83
129data ChallengeRequest = ChallengeRequest { crUri :: String, crToken :: ByteString, crThumbToken :: ByteString } 84-- Provisioner callback
130 85
131data CSR = CSR { csrDomains :: [DomainName], csrData :: ByteString } 86type HttpProvisioner = URI -> ByteString -> ResIO ()
87fileProvisioner :: WritableDir -> HttpProvisioner
88fileProvisioner challengeDir uri thumbtoken = do
89 void $ allocate (return f) removeFile
90 liftIO $ BC.writeFile f thumbtoken
91
92 where
93 f = (coerce challengeDir </>) . takeWhileEnd (/= '/') . uriPath $ uri
94 takeWhileEnd s = reverse . takeWhile s . reverse
132 95
133newtype WritableDir = WritableDir String 96newtype WritableDir = WritableDir String
134ensureWritableDir :: FilePath -> String -> IO WritableDir 97ensureWritableDir :: FilePath -> String -> IO WritableDir
@@ -137,13 +100,6 @@ ensureWritableDir file name = do
137 return $ WritableDir file 100 return $ WritableDir file
138 where e n = error $ "Error: " ++ n ++ " is not writable" 101 where e n = error $ "Error: " ++ n ++ " is not writable"
139 102
140(</>) :: String -> String -> String
141a </> b = a ++ "/" ++ b
142infixr 5 </>
143
144domainToString :: DomainName -> String
145domainToString = T.unpack . decodeUtf8 . Text.Domain.Validate.toByteString
146
147canProvision :: WritableDir -> DomainName -> IO Bool 103canProvision :: WritableDir -> DomainName -> IO Bool
148canProvision challengeDir domain = do 104canProvision challengeDir domain = do
149 token <- (".test." ++) . show <$> getPOSIXTime 105 token <- (".test." ++) . show <$> getPOSIXTime
@@ -155,7 +111,72 @@ canProvision challengeDir domain = do
155 liftIO $ W.get (show uri) 111 liftIO $ W.get (show uri)
156 return $ r ^. responseBody == fromString token 112 return $ r ^. responseBody == fromString token
157 113
114-- The ACME monad
158 115
116data Directory = Directory {
117 _newCert :: String,
118 _newAuthz :: String,
119 _revokeCert :: String,
120 _newReg :: String
121}
122newtype Nonce = Nonce String
123data Env = Env { getDir :: Directory, getKeys :: Keys, getSession :: WS.Session }
124type ACME = RWST Env () Nonce IO
125
126runACME :: URI -> Keys -> ACME a -> IO a
127runACME url keys f = WS.withSession $ \sess -> do
128 Just (dir, nonce) <- getDirectory sess (show url)
129 fst <$> evalRWST f (Env dir keys sess) nonce
130
131post :: (MonadReader Env m, MonadState Nonce m, MonadIO m) => String -> LC.ByteString -> m (Response LC.ByteString)
132post url payload = do
133 sess <- asks getSession
134 r <- liftIO $ WS.postWith noStatusCheck sess url payload
135 put $ r ^?! responseHeader "Replay-Nonce" . to (Nonce . T.unpack . decodeUtf8)
136 return r
137 where
138 noStatusCheck = defaults & checkStatus .~ Just nullChecker
139 nullChecker _ _ _ = Nothing
140
141sendPayload :: (MonadIO m, MonadState Nonce m, MonadReader Env m) => (Directory -> String) -> ByteString -> m (Response LC.ByteString)
142sendPayload reqType payload = do
143 keys <- asks getKeys
144 dir <- asks getDir
145 nonce <- gets coerce
146 signed <- liftIO $ signPayload keys nonce payload
147 post (reqType dir) signed
148
149signPayload :: Keys -> String -> ByteString -> IO LC.ByteString
150signPayload (Keys priv pub) = signPayload' sign pub
151 where
152 sign x = do
153 Just dig <- getDigestByName "SHA256"
154 signBS dig priv x
155
156-- Generating ACME requests
157
158getDirectory :: WS.Session -> String -> IO (Maybe (Directory, Nonce))
159getDirectory sess url = do
160 r <- WS.get sess url
161 let nonce = r ^? responseHeader "Replay-Nonce" . to (Nonce . T.unpack . decodeUtf8)
162 k x = r ^? responseBody . JSON.key x . _String . to T.unpack
163 return $ (,) <$> (Directory <$> k "new-cert" <*> k "new-authz" <*> k "revoke-cert" <*> k "new-reg") <*> nonce
164
165retrieveCert :: (MonadReader Env m, MonadState Nonce m, MonadIO m) => CSR -> m (Response LC.ByteString)
166retrieveCert input = sendPayload _newCert (csr $ csrData input)
167
168notifyChallenge :: (MonadReader Env m, MonadState Nonce m, MonadIO m) => String -> ByteString -> m (Response LC.ByteString)
169notifyChallenge uri thumbtoken = sendPayload (const uri) (challenge thumbtoken)
170
171register :: URI -> EmailAddress -> ACME (Response LC.ByteString)
172register terms email = sendPayload _newReg (registration email (show terms))
173
174challengeRequest :: (MonadIO m, MonadState Nonce m, MonadReader Env m) => DomainName -> m (Response LC.ByteString)
175challengeRequest = sendPayload _newAuthz . authz . domainToString
176
177-- Handling ACME responses
178
179data ChallengeRequest = ChallengeRequest { crUri :: String, crToken :: ByteString, crThumbToken :: ByteString }
159extractCR :: MonadReader Env m => Response LC.ByteString -> m ChallengeRequest 180extractCR :: MonadReader Env m => Response LC.ByteString -> m ChallengeRequest
160extractCR r = do 181extractCR r = do
161 Keys _ pub <- asks getKeys 182 Keys _ pub <- asks getKeys
@@ -195,41 +216,6 @@ checkCertResponse r =
195 where 216 where
196 isSuccess n = n >= 200 && n <= 300 217 isSuccess n = n >= 200 && n <= 300
197 218
198retrieveCert :: (MonadReader Env m, MonadState Nonce m, MonadIO m) => CSR -> m (Response LC.ByteString)
199retrieveCert input = sendPayload _newCert (csr $ csrData input)
200
201notifyChallenge :: (MonadReader Env m, MonadState Nonce m, MonadIO m) => String -> ByteString -> m (Response LC.ByteString)
202notifyChallenge uri thumbtoken = sendPayload (const uri) (challenge thumbtoken)
203
204data Env = Env { getDir :: Directory, getKeys :: Keys, getSession :: WS.Session }
205
206type ACME = RWST Env () Nonce IO
207runACME :: URI -> Keys -> ACME a -> IO a
208runACME url keys f = WS.withSession $ \sess -> do
209 Just (dir, nonce) <- getDirectory sess (show url)
210 fst <$> evalRWST f (Env dir keys sess) nonce
211
212data Directory = Directory {
213 _newCert :: String,
214 _newAuthz :: String,
215 _revokeCert :: String,
216 _newReg :: String
217}
218newtype Nonce = Nonce String
219
220getDirectory :: WS.Session -> String -> IO (Maybe (Directory, Nonce))
221getDirectory sess url = do
222 r <- WS.get sess url
223 let nonce = r ^? responseHeader "Replay-Nonce" . to (Nonce . T.unpack . decodeUtf8)
224 k x = r ^? responseBody . JSON.key x . _String . to T.unpack
225 return $ (,) <$> (Directory <$> k "new-cert" <*> k "new-authz" <*> k "revoke-cert" <*> k "new-reg") <*> nonce
226
227register :: URI -> EmailAddress -> ACME (Response LC.ByteString)
228register terms email = sendPayload _newReg (registration email (show terms))
229
230challengeRequest :: (MonadIO m, MonadState Nonce m, MonadReader Env m) => DomainName -> m (Response LC.ByteString)
231challengeRequest = sendPayload _newAuthz . authz . domainToString
232
233statusLine :: Response body -> String 219statusLine :: Response body -> String
234statusLine r = (r ^. responseStatus . statusCode . to show) ++ " " ++ r ^. responseStatus . statusMessage . to (T.unpack . decodeUtf8) 220statusLine r = (r ^. responseStatus . statusCode . to show) ++ " " ++ r ^. responseStatus . statusMessage . to (T.unpack . decodeUtf8)
235 221
@@ -238,20 +224,44 @@ statusReport r = do
238 liftIO $ putStrLn $ statusLine r 224 liftIO $ putStrLn $ statusLine r
239 return r 225 return r
240 226
241sendPayload :: (MonadIO m, MonadState Nonce m, MonadReader Env m) => (Directory -> String) -> ByteString -> m (Response LC.ByteString) 227-- OpenSSL operations
242sendPayload reqType payload = do
243 keys <- asks getKeys
244 dir <- asks getDir
245 nonce <- gets coerce
246 signed <- liftIO $ signPayload keys nonce payload
247 post (reqType dir) signed
248 228
249post :: (MonadReader Env m, MonadState Nonce m, MonadIO m) => String -> LC.ByteString -> m (Response LC.ByteString) 229data CSR = CSR { csrDomains :: [DomainName], csrData :: ByteString }
250post url payload = do 230genReq :: Keys -> [DomainName] -> IO CSR
251 sess <- asks getSession 231genReq _ [] = error "genReq called with zero domains"
252 r <- liftIO $ WS.postWith noStatusCheck sess url payload 232genReq (Keys priv pub) domains@(domain:_) = withOpenSSL $ do
253 put $ r ^?! responseHeader "Replay-Nonce" . to (Nonce . T.unpack . decodeUtf8) 233 Just dig <- getDigestByName "SHA256"
254 return r 234 req <- newX509Req
235 setSubjectName req [("CN", domainToString domain)]
236 setVersion req 0
237 setPublicKey req pub
238 void $ addExtensions req [(nidSubjectAltName, intercalate ", " (map (("DNS:" ++) . domainToString) domains))]
239 signX509Req req priv (Just dig)
240 CSR domains . toStrict <$> writeX509ReqDER req
255 where 241 where
256 noStatusCheck = defaults & checkStatus .~ Just nullChecker 242 nidSubjectAltName = 85
257 nullChecker _ _ _ = Nothing 243
244data Keys = Keys RSAKeyPair RSAPubKey
245readKeys :: String -> IO (Maybe Keys)
246readKeys privKeyData = do
247 keypair :: SomeKeyPair <- readPrivateKey privKeyData PwTTY
248 let (priv :: Maybe RSAKeyPair) = toKeyPair keypair
249 pub <- maybe (return Nothing) (fmap Just . rsaCopyPublic) priv
250 return $ Keys <$> priv <*> pub
251
252-- General utility
253
254(</>) :: String -> String -> String
255a </> b = a ++ "/" ++ b
256infixr 5 </>
257
258domainToString :: DomainName -> String
259domainToString = T.unpack . decodeUtf8 . Text.Domain.Validate.toByteString
260
261acmeChallengeURI :: DomainName -> BC.ByteString -> URI
262acmeChallengeURI dom tok = URI
263 "http:"
264 (Just $ URIAuth "" (domainToString dom) "")
265 ("/.well-known/acme-challenge" </> BC.unpack tok)
266 ""
267 ""