diff options
author | Andrew Cady <d@jerkface.net> | 2016-01-20 13:48:21 -0500 |
---|---|---|
committer | Andrew Cady <d@jerkface.net> | 2016-01-20 13:48:21 -0500 |
commit | a00019a64306bcbc1597b016f72eaafda72e0f4b (patch) | |
tree | 5e8d6c8963dd693a5a3a6f8d4b9c665367c76dbe | |
parent | 455eca5425738ebdba00c843d921c58af1689584 (diff) |
Use a monad to track nonce values between requests
(Partially implemented.)
-rw-r--r-- | acme-encrypt.cabal | 2 | ||||
-rw-r--r-- | acme.hs | 62 |
2 files changed, 37 insertions, 27 deletions
diff --git a/acme-encrypt.cabal b/acme-encrypt.cabal index cf510a2..4d3aa44 100644 --- a/acme-encrypt.cabal +++ b/acme-encrypt.cabal | |||
@@ -24,7 +24,7 @@ executable acme-encrypt-exe | |||
24 | build-depends: base, | 24 | build-depends: base, |
25 | cryptonite, aeson, bytestring, base64-bytestring, SHA, | 25 | cryptonite, aeson, bytestring, base64-bytestring, SHA, |
26 | text, HsOpenSSL, process, wreq, lens, lens-aeson, | 26 | text, HsOpenSSL, process, wreq, lens, lens-aeson, |
27 | optparse-applicative, directory | 27 | optparse-applicative, directory, mtl |
28 | -- , acme-encrypt | 28 | -- , acme-encrypt |
29 | default-language: Haskell2010 | 29 | default-language: Haskell2010 |
30 | 30 | ||
@@ -1,6 +1,7 @@ | |||
1 | {-# LANGUAGE OverloadedStrings #-} | 1 | {-# LANGUAGE OverloadedStrings #-} |
2 | {-# LANGUAGE RecordWildCards #-} | 2 | {-# LANGUAGE RecordWildCards #-} |
3 | {-# LANGUAGE ScopedTypeVariables #-} | 3 | {-# LANGUAGE ScopedTypeVariables #-} |
4 | {-# LANGUAGE FlexibleContexts #-} | ||
4 | 5 | ||
5 | -------------------------------------------------------------------------------- | 6 | -------------------------------------------------------------------------------- |
6 | -- | Get a certificate from Let's Encrypt using the ACME protocol. | 7 | -- | Get a certificate from Let's Encrypt using the ACME protocol. |
@@ -23,7 +24,8 @@ import Data.Digest.Pure.SHA (bytestringDigest, sha256) | |||
23 | import Data.Maybe | 24 | import Data.Maybe |
24 | import qualified Data.Text as T | 25 | import qualified Data.Text as T |
25 | import Data.Text.Encoding (decodeUtf8) | 26 | import Data.Text.Encoding (decodeUtf8) |
26 | import Network.Wreq hiding (header) | 27 | import Network.Wreq hiding (header, get, post, put) |
28 | import qualified Network.Wreq as W | ||
27 | import OpenSSL | 29 | import OpenSSL |
28 | import OpenSSL.EVP.Digest | 30 | import OpenSSL.EVP.Digest |
29 | import OpenSSL.EVP.PKey | 31 | import OpenSSL.EVP.PKey |
@@ -34,6 +36,8 @@ import Options.Applicative hiding (header) | |||
34 | import qualified Options.Applicative as Opt | 36 | import qualified Options.Applicative as Opt |
35 | import System.Directory | 37 | import System.Directory |
36 | import System.Process (readProcess) | 38 | import System.Process (readProcess) |
39 | import Control.Monad.RWS.Strict | ||
40 | import Data.Coerce | ||
37 | 41 | ||
38 | directoryUrl :: String | 42 | directoryUrl :: String |
39 | directoryUrl = "https://acme-v01.api.letsencrypt.org/directory" | 43 | directoryUrl = "https://acme-v01.api.letsencrypt.org/directory" |
@@ -76,8 +80,7 @@ go (CmdOpts privKeyFile domain email termOverride) = do | |||
76 | keys@(Keys _ pub) <- readKeys privKeyFile | 80 | keys@(Keys _ pub) <- readKeys privKeyFile |
77 | 81 | ||
78 | let terms = fromMaybe defaultTerms termOverride | 82 | let terms = fromMaybe defaultTerms termOverride |
79 | 83 | nonce_ = undefined | |
80 | Just nonce_ <- getNonce | ||
81 | 84 | ||
82 | -- Create user account | 85 | -- Create user account |
83 | forM_ email $ \m -> | 86 | forM_ email $ \m -> |
@@ -104,46 +107,53 @@ go (CmdOpts privKeyFile domain email termOverride) = do | |||
104 | 107 | ||
105 | return () | 108 | return () |
106 | 109 | ||
107 | register :: Keys -> String -> String -> IO (Response LC.ByteString) | 110 | data Env = Env { getDir :: Directory, getKeys :: Keys } |
108 | register keys email terms = sendPayload keys _newReg (registration email terms) | 111 | |
112 | type ACME a = RWST Env () Nonce IO a | ||
113 | runACME :: String -> Keys -> ACME a -> IO a | ||
114 | runACME url keys f = do | ||
115 | Just (dir, nonce) <- getDirectory url | ||
116 | fst <$> evalRWST f (Env dir keys) nonce | ||
109 | 117 | ||
110 | data Directory = Directory { | 118 | data Directory = Directory { |
111 | _newCert :: String, | 119 | _newCert :: String, |
112 | _newAuthz :: String, | 120 | _newAuthz :: String, |
113 | _revokeCert :: String, | 121 | _revokeCert :: String, |
114 | _newReg :: String, | 122 | _newReg :: String |
115 | _nonce :: String | ||
116 | } | 123 | } |
124 | newtype Nonce = Nonce String | ||
125 | testRegister :: String -> IO (Response LC.ByteString) | ||
126 | testRegister email = readKeys "rsa.key" >>= flip (runACME directoryUrl) (register email defaultTerms) | ||
117 | 127 | ||
118 | getDirectory :: String -> IO (Maybe Directory) | 128 | getDirectory :: String -> IO (Maybe (Directory, Nonce)) |
119 | getDirectory url = do | 129 | getDirectory url = do |
120 | r <- get url | 130 | r <- W.get url |
121 | let nonce = r ^? responseHeader "Replay-Nonce" . to (T.unpack . decodeUtf8) | 131 | let nonce = r ^? responseHeader "Replay-Nonce" . to (Nonce . T.unpack . decodeUtf8) |
122 | k x = r ^? responseBody . JSON.key x . _String . to T.unpack | 132 | k x = r ^? responseBody . JSON.key x . _String . to T.unpack |
123 | return $ Directory <$> k "new-cert" <*> k "new-authz" <*> k "revoke-cert" <*> k "new-reg" <*> nonce | 133 | return $ (,) <$> (Directory <$> k "new-cert" <*> k "new-authz" <*> k "revoke-cert" <*> k "new-reg") <*> nonce |
134 | |||
135 | register :: String -> String -> ACME (Response LC.ByteString) | ||
136 | register email terms = sendPayload _newReg (registration email terms) | ||
137 | |||
138 | sendPayload :: (MonadIO m, MonadState Nonce m, MonadReader Env m) => (Directory -> String) -> ByteString -> m (Response LC.ByteString) | ||
139 | sendPayload reqType payload = do | ||
140 | keys <- asks getKeys | ||
141 | dir <- asks getDir | ||
142 | nonce <- gets coerce | ||
143 | signed <- liftIO $ signPayload keys nonce payload | ||
124 | 144 | ||
125 | getNonce :: IO (Maybe String) | 145 | r <- liftIO $ W.post (reqType dir) signed |
126 | getNonce = fmap _nonce <$> getDirectory directoryUrl | 146 | put $ r ^?! responseHeader "Replay-Nonce" . to (Nonce . T.unpack . decodeUtf8) |
147 | return r | ||
127 | 148 | ||
128 | -------------------------------------------------------------------------------- | 149 | -------------------------------------------------------------------------------- |
129 | -- | Sign and write a payload to a file with a nonce-protected header. | 150 | -- | Sign return a payload with a nonce-protected header. |
130 | signPayload :: Keys -> String -> ByteString -> IO LC.ByteString | 151 | signPayload :: Keys -> String -> ByteString -> IO LC.ByteString |
131 | signPayload (Keys priv pub) nonce_ payload = withOpenSSL $ do | 152 | signPayload (Keys priv pub) nonce_ payload = withOpenSSL $ do |
132 | let protected = b64 (header pub nonce_) | 153 | let protected = b64 (header pub nonce_) |
133 | Just dig <- getDigestByName "SHA256" | 154 | Just dig <- getDigestByName "SHA256" |
134 | sig <- b64 <$> signBS dig priv (B.concat [protected, ".", payload]) | 155 | sig <- b64 <$> signBS dig priv (B.concat [protected, ".", payload]) |
135 | return $ buildBody pub protected payload sig | 156 | return $ encode (Request (header' pub) protected payload sig) |
136 | |||
137 | sendPayload :: Keys -> (Directory -> String) -> ByteString -> IO (Response LC.ByteString) | ||
138 | sendPayload keys reqType payload = do | ||
139 | dir <- fromMaybe (error "Error fetching directory") <$> getDirectory directoryUrl | ||
140 | let nonce = _nonce dir | ||
141 | url = reqType dir | ||
142 | signed <- signPayload keys nonce payload | ||
143 | post url signed | ||
144 | |||
145 | buildBody :: RSAKey k => k -> ByteString -> ByteString -> ByteString -> LC.ByteString | ||
146 | buildBody key protected payload sig = encode (Request (header' key) protected payload sig) | ||
147 | 157 | ||
148 | -------------------------------------------------------------------------------- | 158 | -------------------------------------------------------------------------------- |
149 | -- | Base64URL encoding of Integer with padding '=' removed. | 159 | -- | Base64URL encoding of Integer with padding '=' removed. |