acme-not-a-joke-0.1.0.0: src/Acme/NotAJoke/Api/JWS.hs
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
--- | Helpers to work with JSON Web Signatures for ACME.
-- Indeed, almost all ACME API calls are signed with a KID or a JWK and
-- transferred using the JWS format. Thus, some internal helpers are welcome.
module Acme.NotAJoke.Api.JWS where
import Control.Applicative ((<|>))
import Control.Exception (Exception, throwIO)
import Control.Lens hiding ((.=))
import Data.Aeson (FromJSON (..), ToJSON (..), Value (..), (.=))
import Data.ByteString.Lazy (ByteString)
import Data.Coerce (coerce)
import Data.Text (Text)
import qualified Crypto.JOSE.JWK as JWK
import qualified Crypto.JOSE.JWS as JWS
import Acme.NotAJoke.Api.Endpoint
import Acme.NotAJoke.Api.Nonce
newtype PublicJWK = PublicJWK JWK.JWK
deriving (Show, FromJSON, ToJSON)
publicJWK :: JWK.JWK -> Maybe PublicJWK
publicJWK = coerce . view JWK.asPublicKey
newtype KID = KID Text
deriving (Show, FromJSON, ToJSON)
data AcmeHeader p
= AcmeHeader
{ _jwsHeader :: JWS.JWSHeader p
, _acmeURL :: RawEndpoint
, _acmeNonce :: Nonce
, _acmeAuthBit :: Either PublicJWK KID
}
acmeJwsHeader :: Lens' (AcmeHeader p) (JWS.JWSHeader p)
acmeJwsHeader f s@(AcmeHeader{_jwsHeader = a}) =
fmap (\a' -> s{_jwsHeader = a'}) (f a)
acmeNonce :: Lens' (AcmeHeader p) Nonce
acmeNonce f s@(AcmeHeader{_acmeNonce = a}) =
fmap (\a' -> s{_acmeNonce = a'}) (f a)
acmeURL :: Lens' (AcmeHeader p) RawEndpoint
acmeURL f s@(AcmeHeader{_acmeURL = a}) =
fmap (\a' -> s{_acmeURL = a'}) (f a)
acmeAuthBit :: Lens' (AcmeHeader p) (Either PublicJWK KID)
acmeAuthBit f s@(AcmeHeader{_acmeAuthBit = a}) =
fmap (\a' -> s{_acmeAuthBit = a'}) (f a)
instance JWS.HasJWSHeader AcmeHeader where
jwsHeader = acmeJwsHeader
instance JWS.HasParams AcmeHeader where
parseParamsFor proxy hp hu =
AcmeHeader
<$> JWS.parseParamsFor proxy hp hu
<*> JWS.headerRequiredProtected "url" hp hu
<*> JWS.headerRequiredProtected "nonce" hp hu
<*> jwkOrKid
where
jwkOrKid = (JWS.headerRequiredProtected "jwk" hp hu) <|> (JWS.headerRequiredProtected "kid" hp hu)
params h =
[ (True, "nonce" .= view acmeNonce h)
, (True, "url" .= view acmeURL h)
]
<> JWS.params (view acmeJwsHeader h)
<> view (acmeAuthBit . _Left . to (\jwk -> [(True, "jwk" .= jwk)])) h
<> view (acmeAuthBit . _Right . to (\kid -> [(True, "kid" .= kid)])) h
extensions = const ["nonce", "url", "jwk", "kid"]
data NoPublicKeyInJWK = NoPublicKeyInJWK
deriving (Show)
instance Exception NoPublicKeyInJWK
jwkSign :: JWS.JWK -> Endpoint a -> Nonce -> ByteString -> IO (Either JWS.Error (JWS.FlattenedJWS AcmeHeader))
jwkSign jwk ep nonce payload = do
jwk2 <- maybe (throwIO NoPublicKeyInJWK) pure $ publicJWK jwk
JWS.runJOSE $ do
-- alg <- JWS.bestJWSAlg jwk
let alg = JWS.RS256
let header = AcmeHeader (JWS.newJWSHeader (JWS.Protected, alg)) (raw ep) nonce (Left jwk2)
JWS.signJWS payload (pure (header, jwk))
kidSign :: JWS.JWK -> Endpoint a -> KID -> Nonce -> ByteString -> IO (Either JWS.Error (JWS.FlattenedJWS AcmeHeader))
kidSign jwk ep kid nonce payload = JWS.runJOSE $ do
-- alg <- JWS.bestJWSAlg jwk
let alg = JWS.RS256
let header = AcmeHeader (JWS.newJWSHeader (JWS.Protected, alg)) (raw ep) nonce (Right kid)
JWS.signJWS payload (pure (header, jwk))
newtype EmptyObject = EmptyObject Value
deriving (Show, FromJSON, ToJSON)
emptyObject :: EmptyObject
emptyObject = EmptyObject (Object mempty)
newtype EmptyText = EmptyText Value
deriving (Show, FromJSON, ToJSON)
emptyText :: EmptyText
emptyText = EmptyText (String mempty)