summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Cady <d@jerkface.net>2016-01-20 13:48:21 -0500
committerAndrew Cady <d@jerkface.net>2016-01-20 13:48:21 -0500
commita00019a64306bcbc1597b016f72eaafda72e0f4b (patch)
tree5e8d6c8963dd693a5a3a6f8d4b9c665367c76dbe
parent455eca5425738ebdba00c843d921c58af1689584 (diff)
Use a monad to track nonce values between requests
(Partially implemented.)
-rw-r--r--acme-encrypt.cabal2
-rw-r--r--acme.hs62
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
diff --git a/acme.hs b/acme.hs
index 8e0fa69..95a1bfb 100644
--- a/acme.hs
+++ b/acme.hs
@@ -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)
23import Data.Maybe 24import Data.Maybe
24import qualified Data.Text as T 25import qualified Data.Text as T
25import Data.Text.Encoding (decodeUtf8) 26import Data.Text.Encoding (decodeUtf8)
26import Network.Wreq hiding (header) 27import Network.Wreq hiding (header, get, post, put)
28import qualified Network.Wreq as W
27import OpenSSL 29import OpenSSL
28import OpenSSL.EVP.Digest 30import OpenSSL.EVP.Digest
29import OpenSSL.EVP.PKey 31import OpenSSL.EVP.PKey
@@ -34,6 +36,8 @@ import Options.Applicative hiding (header)
34import qualified Options.Applicative as Opt 36import qualified Options.Applicative as Opt
35import System.Directory 37import System.Directory
36import System.Process (readProcess) 38import System.Process (readProcess)
39import Control.Monad.RWS.Strict
40import Data.Coerce
37 41
38directoryUrl :: String 42directoryUrl :: String
39directoryUrl = "https://acme-v01.api.letsencrypt.org/directory" 43directoryUrl = "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
107register :: Keys -> String -> String -> IO (Response LC.ByteString) 110data Env = Env { getDir :: Directory, getKeys :: Keys }
108register keys email terms = sendPayload keys _newReg (registration email terms) 111
112type ACME a = RWST Env () Nonce IO a
113runACME :: String -> Keys -> ACME a -> IO a
114runACME url keys f = do
115 Just (dir, nonce) <- getDirectory url
116 fst <$> evalRWST f (Env dir keys) nonce
109 117
110data Directory = Directory { 118data 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}
124newtype Nonce = Nonce String
125testRegister :: String -> IO (Response LC.ByteString)
126testRegister email = readKeys "rsa.key" >>= flip (runACME directoryUrl) (register email defaultTerms)
117 127
118getDirectory :: String -> IO (Maybe Directory) 128getDirectory :: String -> IO (Maybe (Directory, Nonce))
119getDirectory url = do 129getDirectory 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
135register :: String -> String -> ACME (Response LC.ByteString)
136register email terms = sendPayload _newReg (registration email terms)
137
138sendPayload :: (MonadIO m, MonadState Nonce m, MonadReader Env m) => (Directory -> String) -> ByteString -> m (Response LC.ByteString)
139sendPayload reqType payload = do
140 keys <- asks getKeys
141 dir <- asks getDir
142 nonce <- gets coerce
143 signed <- liftIO $ signPayload keys nonce payload
124 144
125getNonce :: IO (Maybe String) 145 r <- liftIO $ W.post (reqType dir) signed
126getNonce = 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.
130signPayload :: Keys -> String -> ByteString -> IO LC.ByteString 151signPayload :: Keys -> String -> ByteString -> IO LC.ByteString
131signPayload (Keys priv pub) nonce_ payload = withOpenSSL $ do 152signPayload (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
137sendPayload :: Keys -> (Directory -> String) -> ByteString -> IO (Response LC.ByteString)
138sendPayload 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
145buildBody :: RSAKey k => k -> ByteString -> ByteString -> ByteString -> LC.ByteString
146buildBody 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.