acme-not-a-joke-0.2.0.0: src/Acme/NotAJoke/Api/Order.hs
{- | ACME orders (RFC-8555 sections 7.1.3 and 7.4).
An order asks for a certificate for a list of identifiers (domain names,
possibly wildcards such as @*.example.com@). Its life goes:
1. the client creates it ('createOrder', 'postNewOrder'), the server replies
with the order URL (see 'readOrderUrl') and one authorization URL per
identifier (see 'readOrderCreated' and "Acme.NotAJoke.Api.Authorization")
2. the order is @pending@ until the client has validated all the
authorizations (see "Acme.NotAJoke.Api.Challenge"), then @ready@
3. the client sends its CSR to the @finalize@ URL ('postFinalizeOrder'), the
order goes @processing@ then @valid@
4. the order of 'postGetOrder' then has a @certificate@ URL (see
'readCertificateUrl'), from which to download the certificate (see
"Acme.NotAJoke.Api.Certificate")
An order which fails at any point goes @invalid@, with a problem document in
its @error@ field.
'Order' has three states (see "Acme.NotAJoke.Api.Field"):
@\"order-create\"@, @\"order-created\"@ and @\"order-inspected\"@.
-}
module Acme.NotAJoke.Api.Order where
import Data.Aeson (FromJSON (..), ToJSON (..), decode, encode, object, pairs, withObject, withText, (.:), (.:?), (.=))
import Data.Coerce (coerce)
import Data.Maybe (catMaybes)
import Data.Text (Text)
import qualified Data.Text.Encoding as Encoding
import Data.Time.Clock (UTCTime)
import qualified Network.HTTP.Client as HTTP
import qualified Crypto.JOSE.JWS as JWS
import Acme.NotAJoke.Api.CSR
import Acme.NotAJoke.Api.Endpoint
import Acme.NotAJoke.Api.Field
import Acme.NotAJoke.Api.JWS
import Acme.NotAJoke.Api.Nonce
-- | RFC-defined order statuses.
data OrderStatus
= OrderPending
| OrderReady
| OrderProcessing
| OrderValid
| OrderInvalid
deriving (Show, Eq, Ord)
instance FromJSON OrderStatus where
parseJSON = withText "OrderStatus" $ \txt ->
case txt of
"pending" -> pure OrderPending
"ready" -> pure OrderReady
"processing" -> pure OrderProcessing
"valid" -> pure OrderValid
"invalid" -> pure OrderInvalid
_ -> fail $ "invalid order status:" <> show txt
-- | RFC-defined order types (for the subset supported in this library).
data OrderType
= -- | Order is about requesting DNS certificates.
DNSOrder
deriving (Show, Eq, Ord)
instance ToJSON OrderType where
toEncoding DNSOrder = toEncoding ("dns" :: Text)
toJSON DNSOrder = toJSON ("dns" :: Text)
instance FromJSON OrderType where
parseJSON = withText "Ordertype" $ \txt ->
case txt of
"dns" -> pure DNSOrder
_ -> fail $ "invalid ordertype:" <> show txt
-- | RFC-defined order identifier.
data OrderIdentifier
= OrderIdentifier
{ type_ :: OrderType
, value :: Text
}
deriving (Show, Eq, Ord)
instance ToJSON OrderIdentifier where
toEncoding o = pairs ("type" .= o.type_ <> "value" .= o.value)
toJSON o = object ["type" .= o.type_, "value" .= o.value]
instance FromJSON OrderIdentifier where
parseJSON = withObject "OrderIdentifier" $ \o ->
OrderIdentifier
<$> o .: "type"
<*> o .: "value"
{- | RFC-defined order structure, the set of fields depending on the state @a@
(@\"order-create\"@, @\"order-created\"@ or @\"order-inspected\"@).
-}
data Order a
= Order
{ status :: Field a "status" OrderStatus
, expires :: Field a "expires" UTCTime
, identifiers :: Field a "identifiers" [OrderIdentifier]
, notBefore :: Field a "notBefore" (Maybe UTCTime)
, notAfter :: Field a "notAfter" (Maybe UTCTime)
, error :: Field a "error" (Maybe Problem)
, authorizations :: Field a "authorizations" [Url "authorization"]
, finalize :: Field a "finalize" (Url "finalize-order")
, certificate :: Field a "certificate" (Url "certificate")
}
type instance Field "order-create" "status" x = ()
type instance Field "order-create" "expires" x = ()
type instance Field "order-create" "identifiers" x = x
type instance Field "order-create" "notBefore" x = x
type instance Field "order-create" "notAfter" x = x
type instance Field "order-create" "error" x = ()
type instance Field "order-create" "authorizations" x = ()
type instance Field "order-create" "finalize" x = ()
type instance Field "order-create" "certificate" x = ()
-- | The response of the server to 'postNewOrder'.
newtype OrderCreated = OrderCreated Response
deriving (Show)
type instance Field "order-created" "status" x = x
type instance Field "order-created" "expires" x = x
type instance Field "order-created" "identifiers" x = x
type instance Field "order-created" "notBefore" x = x
type instance Field "order-created" "notAfter" x = x
type instance Field "order-created" "error" x = x
type instance Field "order-created" "authorizations" x = x
type instance Field "order-created" "finalize" x = x
type instance Field "order-created" "certificate" x = ()
instance FromJSON (Order "order-created") where
parseJSON = withObject "Order(created)" $ \v ->
Order
<$> v .: "status"
<*> v .: "expires"
<*> v .: "identifiers"
<*> v .:? "notBefore"
<*> v .:? "notAfter"
<*> v .:? "error"
<*> v .: "authorizations"
<*> v .: "finalize"
<*> pure ()
{- | Prepare a new order, with optional @notBefore@ and @notAfter@ dates for
the certificate (servers such as Let's Encrypt do not support them, pass
@(Nothing, Nothing)@) and the identifiers the certificate is for.
-}
createOrder :: (Maybe UTCTime, Maybe UTCTime) -> [OrderIdentifier] -> Order "order-create"
createOrder (nbefore, nafter) ois =
Order () () ois nbefore nafter () () () ()
-- | Lookup the URL of a new order (to poll it with 'postGetOrder').
readOrderUrl :: OrderCreated -> Maybe (Url "order")
readOrderUrl (OrderCreated rsp) = Url . Encoding.decodeUtf8 <$> responseHeader "location" rsp
-- | Lookup the order the server created.
readOrderCreated :: OrderCreated -> Maybe (Order "order-created")
readOrderCreated (OrderCreated rsp) = decode $ HTTP.responseBody rsp
-- | Requests a new order to the server.
postNewOrder :: JWS.JWK -> Endpoint "newOrder" -> KID -> Nonce -> Order "order-create" -> IO (Either AcmeError OrderCreated)
postNewOrder jwk ep kid nonce ord = do
ebody <- (kidSign jwk ep kid nonce $ encode $ serialized)
case ebody of
Right body -> do
e <- postJose ep $ encode body
pure $ fmap OrderCreated e
Left err -> do
pure $ Left $ SigningFailed err
where
serialized =
object $
catMaybes
[ (\x -> "notBefore" .= x) <$> ord.notBefore
, (\x -> "notAfter" .= x) <$> ord.notAfter
, Just $ "identifiers" .= ord.identifiers
]
-- | The response of the server to 'postGetOrder'.
newtype OrderInspected = OrderInspected Response
deriving (Show)
type instance Field "order-inspected" "status" x = x
type instance Field "order-inspected" "expires" x = x
type instance Field "order-inspected" "identifiers" x = x
type instance Field "order-inspected" "notBefore" x = ()
type instance Field "order-inspected" "notAfter" x = ()
type instance Field "order-inspected" "error" x = x
type instance Field "order-inspected" "authorizations" x = x
type instance Field "order-inspected" "finalize" x = x
type instance Field "order-inspected" "certificate" x = Maybe x
instance FromJSON (Order "order-inspected") where
parseJSON = withObject "Order(inspected)" $ \v ->
Order
<$> v .: "status"
<*> v .: "expires"
<*> v .: "identifiers"
<*> pure ()
<*> pure ()
<*> v .:? "error"
<*> v .: "authorizations"
<*> v .: "finalize"
<*> v .:? "certificate"
-- | Lookup the order in a response to 'postGetOrder'.
readOrderInspected :: OrderInspected -> Maybe (Order "order-inspected")
readOrderInspected (OrderInspected rsp) = decode $ HTTP.responseBody rsp
-- | Lookup the URL of the certificate of a valid order.
readCertificateUrl :: OrderInspected -> Maybe (Url "certificate")
readCertificateUrl order =
certificate =<< readOrderInspected order
-- | Fetches a known order to inspect its status.
postGetOrder :: JWS.JWK -> Url "order" -> KID -> Nonce -> IO (Either AcmeError OrderInspected)
postGetOrder jwk orderurl kid nonce = do
ebody <- (kidSign jwk ep kid nonce "")
case ebody of
Right body -> do
e <- postJose ep $ encode body
pure $ fmap OrderInspected e
Left err -> do
pure $ Left $ SigningFailed err
where
ep :: Endpoint "order"
ep = coerce orderurl
{- | RFC-defined finalization request.
Consists of a CSR.
-}
data Finalize
= Finalize
{ csr :: CSR
}
-- | The response of the server to 'postFinalizeOrder'.
newtype OrderFinalized = OrderFinalized Response
deriving (Show)
-- todo: specialize status of a finalized order
-- | Lookup the order in a response to 'postFinalizeOrder'.
readOrderFinalized :: OrderFinalized -> Maybe (Order "order-created")
readOrderFinalized (OrderFinalized rsp) = decode $ HTTP.responseBody rsp
{- | Finalize an order after completing its challenges (i.e., once the order
is @ready@): sends the CSR, whose names must be the identifiers of the
order.
-}
postFinalizeOrder :: JWS.JWK -> KID -> Url "finalize-order" -> Finalize -> Nonce -> IO (Either AcmeError OrderFinalized)
postFinalizeOrder jwk kid finalizeurl finalizeobj nonce = do
ebody <- (kidSign jwk ep kid nonce $ encode $ serialized)
case ebody of
Right body -> do
e <- postJose ep $ encode body
pure $ fmap OrderFinalized e
Left err -> do
pure $ Left $ SigningFailed err
where
ep :: Endpoint "finalize-order"
ep = coerce finalizeurl
serialized =
object $
[ "csr" .= finalizeobj.csr
]