diff options
author | Andrew Cady <d@jerkface.net> | 2016-01-27 20:42:05 -0500 |
---|---|---|
committer | Andrew Cady <d@jerkface.net> | 2016-01-27 20:42:40 -0500 |
commit | 0bb7b5f6884a617301c9ddb0927f5829476483b4 (patch) | |
tree | 59c978a3af6c40e6037908d845b63b559d8e72f9 | |
parent | 53840cb3e183bebead084a1ed550728b69ed88f3 (diff) |
Re-order some definitions (no semantic changes)
-rw-r--r-- | src/Network/ACME.hs | 220 |
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 | |||
49 | import Text.Domain.Validate hiding (validate) | 49 | import Text.Domain.Validate hiding (validate) |
50 | import Text.Email.Validate | 50 | import Text.Email.Validate |
51 | 51 | ||
52 | genReq :: Keys -> [DomainName] -> IO CSR | 52 | -- The `certify` function |
53 | genReq _ [] = error "genReq called with zero domains" | ||
54 | genReq (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 | |||
66 | data Keys = Keys RSAKeyPair RSAPubKey | ||
67 | readKeys :: String -> IO (Maybe Keys) | ||
68 | readKeys 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 | |||
74 | signPayload :: Keys -> String -> ByteString -> IO LC.ByteString | ||
75 | signPayload (Keys priv pub) = signPayload' sign pub | ||
76 | where | ||
77 | sign x = do | ||
78 | Just dig <- getDigestByName "SHA256" | ||
79 | signBS dig priv x | ||
80 | |||
81 | type HttpProvisioner = URI -> ByteString -> ResIO () | ||
82 | fileProvisioner :: WritableDir -> HttpProvisioner | ||
83 | fileProvisioner 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 | |||
91 | acmeChallengeURI :: DomainName -> BC.ByteString -> URI | ||
92 | acmeChallengeURI dom tok = URI | ||
93 | "http:" | ||
94 | (Just $ URIAuth "" (domainToString dom) "") | ||
95 | ("/.well-known/acme-challenge" </> BC.unpack tok) | ||
96 | "" | ||
97 | "" | ||
98 | 53 | ||
99 | certify :: URI -> Keys -> Maybe (URI, EmailAddress) -> HttpProvisioner -> CSR -> IO (Either String X509) | 54 | certify :: URI -> Keys -> Maybe (URI, EmailAddress) -> HttpProvisioner -> CSR -> IO (Either String X509) |
100 | certify directoryUrl keys reg provision certReq = | 55 | certify 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 | ||
129 | data ChallengeRequest = ChallengeRequest { crUri :: String, crToken :: ByteString, crThumbToken :: ByteString } | 84 | -- Provisioner callback |
130 | 85 | ||
131 | data CSR = CSR { csrDomains :: [DomainName], csrData :: ByteString } | 86 | type HttpProvisioner = URI -> ByteString -> ResIO () |
87 | fileProvisioner :: WritableDir -> HttpProvisioner | ||
88 | fileProvisioner 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 | ||
133 | newtype WritableDir = WritableDir String | 96 | newtype WritableDir = WritableDir String |
134 | ensureWritableDir :: FilePath -> String -> IO WritableDir | 97 | ensureWritableDir :: 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 | ||
141 | a </> b = a ++ "/" ++ b | ||
142 | infixr 5 </> | ||
143 | |||
144 | domainToString :: DomainName -> String | ||
145 | domainToString = T.unpack . decodeUtf8 . Text.Domain.Validate.toByteString | ||
146 | |||
147 | canProvision :: WritableDir -> DomainName -> IO Bool | 103 | canProvision :: WritableDir -> DomainName -> IO Bool |
148 | canProvision challengeDir domain = do | 104 | canProvision 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 | ||
116 | data Directory = Directory { | ||
117 | _newCert :: String, | ||
118 | _newAuthz :: String, | ||
119 | _revokeCert :: String, | ||
120 | _newReg :: String | ||
121 | } | ||
122 | newtype Nonce = Nonce String | ||
123 | data Env = Env { getDir :: Directory, getKeys :: Keys, getSession :: WS.Session } | ||
124 | type ACME = RWST Env () Nonce IO | ||
125 | |||
126 | runACME :: URI -> Keys -> ACME a -> IO a | ||
127 | runACME 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 | |||
131 | post :: (MonadReader Env m, MonadState Nonce m, MonadIO m) => String -> LC.ByteString -> m (Response LC.ByteString) | ||
132 | post 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 | |||
141 | sendPayload :: (MonadIO m, MonadState Nonce m, MonadReader Env m) => (Directory -> String) -> ByteString -> m (Response LC.ByteString) | ||
142 | sendPayload 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 | |||
149 | signPayload :: Keys -> String -> ByteString -> IO LC.ByteString | ||
150 | signPayload (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 | |||
158 | getDirectory :: WS.Session -> String -> IO (Maybe (Directory, Nonce)) | ||
159 | getDirectory 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 | |||
165 | retrieveCert :: (MonadReader Env m, MonadState Nonce m, MonadIO m) => CSR -> m (Response LC.ByteString) | ||
166 | retrieveCert input = sendPayload _newCert (csr $ csrData input) | ||
167 | |||
168 | notifyChallenge :: (MonadReader Env m, MonadState Nonce m, MonadIO m) => String -> ByteString -> m (Response LC.ByteString) | ||
169 | notifyChallenge uri thumbtoken = sendPayload (const uri) (challenge thumbtoken) | ||
170 | |||
171 | register :: URI -> EmailAddress -> ACME (Response LC.ByteString) | ||
172 | register terms email = sendPayload _newReg (registration email (show terms)) | ||
173 | |||
174 | challengeRequest :: (MonadIO m, MonadState Nonce m, MonadReader Env m) => DomainName -> m (Response LC.ByteString) | ||
175 | challengeRequest = sendPayload _newAuthz . authz . domainToString | ||
176 | |||
177 | -- Handling ACME responses | ||
178 | |||
179 | data ChallengeRequest = ChallengeRequest { crUri :: String, crToken :: ByteString, crThumbToken :: ByteString } | ||
159 | extractCR :: MonadReader Env m => Response LC.ByteString -> m ChallengeRequest | 180 | extractCR :: MonadReader Env m => Response LC.ByteString -> m ChallengeRequest |
160 | extractCR r = do | 181 | extractCR 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 | ||
198 | retrieveCert :: (MonadReader Env m, MonadState Nonce m, MonadIO m) => CSR -> m (Response LC.ByteString) | ||
199 | retrieveCert input = sendPayload _newCert (csr $ csrData input) | ||
200 | |||
201 | notifyChallenge :: (MonadReader Env m, MonadState Nonce m, MonadIO m) => String -> ByteString -> m (Response LC.ByteString) | ||
202 | notifyChallenge uri thumbtoken = sendPayload (const uri) (challenge thumbtoken) | ||
203 | |||
204 | data Env = Env { getDir :: Directory, getKeys :: Keys, getSession :: WS.Session } | ||
205 | |||
206 | type ACME = RWST Env () Nonce IO | ||
207 | runACME :: URI -> Keys -> ACME a -> IO a | ||
208 | runACME 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 | |||
212 | data Directory = Directory { | ||
213 | _newCert :: String, | ||
214 | _newAuthz :: String, | ||
215 | _revokeCert :: String, | ||
216 | _newReg :: String | ||
217 | } | ||
218 | newtype Nonce = Nonce String | ||
219 | |||
220 | getDirectory :: WS.Session -> String -> IO (Maybe (Directory, Nonce)) | ||
221 | getDirectory 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 | |||
227 | register :: URI -> EmailAddress -> ACME (Response LC.ByteString) | ||
228 | register terms email = sendPayload _newReg (registration email (show terms)) | ||
229 | |||
230 | challengeRequest :: (MonadIO m, MonadState Nonce m, MonadReader Env m) => DomainName -> m (Response LC.ByteString) | ||
231 | challengeRequest = sendPayload _newAuthz . authz . domainToString | ||
232 | |||
233 | statusLine :: Response body -> String | 219 | statusLine :: Response body -> String |
234 | statusLine r = (r ^. responseStatus . statusCode . to show) ++ " " ++ r ^. responseStatus . statusMessage . to (T.unpack . decodeUtf8) | 220 | statusLine 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 | ||
241 | sendPayload :: (MonadIO m, MonadState Nonce m, MonadReader Env m) => (Directory -> String) -> ByteString -> m (Response LC.ByteString) | 227 | -- OpenSSL operations |
242 | sendPayload 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 | ||
249 | post :: (MonadReader Env m, MonadState Nonce m, MonadIO m) => String -> LC.ByteString -> m (Response LC.ByteString) | 229 | data CSR = CSR { csrDomains :: [DomainName], csrData :: ByteString } |
250 | post url payload = do | 230 | genReq :: Keys -> [DomainName] -> IO CSR |
251 | sess <- asks getSession | 231 | genReq _ [] = error "genReq called with zero domains" |
252 | r <- liftIO $ WS.postWith noStatusCheck sess url payload | 232 | genReq (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 | |
244 | data Keys = Keys RSAKeyPair RSAPubKey | ||
245 | readKeys :: String -> IO (Maybe Keys) | ||
246 | readKeys 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 | ||
255 | a </> b = a ++ "/" ++ b | ||
256 | infixr 5 </> | ||
257 | |||
258 | domainToString :: DomainName -> String | ||
259 | domainToString = T.unpack . decodeUtf8 . Text.Domain.Validate.toByteString | ||
260 | |||
261 | acmeChallengeURI :: DomainName -> BC.ByteString -> URI | ||
262 | acmeChallengeURI dom tok = URI | ||
263 | "http:" | ||
264 | (Just $ URIAuth "" (domainToString dom) "") | ||
265 | ("/.well-known/acme-challenge" </> BC.unpack tok) | ||
266 | "" | ||
267 | "" | ||