summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAndrew Cady <d@jerkface.net>2016-01-22 11:36:37 -0500
committerAndrew Cady <d@jerkface.net>2016-01-22 18:19:25 -0500
commit15d6572b9fa0ff6b0105eaa26583f496b18f78b4 (patch)
tree2a60a040495c9c8080bae50e6dd871a42446ad6b /src
parent3581adc163fd0b41485d822944efe6cdd4607aed (diff)
Factored out Network.ACME library
Diffstat (limited to 'src')
-rw-r--r--src/Network/ACME.hs198
1 files changed, 198 insertions, 0 deletions
diff --git a/src/Network/ACME.hs b/src/Network/ACME.hs
new file mode 100644
index 0000000..f8135e6
--- /dev/null
+++ b/src/Network/ACME.hs
@@ -0,0 +1,198 @@
1{-# LANGUAGE FlexibleContexts #-}
2{-# LANGUAGE MultiParamTypeClasses #-}
3{-# LANGUAGE OverloadedStrings #-}
4{-# LANGUAGE RecordWildCards #-}
5{-# LANGUAGE ScopedTypeVariables #-}
6
7module Network.ACME (
8 Keys(..),
9 thumbprint,
10 JWK(..),
11 toStrict,
12 csr,
13 challenge,
14 registration,
15 authz,
16 signPayload,
17 ) where
18
19import Control.Lens hiding ((.=))
20import Control.Monad
21import Control.Monad.RWS.Strict
22import Crypto.Number.Serialize (i2osp)
23import Data.Aeson (ToJSON (..), Value, encode, object,
24 (.=))
25import Data.Aeson.Lens hiding (key)
26import qualified Data.Aeson.Lens as JSON
27import Data.ByteString (ByteString)
28import qualified Data.ByteString as B
29import qualified Data.ByteString.Base64.URL as Base64
30import qualified Data.ByteString.Char8 as BC
31import qualified Data.ByteString.Lazy as LB
32import qualified Data.ByteString.Lazy.Char8 as LC
33import Data.Coerce
34import Data.Digest.Pure.SHA (bytestringDigest, sha256)
35import Data.Maybe
36import Data.String (fromString)
37import qualified Data.Text as T
38import Data.Text.Encoding (decodeUtf8, encodeUtf8)
39import Data.Time.Clock.POSIX (getPOSIXTime)
40import Network.Wreq (Response, checkStatus, defaults,
41 responseBody, responseHeader,
42 responseStatus, statusCode,
43 statusMessage)
44import qualified Network.Wreq as W
45import qualified Network.Wreq.Session as WS
46import OpenSSL
47import OpenSSL.EVP.Digest
48import OpenSSL.EVP.PKey
49import OpenSSL.EVP.Sign
50import OpenSSL.PEM
51import OpenSSL.RSA
52import OpenSSL.X509.Request
53
54data Keys = Keys SomeKeyPair RSAPubKey
55
56--------------------------------------------------------------------------------
57-- | Sign return a payload with a nonce-protected header.
58signPayload :: Keys -> String -> ByteString -> IO LC.ByteString
59signPayload (Keys priv pub) nonce_ payload = withOpenSSL $ do
60 let protected = b64 (header pub nonce_)
61 Just dig <- getDigestByName "SHA256"
62 sig <- b64 <$> signBS dig priv (B.concat [protected, ".", payload])
63 return $ encode (Request (header' pub) protected payload sig)
64
65--------------------------------------------------------------------------------
66-- | Base64URL encoding of Integer with padding '=' removed.
67b64i :: Integer -> ByteString
68b64i = b64 . i2osp
69
70b64 :: ByteString -> ByteString
71b64 = B.takeWhile (/= 61) . Base64.encode
72
73toStrict :: LB.ByteString -> ByteString
74toStrict = B.concat . LB.toChunks
75
76header' :: RSAKey k => k -> Header
77header' key = Header "RS256" (JWK (rsaE key) "RSA" (rsaN key)) Nothing
78
79header :: RSAKey k => k -> String -> ByteString
80header key nonce = (toStrict . encode)
81 (Header "RS256" (JWK (rsaE key) "RSA" (rsaN key)) (Just nonce))
82
83-- | Registration payload to sign with user key.
84registration :: String -> String -> ByteString
85registration emailAddr terms = (b64 . toStrict . encode) (Reg emailAddr terms)
86
87-- | Challenge request payload to sign with user key.
88authz :: String -> ByteString
89authz = b64. toStrict . encode . Authz
90
91-- | Challenge response payload to sign with user key.
92challenge :: ByteString -> ByteString
93challenge = b64 . toStrict . encode . Challenge . BC.unpack
94
95-- | CSR request payload to sign with user key.
96csr :: ByteString -> ByteString
97csr = b64 . toStrict . encode . CSR . b64
98
99thumbprint :: JWK -> ByteString
100thumbprint = b64 . toStrict . bytestringDigest . sha256 . encodeOrdered
101
102-- | There is an `encodePretty'` in `aeson-pretty`, but do it by hand here.
103encodeOrdered :: JWK -> LB.ByteString
104encodeOrdered JWK{..} = LC.pack $
105 "{\"e\":\"" ++ hE' ++ "\",\"kty\":\"" ++ hKty ++ "\",\"n\":\"" ++ hN' ++ "\"}"
106 where
107 hE' = BC.unpack (b64i hE)
108 hN' = BC.unpack (b64i hN)
109
110
111--------------------------------------------------------------------------------
112data Header = Header
113 { hAlg :: String
114 , hJwk :: JWK
115 , hNonce :: Maybe String
116 }
117 deriving Show
118
119data JWK = JWK
120 { hE :: Integer
121 , hKty :: String
122 , hN :: Integer
123 }
124 deriving Show
125
126instance ToJSON Header where
127 toJSON Header{..} = object $
128 [ "alg" .= hAlg
129 , "jwk" .= toJSON hJwk
130 ] ++ maybeToList (("nonce" .=) <$> hNonce)
131
132instance ToJSON JWK where
133 toJSON JWK{..} = object
134 [ "e" .= decodeUtf8 (b64i hE)
135 , "kty" .= hKty
136 , "n" .= decodeUtf8 (b64i hN)
137 ]
138
139data Reg = Reg
140 { rMail :: String
141 , rAgreement :: String
142 }
143 deriving Show
144
145instance ToJSON Reg where
146 toJSON Reg{..} = object
147 [ "resource" .= ("new-reg" :: String)
148 , "contact" .= ["mailto:" ++ rMail]
149 , "agreement" .= rAgreement
150 ]
151
152data Request = Request
153 { rHeader :: Header
154 , rProtected :: ByteString
155 , rPayload :: ByteString
156 , rSignature :: ByteString
157 }
158 deriving Show
159
160instance ToJSON Request where
161 toJSON Request{..} = object
162 [ "header" .= toJSON rHeader
163 , "protected" .= decodeUtf8 rProtected
164 , "payload" .= decodeUtf8 rPayload
165 , "signature" .= decodeUtf8 rSignature
166 ]
167
168data Authz = Authz
169 { aDomain :: String
170 }
171
172instance ToJSON Authz where
173 toJSON Authz{..} = object
174 [ "resource" .= ("new-authz" :: String)
175 , "identifier" .= object
176 [ "type" .= ("dns" :: String)
177 , "value" .= aDomain
178 ]
179 ]
180
181data Challenge = Challenge
182 { cKeyAuth :: String
183 }
184
185instance ToJSON Challenge where
186 toJSON Challenge{..} = object
187 [ "resource" .= ("challenge" :: String)
188 , "keyAuthorization" .= cKeyAuth
189 ]
190
191data CSR = CSR ByteString
192 deriving Show
193
194instance ToJSON CSR where
195 toJSON (CSR s) = object
196 [ "resource" .= ("new-cert" :: String)
197 , "csr" .= decodeUtf8 s
198 ]