packages feed

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

{- | Runs the whole ACME flow (the \"dance\") for an order, from the directory
to the certificate, calling back your code at each step.

The caller provides an 'AcmeDancer': the server, the account key and account,
the CSR and the order, and a step handler. The step handler is where the
proofs of the challenges are published (DNS records, files, TLS
certificates): this library does not talk to DNS providers nor serve
anything on its own (see the @acme-not-a-joke-wai@ package for HTTP-01 and
TLS-ALPN-01).

@
jwk <- fromJust \<$\> loadJWKFile \"staging\/key.jwk\"
let o = createOrder (Nothing, Nothing) [OrderIdentifier DNSOrder \"example.com\"]
runAcmeDance_dns01 (AcmeDancer staging_letsencryptv2 jwk (fetchAccount [\"mailto:me\@example.com\"]) csr o (ghciDance \"certificate.pem\"))
@
-}
module Acme.NotAJoke.Dancer where

import Control.Concurrent (threadDelay)
import Control.Monad (void)
import qualified Crypto.JOSE.JWK as JWK
import Data.Text (Text)

import Acme.NotAJoke.Api.Account
import Acme.NotAJoke.Api.CSR
import Acme.NotAJoke.Api.Certificate
import Acme.NotAJoke.Api.Challenge (Token (..), isDNS01, isHTTP01, isTLSALPN01)
import Acme.NotAJoke.Api.Endpoint
import Acme.NotAJoke.Api.Order
import Acme.NotAJoke.Api.Validation
import Acme.NotAJoke.Client

{- | What the caller provides to run the dance.

The account must exist (see 'Acme.NotAJoke.Api.Account.postCreateAccount'):
the dance only fetches it.
-}
data AcmeDancer
    = AcmeDancer
    { baseUrl :: BaseUrl
    -- ^ the ACME server (e.g., 'Acme.NotAJoke.LetsEncrypt.staging_letsencryptv2')
    , accountJwk :: JWK.JWK
    -- ^ the account key, which signs all the requests
    , account :: Account "account-fetch"
    -- ^ the account to fetch (see 'fetchAccount')
    , csr :: CSR
    -- ^ the CSR, for the identifiers of the order, signed with the
    -- certificate key (which differs from the account key)
    , order :: Order "order-create"
    -- ^ the order (see 'createOrder')
    , handleStep :: DanceStep -> IO ()
    -- ^ called at each step: this is where the proofs are published, where
    -- the dance waits between polls, and where the certificate is stored
    }

{- | The steps of the dance, given in turn to the 'handleStep' of the
'AcmeDancer'.

The dance runs synchronously, in the thread calling 'runAcmeDance': it waits
for 'handleStep' to return before going on. A step handler must at least
publish the proofs on 'Validation', wait on 'WaitingForValidation' (the dance
does not sleep on its own between polls) and keep the certificate on 'Done'.
The dance ends after 'Done', 'InvalidOrder', 'OtherError' or 'AcmeFailure'.
-}
data DanceStep
    = {- | the client is about to validate this authorization, the next
      'Validation' step carries its proof (useful to know which identifier
      the proof is for, e.g. with 'dns01RecordName', when the order has more
      than one identifier)
      -}
      Authorizing AcmeAuthorization
    | {- | the proof must be answerable (e.g. the DNS record set, the
      token served) before the step handler returns; there is one
      'Validation' per authorization to validate, and the server is told to
      check the challenges only after the last one, so all proofs must be
      answerable at the same time
      -}
      Validation (Token, KeyAuthorization, ValidationProof)
    | -- | the dance is about to poll the order for the n-th time (starting
      -- at 0), the step handler should wait before returning (e.g., n seconds)
      WaitingForValidation Int
    | -- | the order is ready and the CSR was sent
      OrderIsFinalized OrderFinalized
    | -- | the order is valid, the certificate is about to be downloaded
      ValidOrder OrderInspected
    | -- | the order failed (e.g., a challenge failed), the dance stops
      InvalidOrder OrderInspected
    | -- | the server replied something unexpected, the dance stops
      OtherError Text
    | -- | an API call failed, the dance stops
      AcmeFailure AcmeError
    | -- | the certificate is downloaded, the dance is over
      Done AcmeSingle Certificate
    | -- | progress of the first requests (see 'prepareAcmeOrder')
      Prepare PrepareStep

{- | Runs the dance with DNS-01 challenges: the 'ValidationProof' given in each
'Validation' step must be published as a TXT record at the
'dns01RecordName' of the identifier given in the preceding 'Authorizing' step
before the step handler returns.

DNS-01 challenges can validate wildcard identifiers: an order for
@*.example.com@ needs a TXT record at @_acme-challenge.example.com@. An order
for both @*.example.com@ and @example.com@ needs two TXT records at that same
name, so a step handler must add records rather than replace them.
-}
runAcmeDance_dns01 :: AcmeDancer -> IO ()
runAcmeDance_dns01 = runAcmeDance isDNS01

{- | Runs the dance with an HTTP-01 challenge: the 'KeyAuthorization' given in
the 'Validation' step must be served at
@http:\/\/{domain}\/.well-known\/acme-challenge\/{token}@ (on port 80) before the
step handler returns.

HTTP-01 challenges cannot validate wildcard identifiers, use
'runAcmeDance_dns01' for these.
-}
runAcmeDance_http01 :: AcmeDancer -> IO ()
runAcmeDance_http01 = runAcmeDance isHTTP01

{- | Runs the dance with a TLS-ALPN-01 challenge (RFC-8737): before the step
handler returns from the 'Validation' step, the TLS server on port 443 of the
domain must answer handshakes negotiating the @acme-tls/1@ protocol (ALPN)
for this server name with a self-signed certificate carrying the SHA-256
digest of the 'KeyAuthorization' (see
'Acme.NotAJoke.CertManagement.createTLSALPN01Certificate').

TLS-ALPN-01 challenges cannot validate wildcard identifiers, use
'runAcmeDance_dns01' for these.
-}
runAcmeDance_tlsalpn01 :: AcmeDancer -> IO ()
runAcmeDance_tlsalpn01 = runAcmeDance isTLSALPN01

{- | Runs the whole dance for an order, validating authorizations with the
challenges picked by the 'MatchChallenge': fetch the directory and the
account, create the order, emit the proofs, tell the server to check them,
poll the order, finalize it with the CSR, and download the certificate.
-}
runAcmeDance :: MatchChallenge -> AcmeDancer -> IO ()
runAcmeDance matchChallenge dancer = do
    prepared <-
        prepareAcmeOrder
            dancer.baseUrl
            dancer.accountJwk
            dancer.account
            dancer.csr
            dancer.order
            matchChallenge
            handleAcmeSingleStep
    either failed go prepared
  where
    go acme = do
        mapM_ announce acme.pendingAuthorizations
        replyAll acme acme.pendingAuthorizations

    announce authz = do
        dancer.handleStep $ Authorizing authz
        dancer.handleStep $ Validation authz.proof

    replyAll acme [] = waitForValidOrder 0 acme
    replyAll acme (authz : rest) = do
        replied <- authz.replyChallenge
        case replied of
            Left err -> failed err
            Right _ -> replyAll acme rest

    handleAcmeSingleStep = dancer.handleStep . Prepare

    failed = dancer.handleStep . AcmeFailure

    waitForValidOrder n acme = do
        dancer.handleStep (WaitingForValidation n)
        polled <- acme.pollOrder
        case polled of
            Left err -> failed err
            Right recentOrder ->
                case Acme.NotAJoke.Api.Order.status <$> readOrderInspected recentOrder of
                    Just OrderPending -> waitForValidOrder (succ n) acme
                    Just OrderProcessing -> waitForValidOrder (succ n) acme
                    Just OrderReady -> do
                        finalized <- acme.finalizeOrder
                        case finalized of
                            Left err -> failed err
                            Right x -> do
                                dancer.handleStep $ OrderIsFinalized x
                                waitForValidOrder (succ n) acme
                    Just OrderValid -> handleValid acme recentOrder
                    Just OrderInvalid -> dancer.handleStep $ InvalidOrder recentOrder
                    _ -> dancer.handleStep $ OtherError "could not inspect order"

    handleValid acme o = do
        dancer.handleStep $ ValidOrder o
        case readCertificateUrl o of
            Nothing -> dancer.handleStep $ OtherError "valid order without certificate url"
            Just certUrl -> do
                certif <- acme.fetchCertificate certUrl
                either failed (dancer.handleStep . Done acme) certif

{- | A dance for running from within GHCI (i.e., printing and expecting you to
press ENTER to continue).
-}
ghciDance :: FilePath -> DanceStep -> IO ()
ghciDance certPath x =
    case x of
        Authorizing authz -> do
            print ("authorizing" :: Text, authz.authIdentifier, ("wildcard" :: Text, authz.authWildcard))
            print ("TXT record (dns01) is" :: Text, dns01RecordName authz.authIdentifier)
        Validation (tok, keyAuth, sha) -> do
            print ("token (http01) is" :: Text, showToken tok)
            print ("key authorization (http01) is" :: Text, showKeyAuth keyAuth)
            print ("sha256 (dns01) is" :: Text, showProof sha)
            print ("press enter to continue" :: Text)
            void getLine
        OrderIsFinalized o -> do
            print ("finalized" :: Text, o)
        ValidOrder o -> do
            print ("order valid" :: Text, o)
        WaitingForValidation n -> do
            print ("waiting" :: Text, n)
            threadDelay $ n * 1000000
        Done _ cert -> do
            storeCert certPath cert
            print cert
        InvalidOrder o -> do
            print ("order invalid" :: Text, o)
        OtherError txt -> do
            print ("order invalid" :: Text, txt)
        AcmeFailure err -> do
            print ("acme failure" :: Text, readProblem err, err)
        Prepare Starting -> do
            print ("starting" :: Text)
        Prepare GettingNonce -> do
            print ("getting-nonce" :: Text)
        Prepare (GotDirectory d) -> do
            print ("listed directory" :: Text, d)
        Prepare (GotAccount a) -> do
            print ("got account" :: Text, a)
        Prepare (GotOrder o) -> do
            print ("got order" :: Text, o)
        Prepare (GotAuthorization a) -> do
            print ("got authorization" :: Text, a)

-- | The text of a token (the last path segment of the HTTP-01 URL).
showToken :: Token -> Text
showToken (Token x1) = x1

-- | The text of a key authorization (the body served for HTTP-01).
showKeyAuth :: KeyAuthorization -> Text
showKeyAuth (KeyAuthorization x1) = x1

-- | The text of a proof (the value of the TXT record for DNS-01).
showProof :: ValidationProof -> Text
showProof (ValidationProof x1) = x1