summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Cady <d@jerkface.net>2016-01-20 18:08:10 -0500
committerAndrew Cady <d@jerkface.net>2016-01-20 18:08:10 -0500
commit439edb4f1d86df776844495a552fd8656bc4b2f9 (patch)
tree4e1a683af5d9801a02d511f61b2a8e485634621f
parent3123d316b5c9c910af965c7d207789bda49d2b3d (diff)
last step doesn't work...
-rw-r--r--acme.hs106
1 files changed, 82 insertions, 24 deletions
diff --git a/acme.hs b/acme.hs
index 6bbda40..e2bfd30 100644
--- a/acme.hs
+++ b/acme.hs
@@ -23,7 +23,7 @@ import qualified Data.ByteString.Lazy.Char8 as LC
23import Data.Digest.Pure.SHA (bytestringDigest, sha256) 23import Data.Digest.Pure.SHA (bytestringDigest, sha256)
24import Data.Maybe 24import Data.Maybe
25import qualified Data.Text as T 25import qualified Data.Text as T
26import Data.Text.Encoding (decodeUtf8) 26import Data.Text.Encoding (decodeUtf8, encodeUtf8)
27import Network.Wreq (Response, responseHeader, responseBody, responseStatus, statusCode, statusMessage, defaults, checkStatus) 27import Network.Wreq (Response, responseHeader, responseBody, responseStatus, statusCode, statusMessage, defaults, checkStatus)
28import qualified Network.Wreq.Session as WS 28import qualified Network.Wreq.Session as WS
29import OpenSSL 29import OpenSSL
@@ -38,6 +38,8 @@ import System.Directory
38import System.Process (readProcess) 38import System.Process (readProcess)
39import Control.Monad.RWS.Strict 39import Control.Monad.RWS.Strict
40import Data.Coerce 40import Data.Coerce
41import Control.Concurrent (threadDelay)
42import System.Exit
41 43
42directoryUrl :: String 44directoryUrl :: String
43directoryUrl = "https://acme-v01.api.letsencrypt.org/directory" 45directoryUrl = "https://acme-v01.api.letsencrypt.org/directory"
@@ -51,6 +53,7 @@ main = execParser opts >>= go
51data CmdOpts = CmdOpts { 53data CmdOpts = CmdOpts {
52 optKeyFile :: String, 54 optKeyFile :: String,
53 optDomain :: String, 55 optDomain :: String,
56 optChallengeDir :: String,
54 optEmail :: Maybe String, 57 optEmail :: Maybe String,
55 optTerms :: Maybe String 58 optTerms :: Maybe String
56} 59}
@@ -61,11 +64,17 @@ defaultTerms = "https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf"
61cmdopts :: Parser CmdOpts 64cmdopts :: Parser CmdOpts
62cmdopts = CmdOpts <$> strOption (long "key" <> metavar "FILE" <> help "filename of your private RSA key") 65cmdopts = CmdOpts <$> strOption (long "key" <> metavar "FILE" <> help "filename of your private RSA key")
63 <*> strOption (long "domain" <> metavar "DOMAIN" <> help "the domain name to certify") 66 <*> strOption (long "domain" <> metavar "DOMAIN" <> help "the domain name to certify")
67 <*> strOption (long "dir" <> metavar "DIR" <> help "output directory for ACME challenges")
64 <*> optional (strOption (long "email" <> metavar "ADDRESS" <> help "an email address with which to register an account")) 68 <*> optional (strOption (long "email" <> metavar "ADDRESS" <> help "an email address with which to register an account"))
65 <*> optional (strOption (long "terms" <> metavar "URL" <> help "the terms param of the registration request")) 69 <*> optional (strOption (long "terms" <> metavar "URL" <> help "the terms param of the registration request"))
66 70
67genKey :: String -> IO () 71genKey :: String -> IO ()
68genKey privKeyFile = readProcess "openssl" (words "genrsa 4096 -out" ++ [privKeyFile]) "" >>= writeFile privKeyFile 72genKey privKeyFile = void $ readProcess "openssl" (words "genrsa -out" ++ [privKeyFile, "4096"]) ""
73
74genReq :: String -> String -> String -> IO ()
75genReq privKeyFile domain out = void $ readProcess "openssl" (args privKeyFile domain out) ""
76 where
77 args k d o = words "req -new -sha256 -outform DER -key" ++ [k, "-subj", "/CN=" ++ d, "-out", o]
69 78
70data Keys = Keys SomeKeyPair RSAPubKey 79data Keys = Keys SomeKeyPair RSAPubKey
71readKeys :: String -> IO Keys 80readKeys :: String -> IO Keys
@@ -75,37 +84,77 @@ readKeys privKeyFile = do
75 return $ Keys priv pub 84 return $ Keys priv pub
76 85
77go :: CmdOpts -> IO () 86go :: CmdOpts -> IO ()
78go (CmdOpts privKeyFile domain email termOverride) = do 87go (CmdOpts privKeyFile domain challengeDir email termOverride) = do
88
79 doesFileExist privKeyFile >>= flip unless (genKey privKeyFile) 89 doesFileExist privKeyFile >>= flip unless (genKey privKeyFile)
80 keys@(Keys _ pub) <- readKeys privKeyFile
81 90
91 let domainKeyFile = domain </> "rsa.key"
92 domainCSRFile = domain </> "csr.der"
93 domainCertFile = domain </> "cert.der"
94
95 doesDirectoryExist domain >>= flip unless (createDirectory domain)
96 doesFileExist domainKeyFile >>= flip unless (genKey domainKeyFile)
97 doesFileExist domainCSRFile >>= flip unless (genReq domainKeyFile domain domainCSRFile)
98
99 csrData <- B.readFile domainCSRFile
100
101 keys@(Keys _ pub) <- readKeys privKeyFile
82 let terms = fromMaybe defaultTerms termOverride 102 let terms = fromMaybe defaultTerms termOverride
83 nonce_ = undefined
84 103
85 -- Create user account 104 -- Create user account
86 forM_ email $ \m -> 105 runACME directoryUrl keys $ do
87 LB.writeFile "registration.body" =<< signPayload keys nonce_ (registration m terms) 106 forM_ email $ register terms >=> statusReport
88 107
89 -- Obtain a challenge 108 r <- challengeRequest domain >>= statusReport
90 LB.writeFile "challenge-request.body" =<< signPayload keys nonce_ (authz domain) 109 let
110 httpChallenge = responseBody . JSON.key "challenges" . to universe . traverse . (filtered . has $ ix "type" . only "http-01")
111 httpChallenge' = responseBody . JSON.key "challenges" . to universe . traverse . (filtered . has $ ix "type" . only "http-01")
112 token = r ^?! httpChallenge . JSON.key "token" . _String . to encodeUtf8
113 crUri = r ^?! httpChallenge' . JSON.key "uri" . _String . to T.unpack
114 thumb = thumbprint (JWK (rsaE pub) "RSA" (rsaN pub))
115 thumbtoken = toStrict (LB.fromChunks [token, ".", thumb])
91 116
92 -- Answser the challenge 117 liftIO $ writeFile (challengeDir </> BC.unpack token) (BC.unpack thumbtoken)
93 let thumb = thumbprint (JWK (rsaE pub) "RSA" (rsaN pub))
94 -- Extracted from POST response above.
95 token = "DjyJpI3HVWAmsAwMT5ZFpW8dj19cel6ml6qaBUeGpCg"
96 thumbtoken = toStrict (LB.fromChunks [token, ".", thumb])
97 118
98 putStrLn ("Serve http://" ++ domain ++ "/.well-known/acme-challenge/" ++ BC.unpack token) 119 -- Wait for challenge validation
99 putStrLn ("With content:\n" ++ BC.unpack thumbtoken) 120 -- TODO: first hit the local server to test whether this is valid
121 r <- pollChallenge crUri thumbtoken >>= statusReport
100 122
101 -- Notify Let's Encrypt we answsered the challenge 123 if r ^. responseStatus . statusCode . to isSuccess then do
102 LB.writeFile "challenge-response.body" =<< signPayload keys nonce_ (challenge thumbtoken)
103 124
104 -- Wait for challenge validation Send a CSR and get a certificate 125 liftIO $ void exitSuccess
105 csr_ <- B.readFile (domain ++ ".csr.der") 126 -- Send a CSR and get a certificate
106 LB.writeFile "csr-request.body" =<< signPayload keys nonce_ (csr csr_) 127 void $ saveCert csrData domainCertFile >>= statusReport
107 128
108 return () 129 else liftIO $ do
130 putStrLn "Error"
131 print r
132 print $ r ^? responseBody . JSON.key "status" . _String
133
134 where
135 a </> b = a ++ "/" ++ b
136 isSuccess n = n >= 200 && n <= 300
137
138saveCert :: (MonadReader Env m, MonadState Nonce m, MonadIO m) => ByteString -> FilePath -> m (Response LC.ByteString)
139saveCert input output = do
140 r <- sendPayload _newCert (csr input)
141 liftIO $ LC.writeFile output $ r ^. responseBody
142 return r
143
144pollChallenge :: (MonadReader Env m, MonadState Nonce m, MonadIO m) => String -> ByteString -> m (Response LC.ByteString)
145pollChallenge crUri thumbtoken = do
146 liftIO $ putStrLn "polling..."
147 r <- sendPayload (const crUri) (challenge thumbtoken) >>= statusReport
148 let status = r ^? responseBody . JSON.key "status" . _String
149 if status == Just "pending"
150 then do
151 liftIO . print $ r ^. responseBody
152 liftIO . threadDelay $ 2000 * 1000
153 pollChallenge crUri thumbtoken
154 else do
155 liftIO $ putStrLn "done polling."
156 liftIO $ print r
157 return r
109 158
110data Env = Env { getDir :: Directory, getKeys :: Keys, getSession :: WS.Session } 159data Env = Env { getDir :: Directory, getKeys :: Keys, getSession :: WS.Session }
111 160
@@ -123,7 +172,13 @@ data Directory = Directory {
123} 172}
124newtype Nonce = Nonce String 173newtype Nonce = Nonce String
125testRegister :: String -> IO (Response LC.ByteString) 174testRegister :: String -> IO (Response LC.ByteString)
126testRegister email = readKeys "rsa.key" >>= flip (runACME directoryUrl) (register email defaultTerms) >>= statusReport 175testRegister email = runTest (register defaultTerms email) >>= statusReport
176
177runTest :: ACME b -> IO b
178runTest t = readKeys "rsa.key" >>= flip (runACME directoryUrl) t
179
180testCR :: IO (Response LC.ByteString)
181testCR = runTest $ challengeRequest "fifty.childrenofmay.org"
127 182
128getDirectory :: WS.Session -> String -> IO (Maybe (Directory, Nonce)) 183getDirectory :: WS.Session -> String -> IO (Maybe (Directory, Nonce))
129getDirectory sess url = do 184getDirectory sess url = do
@@ -133,7 +188,10 @@ getDirectory sess url = do
133 return $ (,) <$> (Directory <$> k "new-cert" <*> k "new-authz" <*> k "revoke-cert" <*> k "new-reg") <*> nonce 188 return $ (,) <$> (Directory <$> k "new-cert" <*> k "new-authz" <*> k "revoke-cert" <*> k "new-reg") <*> nonce
134 189
135register :: String -> String -> ACME (Response LC.ByteString) 190register :: String -> String -> ACME (Response LC.ByteString)
136register email terms = sendPayload _newReg (registration email terms) 191register terms email = sendPayload _newReg (registration email terms)
192
193challengeRequest :: (MonadIO m, MonadState Nonce m, MonadReader Env m) => String -> m (Response LC.ByteString)
194challengeRequest domain = sendPayload _newAuthz (authz domain)
137 195
138statusLine :: Response body -> String 196statusLine :: Response body -> String
139statusLine r = (r ^. responseStatus . statusCode . to show) ++ " " ++ r ^. responseStatus . statusMessage . to (T.unpack . decodeUtf8) 197statusLine r = (r ^. responseStatus . statusCode . to show) ++ " " ++ r ^. responseStatus . statusMessage . to (T.unpack . decodeUtf8)