summaryrefslogtreecommitdiff
path: root/acme.hs
diff options
context:
space:
mode:
Diffstat (limited to 'acme.hs')
-rw-r--r--acme.hs146
1 files changed, 2 insertions, 144 deletions
diff --git a/acme.hs b/acme.hs
index 5ea5eeb..8257390 100644
--- a/acme.hs
+++ b/acme.hs
@@ -49,6 +49,8 @@ import Options.Applicative hiding (header)
49import qualified Options.Applicative as Opt 49import qualified Options.Applicative as Opt
50import System.Directory 50import System.Directory
51 51
52import Network.ACME
53
52stagingDirectoryUrl, liveDirectoryUrl :: String 54stagingDirectoryUrl, liveDirectoryUrl :: String
53liveDirectoryUrl = "https://acme-v01.api.letsencrypt.org/directory" 55liveDirectoryUrl = "https://acme-v01.api.letsencrypt.org/directory"
54stagingDirectoryUrl = "https://acme-staging.api.letsencrypt.org/directory" 56stagingDirectoryUrl = "https://acme-staging.api.letsencrypt.org/directory"
@@ -118,7 +120,6 @@ genReq domainKeyFile domain = withOpenSSL $ do
118 signX509Req req priv (Just dig) 120 signX509Req req priv (Just dig)
119 writeX509ReqDER req 121 writeX509ReqDER req
120 122
121data Keys = Keys SomeKeyPair RSAPubKey
122readKeys :: String -> IO Keys 123readKeys :: String -> IO Keys
123readKeys privKeyFile = do 124readKeys privKeyFile = do
124 priv <- readFile privKeyFile >>= flip readPrivateKey PwTTY 125 priv <- readFile privKeyFile >>= flip readPrivateKey PwTTY
@@ -288,146 +289,3 @@ post url payload = do
288 noStatusCheck = defaults & checkStatus .~ Just nullChecker 289 noStatusCheck = defaults & checkStatus .~ Just nullChecker
289 nullChecker _ _ _ = Nothing 290 nullChecker _ _ _ = Nothing
290 291
291--------------------------------------------------------------------------------
292-- | Sign return a payload with a nonce-protected header.
293signPayload :: Keys -> String -> ByteString -> IO LC.ByteString
294signPayload (Keys priv pub) nonce_ payload = withOpenSSL $ do
295 let protected = b64 (header pub nonce_)
296 Just dig <- getDigestByName "SHA256"
297 sig <- b64 <$> signBS dig priv (B.concat [protected, ".", payload])
298 return $ encode (Request (header' pub) protected payload sig)
299
300--------------------------------------------------------------------------------
301-- | Base64URL encoding of Integer with padding '=' removed.
302b64i :: Integer -> ByteString
303b64i = b64 . i2osp
304
305b64 :: ByteString -> ByteString
306b64 = B.takeWhile (/= 61) . Base64.encode
307
308toStrict :: LB.ByteString -> ByteString
309toStrict = B.concat . LB.toChunks
310
311header' :: RSAKey k => k -> Header
312header' key = Header "RS256" (JWK (rsaE key) "RSA" (rsaN key)) Nothing
313
314header :: RSAKey k => k -> String -> ByteString
315header key nonce = (toStrict . encode)
316 (Header "RS256" (JWK (rsaE key) "RSA" (rsaN key)) (Just nonce))
317
318-- | Registration payload to sign with user key.
319registration :: String -> String -> ByteString
320registration emailAddr terms = (b64 . toStrict . encode) (Reg emailAddr terms)
321
322-- | Challenge request payload to sign with user key.
323authz :: String -> ByteString
324authz = b64. toStrict . encode . Authz
325
326-- | Challenge response payload to sign with user key.
327challenge :: ByteString -> ByteString
328challenge = b64 . toStrict . encode . Challenge . BC.unpack
329
330-- | CSR request payload to sign with user key.
331csr :: ByteString -> ByteString
332csr = b64 . toStrict . encode . CSR . b64
333
334thumbprint :: JWK -> ByteString
335thumbprint = b64 . toStrict . bytestringDigest . sha256 . encodeOrdered
336
337-- | There is an `encodePretty'` in `aeson-pretty`, but do it by hand here.
338encodeOrdered :: JWK -> LB.ByteString
339encodeOrdered JWK{..} = LC.pack $
340 "{\"e\":\"" ++ hE' ++ "\",\"kty\":\"" ++ hKty ++ "\",\"n\":\"" ++ hN' ++ "\"}"
341 where
342 hE' = BC.unpack (b64i hE)
343 hN' = BC.unpack (b64i hN)
344
345
346--------------------------------------------------------------------------------
347data Header = Header
348 { hAlg :: String
349 , hJwk :: JWK
350 , hNonce :: Maybe String
351 }
352 deriving Show
353
354data JWK = JWK
355 { hE :: Integer
356 , hKty :: String
357 , hN :: Integer
358 }
359 deriving Show
360
361instance ToJSON Header where
362 toJSON Header{..} = object $
363 [ "alg" .= hAlg
364 , "jwk" .= toJSON hJwk
365 ] ++ maybeToList (("nonce" .=) <$> hNonce)
366
367instance ToJSON JWK where
368 toJSON JWK{..} = object
369 [ "e" .= decodeUtf8 (b64i hE)
370 , "kty" .= hKty
371 , "n" .= decodeUtf8 (b64i hN)
372 ]
373
374data Reg = Reg
375 { rMail :: String
376 , rAgreement :: String
377 }
378 deriving Show
379
380instance ToJSON Reg where
381 toJSON Reg{..} = object
382 [ "resource" .= ("new-reg" :: String)
383 , "contact" .= ["mailto:" ++ rMail]
384 , "agreement" .= rAgreement
385 ]
386
387data Request = Request
388 { rHeader :: Header
389 , rProtected :: ByteString
390 , rPayload :: ByteString
391 , rSignature :: ByteString
392 }
393 deriving Show
394
395instance ToJSON Request where
396 toJSON Request{..} = object
397 [ "header" .= toJSON rHeader
398 , "protected" .= decodeUtf8 rProtected
399 , "payload" .= decodeUtf8 rPayload
400 , "signature" .= decodeUtf8 rSignature
401 ]
402
403data Authz = Authz
404 { aDomain :: String
405 }
406
407instance ToJSON Authz where
408 toJSON Authz{..} = object
409 [ "resource" .= ("new-authz" :: String)
410 , "identifier" .= object
411 [ "type" .= ("dns" :: String)
412 , "value" .= aDomain
413 ]
414 ]
415
416data Challenge = Challenge
417 { cKeyAuth :: String
418 }
419
420instance ToJSON Challenge where
421 toJSON Challenge{..} = object
422 [ "resource" .= ("challenge" :: String)
423 , "keyAuthorization" .= cKeyAuth
424 ]
425
426data CSR = CSR ByteString
427 deriving Show
428
429instance ToJSON CSR where
430 toJSON (CSR s) = object
431 [ "resource" .= ("new-cert" :: String)
432 , "csr" .= decodeUtf8 s
433 ]