diff options
author | Andrew Cady <d@jerkface.net> | 2016-01-20 18:08:10 -0500 |
---|---|---|
committer | Andrew Cady <d@jerkface.net> | 2016-01-20 18:08:10 -0500 |
commit | 439edb4f1d86df776844495a552fd8656bc4b2f9 (patch) | |
tree | 4e1a683af5d9801a02d511f61b2a8e485634621f | |
parent | 3123d316b5c9c910af965c7d207789bda49d2b3d (diff) |
last step doesn't work...
-rw-r--r-- | acme.hs | 106 |
1 files changed, 82 insertions, 24 deletions
@@ -23,7 +23,7 @@ import qualified Data.ByteString.Lazy.Char8 as LC | |||
23 | import Data.Digest.Pure.SHA (bytestringDigest, sha256) | 23 | import Data.Digest.Pure.SHA (bytestringDigest, sha256) |
24 | import Data.Maybe | 24 | import Data.Maybe |
25 | import qualified Data.Text as T | 25 | import qualified Data.Text as T |
26 | import Data.Text.Encoding (decodeUtf8) | 26 | import Data.Text.Encoding (decodeUtf8, encodeUtf8) |
27 | import Network.Wreq (Response, responseHeader, responseBody, responseStatus, statusCode, statusMessage, defaults, checkStatus) | 27 | import Network.Wreq (Response, responseHeader, responseBody, responseStatus, statusCode, statusMessage, defaults, checkStatus) |
28 | import qualified Network.Wreq.Session as WS | 28 | import qualified Network.Wreq.Session as WS |
29 | import OpenSSL | 29 | import OpenSSL |
@@ -38,6 +38,8 @@ import System.Directory | |||
38 | import System.Process (readProcess) | 38 | import System.Process (readProcess) |
39 | import Control.Monad.RWS.Strict | 39 | import Control.Monad.RWS.Strict |
40 | import Data.Coerce | 40 | import Data.Coerce |
41 | import Control.Concurrent (threadDelay) | ||
42 | import System.Exit | ||
41 | 43 | ||
42 | directoryUrl :: String | 44 | directoryUrl :: String |
43 | directoryUrl = "https://acme-v01.api.letsencrypt.org/directory" | 45 | directoryUrl = "https://acme-v01.api.letsencrypt.org/directory" |
@@ -51,6 +53,7 @@ main = execParser opts >>= go | |||
51 | data CmdOpts = CmdOpts { | 53 | data 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" | |||
61 | cmdopts :: Parser CmdOpts | 64 | cmdopts :: Parser CmdOpts |
62 | cmdopts = CmdOpts <$> strOption (long "key" <> metavar "FILE" <> help "filename of your private RSA key") | 65 | cmdopts = 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 | ||
67 | genKey :: String -> IO () | 71 | genKey :: String -> IO () |
68 | genKey privKeyFile = readProcess "openssl" (words "genrsa 4096 -out" ++ [privKeyFile]) "" >>= writeFile privKeyFile | 72 | genKey privKeyFile = void $ readProcess "openssl" (words "genrsa -out" ++ [privKeyFile, "4096"]) "" |
73 | |||
74 | genReq :: String -> String -> String -> IO () | ||
75 | genReq 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 | ||
70 | data Keys = Keys SomeKeyPair RSAPubKey | 79 | data Keys = Keys SomeKeyPair RSAPubKey |
71 | readKeys :: String -> IO Keys | 80 | readKeys :: String -> IO Keys |
@@ -75,37 +84,77 @@ readKeys privKeyFile = do | |||
75 | return $ Keys priv pub | 84 | return $ Keys priv pub |
76 | 85 | ||
77 | go :: CmdOpts -> IO () | 86 | go :: CmdOpts -> IO () |
78 | go (CmdOpts privKeyFile domain email termOverride) = do | 87 | go (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 | |||
138 | saveCert :: (MonadReader Env m, MonadState Nonce m, MonadIO m) => ByteString -> FilePath -> m (Response LC.ByteString) | ||
139 | saveCert input output = do | ||
140 | r <- sendPayload _newCert (csr input) | ||
141 | liftIO $ LC.writeFile output $ r ^. responseBody | ||
142 | return r | ||
143 | |||
144 | pollChallenge :: (MonadReader Env m, MonadState Nonce m, MonadIO m) => String -> ByteString -> m (Response LC.ByteString) | ||
145 | pollChallenge 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 | ||
110 | data Env = Env { getDir :: Directory, getKeys :: Keys, getSession :: WS.Session } | 159 | data Env = Env { getDir :: Directory, getKeys :: Keys, getSession :: WS.Session } |
111 | 160 | ||
@@ -123,7 +172,13 @@ data Directory = Directory { | |||
123 | } | 172 | } |
124 | newtype Nonce = Nonce String | 173 | newtype Nonce = Nonce String |
125 | testRegister :: String -> IO (Response LC.ByteString) | 174 | testRegister :: String -> IO (Response LC.ByteString) |
126 | testRegister email = readKeys "rsa.key" >>= flip (runACME directoryUrl) (register email defaultTerms) >>= statusReport | 175 | testRegister email = runTest (register defaultTerms email) >>= statusReport |
176 | |||
177 | runTest :: ACME b -> IO b | ||
178 | runTest t = readKeys "rsa.key" >>= flip (runACME directoryUrl) t | ||
179 | |||
180 | testCR :: IO (Response LC.ByteString) | ||
181 | testCR = runTest $ challengeRequest "fifty.childrenofmay.org" | ||
127 | 182 | ||
128 | getDirectory :: WS.Session -> String -> IO (Maybe (Directory, Nonce)) | 183 | getDirectory :: WS.Session -> String -> IO (Maybe (Directory, Nonce)) |
129 | getDirectory sess url = do | 184 | getDirectory 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 | ||
135 | register :: String -> String -> ACME (Response LC.ByteString) | 190 | register :: String -> String -> ACME (Response LC.ByteString) |
136 | register email terms = sendPayload _newReg (registration email terms) | 191 | register terms email = sendPayload _newReg (registration email terms) |
192 | |||
193 | challengeRequest :: (MonadIO m, MonadState Nonce m, MonadReader Env m) => String -> m (Response LC.ByteString) | ||
194 | challengeRequest domain = sendPayload _newAuthz (authz domain) | ||
137 | 195 | ||
138 | statusLine :: Response body -> String | 196 | statusLine :: Response body -> String |
139 | statusLine r = (r ^. responseStatus . statusCode . to show) ++ " " ++ r ^. responseStatus . statusMessage . to (T.unpack . decodeUtf8) | 197 | statusLine r = (r ^. responseStatus . statusCode . to show) ++ " " ++ r ^. responseStatus . statusMessage . to (T.unpack . decodeUtf8) |