packages feed

acme-not-a-joke-0.2.0.0: src/Acme/NotAJoke/Api/KeyChange.hs

{- | Account key rollover (RFC-8555 section 7.3.5).

Changing the key of an account takes a nested JWS posted to the @keyChange@
endpoint of the 'Acme.NotAJoke.Api.Directory.Directory':

* the inner JWS is signed with the new key, carries the new (public) key in a
  @jwk@ header and has no nonce; its payload names the account and its old
  (public) key,
* the outer JWS is a regular ACME request signed with the old key (with the
  @kid@ of the account and a nonce), whose payload is the inner JWS.

Once the server has accepted the request, the account is only reachable with
the new key (the KID of the account does not change).
-}
module Acme.NotAJoke.Api.KeyChange where

import Control.Exception (throwIO)
import Control.Lens hiding ((.=))
import Data.Aeson (ToJSON (..), encode, object, (.=))

import qualified Crypto.JOSE.JWS as JWS

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

{- | Protected header of the inner JWS of a key change: like an 'AcmeHeader'
with a @jwk@ (the new key) but without a nonce.
-}
data KeyChangeHeader p
    = KeyChangeHeader
    { _keyChangeJwsHeader :: JWS.JWSHeader p
    , _keyChangeURL :: RawEndpoint
    , _keyChangeNewKey :: PublicJWK
    }

keyChangeJwsHeader :: Lens' (KeyChangeHeader p) (JWS.JWSHeader p)
keyChangeJwsHeader f s@(KeyChangeHeader{_keyChangeJwsHeader = a}) =
    fmap (\a' -> s{_keyChangeJwsHeader = a'}) (f a)

keyChangeURL :: Lens' (KeyChangeHeader p) RawEndpoint
keyChangeURL f s@(KeyChangeHeader{_keyChangeURL = a}) =
    fmap (\a' -> s{_keyChangeURL = a'}) (f a)

keyChangeNewKey :: Lens' (KeyChangeHeader p) PublicJWK
keyChangeNewKey f s@(KeyChangeHeader{_keyChangeNewKey = a}) =
    fmap (\a' -> s{_keyChangeNewKey = a'}) (f a)

instance JWS.HasJWSHeader KeyChangeHeader where
    jwsHeader = keyChangeJwsHeader

instance JWS.HasParams KeyChangeHeader where
    parseParamsFor proxy hp hu =
        KeyChangeHeader
            <$> JWS.parseParamsFor proxy hp hu
            <*> JWS.headerRequiredProtected "url" hp hu
            <*> JWS.headerRequiredProtected "jwk" hp hu
    params h =
        [ (True, "url" .= view keyChangeURL h)
        , (True, "jwk" .= view keyChangeNewKey h)
        ]
            <> JWS.params (view keyChangeJwsHeader h)
    extensions = const ["url", "jwk"]

-- | Payload of the inner JWS of a key change.
data KeyChange
    = KeyChange
    { account :: KID
    -- ^ the account whose key changes (its KID, i.e., its URL)
    , oldKey :: PublicJWK
    -- ^ the current key of the account
    }
    deriving (Show)

instance ToJSON KeyChange where
    toJSON kc =
        object
            [ "account" .= kc.account
            , "oldKey" .= kc.oldKey
            ]

newtype KeyChanged = KeyChanged Response
    deriving (Show)

{- | Signs the inner JWS of a key change with the new key.

Throws 'NoPublicKeyInJWK' if a public key cannot be derived from the old or
the new key (as 'jwkSign' does).
-}
keyChangeInnerSign ::
    -- | old key of the account
    JWS.JWK ->
    -- | new key of the account
    JWS.JWK ->
    KID ->
    Endpoint "keyChange" ->
    IO (Either JWS.Error (JWS.FlattenedJWS KeyChangeHeader))
keyChangeInnerSign oldJwk newJwk kid ep = do
    oldPub <- maybe (throwIO NoPublicKeyInJWK) pure $ publicJWK oldJwk
    newPub <- maybe (throwIO NoPublicKeyInJWK) pure $ publicJWK newJwk
    let payload = encode $ KeyChange kid oldPub
    JWS.runJOSE $ do
        -- alg <- JWS.bestJWSAlg newJwk
        let alg = JWS.RS256
        let header = KeyChangeHeader (JWS.newJWSHeader (JWS.Protected, alg)) (raw ep) newPub
        JWS.signJWS payload (pure (header, newJwk))

{- | Builds the whole (nested) key-change request: the inner JWS signed with
the new key, wrapped in an outer JWS signed with the old key.
-}
keyChangeSign ::
    -- | old key of the account
    JWS.JWK ->
    -- | new key of the account
    JWS.JWK ->
    KID ->
    Endpoint "keyChange" ->
    Nonce ->
    IO (Either JWS.Error (JWS.FlattenedJWS AcmeHeader))
keyChangeSign oldJwk newJwk kid ep nonce = do
    einner <- keyChangeInnerSign oldJwk newJwk kid ep
    case einner of
        Left err -> pure $ Left err
        Right inner -> kidSign oldJwk ep kid nonce (encode inner)

{- | Changes the key of an account (a single API call).

The server replies with a 409 Conflict (a 'ServerRejected', whose @location@
header is the account using it) when the new key is already used by another
account.
-}
postKeyChange ::
    -- | old key of the account
    JWS.JWK ->
    -- | new key of the account
    JWS.JWK ->
    KID ->
    Endpoint "keyChange" ->
    Nonce ->
    IO (Either AcmeError KeyChanged)
postKeyChange oldJwk newJwk kid ep nonce = do
    ebody <- keyChangeSign oldJwk newJwk kid ep nonce
    case ebody of
        Right body -> do
            e <- postJose ep $ encode body
            pure $ fmap KeyChanged e
        Left err -> do
            pure $ Left $ SigningFailed err