packages feed

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

{- | ACME authorizations (RFC-8555 section 7.1.4).

An order has one authorization per identifier: the client proves it controls
the identifier by completing one of the challenges of the authorization (see
"Acme.NotAJoke.Api.Challenge"). For a wildcard identifier such as
@*.example.com@, the authorization is for @example.com@ with @wildcard@ set,
and only DNS-01 challenges are offered.

An authorization already validated (e.g., for an earlier order of the same
account) can be @valid@ from the start, with nothing left to do.
-}
module Acme.NotAJoke.Api.Authorization where

import Data.Aeson (FromJSON (..), ToJSON (..), decode, encode, object, pairs, withObject, withText, (.:), (.:?), (.=))
import Data.Coerce (coerce)
import Data.Text (Text)
import Data.Time.Clock (UTCTime)
import qualified Network.HTTP.Client as HTTP

import qualified Crypto.JOSE.JWS as JWS

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

-- | RFC-defined authorization statuses.
data AuthorizationStatus
    = AuthorizationPending
    | AuthorizationValid
    | AuthorizationInvalid
    | AuthorizationDeactivated
    | AuthorizationExpired
    | AuthorizationRevoked
    deriving (Show, Eq, Ord)

instance FromJSON AuthorizationStatus where
    parseJSON = withText "AuthorizationStatus" $ \txt ->
        case txt of
            "pending" -> pure AuthorizationPending
            "valid" -> pure AuthorizationValid
            "invalid" -> pure AuthorizationInvalid
            "deactivated" -> pure AuthorizationDeactivated
            "expired" -> pure AuthorizationExpired
            "revoked" -> pure AuthorizationRevoked
            _ -> fail $ "invalid authorization status:" <> show txt

-- | RFC-defined authorization types (only the subset supported in this library).
data AuthorizationType
    = DNSAuthorization
    deriving (Show, Eq, Ord)

instance ToJSON AuthorizationType where
    toEncoding DNSAuthorization = toEncoding ("dns" :: Text)
    toJSON DNSAuthorization = toJSON ("dns" :: Text)
instance FromJSON AuthorizationType where
    parseJSON = withText "Authorizationtype" $ \txt ->
        case txt of
            "dns" -> pure DNSAuthorization
            _ -> fail $ "invalid ordertype:" <> show txt

-- | RFC-defined authorization identifiers.
data AuthorizationIdentifier
    = AuthorizationIdentifier
    { type_ :: AuthorizationType
    , value :: Text
    }
    deriving (Show, Eq, Ord)

instance ToJSON AuthorizationIdentifier where
    toEncoding o = pairs ("type" .= o.type_ <> "value" .= o.value)
    toJSON o = object ["type" .= o.type_, "value" .= o.value]
instance FromJSON AuthorizationIdentifier where
    parseJSON = withObject "AuthorizationIdentifier" $ \o ->
        AuthorizationIdentifier
            <$> o .: "type"
            <*> o .: "value"

{- | RFC-defined authorization resource, the set of fields depending on the
state @a@ (only @\"authorization-inspected\"@ for now).
-}
data Authorization a
    = Authorization
    { status :: Field a "status" AuthorizationStatus
    , identifier :: Field a "status" AuthorizationIdentifier
    , expires :: Field a "expires" (Maybe UTCTime)
    , challenges :: Field a "challenges" [Challenge "challenge-unspecified"]
    , wildcard :: Field a "wildcard" (Maybe Bool)
    }

-- | The response of the server to 'postGetAuthorization'.
newtype AuthorizationInspected = AuthorizationInspected Response
    deriving (Show)

type instance Field "authorization-inspected" "status" x = x
type instance Field "authorization-inspected" "identifier" x = x
type instance Field "authorization-inspected" "expires" x = x
type instance Field "authorization-inspected" "challenges" x = x
type instance Field "authorization-inspected" "wildcard" x = x

instance FromJSON (Authorization "authorization-inspected") where
    parseJSON = withObject "Authorization(inspected)" $ \v ->
        Authorization
            <$> v .: "status"
            <*> v .: "identifier"
            <*> v .: "expires"
            <*> v .: "challenges"
            <*> v .:? "wildcard"

-- | Lookup an Authorization.
readAuthorization :: AuthorizationInspected -> Maybe (Authorization "authorization-inspected")
readAuthorization (AuthorizationInspected rsp) = decode $ HTTP.responseBody rsp

-- | Inspects an authorization from its URL.
postGetAuthorization :: JWS.JWK -> KID -> Url "authorization" -> Nonce -> IO (Either AcmeError AuthorizationInspected)
postGetAuthorization jwk kid authUrl nonce = do
    ebody <- (kidSign jwk ep kid nonce "")
    case ebody of
        Right body -> do
            e <- postJose ep $ encode body
            pure $ fmap AuthorizationInspected e
        Left err -> do
            pure $ Left $ SigningFailed err
  where
    ep :: Endpoint "authorization"
    ep = coerce authUrl