Skip to main content

acme_proxy_protocol/acme/
order.rs

1//! The order state machine: creating an order, deactivating an authorization,
2//! claiming and validating a challenge, and finalizing — as operations on
3//! stored rows rather than on HTTP requests.
4//!
5//! Revocation is [`super::revoke`]: it starts from a certificate rather than
6//! from an order, and an operator reaches it without one.
7
8use std::net::IpAddr;
9use std::sync::Arc;
10
11use axum::http::StatusCode;
12use base64::prelude::*;
13use serde::Deserialize;
14use serde_json::Value;
15use tracing::{error, info, warn};
16use uuid::Uuid;
17
18use super::access::signer_account;
19use super::error::Error;
20use super::policy::{challenge_problem, check_identifiers};
21use super::rules::{
22    check_csr_matches_order, csr_identifiers, is_wildcard, names_an_ip_address, normalize_dns_name,
23    parse_csr, parse_rfc3339, well_formed_name,
24};
25use crate::profile::Profile;
26use acme_proxy_core::audit::RequestContext;
27use acme_proxy_core::error::Problem;
28use acme_proxy_core::identifier::Identifier;
29use acme_proxy_core::jws::signature::jwk_thumbprint;
30use acme_proxy_jobs::auditor::Auditor;
31use acme_proxy_jobs::jobs::JobQueue;
32use acme_proxy_jobs::notify::ChallengeFailedData;
33use acme_proxy_jobs::notify::NotifyEvent;
34use acme_proxy_net::challenge::ValidationContext;
35use acme_proxy_policy::filter::IdentifierStage;
36use acme_proxy_policy::filter::Stage as FilterStage;
37use acme_proxy_store::account::Account;
38use acme_proxy_store::authz::Authorization;
39use acme_proxy_store::authz::Challenge;
40use acme_proxy_store::authz::ValidationClaim;
41use acme_proxy_store::db::Database;
42use acme_proxy_store::nonce::now_secs;
43use acme_proxy_store::order::Order;
44use acme_proxy_store::status::AuthzStatus;
45use acme_proxy_store::status::ChallengeStatus;
46use acme_proxy_store::status::OrderStatus;
47
48/// A newOrder payload (RFC 8555 §7.4).
49#[derive(Debug, Default, Deserialize)]
50#[serde(default)]
51pub struct NewOrderPayload {
52    pub identifiers: Vec<Identifier>,
53    #[serde(rename = "notBefore")]
54    pub not_before: Option<String>,
55    #[serde(rename = "notAfter")]
56    pub not_after: Option<String>,
57    /// RFC 9773 §5: "A string uniquely identifying a previously issued
58    /// certificate that this order is intended to replace", in the certID form
59    /// of §4.1.
60    pub replaces: Option<String>,
61}
62
63/// A finalize payload (RFC 8555 §7.4).
64#[derive(Debug, Deserialize)]
65pub struct FinalizePayload {
66    pub csr: String,
67}
68
69/// Collapses per-identifier rejections into the one problem the client sees
70/// (RFC 8555 §6.7.1).
71///
72/// A single rejection is returned as itself: wrapping one problem in a
73/// `compound` would bury the type a client actually switches on for no gain.
74/// Several become a `compound` whose status is the most severe of the parts —
75/// so a batch containing a policy refusal reads as 403 rather than being
76/// downgraded to 400 by a malformed name sitting next to it.
77fn compound_identifier_problem(mut rejections: Vec<Problem>) -> Problem {
78    if rejections.len() == 1 {
79        return rejections.remove(0);
80    }
81
82    let status = rejections
83        .iter()
84        .map(Problem::status)
85        .max()
86        .unwrap_or(StatusCode::BAD_REQUEST);
87
88    Problem::compound(status, "Some of the identifiers requested were rejected")
89        .with_subproblems(rejections)
90}
91
92/// Validates a newOrder's `replaces` field (RFC 9773 §5) and returns the certID
93/// to store, so it can be reflected back on this and every later read.
94///
95/// §5 asks for three checks — "that the identified certificate and the newOrder
96/// request correspond to the same ACME Account, that they share at least one
97/// identifier, and that the identified certificate has not already been marked
98/// as replaced by a different Order that is not `invalid`" — and leaves
99/// anything stricter ("such as requiring exact identifier matching") to server
100/// policy. This implements exactly the three, and no more: a renewal that drops
101/// or adds a name is an ordinary, legitimate thing to do.
102///
103/// The statuses differ by design. Only the already-replaced case is pinned by
104/// the RFC (409 + `alreadyReplaced`); the rest are 400 `malformed`, matching
105/// what every other unknown-or-unowned resource in this codebase returns.
106async fn check_replaces(
107    cert_id: &str,
108    profile: &str,
109    account_id: Uuid,
110    identifiers: &[Identifier],
111    database: &Arc<Database>,
112) -> Result<String, Problem> {
113    // Parsed with the same helper `GET /renewalInfo/{certID}` uses: §5 defines
114    // the field as "constructed in the same way as the path component for GET
115    // requests described in Section 4.1", so the two must not drift.
116    let parsed = acme_proxy_core::cert::parse_ari_cert_id(cert_id).map_err(|error| {
117        warn!(event = "replaces_malformed", outcome = "failure", replaces = %cert_id, error = %error);
118        Problem::malformed(format!("Invalid `replaces` certID: {error}"))
119    })?;
120
121    let predecessor = Order::find_by_cert_serial(profile, &parsed.serial_hex(), database)
122        .await
123        .map_err(|error| {
124            error!(event = "replaces_lookup_failed", outcome = "failure", error = %error);
125            Problem::server_internal("Predecessor lookup failed")
126        })?
127        .ok_or_else(|| {
128            warn!(event = "replaces_unknown", outcome = "failure", replaces = %cert_id);
129            Problem::malformed("`replaces` names no certificate issued here")
130        })?;
131
132    // Same check the ARI handler makes, for the same reason: a serial alone does
133    // not identify a certificate, and the AKI half must not be decorative.
134    // Certificates issued before the local CA emitted an AKI have none, so a
135    // missing extension means "cannot check" rather than "reject".
136    if let Some(certificate) = predecessor.certificate.as_ref()
137        && let Ok(leaf_der) = acme_proxy_core::cert::leaf_der_from_chain(certificate)
138        && let Ok((aki, _)) = acme_proxy_core::cert::ari_cert_id_parts(&leaf_der)
139        && aki != parsed.aki
140    {
141        warn!(event = "replaces_aki_mismatch", outcome = "failure", replaces = %cert_id);
142        return Err(Problem::malformed(
143            "`replaces` key identifier does not match the certificate",
144        ));
145    }
146
147    // "…correspond to the same ACME Account". Also what stops one account
148    // probing another's certificates through this field.
149    if predecessor.account_id != account_id {
150        warn!(event = "replaces_wrong_account", outcome = "failure", replaces = %cert_id, order_id = %predecessor.id);
151        return Err(Problem::malformed(
152            "`replaces` names a certificate belonging to another account",
153        ));
154    }
155
156    // "…that they share at least one identifier".
157    let shares_identifier = identifiers.iter().any(|wanted| {
158        predecessor
159            .identifiers
160            .iter()
161            .any(|had| had.typ == wanted.typ && had.value == wanted.value)
162    });
163    if !shares_identifier {
164        warn!(event = "replaces_no_shared_identifier", outcome = "failure", replaces = %cert_id);
165        return Err(Problem::malformed(
166            "`replaces` names a certificate sharing no identifier with this order",
167        ));
168    }
169
170    // "…has not already been marked as replaced by a different Order that is
171    // not `invalid`" — the one case §5 gives a status and a type for.
172    if let Some(existing) = Order::find_by_replaces(profile, cert_id, database)
173        .await
174        .map_err(|error| {
175            error!(event = "replaces_conflict_lookup_failed", outcome = "failure", error = %error);
176            Problem::server_internal("Replacement lookup failed")
177        })?
178    {
179        warn!(
180            event = "replaces_already_claimed",
181            outcome = "failure",
182            replaces = %cert_id,
183            existing_order_id = %existing.id,
184        );
185        return Err(Problem::already_replaced(
186            "This certificate has already been marked as replaced by another order",
187        ));
188    }
189
190    info!(event = "replaces_accepted", outcome = "success", replaces = %cert_id, predecessor_order_id = %predecessor.id);
191    Ok(cert_id.to_string())
192}
193
194/// Whether a failed order INSERT was the `replaces` claim losing a race.
195///
196/// Matched on the offending *column* rather than on "any unique violation": the
197/// same transaction also inserts authorizations and challenges, each under its
198/// own `UNIQUE` constraint, and reporting one of those as `alreadyReplaced`
199/// would send a client chasing something entirely unrelated.
200///
201/// Each dialect names a different half of the same index, so both spellings go
202/// in and `sql::is_unique_violation_on` asks whichever the driver can answer.
203/// SQLite reports a partial unique index violation as `UNIQUE constraint
204/// failed: orders.profile, orders.replaces`, naming the columns and never the
205/// index; PostgreSQL reports `duplicate key value violates unique constraint
206/// "idx_orders_replaces_claim"`, naming the index and never the columns.
207/// Matching on the SQLite text alone made this silently stop recognising the
208/// collision under PostgreSQL, turning a `409 alreadyReplaced` into a `500`.
209/// Pinned by
210/// `acme_proxy_store::db::tests::one_predecessor_can_only_be_claimed_by_one_live_order`,
211/// which asserts on the message this reads.
212fn is_replaces_conflict(error: &sqlx::Error) -> bool {
213    acme_proxy_store::sql::is_unique_violation_on(
214        error,
215        "orders.replaces",
216        "idx_orders_replaces_claim",
217    )
218}
219
220/// Builds the `certificate_issue_failed` row shared by `finalize`'s four
221/// refusal arms.
222///
223/// A free function taking `&Order` rather than a closure capturing it, so the
224/// order stays free to be claimed after the refusals are behind it.
225fn issue_failed(
226    profile: &str,
227    account_id: Uuid,
228    order: &Order,
229    client: &acme_proxy_core::audit::ClientContext,
230    reason: &'static str,
231    detail: &str,
232) -> acme_proxy_core::audit::AuditRecord {
233    acme_proxy_core::audit::AuditRecord::new(
234        acme_proxy_core::audit::AuditEvent::CertificateIssueFailed,
235        profile,
236        acme_proxy_core::audit::Actor::acme(account_id),
237    )
238    .with_order(order.id, order.account_id, &order.identifiers)
239    .with_client(client.clone())
240    .with_reason(reason)
241    .with_detail(detail)
242}
243
244/// The order-side operations of one endpoint.
245///
246/// A borrowed bundle rather than an owned service: every caller already holds
247/// these — the ACME handlers in `AppState`, the web admin in `AdminState`, a
248/// background job in its own state — and building one is three references.
249/// The profile is the endpoint's whole configuration (its signer, filter,
250/// validators and notifier), which is what makes one operation mean the same
251/// thing whichever front end reached it.
252pub struct OrderService<'a> {
253    pub database: &'a Arc<Database>,
254    pub audit: &'a Auditor,
255    pub profile: &'a Profile,
256}
257
258/// The order's identifiers as it will store them — normalized, deduplicated,
259/// first-seen order kept — or every reason they cannot be.
260///
261/// Checked before the account is resolved, so a malformed order is refused as
262/// malformed whoever sent it.
263fn validated_identifiers(
264    mut identifiers: Vec<Identifier>,
265    profile: &Profile,
266) -> Result<Vec<Identifier>, Problem> {
267    let challenges = &profile.challenges;
268
269    if identifiers.is_empty() {
270        warn!(event = "order_no_identifiers", outcome = "failure");
271        return Err(Problem::malformed("No identifiers"));
272    }
273    // Before anything is normalized or looked at: the cost this refuses is the
274    // work below, and every bit of it scales with the count.
275    if identifiers.len() > profile.order.max_identifiers {
276        warn!(
277            event = "order_too_many_identifiers",
278            outcome = "failure",
279            identifiers_count = identifiers.len(),
280            limit = profile.order.max_identifiers
281        );
282        return Err(Problem::malformed(format!(
283            "An order may name at most {} identifiers; this one names {}",
284            profile.order.max_identifiers,
285            identifiers.len()
286        )));
287    }
288    if let Some(bad) = identifiers.iter().find(|id| id.typ != "dns") {
289        warn!(event = "order_identifier_type_unsupported", outcome = "failure", typ = %bad.typ);
290        return Err(Problem::unsupported_identifier(
291            "Only dns identifiers supported",
292        ));
293    }
294
295    for identifier in &mut identifiers {
296        identifier.value = normalize_dns_name(&identifier.value);
297    }
298
299    // Two spellings of one name are one identifier. `A.example.com` and
300    // `a.example.com.` normalize to the same value, and the order's
301    // `UNIQUE (order_id, identifier)` would answer the second one with a
302    // 500 on the write. Keeping first-seen order leaves the object the
303    // client reads back in the order it asked.
304    let mut seen = std::collections::HashSet::new();
305    identifiers.retain(|identifier| seen.insert(identifier.value.clone()));
306
307    // Every offending name at once, each attributed to itself (RFC 8555 §6.7.1).
308    // Reporting only the first would make a ten-name order a ten-round-trip
309    // guessing game — §6.7.1's own rationale: a client "may choose to submit
310    // another order containing only the eight identifiers not listed".
311    let rejections: Vec<Problem> = identifiers
312        .iter()
313        .filter_map(|identifier| {
314            if !well_formed_name(&identifier.value) {
315                warn!(event = "order_identifier_malformed", outcome = "failure", value = %identifier.value);
316                Some(
317                    Problem::malformed(format!(
318                        "Malformed identifier {}: not a DNS name (a `*` is only legal as a single leading `*.`)",
319                        identifier.value
320                    ))
321                    .with_identifier(identifier),
322                )
323            } else if names_an_ip_address(&identifier.value) {
324                warn!(event = "order_identifier_is_address", outcome = "failure", value = %identifier.value);
325                Some(
326                    Problem::rejected_identifier(format!(
327                        "Identifier {} is an IP address, which a dns identifier cannot name",
328                        identifier.value
329                    ))
330                    .with_identifier(identifier),
331                )
332            } else if challenges
333                .types_for(is_wildcard(&identifier.value))
334                .is_empty()
335            {
336                warn!(event = "order_identifier_wildcard_rejected", outcome = "failure", value = %identifier.value);
337                Some(
338                    Problem::rejected_identifier(format!(
339                        "Wildcard identifier {} requires the dns-01 challenge, which is not enabled",
340                        identifier.value
341                    ))
342                    .with_identifier(identifier),
343                )
344            } else {
345                None
346            }
347        })
348        .collect();
349
350    if !rejections.is_empty() {
351        return Err(compound_identifier_problem(rejections));
352    }
353    Ok(identifiers)
354}
355
356/// Whether a trigger on a `pending` challenge under `authz` and `order` could
357/// still start a validation — [`OrderService::claim_challenge`]'s refusals
358/// (expired, deactivated, invalid) and its "decided by a sibling" read as a
359/// predicate, for a POST-as-GET that must not refuse but must not invite a
360/// poll either. A change to one belongs in the other.
361pub fn challenge_can_be_triggered(authz: &Authorization, order: &Order, now: i64) -> bool {
362    authz.status == AuthzStatus::Pending
363        && authz.expires > now
364        && order.status != OrderStatus::Invalid
365}
366
367impl OrderService<'_> {
368    /// Creates an order and its authorizations (RFC 8555 §7.4), for the account
369    /// that signed the request.
370    ///
371    /// `cached` is the account the JWS `kid` already resolved, if any. It is
372    /// resolved here, *after* the identifiers are checked, so a malformed order
373    /// is refused as malformed whoever sent it. `request` is where the reverse
374    /// lookup that stamps the order comes from — run late, since every refusal
375    /// above it would have wasted one.
376    pub async fn new_order(
377        &self,
378        payload: NewOrderPayload,
379        cached: Option<Account>,
380        pubkey: &[u8],
381        client_ip: Option<IpAddr>,
382        request: &RequestContext,
383    ) -> Result<(Order, Vec<Uuid>), Error> {
384        let (database, profile, audit) = (self.database, self.profile, self.audit);
385        let identifiers = validated_identifiers(payload.identifiers, profile)?;
386
387        let not_before = match payload.not_before {
388            Some(ref s) => Some(parse_rfc3339("notBefore", s)?),
389            None => None,
390        };
391        let not_after = match payload.not_after {
392            Some(ref s) => Some(parse_rfc3339("notAfter", s)?),
393            None => None,
394        };
395
396        let account = signer_account(cached, &profile.name, pubkey, database).await?;
397
398        check_identifiers(
399            &profile.filter,
400            client_ip,
401            &account.id.to_string(),
402            &profile.name,
403            IdentifierStage::NewOrder,
404            &identifiers,
405            database,
406        )
407        .await?;
408
409        // RFC 9773 §5, run after `signer_account` so the "same ACME Account" check
410        // has an account to compare against.
411        let replaces = match payload.replaces {
412            Some(ref cert_id) => Some(
413                check_replaces(cert_id, &profile.name, account.id, &identifiers, database).await?,
414            ),
415            None => None,
416        };
417
418        let expires = now_secs() + profile.order.validity_seconds as i64;
419
420        // The reverse lookup runs here rather than at the top of the handler: every
421        // refusal above (malformed name, wildcard without dns-01, `alreadyReplaced`)
422        // returns without an order to stamp, and a PTR query for a request that is
423        // about to be turned away buys nothing.
424        let client = audit.client(request).await;
425        let mut order = Order::new(
426            &profile.name,
427            account.id,
428            identifiers,
429            expires,
430            not_before,
431            not_after,
432        )
433        .with_client(&client);
434        order.replaces = replaces;
435        let mut authz_ids = Vec::with_capacity(order.identifiers.len());
436
437        let persisted = async {
438            let mut tx = database.transaction().await?;
439            order.insert(tx.conn()).await?;
440
441            for identifier in &order.identifiers {
442                let authz = Authorization::new(order.id, identifier.clone(), order.expires);
443                authz.insert(tx.conn()).await?;
444                for typ in profile.challenges.types_for(is_wildcard(&identifier.value)) {
445                    Challenge::new(authz.id, typ).insert(tx.conn()).await?;
446                }
447                authz_ids.push(authz.id);
448            }
449
450            tx.commit().await
451        }
452        .await;
453
454        persisted.map_err(|error| {
455            // The predecessor was claimed by another order between `check_replaces`
456            // reading and this transaction committing. The partial unique index on
457            // `(profile, replaces)` is what catches it; without that arm the loser
458            // of the race would get a 500 for a condition RFC 9773 §5 gives a
459            // status and a type for.
460            if is_replaces_conflict(&error) {
461                warn!(event = "replaces_claim_race_lost", outcome = "failure", account_id = %account.id);
462                return Problem::already_replaced(
463                    "This certificate has already been marked as replaced by another order",
464                );
465            }
466            error!(
467                event = "order_creation_failed",
468                outcome = "failure",
469                error = %error,
470                account_id = %account.id
471            );
472            Problem::server_internal("Order persistence failed")
473        })?;
474
475        info!(
476            event = "order_created",
477            outcome = "success",
478            order_id = %order.id,
479            account_id = %account.id,
480            identifiers_count = order.identifiers.len()
481        );
482
483        Ok((order, authz_ids))
484    }
485
486    /// Deactivates `authz` and re-derives its order's status (RFC 8555 §7.5.2).
487    ///
488    /// Already-`deactivated` is a no-op rather than an error: §7.5.2 describes the
489    /// client sending the same static object to *each* authorization of an
490    /// identifier, and a retry after a partial failure must not start reporting
491    /// errors halfway through.
492    pub async fn deactivate_authz(
493        &self,
494        authz: &mut Authorization,
495        order: &mut Order,
496    ) -> Result<(), Error> {
497        let database = self.database;
498        if authz.status == AuthzStatus::Deactivated {
499            return Ok(());
500        }
501
502        // A certificate already exists for this order, so relinquishing the
503        // authorization it was issued under would claim something untrue. §7.5.2 is
504        // about giving up the *ability* to issue, not about undoing issuance —
505        // that is what revocation (§7.6) is for.
506        if order.status == OrderStatus::Valid {
507            warn!(event = "authz_deactivate_refused_order_valid", outcome = "failure", authz_id = %authz.id, order_id = %order.id);
508            return Err(Problem::malformed(
509                "Cannot deactivate an authorization whose order has already been issued; revoke the certificate instead",
510            )
511            .into());
512        }
513
514        // The same, one step earlier: the issuance is queued and may already be
515        // signing. Refusing here keeps the answer honest; the `signer_issue` job
516        // re-checks every authorization before it signs, which is what covers a
517        // finalize that lands between this read and the write below.
518        if order.status == OrderStatus::Processing {
519            warn!(event = "authz_deactivate_refused_order_processing", outcome = "failure", authz_id = %authz.id, order_id = %order.id);
520            return Err(Problem::malformed(
521                "Cannot deactivate an authorization whose order is being issued",
522            )
523            .into());
524        }
525
526        if authz.status != AuthzStatus::Pending && authz.status != AuthzStatus::Valid {
527            warn!(event = "authz_deactivate_refused_terminal", outcome = "failure", authz_id = %authz.id, status = %authz.status);
528            return Err(Problem::malformed(
529                "Authorization is in a terminal state and cannot be deactivated",
530            )
531            .into());
532        }
533
534        // §7.5.2: "The server MUST NOT treat deactivated authorization objects as
535        // sufficient for issuing certificates." For a `pending` order that falls
536        // out of the readiness check on its own, but an order already promoted to
537        // `ready` would still finalize — so demote it.
538        //
539        // Both in one transaction. Between them, an order sits `ready` with a
540        // deactivated authorization under it: finalizable for a name the client has
541        // just given up, which is exactly what §7.5.2 forbids.
542        //
543        // Both writes are guarded, so the in-memory statuses read above only
544        // choose which error to give: a verdict that landed since leaves the
545        // authorization alone, and the order is demoted only if it is `ready`.
546        let outcome = async {
547            let mut tx = database.transaction().await?;
548            let deactivated = Authorization::set_deactivated(authz.id, tx.conn()).await?;
549            let demoted = deactivated && Order::set_pending(order.id, tx.conn()).await?;
550            tx.commit().await?;
551            Ok::<_, sqlx::Error>((deactivated, demoted))
552        }
553        .await;
554
555        let (deactivated, demoted) = outcome.map_err(|error| {
556            error!(event = "authz_deactivate_failed", outcome = "failure", authz_id = %authz.id, error = %error);
557            Problem::server_internal("Authorization deactivation failed")
558        })?;
559        if !deactivated {
560            warn!(event = "authz_deactivate_refused_terminal", outcome = "failure", authz_id = %authz.id, status = %authz.status);
561            return Err(Problem::malformed(
562                "Authorization is in a terminal state and cannot be deactivated",
563            )
564            .into());
565        }
566
567        // Only once the transaction has committed: a rollback must not leave these
568        // objects claiming a status the database never took.
569        authz.status = AuthzStatus::Deactivated;
570        if demoted {
571            order.status = OrderStatus::Pending;
572        }
573
574        info!(event = "authz_deactivated", outcome = "success", authz_id = %authz.id, order_id = %order.id);
575        Ok(())
576    }
577
578    /// Decides whether a challenge trigger (RFC 8555 §7.5.1) starts a
579    /// validation, and if so claims the challenge for it.
580    ///
581    /// [`ValidationClaim::Decided`] is not a refusal: the challenge is already
582    /// decided — here or by a sibling — or another trigger holds the claim, and
583    /// the caller answers with the challenge as it stands. `challenge` is
584    /// refreshed where the row moved under the read, so that answer is the
585    /// current object rather than the one the caller loaded.
586    /// [`ValidationClaim::Claimed`] obliges the caller to follow with
587    /// [`run_validation`](Self::run_validation), and
588    /// [`ValidationClaim::Limited`] is `429 rateLimited`: the account has
589    /// `challenge.max_in_flight_per_account` validations running already.
590    ///
591    /// [`challenge_can_be_triggered`] is the read side of the same checks and
592    /// changes with them.
593    pub async fn claim_challenge(
594        &self,
595        challenge: &mut Challenge,
596        authz: &Authorization,
597        order: &Order,
598    ) -> Result<ValidationClaim, Error> {
599        if authz.status != AuthzStatus::Valid && authz.expires <= now_secs() {
600            warn!(event = "authz_expired", outcome = "failure", authz_id = %authz.id, expires = authz.expires);
601            return Err(Problem::malformed("Authorization has expired").into());
602        }
603
604        // The client gave this authorization up (RFC 8555 §7.5.2). Validating a
605        // challenge under it would walk it straight back to `valid` — which §7.5.2
606        // forbids being sufficient for issuance — so refuse before doing any work.
607        if authz.status == AuthzStatus::Deactivated {
608            warn!(event = "authz_already_deactivated", outcome = "failure", authz_id = %authz.id);
609            return Err(Problem::malformed("Authorization has been deactivated").into());
610        }
611
612        // Already answered, here or by a sibling: §7.5.1's "client requests for
613        // retries do not cause a state change".
614        let decided = challenge.status == ChallengeStatus::Valid
615            || challenge.status == ChallengeStatus::Invalid
616            || authz.status == AuthzStatus::Valid;
617        if decided {
618            return Ok(ValidationClaim::Decided);
619        }
620
621        // Undecided, but a sibling challenge failed (§7.1.6: one failure makes
622        // the authorization `invalid`), or another authorization of the order
623        // did. Nothing this challenge could prove would change that, and
624        // answering with the challenge as it stands would leave the client
625        // polling a `pending` object that can never move.
626        if authz.status == AuthzStatus::Invalid || order.status == OrderStatus::Invalid {
627            // Not before a second look at the challenge. `load_owned_challenge`
628            // reads the three rows one statement at a time, while a verdict
629            // writes all three in one transaction: a request whose challenge
630            // read lands before that commit and whose authorization read lands
631            // after sees an undecided challenge under an `invalid`
632            // authorization — a pair the write never leaves behind. Refusing on
633            // it would answer a client polling its own verdict with a `400`,
634            // where §7.5.1's answer is the object.
635            let fresh = Challenge::find_by_id(challenge.id.to_string().as_str(), self.database)
636                .await
637                .map_err(|error| {
638                    error!(event = "challenge_lookup_failed", outcome = "failure", challenge_id = %challenge.id, error = %error);
639                    Problem::server_internal("Challenge lookup failed")
640                })?;
641            if let Some(fresh) = fresh
642                && (fresh.status == ChallengeStatus::Valid
643                    || fresh.status == ChallengeStatus::Invalid)
644            {
645                *challenge = fresh;
646                return Ok(ValidationClaim::Decided);
647            }
648
649            warn!(event = "challenge_trigger_refused_invalid", outcome = "failure", authz_id = %authz.id, order_id = %order.id);
650            return Err(Problem::malformed(
651                "The authorization or its order is already invalid; create a new order",
652            )
653            .into());
654        }
655
656        // The claim, and the reason it is a claim rather than the status check
657        // above: `challenges.validate` reaches out to an address the *client*
658        // named, so two triggers that both read this row as `pending` become two
659        // probes of that host from this server — bounded only by
660        // `server.max_concurrent_requests`, on a default configuration with no
661        // filter to refuse them. Deciding it in the `UPDATE` makes "one validation
662        // per challenge" a property of the row instead of one of scheduling.
663        //
664        // The loser answers with the challenge as it now stands, which reports
665        // `processing` — §8.2's answer for a challenge the server is still
666        // working on.
667        let claimed = challenge
668            .claim_for_validation(self.profile.challenges.max_in_flight_per_account(), self.database)
669            .await
670            .map_err(|error| {
671                error!(event = "challenge_claim_failed", outcome = "failure", challenge_id = %challenge.id, error = %error);
672                Problem::server_internal("Challenge could not be claimed for validation")
673            })?;
674        if claimed == ValidationClaim::Limited {
675            warn!(event = "challenge_trigger_rate_limited", outcome = "failure", account_id = %order.account_id, challenge_id = %challenge.id);
676        }
677        Ok(claimed)
678    }
679
680    /// Validates a challenge [`claim_challenge`](Self::claim_challenge) claimed,
681    /// and records the answer.
682    ///
683    /// Either outcome is recorded — a failed validation is the challenge's
684    /// answer, not an error of this call — so `Err` means only that the answer
685    /// could not be computed or stored. On failure the operator hears about it
686    /// through `challenge_failed`, after the commit.
687    ///
688    /// `client_ip` is the address the notification names: the client that
689    /// triggered the validation, when there was one.
690    pub async fn run_validation(
691        &self,
692        account: &Account,
693        challenge: &mut Challenge,
694        authz: &mut Authorization,
695        order: &mut Order,
696        client_ip: Option<IpAddr>,
697    ) -> Result<(), Error> {
698        let (database, profile) = (self.database, self.profile);
699        let thumbprint = jwk_thumbprint(&account.pubkey).map_err(|error| {
700            error!(event = "authz_thumbprint_failed", outcome = "failure", account_id = %account.id, error = %error);
701            Problem::server_internal("Key authorization could not be computed")
702        })?;
703        let key_authorization = format!("{}.{}", challenge.token, thumbprint);
704        let challenge_id = challenge.id.to_string();
705
706        let context = ValidationContext {
707            identifier: authz.base_identifier(),
708            wildcard: authz.is_wildcard(),
709            token: &challenge.token,
710            key_authorization: &key_authorization,
711            challenge_id: &challenge_id,
712        };
713
714        match profile.challenges.validate(&challenge.typ, &context).await {
715            Ok(()) => {
716                commit_validation(challenge, authz, order, database).await?;
717            }
718            Err(error) => {
719                let problem =
720                    challenge_problem(&error, &challenge.typ, authz.base_identifier()).to_value();
721                warn!(
722                    event = "challenge_failed",
723                    outcome = "failure",
724                    challenge_id = %challenge_id,
725                    typ = %challenge.typ,
726                    kind = error.kind()
727                );
728
729                let recorded =
730                    commit_validation_failure(challenge, authz, order, &problem, database).await?;
731                if !recorded {
732                    return Ok(());
733                }
734
735                // After the commit, not before. Dispatched first, a persistence
736                // failure would have notified an operator about a failure that
737                // was never recorded — and the client, which gets a 500, would
738                // see the challenge still `pending`.
739                profile
740                    .notify
741                    .dispatch(NotifyEvent::ChallengeFailed(ChallengeFailedData {
742                        profile: profile.name.clone(),
743                        order_id: order.id.to_string(),
744                        account_id: account.id.to_string(),
745                        authz_id: authz.id.to_string(),
746                        challenge_id: challenge.id.clone().to_string(),
747                        challenge_type: challenge.typ.clone(),
748                        identifier: authz.base_identifier().to_string(),
749                        error: error.kind().to_string(),
750                        client_ip: client_ip
751                            .map(|ip| acme_proxy_core::client::canonical(ip).to_string()),
752                    }))
753                    .await;
754            }
755        }
756        Ok(())
757    }
758
759    /// Records a claimed validation the server has given up on.
760    ///
761    /// The queue retires a row when its attempts run out or its deadline passes,
762    /// and a challenge left `processing` at that point would be polled by its
763    /// client, saying nothing, until the authorization expired. This writes the
764    /// same failure `run_validation` writes — challenge, authorization and order
765    /// together in one transaction — so the client sees an `invalid` order and
766    /// stops.
767    ///
768    /// Deliberately **no `challenge_failed` notification**: nothing was learned
769    /// about the client's own setup, which is what that event reports. The
770    /// `challenge_validation_abandoned` log line is about this server instead.
771    pub async fn abandon_validation(
772        &self,
773        challenge: &mut Challenge,
774        authz: &mut Authorization,
775        order: &mut Order,
776        reason: &str,
777    ) -> Result<(), Error> {
778        let problem =
779            Problem::server_internal(format!("Challenge validation was not completed: {reason}"))
780                .to_value();
781        commit_validation_failure(challenge, authz, order, &problem, self.database).await?;
782        Ok(())
783    }
784
785    /// Finalizes `order` with the base64url CSR a client sent (RFC 8555 §7.4):
786    /// checks the CSR, then claims the order and queues its issuance, returning
787    /// it `processing`. The certificate arrives when a worker has run the
788    /// `signer_issue` job ([`super::issue`]); the client polls for it.
789    ///
790    /// `account` must already own `order` (`access::load_owned_order`).
791    pub async fn finalize(
792        &self,
793        account: &Account,
794        mut order: Order,
795        csr: &str,
796        client_ip: Option<IpAddr>,
797        request: &RequestContext,
798        jobs: &JobQueue,
799    ) -> Result<Order, Error> {
800        let (database, profile, audit) = (self.database, self.profile, self.audit);
801        let filter = &profile.filter;
802
803        let id = order.id.to_string();
804        if order.status != OrderStatus::Ready {
805            warn!(event = "order_finalize_not_ready", outcome = "failure", order_id = %id, status = %order.status);
806            return Err(Problem::order_not_ready("Order is not ready").into());
807        }
808
809        // From here down every refusal is a *CA* refusal — the CSR was rejected, or
810        // a filter said no, or issuance failed — so each one is an `audit_log` row
811        // rather than only a log line. Above this point the refusals are protocol
812        // bookkeeping (unknown order, wrong owner, not ready) with no CA action
813        // attempted, and recording them would bury the ones that matter.
814        //
815        // The reverse lookup runs once here and is reused by whichever arm answers.
816        let client = audit.client(request).await;
817        let failed = |order: &Order, reason: &'static str, detail: &str| {
818            issue_failed(&profile.name, account.id, order, &client, reason, detail)
819        };
820
821        let csr_der = match BASE64_URL_SAFE_NO_PAD.decode(csr) {
822            Ok(der) => der,
823            Err(_) => {
824                audit
825                    .record(failed(&order, "badCSR", "CSR base64 invalid"))
826                    .await;
827                return Err(Problem::bad_csr("CSR base64 invalid").into());
828            }
829        };
830        let csr = match parse_csr(&csr_der) {
831            Ok(csr) => csr,
832            Err(problem) => {
833                audit
834                    .record(failed(&order, "badCSR", "CSR is unparsable"))
835                    .await;
836                return Err(problem.into());
837            }
838        };
839
840        // Before the filter chain: this is the most fundamental and least
841        // expensive check, and doing it first guarantees that a filter — or the script
842        // of a `custom` backend — never sees anything but a CSR already in agreement with its
843        // order.
844        if let Err(problem) = check_csr_matches_order(&csr, &csr_der, &order.identifiers) {
845            audit
846                .record(failed(
847                    &order,
848                    "badCSR",
849                    "CSR identifiers do not match the order",
850                ))
851                .await;
852            return Err(problem.into());
853        }
854
855        // Gated on the *identifier* stage specifically: a policy of nothing but
856        // connection-stage rules would otherwise pay for a CSR projection nothing
857        // reads.
858        if filter.has_rules_at(FilterStage::Identifiers) {
859            let requested = csr_identifiers(&csr);
860            if let Err(problem) = check_identifiers(
861                filter,
862                client_ip,
863                order.account_id.to_string().as_str(),
864                &profile.name,
865                IdentifierStage::Csr,
866                &requested,
867                database,
868            )
869            .await
870            {
871                // A refusal and a policy the server could not evaluate are
872                // different things, and the trail said "badCSR" for both until
873                // three-valued verdicts made the second visible. `500` here means
874                // nobody decided anything about this CSR.
875                let (reason, detail) = if problem.status() == StatusCode::BAD_REQUEST {
876                    ("badCSR", "the filter policy refused the CSR identifiers")
877                } else {
878                    (
879                        "serverInternal",
880                        "the filter policy could not be evaluated for the CSR identifiers",
881                    )
882                };
883                audit.record(failed(&order, reason, detail)).await;
884                return Err(problem.into());
885            }
886        }
887
888        // Claimed here rather than at the `ready` check above, so no refusal
889        // above owes a release — and claimed **with** the job that settles it,
890        // in one transaction, so no crash can leave an order `processing` with
891        // nothing coming for it.
892        //
893        // The loser gets §7.4's own answer — `403 orderNotReady`, on which the
894        // client POST-as-GETs the order and sees `processing`, then `valid`. No
895        // audit row: like the not-ready refusal above, this is protocol
896        // bookkeeping with no CA action attempted.
897        let spec = super::issue::signer_issue_spec(&order, &csr_der, &client, client_ip);
898        let claimed = async {
899            let mut tx = database.transaction().await?;
900            if !order.claim_for_finalize_on(tx.conn()).await? {
901                return Ok(false);
902            }
903            jobs.enqueue_in(&spec, tx.conn()).await?;
904            tx.commit().await?;
905            Ok::<bool, sqlx::Error>(true)
906        }
907        .await;
908        match claimed {
909            Ok(true) => {}
910            Ok(false) => {
911                warn!(
912                    event = "order_finalize_claim_refused",
913                    outcome = "failure",
914                    order_id = %id
915                );
916                return Err(Problem::order_not_ready("Order is already being finalized").into());
917            }
918            Err(error) => {
919                error!(
920                    event = "order_mark_processing_failed",
921                    outcome = "failure",
922                    order_id = %id,
923                    error = %error
924                );
925                return Err(Problem::server_internal("Order finalize failed").into());
926            }
927        }
928        jobs.wake();
929
930        info!(event = "order_finalize_queued", outcome = "success", order_id = %id);
931        Ok(order)
932    }
933}
934
935/// Records a successful validation as **one** transaction: the challenge becomes
936/// `valid`, its authorization becomes `valid`, and the order is promoted to
937/// `ready` if that was the last one outstanding.
938///
939/// Three separate statements — which is what this was — can stop between any
940/// two. The gap that matters is the last one: an order left `pending` with every
941/// authorization already `valid` can never be finalized and nothing re-derives
942/// readiness, because the check only ever ran from here and the client has no
943/// challenge left to answer to make it run again. The order is stuck until it
944/// expires. `new_order` has always used one transaction for the same
945/// reason.
946///
947/// It also fixes a second, quieter bug. The readiness check used to re-read the
948/// authorizations *from the pool* after the write above had committed, so two
949/// concurrent validations of two authorizations of one order could each read
950/// before the other's write landed: neither would see a complete set, and
951/// neither would promote. Reading inside the transaction that just wrote means
952/// SQLite serializes the two writers, and whichever commits second is the one
953/// that sees them all `valid`. **PostgreSQL does not**: under its default READ
954/// COMMITTED isolation the two transactions lock different authorization rows,
955/// and each read sees only its own uncommitted write, so the race above is
956/// still open there.
957async fn commit_validation(
958    challenge: &mut Challenge,
959    authz: &mut Authorization,
960    order: &mut Order,
961    database: &Arc<Database>,
962) -> Result<(), Problem> {
963    let validated = now_secs();
964    let outcome = async {
965        let mut tx = database.transaction().await?;
966        // Every write below is guarded on the row still being undecided, and
967        // each reports whether it happened: this runs in a queued job, and the
968        // rows it read may have moved since. A sibling challenge may have
969        // decided the authorization, or the client may have deactivated it; the
970        // verdict is then recorded on the challenge alone and nothing above it
971        // changes.
972        let challenge_written = Challenge::set_valid(challenge.id, validated, tx.conn()).await?;
973        let authz_written =
974            challenge_written && Authorization::set_valid(authz.id, tx.conn()).await?;
975
976        // The guarded writes above have already taken the RESERVED lock by the
977        // time this reads — `transaction()` issues a deferred BEGIN — so this
978        // sees its own write and no other writer can interleave. Putting a read
979        // first here would break that. `set_ready` is guarded on `pending`, so
980        // the order's status as the job read it does not matter.
981        let promoted = authz_written && {
982            let authzs = Authorization::find_by_order_with(order.id, tx.conn()).await?;
983            authzs.len() == order.identifiers.len()
984                && authzs
985                    .iter()
986                    .all(|authz| authz.status == AuthzStatus::Valid)
987                && Order::set_ready(order.id, tx.conn()).await?
988        };
989        tx.commit().await?;
990        Ok::<_, sqlx::Error>((challenge_written, authz_written, promoted))
991    }
992    .await;
993
994    match outcome {
995        Ok((challenge_written, authz_written, promoted)) => {
996            // In-memory sync only after the commit, and only for what was
997            // written; see `Authorization::set_valid`.
998            if challenge_written {
999                challenge.status = ChallengeStatus::Valid;
1000                challenge.validated = Some(validated);
1001            }
1002            if authz_written {
1003                authz.status = AuthzStatus::Valid;
1004            } else if challenge_written {
1005                info!(event = "challenge_verdict_superseded", outcome = "advisory", challenge_id = %challenge.id, authz_id = %authz.id);
1006            }
1007            if promoted {
1008                order.status = OrderStatus::Ready;
1009            }
1010            Ok(())
1011        }
1012        Err(error) => {
1013            error!(
1014                event = "challenge_validation_persist_failed",
1015                outcome = "failure",
1016                challenge_id = %challenge.id,
1017                authz_id = %authz.id,
1018                order_id = %order.id,
1019                error = %error
1020            );
1021            Err(Problem::server_internal("Challenge validation failed"))
1022        }
1023    }
1024}
1025
1026/// The failure arm of [`commit_validation`], same shape: the challenge takes the
1027/// problem document explaining why, and its authorization and order both become
1028/// `invalid`, in one transaction.
1029///
1030/// Guarded the same way. A failure that lands after a sibling challenge made the
1031/// authorization `valid` is recorded on the challenge only: the authorization
1032/// was proven, and its order — perhaps already `valid`, holding a live
1033/// certificate — must not follow the failed sibling to `invalid`.
1034///
1035/// Returns whether the challenge itself took the verdict, which is what decides
1036/// whether an operator hears about it.
1037async fn commit_validation_failure(
1038    challenge: &mut Challenge,
1039    authz: &mut Authorization,
1040    order: &mut Order,
1041    problem: &Value,
1042    database: &Arc<Database>,
1043) -> Result<bool, Problem> {
1044    let outcome = async {
1045        let mut tx = database.transaction().await?;
1046        let challenge_written = Challenge::set_invalid(challenge.id, problem, tx.conn()).await?;
1047        let authz_written =
1048            challenge_written && Authorization::set_invalid(authz.id, tx.conn()).await?;
1049        let order_written =
1050            authz_written && Order::set_invalid(order.id, problem, tx.conn()).await?;
1051        tx.commit().await?;
1052        Ok::<_, sqlx::Error>((challenge_written, authz_written, order_written))
1053    }
1054    .await;
1055
1056    match outcome {
1057        Ok((challenge_written, authz_written, order_written)) => {
1058            if challenge_written {
1059                challenge.status = ChallengeStatus::Invalid;
1060                challenge.error = Some(problem.clone());
1061            }
1062            if authz_written {
1063                authz.status = AuthzStatus::Invalid;
1064            } else if challenge_written {
1065                info!(event = "challenge_verdict_superseded", outcome = "advisory", challenge_id = %challenge.id, authz_id = %authz.id);
1066            }
1067            if order_written {
1068                order.status = OrderStatus::Invalid;
1069                order.error = Some(problem.clone());
1070            }
1071            Ok(challenge_written)
1072        }
1073        Err(error) => {
1074            error!(
1075                event = "challenge_failure_persist_failed",
1076                outcome = "failure",
1077                challenge_id = %challenge.id,
1078                authz_id = %authz.id,
1079                order_id = %order.id,
1080                error = %error
1081            );
1082            Err(Problem::server_internal("Challenge validation failed"))
1083        }
1084    }
1085}
1086
1087/// `pub(crate)` so the sibling job suite can reuse `profile` and `account`
1088/// rather than growing a second copy of each — the rule `crates/signer/src/testutil.rs`
1089/// exists for, applied to two fixtures too entangled with this module's
1090/// `OrderService` to live there.
1091#[cfg(test)]
1092pub(crate) mod tests {
1093    use super::*;
1094    use crate::profile::ProfileParts;
1095    use acme_proxy_core::identifier::Identifier;
1096    use acme_proxy_jobs::notify::NotifyDispatcher;
1097    use acme_proxy_net::challenge::ChallengeError;
1098    use acme_proxy_net::challenge::ChallengeRegistry;
1099    use acme_proxy_net::challenge::ChallengeValidator;
1100    use std::time::Duration;
1101
1102    /// A `default` profile over `database`: an in-memory CA, no filter, no
1103    /// notifier, and `challenges` as the validators.
1104    pub(crate) fn profile(database: &Arc<Database>, challenges: ChallengeRegistry) -> Profile {
1105        let ca = acme_proxy_signer::local_ca::LocalCa::generate_in_memory(
1106            "ecdsa-p256",
1107            90,
1108            database.clone(),
1109        )
1110        .unwrap();
1111        profile_with(database, challenges, Arc::new(ca))
1112    }
1113
1114    /// [`profile`] over a signer of the caller's choosing.
1115    pub(crate) fn profile_with(
1116        database: &Arc<Database>,
1117        challenges: ChallengeRegistry,
1118        signer: Arc<dyn acme_proxy_signer::SignerBackend>,
1119    ) -> Profile {
1120        Profile::new(
1121            "default",
1122            "http://localhost:3000",
1123            ProfileParts {
1124                signer_info: signer.info(),
1125                filter: Arc::new(acme_proxy_policy::filter::FilterPolicy::default()),
1126                challenges: Arc::new(challenges),
1127                order: acme_proxy_core::config::OrderConfig::default(),
1128                eab: acme_proxy_core::config::EabConfig::default(),
1129                meta: acme_proxy_core::config::MetaConfig::default(),
1130                notify: Arc::new(NotifyDispatcher::disabled(
1131                    acme_proxy_jobs::testutil::idle_job_queue(database.clone()),
1132                )),
1133            },
1134        )
1135    }
1136
1137    /// An account whose stored key is a real SPKI, so a key authorization can
1138    /// be computed from it.
1139    pub(crate) async fn account(database: &Arc<Database>) -> Account {
1140        use rcgen::PublicKeyData;
1141        let key = rcgen::KeyPair::generate().unwrap();
1142        Account::find_or_create(
1143            "default",
1144            &key.subject_public_key_info(),
1145            vec![],
1146            &acme_proxy_core::audit::ClientContext::default(),
1147            database,
1148        )
1149        .await
1150        .unwrap()
1151        .0
1152    }
1153
1154    /// An order for `names`, one pending authorization and `http-01` challenge
1155    /// each.
1156    async fn pending_order(
1157        database: &Arc<Database>,
1158        account: &Account,
1159        names: &[&str],
1160    ) -> (Order, Vec<(Authorization, Challenge)>) {
1161        let order = Order::create(
1162            "default",
1163            account.id,
1164            acme_proxy_store::testutil::dns_identifiers(names),
1165            now_secs() + 3600,
1166            None,
1167            None,
1168            database,
1169        )
1170        .await
1171        .unwrap();
1172        let mut authzs = Vec::new();
1173        for name in names {
1174            let authz =
1175                Authorization::create(order.id, Identifier::dns(*name), order.expires, database)
1176                    .await
1177                    .unwrap();
1178            let challenge = Challenge::create(authz.id, "http-01", database)
1179                .await
1180                .unwrap();
1181            authzs.push((authz, challenge));
1182        }
1183        (order, authzs)
1184    }
1185
1186    async fn reload(database: &Database, order: &Order) -> Order {
1187        Order::find_by_id(&order.id.to_string(), database)
1188            .await
1189            .unwrap()
1190            .unwrap()
1191    }
1192
1193    async fn reload_authz(database: &Database, authz: &Authorization) -> Authorization {
1194        Authorization::find_by_id(&authz.id.to_string(), database)
1195            .await
1196            .unwrap()
1197            .unwrap()
1198    }
1199
1200    /// A validator refusing every attempt.
1201    struct Refusing;
1202
1203    #[async_trait::async_trait]
1204    impl ChallengeValidator for Refusing {
1205        fn typ(&self) -> &'static str {
1206            "http-01"
1207        }
1208        async fn validate(&self, _ctx: &ValidationContext<'_>) -> Result<(), ChallengeError> {
1209            Err(ChallengeError::IncorrectResponse("wrong body".into()))
1210        }
1211    }
1212
1213    #[tokio::test]
1214    async fn deactivating_under_a_ready_order_demotes_it_in_the_same_write() {
1215        let database = Arc::new(Database::connect_in_memory().await.unwrap());
1216        let profile = profile(&database, ChallengeRegistry::default());
1217        let audit = Auditor::offline(database.clone());
1218        let orders = OrderService {
1219            database: &database,
1220            audit: &audit,
1221            profile: &profile,
1222        };
1223        let account = account(&database).await;
1224        let (mut order, mut authzs) = pending_order(&database, &account, &["a.example.com"]).await;
1225        let (authz, challenge) = &mut authzs[0];
1226
1227        assert_eq!(
1228            orders
1229                .claim_challenge(challenge, authz, &order)
1230                .await
1231                .unwrap(),
1232            ValidationClaim::Claimed
1233        );
1234        orders
1235            .run_validation(&account, challenge, authz, &mut order, None)
1236            .await
1237            .unwrap();
1238        assert_eq!(reload(&database, &order).await.status, OrderStatus::Ready);
1239
1240        orders.deactivate_authz(authz, &mut order).await.unwrap();
1241        assert_eq!(authz.status, AuthzStatus::Deactivated);
1242        assert_eq!(reload(&database, &order).await.status, OrderStatus::Pending);
1243
1244        // A repeat is a no-op, not an error.
1245        orders.deactivate_authz(authz, &mut order).await.unwrap();
1246
1247        // And the challenge under it can no longer be triggered.
1248        let refused = orders
1249            .claim_challenge(challenge, authz, &order)
1250            .await
1251            .unwrap_err();
1252        assert_eq!(
1253            Problem::from(refused).to_value()["detail"],
1254            "Authorization has been deactivated"
1255        );
1256    }
1257
1258    #[tokio::test]
1259    async fn an_issued_order_refuses_deactivation() {
1260        let database = Arc::new(Database::connect_in_memory().await.unwrap());
1261        let profile = profile(&database, ChallengeRegistry::default());
1262        let audit = Auditor::offline(database.clone());
1263        let orders = OrderService {
1264            database: &database,
1265            audit: &audit,
1266            profile: &profile,
1267        };
1268        let account = account(&database).await;
1269        let (mut order, mut authzs) = pending_order(&database, &account, &["a.example.com"]).await;
1270        order.status = OrderStatus::Valid;
1271
1272        let refused = orders
1273            .deactivate_authz(&mut authzs[0].0, &mut order)
1274            .await
1275            .unwrap_err();
1276        assert_eq!(Problem::from(refused).status(), 400);
1277        assert_eq!(authzs[0].0.status, AuthzStatus::Pending);
1278    }
1279
1280    /// The claim is what makes "one validation per challenge" a property of the
1281    /// row: the second trigger answers without validating.
1282    #[tokio::test]
1283    async fn a_challenge_is_claimed_once() {
1284        let database = Arc::new(Database::connect_in_memory().await.unwrap());
1285        let profile = profile(&database, ChallengeRegistry::default());
1286        let audit = Auditor::offline(database.clone());
1287        let orders = OrderService {
1288            database: &database,
1289            audit: &audit,
1290            profile: &profile,
1291        };
1292        let account = account(&database).await;
1293        let (order, mut authzs) = pending_order(&database, &account, &["a.example.com"]).await;
1294        let (authz, challenge) = &mut authzs[0];
1295        let mut twin = Challenge::find_by_id(&challenge.id.to_string(), &database)
1296            .await
1297            .unwrap()
1298            .unwrap();
1299
1300        assert_eq!(
1301            orders
1302                .claim_challenge(challenge, authz, &order)
1303                .await
1304                .unwrap(),
1305            ValidationClaim::Claimed
1306        );
1307        assert_eq!(
1308            orders
1309                .claim_challenge(&mut twin, authz, &order)
1310                .await
1311                .unwrap(),
1312            ValidationClaim::Decided
1313        );
1314    }
1315
1316    /// Two authorizations of one order validated at once: whichever commits
1317    /// second reads both as `valid` inside its own transaction and promotes.
1318    #[tokio::test]
1319    async fn concurrent_validations_of_one_order_promote_it() {
1320        let database = Arc::new(Database::connect_in_memory().await.unwrap());
1321        let profile = profile(&database, ChallengeRegistry::default());
1322        let audit = Auditor::offline(database.clone());
1323        let orders = OrderService {
1324            database: &database,
1325            audit: &audit,
1326            profile: &profile,
1327        };
1328        let account = account(&database).await;
1329        let (order, authzs) =
1330            pending_order(&database, &account, &["a.example.com", "b.example.com"]).await;
1331        let mut authzs = authzs.into_iter();
1332        let (mut authz_a, mut challenge_a) = authzs.next().unwrap();
1333        let (mut authz_b, mut challenge_b) = authzs.next().unwrap();
1334        let (mut order_a, mut order_b) = (
1335            reload(&database, &order).await,
1336            reload(&database, &order).await,
1337        );
1338
1339        let a = async {
1340            assert_eq!(
1341                orders
1342                    .claim_challenge(&mut challenge_a, &authz_a, &order_a)
1343                    .await
1344                    .unwrap(),
1345                ValidationClaim::Claimed
1346            );
1347            orders
1348                .run_validation(&account, &mut challenge_a, &mut authz_a, &mut order_a, None)
1349                .await
1350                .unwrap();
1351        };
1352        let b = async {
1353            assert_eq!(
1354                orders
1355                    .claim_challenge(&mut challenge_b, &authz_b, &order_b)
1356                    .await
1357                    .unwrap(),
1358                ValidationClaim::Claimed
1359            );
1360            orders
1361                .run_validation(&account, &mut challenge_b, &mut authz_b, &mut order_b, None)
1362                .await
1363                .unwrap();
1364        };
1365        tokio::join!(a, b);
1366
1367        assert_eq!(reload(&database, &order).await.status, OrderStatus::Ready);
1368    }
1369
1370    #[tokio::test]
1371    async fn a_failed_validation_invalidates_challenge_authorization_and_order_together() {
1372        let database = Arc::new(Database::connect_in_memory().await.unwrap());
1373        let profile = profile(
1374            &database,
1375            ChallengeRegistry::new(
1376                vec![Arc::new(Refusing)],
1377                vec!["http-01".to_string()],
1378                false,
1379                Duration::from_secs(5),
1380            ),
1381        );
1382        let audit = Auditor::offline(database.clone());
1383        let orders = OrderService {
1384            database: &database,
1385            audit: &audit,
1386            profile: &profile,
1387        };
1388        let account = account(&database).await;
1389        let (mut order, mut authzs) = pending_order(&database, &account, &["a.example.com"]).await;
1390        let (authz, challenge) = &mut authzs[0];
1391
1392        assert_eq!(
1393            orders
1394                .claim_challenge(challenge, authz, &order)
1395                .await
1396                .unwrap(),
1397            ValidationClaim::Claimed
1398        );
1399        orders
1400            .run_validation(&account, challenge, authz, &mut order, None)
1401            .await
1402            .expect("a refused validation is the challenge's answer, not an error");
1403
1404        let stored = Challenge::find_by_id(&challenge.id.to_string(), &database)
1405            .await
1406            .unwrap()
1407            .unwrap();
1408        assert_eq!(stored.status, ChallengeStatus::Invalid);
1409        assert_eq!(
1410            stored.error.unwrap()["type"],
1411            "urn:ietf:params:acme:error:incorrectResponse"
1412        );
1413        let reloaded = reload(&database, &order).await;
1414        assert_eq!(reloaded.status, OrderStatus::Invalid);
1415        assert_eq!(authz.status, AuthzStatus::Invalid);
1416    }
1417
1418    /// Two spellings of one name are one identifier, not a unique-violation
1419    /// 500 on the write.
1420    #[tokio::test]
1421    async fn duplicate_identifiers_become_one() {
1422        let database = Arc::new(Database::connect_in_memory().await.unwrap());
1423        let profile = profile(&database, ChallengeRegistry::default());
1424        let audit = Auditor::offline(database.clone());
1425        let orders = OrderService {
1426            database: &database,
1427            audit: &audit,
1428            profile: &profile,
1429        };
1430        let account = account(&database).await;
1431        let pubkey = account.pubkey.clone();
1432        let payload = NewOrderPayload {
1433            identifiers: vec![
1434                Identifier::dns("A.example.com"),
1435                Identifier::dns("a.example.com."),
1436            ],
1437            ..Default::default()
1438        };
1439
1440        let (order, authz_ids) = orders
1441            .new_order(
1442                payload,
1443                Some(account),
1444                &pubkey,
1445                None,
1446                &RequestContext::default(),
1447            )
1448            .await
1449            .unwrap();
1450
1451        assert_eq!(
1452            order.identifiers,
1453            acme_proxy_store::testutil::dns_identifiers(&["a.example.com"])
1454        );
1455        assert_eq!(authz_ids.len(), 1);
1456    }
1457
1458    /// A registry that marks every challenge `valid` without a probe.
1459    fn bypassing() -> ChallengeRegistry {
1460        ChallengeRegistry::new(
1461            vec![],
1462            vec!["http-01".to_string(), "dns-01".to_string()],
1463            true,
1464            Duration::from_secs(5),
1465        )
1466    }
1467
1468    /// A registry whose only validator refuses.
1469    fn refusing() -> ChallengeRegistry {
1470        ChallengeRegistry::new(
1471            vec![Arc::new(Refusing)],
1472            vec!["http-01".to_string()],
1473            false,
1474            Duration::from_secs(5),
1475        )
1476    }
1477
1478    /// Two challenges of one authorization, both claimed before either is
1479    /// decided. The first passes and the order goes on to be issued; the second
1480    /// then fails. Its verdict lands on the challenge alone: the order holds a
1481    /// live certificate and must stay `valid`, or `Order::cleanup` would delete
1482    /// the only row that can revoke it.
1483    #[tokio::test]
1484    async fn a_late_sibling_failure_leaves_an_issued_order_valid() {
1485        let database = Arc::new(Database::connect_in_memory().await.unwrap());
1486        let passing = profile(&database, bypassing());
1487        let failing = profile(&database, refusing());
1488        let audit = Auditor::offline(database.clone());
1489        let account = account(&database).await;
1490        let (mut order, mut authzs) = pending_order(&database, &account, &["a.example.com"]).await;
1491        let (authz, http) = &mut authzs[0];
1492        let mut dns = Challenge::create(authz.id, "dns-01", &database)
1493            .await
1494            .unwrap();
1495
1496        let on = |profile| OrderService {
1497            database: &database,
1498            audit: &audit,
1499            profile,
1500        };
1501        assert_eq!(
1502            on(&passing)
1503                .claim_challenge(&mut dns, authz, &order)
1504                .await
1505                .unwrap(),
1506            ValidationClaim::Claimed
1507        );
1508        assert_eq!(
1509            on(&passing)
1510                .claim_challenge(http, authz, &order)
1511                .await
1512                .unwrap(),
1513            ValidationClaim::Claimed
1514        );
1515
1516        let mut authz_seen_by_second = reload_authz(&database, authz).await;
1517        let mut order_seen_by_second = reload(&database, &order).await;
1518        on(&passing)
1519            .run_validation(&account, &mut dns, authz, &mut order, None)
1520            .await
1521            .unwrap();
1522        assert_eq!(reload(&database, &order).await.status, OrderStatus::Ready);
1523        sqlx::query("UPDATE orders SET status = 'valid' WHERE id = ?;")
1524            .bind(order.id)
1525            .execute(database.raw_pool())
1526            .await
1527            .unwrap();
1528
1529        on(&failing)
1530            .run_validation(
1531                &account,
1532                http,
1533                &mut authz_seen_by_second,
1534                &mut order_seen_by_second,
1535                None,
1536            )
1537            .await
1538            .unwrap();
1539
1540        assert_eq!(http.status, ChallengeStatus::Invalid);
1541        assert_eq!(reload(&database, &order).await.status, OrderStatus::Valid);
1542        assert_eq!(
1543            reload_authz(&database, authz).await.status,
1544            AuthzStatus::Valid
1545        );
1546    }
1547
1548    /// A client deactivates an authorization while its challenge is being
1549    /// validated (§7.5.2). The verdict that arrives afterwards must not walk the
1550    /// authorization back to `valid`, nor promote the order.
1551    #[tokio::test]
1552    async fn a_verdict_after_deactivation_leaves_the_authorization_deactivated() {
1553        let database = Arc::new(Database::connect_in_memory().await.unwrap());
1554        let profile = profile(&database, bypassing());
1555        let audit = Auditor::offline(database.clone());
1556        let orders = OrderService {
1557            database: &database,
1558            audit: &audit,
1559            profile: &profile,
1560        };
1561        let account = account(&database).await;
1562        let (mut order, mut authzs) = pending_order(&database, &account, &["a.example.com"]).await;
1563        let (authz, challenge) = &mut authzs[0];
1564
1565        assert_eq!(
1566            orders
1567                .claim_challenge(challenge, authz, &order)
1568                .await
1569                .unwrap(),
1570            ValidationClaim::Claimed
1571        );
1572        let mut authz_seen_by_job = reload_authz(&database, authz).await;
1573        orders.deactivate_authz(authz, &mut order).await.unwrap();
1574
1575        orders
1576            .run_validation(
1577                &account,
1578                challenge,
1579                &mut authz_seen_by_job,
1580                &mut order,
1581                None,
1582            )
1583            .await
1584            .unwrap();
1585
1586        assert_eq!(
1587            reload_authz(&database, authz).await.status,
1588            AuthzStatus::Deactivated
1589        );
1590        assert_eq!(reload(&database, &order).await.status, OrderStatus::Pending);
1591    }
1592
1593    /// One account's validations are capped: the trigger over the cap is
1594    /// `429 rateLimited`, and the challenge is left `pending` so the client
1595    /// simply asks again once one of its own has settled.
1596    #[tokio::test]
1597    async fn an_account_over_its_validation_cap_is_rate_limited() {
1598        let database = Arc::new(Database::connect_in_memory().await.unwrap());
1599        let profile = profile(&database, bypassing().with_max_in_flight_per_account(1));
1600        let audit = Auditor::offline(database.clone());
1601        let orders = OrderService {
1602            database: &database,
1603            audit: &audit,
1604            profile: &profile,
1605        };
1606        let account = account(&database).await;
1607        let (first_order, mut first) = pending_order(&database, &account, &["a.example.com"]).await;
1608        let (second_order, mut second) =
1609            pending_order(&database, &account, &["b.example.com"]).await;
1610        let (first_authz, first_challenge) = &mut first[0];
1611        let (second_authz, second_challenge) = &mut second[0];
1612
1613        assert_eq!(
1614            orders
1615                .claim_challenge(first_challenge, first_authz, &first_order)
1616                .await
1617                .unwrap(),
1618            ValidationClaim::Claimed
1619        );
1620        assert_eq!(
1621            orders
1622                .claim_challenge(second_challenge, second_authz, &second_order)
1623                .await
1624                .unwrap(),
1625            ValidationClaim::Limited
1626        );
1627        assert_eq!(second_challenge.status, ChallengeStatus::Pending);
1628
1629        // The first settles, and the second is claimable again.
1630        let mut order = reload(&database, &first_order).await;
1631        orders
1632            .run_validation(&account, first_challenge, first_authz, &mut order, None)
1633            .await
1634            .unwrap();
1635        assert_eq!(
1636            orders
1637                .claim_challenge(second_challenge, second_authz, &second_order)
1638                .await
1639                .unwrap(),
1640            ValidationClaim::Claimed
1641        );
1642    }
1643
1644    /// Once one authorization has failed, its order is `invalid`, and a trigger
1645    /// of a challenge under a sibling authorization is refused rather than
1646    /// probing the client's host for nothing.
1647    #[tokio::test]
1648    async fn a_trigger_under_an_invalid_order_is_refused() {
1649        let database = Arc::new(Database::connect_in_memory().await.unwrap());
1650        let profile = profile(&database, refusing());
1651        let audit = Auditor::offline(database.clone());
1652        let orders = OrderService {
1653            database: &database,
1654            audit: &audit,
1655            profile: &profile,
1656        };
1657        let account = account(&database).await;
1658        let (mut order, mut authzs) =
1659            pending_order(&database, &account, &["a.example.com", "b.example.com"]).await;
1660        let (first, rest) = authzs.split_at_mut(1);
1661        let (authz_a, challenge_a) = &mut first[0];
1662        let (authz_b, challenge_b) = &mut rest[0];
1663
1664        assert_eq!(
1665            orders
1666                .claim_challenge(challenge_a, authz_a, &order)
1667                .await
1668                .unwrap(),
1669            ValidationClaim::Claimed
1670        );
1671        orders
1672            .run_validation(&account, challenge_a, authz_a, &mut order, None)
1673            .await
1674            .unwrap();
1675        assert_eq!(order.status, OrderStatus::Invalid);
1676
1677        let refused = orders
1678            .claim_challenge(challenge_b, authz_b, &order)
1679            .await
1680            .unwrap_err();
1681        assert_eq!(Problem::from(refused).status(), 400);
1682
1683        // And the row-level guard holds on its own, for a caller that read the
1684        // order before it failed.
1685        assert_eq!(
1686            challenge_b
1687                .claim_for_validation(0, &database)
1688                .await
1689                .unwrap(),
1690            ValidationClaim::Decided
1691        );
1692    }
1693
1694    /// A trigger whose reads straddle its **own** verdict is not a sibling
1695    /// failure, and is answered with the challenge (§7.5.1) rather than a `400`.
1696    ///
1697    /// `load_owned_challenge` reads the challenge, the authorization and the
1698    /// order one statement at a time, while the verdict writes all three in one
1699    /// transaction. A request that reads the challenge before that commit and
1700    /// the authorization after it holds an undecided challenge under an
1701    /// `invalid` authorization — the pair reproduced here without any timing, by
1702    /// reading the challenge back while it is still `processing` and claiming
1703    /// with it once the verdict has landed.
1704    #[tokio::test]
1705    async fn a_trigger_that_straddles_its_own_verdict_is_answered_with_the_challenge() {
1706        let database = Arc::new(Database::connect_in_memory().await.unwrap());
1707        let profile = profile(&database, refusing());
1708        let audit = Auditor::offline(database.clone());
1709        let orders = OrderService {
1710            database: &database,
1711            audit: &audit,
1712            profile: &profile,
1713        };
1714        let account = account(&database).await;
1715        let (mut order, mut authzs) = pending_order(&database, &account, &["a.example.com"]).await;
1716        let (authz, challenge) = &mut authzs[0];
1717
1718        assert_eq!(
1719            orders
1720                .claim_challenge(challenge, authz, &order)
1721                .await
1722                .unwrap(),
1723            ValidationClaim::Claimed
1724        );
1725
1726        // The early read: the challenge as the claim left it, which is what a
1727        // concurrent trigger carries into `claim_challenge`.
1728        let mut stale = Challenge::find_by_id(challenge.id.to_string().as_str(), &database)
1729            .await
1730            .unwrap()
1731            .unwrap();
1732        assert_eq!(stale.status, ChallengeStatus::Processing);
1733
1734        orders
1735            .run_validation(&account, challenge, authz, &mut order, None)
1736            .await
1737            .unwrap();
1738        assert_eq!(authz.status, AuthzStatus::Invalid);
1739        assert_eq!(order.status, OrderStatus::Invalid);
1740
1741        // The late read of the authorization now refuses nothing: the second
1742        // look at the challenge finds the verdict and returns it.
1743        assert_eq!(
1744            orders
1745                .claim_challenge(&mut stale, authz, &order)
1746                .await
1747                .unwrap(),
1748            ValidationClaim::Decided
1749        );
1750        assert_eq!(stale.status, ChallengeStatus::Invalid);
1751        assert!(
1752            stale.error.is_some(),
1753            "the refreshed challenge carries the verdict the client came for"
1754        );
1755    }
1756
1757    /// The unique-violation arm in `new_order` only fires when two
1758    /// newOrder requests race — `check_replaces` and the partial index share a
1759    /// predicate, so nothing but real concurrency can make them disagree. This
1760    /// drives the matcher against errors the database actually produces, which
1761    /// is the part that can silently rot: SQLite names the offending *columns*,
1762    /// not the index, so a matcher written against the index name would fall
1763    /// through to a 500 and no test of the happy path would notice.
1764    #[tokio::test]
1765    async fn a_replaces_collision_is_told_apart_from_other_unique_violations() {
1766        let database = Database::connect_in_memory().await.unwrap();
1767        sqlx::query(
1768            "INSERT INTO accounts (id, profile, pubkey, contact, status, created_at) \
1769             VALUES ('acct', 'default', X'00', '[]', 'valid', 0);",
1770        )
1771        .execute(database.raw_pool())
1772        .await
1773        .unwrap();
1774
1775        let order = |id: &'static str, replaces: &'static str| {
1776            let pool = database.raw_pool().clone();
1777            async move {
1778                sqlx::query(
1779                    "INSERT INTO orders (id, profile, account_id, status, identifiers, expires, \
1780                     replaces, created_at) VALUES (?, 'default', 'acct', 'pending', '[]', 0, ?, 0);",
1781                )
1782                .bind(id)
1783                .bind(replaces)
1784                .execute(&pool)
1785                .await
1786            }
1787        };
1788
1789        order("first", "predecessor-cert-id").await.unwrap();
1790        let collision = order("second", "predecessor-cert-id").await.unwrap_err();
1791        assert!(is_replaces_conflict(&collision), "got {collision}");
1792
1793        // The same transaction inserts authorizations under their own
1794        // `UNIQUE(order_id, identifier)`. That must not be reported to a client
1795        // as `alreadyReplaced`.
1796        let authz = |id: &'static str| {
1797            let pool = database.raw_pool().clone();
1798            async move {
1799                sqlx::query(
1800                    "INSERT INTO authorizations (id, order_id, identifier, status, expires, \
1801                     created_at) VALUES (?, 'first', '{\"type\":\"dns\",\"value\":\"a.example.com\"}', \
1802                     'pending', 0, 0);",
1803                )
1804                .bind(id)
1805                .execute(&pool)
1806                .await
1807            }
1808        };
1809        authz("authz-one").await.unwrap();
1810        let other = authz("authz-two").await.unwrap_err();
1811        assert!(
1812            !is_replaces_conflict(&other),
1813            "an authorization collision must not read as alreadyReplaced: {other}"
1814        );
1815
1816        // And an unrelated failure is not swept in either.
1817        let missing = sqlx::query("INSERT INTO orders (id) VALUES ('x');")
1818            .execute(database.raw_pool())
1819            .await
1820            .unwrap_err();
1821        assert!(!is_replaces_conflict(&missing));
1822    }
1823
1824    /// A `ready` order for `a.example.com` plus a CSR matching it, base64url.
1825    pub(crate) async fn ready_order(
1826        database: &Arc<Database>,
1827        account: &Account,
1828    ) -> (Order, String) {
1829        let (order, authzs) = pending_order(database, account, &["a.example.com"]).await;
1830        for (authz, _) in &authzs {
1831            assert!(
1832                Authorization::set_valid(authz.id, database.raw_pool())
1833                    .await
1834                    .unwrap()
1835            );
1836        }
1837        assert!(
1838            Order::set_ready(order.id, database.raw_pool())
1839                .await
1840                .unwrap()
1841        );
1842        let key = rcgen::KeyPair::generate().unwrap();
1843        let csr = rcgen::CertificateParams::new(vec!["a.example.com".to_string()])
1844            .unwrap()
1845            .serialize_request(&key)
1846            .unwrap();
1847        (
1848            reload(database, &order).await,
1849            BASE64_URL_SAFE_NO_PAD.encode(csr.der()),
1850        )
1851    }
1852
1853    /// Finalizes a fresh `ready` order, returning the database, the order as
1854    /// it was, and what `finalize` answered.
1855    async fn finalize_ready() -> (Arc<Database>, Order, Result<Order, Error>) {
1856        let database = Arc::new(Database::connect_in_memory().await.unwrap());
1857        let profile = profile(&database, ChallengeRegistry::default());
1858        let audit = Auditor::offline(database.clone());
1859        let orders = OrderService {
1860            database: &database,
1861            audit: &audit,
1862            profile: &profile,
1863        };
1864        let account = account(&database).await;
1865        let (order, csr) = ready_order(&database, &account).await;
1866        let before = reload(&database, &order).await;
1867        let jobs = acme_proxy_jobs::testutil::idle_job_queue(database.clone());
1868        let outcome = orders
1869            .finalize(
1870                &account,
1871                order,
1872                &csr,
1873                None,
1874                &RequestContext::default(),
1875                &jobs,
1876            )
1877            .await;
1878        (database, before, outcome)
1879    }
1880
1881    /// Finalize signs nothing: it claims the order and queues its issuance in
1882    /// one write, answering `processing` — the process answering ACME holds no
1883    /// backend.
1884    #[tokio::test]
1885    async fn finalize_claims_the_order_and_queues_its_issuance() {
1886        let (database, order, outcome) = finalize_ready().await;
1887        let answered = outcome.unwrap();
1888        assert_eq!(answered.status, OrderStatus::Processing);
1889        let stored = reload(&database, &order).await;
1890        assert_eq!(stored.status, OrderStatus::Processing);
1891        assert!(stored.certificate.is_none(), "nothing was signed here");
1892
1893        let job = acme_proxy_store::job::Job::find_live(
1894            super::super::issue::SIGNER_ISSUE_KIND,
1895            &order.id.to_string(),
1896            &database,
1897        )
1898        .await
1899        .unwrap()
1900        .expect("the issuance is queued");
1901        assert_eq!(job.payload["order_id"], order.id.to_string());
1902        assert_eq!(job.payload["profile"], "default");
1903        assert!(
1904            job.payload["csr"]
1905                .as_str()
1906                .is_some_and(|csr| !csr.is_empty())
1907        );
1908        assert_eq!(job.deadline, Some(order.expires));
1909    }
1910
1911    /// Two finalizes racing on one order: the loser's claim fails, it is told
1912    /// §7.4's `orderNotReady`, and only one issuance is queued.
1913    #[tokio::test]
1914    async fn a_second_finalize_loses_the_claim() {
1915        let database = Arc::new(Database::connect_in_memory().await.unwrap());
1916        let profile = profile(&database, ChallengeRegistry::default());
1917        let audit = Auditor::offline(database.clone());
1918        let orders = OrderService {
1919            database: &database,
1920            audit: &audit,
1921            profile: &profile,
1922        };
1923        let account = account(&database).await;
1924        let (order, csr) = ready_order(&database, &account).await;
1925        let jobs = acme_proxy_jobs::testutil::idle_job_queue(database.clone());
1926
1927        // Both requests read the order `ready`.
1928        let rival = reload(&database, &order).await;
1929        orders
1930            .finalize(
1931                &account,
1932                order,
1933                &csr,
1934                None,
1935                &RequestContext::default(),
1936                &jobs,
1937            )
1938            .await
1939            .unwrap();
1940        let error = orders
1941            .finalize(
1942                &account,
1943                rival,
1944                &csr,
1945                None,
1946                &RequestContext::default(),
1947                &jobs,
1948            )
1949            .await
1950            .unwrap_err();
1951        let problem = Problem::from(error).to_value();
1952        assert_eq!(problem["type"], "urn:ietf:params:acme:error:orderNotReady");
1953        assert_eq!(problem["detail"], "Order is already being finalized");
1954    }
1955
1956    /// The claim and the job are one write: if the job cannot be queued, the
1957    /// order is not claimed either, so it is never `processing` with nothing
1958    /// coming for it.
1959    #[tokio::test]
1960    async fn a_failed_enqueue_leaves_the_order_ready() {
1961        let database = Arc::new(Database::connect_in_memory().await.unwrap());
1962        let profile = profile(&database, ChallengeRegistry::default());
1963        let audit = Auditor::offline(database.clone());
1964        let orders = OrderService {
1965            database: &database,
1966            audit: &audit,
1967            profile: &profile,
1968        };
1969        let account = account(&database).await;
1970        let (order, csr) = ready_order(&database, &account).await;
1971        let jobs = acme_proxy_jobs::testutil::idle_job_queue(database.clone());
1972        sqlx::query("DROP TABLE jobs;")
1973            .execute(database.raw_pool())
1974            .await
1975            .unwrap();
1976
1977        let before = reload(&database, &order).await;
1978        let error = orders
1979            .finalize(
1980                &account,
1981                order,
1982                &csr,
1983                None,
1984                &RequestContext::default(),
1985                &jobs,
1986            )
1987            .await
1988            .unwrap_err();
1989        assert_eq!(
1990            Problem::from(error).to_value()["detail"],
1991            "Order finalize failed"
1992        );
1993        assert_eq!(reload(&database, &before).await.status, OrderStatus::Ready);
1994    }
1995
1996    /// [`challenge_can_be_triggered`] against the claim it mirrors: true
1997    /// exactly where a trigger would claim the challenge, for every state the
1998    /// claim refuses or reads as decided.
1999    #[tokio::test]
2000    async fn the_trigger_predicate_agrees_with_the_claim() {
2001        let database = Arc::new(Database::connect_in_memory().await.unwrap());
2002        let profile = profile(&database, ChallengeRegistry::default());
2003        let audit = Auditor::offline(database.clone());
2004        let orders = OrderService {
2005            database: &database,
2006            audit: &audit,
2007            profile: &profile,
2008        };
2009        let account = account(&database).await;
2010        type Mutation = fn(&mut Authorization, &mut Order);
2011        let cases: [(&str, Mutation); 6] = [
2012            ("pending", |_, _| {}),
2013            ("expired", |authz, _| authz.expires = now_secs() - 1),
2014            ("deactivated", |authz, _| {
2015                authz.status = AuthzStatus::Deactivated
2016            }),
2017            ("invalid authorization", |authz, _| {
2018                authz.status = AuthzStatus::Invalid
2019            }),
2020            ("invalid order", |_, order| {
2021                order.status = OrderStatus::Invalid
2022            }),
2023            ("valid authorization", |authz, _| {
2024                authz.status = AuthzStatus::Valid
2025            }),
2026        ];
2027        for (case, mutate) in cases {
2028            let (mut order, mut authzs) =
2029                pending_order(&database, &account, &["example.com"]).await;
2030            let (mut authz, mut challenge) = authzs.remove(0);
2031            mutate(&mut authz, &mut order);
2032            let predicted = challenge_can_be_triggered(&authz, &order, now_secs());
2033            let claimed = matches!(
2034                orders.claim_challenge(&mut challenge, &authz, &order).await,
2035                Ok(ValidationClaim::Claimed)
2036            );
2037            assert_eq!(predicted, claimed, "{case}");
2038            assert_eq!(predicted, case == "pending", "{case}");
2039        }
2040    }
2041}