packages feed

acme-not-a-joke-0.1.0.0: src/Acme/NotAJoke/Client.hs

{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-}

module Acme.NotAJoke.Client where

import qualified Data.List as List
import Data.Maybe (fromJust)

import qualified Crypto.JOSE.JWK as JWK

import Acme.NotAJoke.Api.Account
import Acme.NotAJoke.Api.Authorization
import Acme.NotAJoke.Api.CSR
import Acme.NotAJoke.Api.Certificate
import Acme.NotAJoke.Api.Challenge
import Acme.NotAJoke.Api.Directory
import Acme.NotAJoke.Api.Endpoint
import Acme.NotAJoke.Api.Nonce as Nonce
import Acme.NotAJoke.Api.Order
import Acme.NotAJoke.Api.Validation

{- | An IO-type for ACME primitives.
As we iterate on this lib, this type may change to become a monad-stack/mtl-mashup.
-}
type AcmePrim a = IO (Maybe a)

{- | An object carrying all functions to generate a single authorization from a
single order with a DNS challenge.
-}
data AcmeSingle = AcmeSingle
    { dir :: Directory
    -- ^ directory for the Server
    , nonces :: Nonce.Fetcher
    -- ^ an object to generate new nonces for this ACME server, saving found nonces opportunistically
    , pollOrder :: AcmePrim OrderInspected
    -- ^ fetches the status of an order
    , fetchAuthorization :: AcmePrim AuthorizationInspected
    -- ^ fetches the authorization, this function is called by prepareAcmeOrder so you may not need to inspect authorization yourself
    , proof :: (Token, KeyAuthorization, ValidationProof)
    -- ^ the validation proof (i.e., the value to set in DNS records and so on)
    , replyChallenge :: AcmePrim ChallengeAttempted
    -- ^ tells the server it can validates the challenge (i.e., after writing the proof in some DNS record)
    , pollChallenge :: AcmePrim ChallengeAttempted
    -- ^ fetches the status of a challenge (mainly to know if the server has validated the challenge)
    , finalizeOrder :: AcmePrim OrderFinalized
    -- ^ finalize the order (i.e, effectively sends the CSR to the server)
    , fetchCertificate :: Url "certificate" -> AcmePrim Certificate
    -- ^ fetch the signed certificate, the URL comes from a OrderInspected (see readOrderInspected)
    }

data PrepareStep
    = Starting
    | GotDirectory Directory
    | GettingNonce
    | GotAccount AccountCreated
    | GotOrder OrderCreated
    | GotAuthorization AuthorizationInspected

type MatchChallenge = Challenge "challenge-unspecified" -> Bool

-- TODO:

-- * step for errors

-- * return shortcuts in handleStep function
prepareAcmeOrder ::
    BaseUrl ->
    JWK.JWK ->
    Account "account-fetch" ->
    CSR ->
    Order "order-create" ->
    MatchChallenge ->
    (PrepareStep -> IO ()) ->
    IO AcmeSingle
prepareAcmeOrder baseurl jwk account csr1 order matchChallenge handleStep = do
    handleStep $ Starting

    -- unauthenticated info
    acmeDir <- fetchDirectory (directory baseurl)
    nf <- fetcher (handleStep GettingNonce >> getNonce acmeDir.newNonce)
    let mknonce = nf.produce
    let nonceify = fromJust <$> mknonce
    handleStep $ GotDirectory acmeDir

    -- fetch account
    nonce1 <- nonceify
    Just accountCreated <- saveNonce nf (postFetchAccount jwk acmeDir.newAccount nonce1 account)
    let (Just kid) = readKID accountCreated
    handleStep $ GotAccount accountCreated

    -- prepare new order
    nonce2 <- nonceify
    Just orderCreated <- saveNonce nf (postNewOrder jwk acmeDir.newOrder kid nonce2 order)
    let Just authUrl = safeHead . authorizations =<< readOrderCreated orderCreated
    handleStep $ GotOrder orderCreated

    -- poller for order
    let Just orderUrl = readOrderUrl orderCreated
    let fpollOrder = saveNonce nf (postGetOrder jwk orderUrl kid =<< nonceify)

    -- read authorization's dns challenge
    let ffetchAuthorization = saveNonce nf (postGetAuthorization jwk kid authUrl =<< nonceify)
    Just authorizationInspected <- ffetchAuthorization
    handleStep $ GotAuthorization authorizationInspected
    let Just challenge = List.find matchChallenge . challenges =<< readAuthorization authorizationInspected

    -- challenge validation proof
    let tok = challenge.token
    let keyAuth = keyAuthorization tok jwk
    let proofVal = sha256digest keyAuth

    -- read authorization's dns challenge
    let freplyChallenge = saveNonce nf (postReplyChallenge jwk kid challenge =<< nonceify)
    let fpollChallenge = saveNonce nf (postGetChallenge jwk kid challenge =<< nonceify)

    -- finalize order
    let Just finalizeOrderUrl = fmap finalize $ readOrderCreated orderCreated
    let ffinalizeOrder = saveNonce nf (postFinalizeOrder jwk kid finalizeOrderUrl (Finalize csr1) =<< nonceify)

    -- fetch certificate (at last)
    let ffetchCertificate certificateUrl = saveNonce nf (postGetCertificate jwk kid certificateUrl =<< nonceify)

    pure $ AcmeSingle acmeDir nf fpollOrder ffetchAuthorization (tok, keyAuth, proofVal) freplyChallenge fpollChallenge ffinalizeOrder ffetchCertificate
  where
    safeHead :: [x] -> Maybe x
    safeHead [] = Nothing
    safeHead (x : _) = Just x