packages feed

acme-not-a-joke-0.2.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 Control.Monad.Error.Lens (throwing)
import Control.Monad.Except (MonadError)
import Data.Aeson (FromJSON (..), ToJSON (..), Value (..), (.=))
import Data.ByteString.Lazy (ByteString)
import Data.Coerce (coerce)
import Data.Text (Text)

import qualified Crypto.JOSE.JWA.JWK as JWA
import qualified Crypto.JOSE.JWK as JWK
import qualified Crypto.JOSE.JWS as JWS

import Acme.NotAJoke.Api.Endpoint
import Acme.NotAJoke.Api.Nonce

-- | The public part of a JWK, as sent in the @jwk@ header of a request.
newtype PublicJWK = PublicJWK JWK.JWK
    deriving (Show, FromJSON, ToJSON)

-- | The public part of a JWK (if it has one, e.g. not a symmetric key).
publicJWK :: JWK.JWK -> Maybe PublicJWK
publicJWK = coerce . view JWK.asPublicKey

{- | The key identifier of an account, i.e. its URL (see
'Acme.NotAJoke.Api.Account.readKID'), as sent in the @kid@ header of the
requests made once the account exists.
-}
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

{- | The JWS algorithm used to sign ACME requests with a given key.

If the key pins an algorithm (its @alg@ parameter, see 'setAcmeJWSAlg'), that
algorithm is used. Otherwise the algorithm follows from the key material:

* RSA keys sign with @RS256@ (the one algorithm all ACME servers must accept,
  RFC-8555 section 6.2),
* EC keys sign with @ES256@ (P-256), @ES384@ (P-384), @ES512@ (P-521) or
  @ES256K@ (secp256k1),
* OKP keys (Ed25519 and Ed448) sign with @EdDSA@.

Symmetric keys, as well as the @none@ and MAC algorithms, are refused: ACME
servers must reject them. X25519 and X448 keys cannot sign at all. Which of
the other algorithms are accepted is up to the ACME server (Let's Encrypt
accepts @RS256@, @ES256@ and @ES384@).

Call this function (or 'acmeProtectedHeader') to build any JWS for ACME,
including those without an 'AcmeHeader' (e.g., the inner JWS of a key change).
-}
acmeJWSAlg :: (MonadError e m, JWS.AsError e) => JWK.JWK -> m JWS.Alg
acmeJWSAlg jwk =
    case view JWK.jwkAlg jwk of
        Just (JWK.JWSAlg alg)
            | alg `elem` [JWS.None, JWS.HS256, JWS.HS384, JWS.HS512] ->
                throwing JWS._AlgorithmMismatch ("ACME refuses the JWS algorithm " <> show alg)
            | otherwise -> pure alg
        Just (JWK.JWEAlg _) ->
            throwing JWS._AlgorithmMismatch "this JWK is an encryption key"
        Nothing ->
            case view JWK.jwkMaterial jwk of
                JWA.RSAKeyMaterial _ -> pure JWS.RS256
                JWA.ECKeyMaterial k -> pure $ case view JWA.ecCrv k of
                    JWA.P_256 -> JWS.ES256
                    JWA.P_384 -> JWS.ES384
                    JWA.P_521 -> JWS.ES512
                    JWA.Secp256k1 -> JWS.ES256K
                JWA.OKPKeyMaterial (JWA.Ed25519Key _ _) -> pure JWS.EdDSA
                JWA.OKPKeyMaterial (JWA.Ed448Key _ _) -> pure JWS.EdDSA
                JWA.OKPKeyMaterial _ ->
                    throwing JWS._KeyMismatch "X25519 and X448 keys cannot sign"
                JWA.OctKeyMaterial _ ->
                    throwing JWS._AlgorithmMismatch "ACME refuses MAC-based (symmetric) signatures"

{- | Pins the JWS algorithm with which a key signs ACME requests (see
'acmeJWSAlg'), e.g., @PS256@ for an RSA key if your ACME server accepts it.

The algorithm is stored in the @alg@ parameter of the JWK, hence it is kept
when the key is written with 'Acme.NotAJoke.KeyManagement.writeJWKFile'.
Signing fails if the algorithm does not fit the key.
-}
setAcmeJWSAlg :: JWS.Alg -> JWK.JWK -> JWK.JWK
setAcmeJWSAlg alg = Control.Lens.set JWK.jwkAlg (Just (JWK.JWSAlg alg))

{- | A protected JWS header signed with the algorithm of 'acmeJWSAlg', the
starting point of the header of any ACME request.
-}
acmeProtectedHeader :: (MonadError e m, JWS.AsError e) => JWK.JWK -> m (JWS.JWSHeader JWS.Protection)
acmeProtectedHeader jwk = do
    alg <- acmeJWSAlg jwk
    pure $ JWS.newJWSHeader (JWS.Protected, alg)

{- | Signs a request with a key and carries its public key (in a @jwk@
header).

Used to create or fetch an account, and to revoke a certificate with the key
of the certificate. The algorithm follows from the key, see 'acmeJWSAlg'.
-}
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
        hdr <- acmeProtectedHeader jwk
        let header = AcmeHeader hdr (raw ep) nonce (Left jwk2)
        JWS.signJWS payload (pure (header, jwk))

{- | Signs a request with the key of an account and names the account (in a
@kid@ header).

The algorithm follows from the key, see 'acmeJWSAlg'.
-}
kidSign :: JWS.JWK -> Endpoint a -> KID -> Nonce -> ByteString -> IO (Either JWS.Error (JWS.FlattenedJWS AcmeHeader))
kidSign jwk ep kid nonce payload = JWS.runJOSE $ do
    hdr <- acmeProtectedHeader jwk
    let header = AcmeHeader hdr (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)