Skip to main content

acme_proxy/sqlite/
authz.rs

1use serde_json::Value;
2use sqlx::Row;
3use sqlx::sqlite::SqliteRow;
4use tracing::{debug, info};
5use uuid::Uuid;
6
7use crate::random::random_token;
8use crate::sqlite::db::Database;
9use crate::sqlite::nonce::now_secs;
10use crate::sqlite::order::{Identifier, rfc3339};
11use crate::sqlite::status::{self, AuthzStatus, ChallengeStatus};
12
13/// An ACME authorization (RFC 8555 §7.1.4). One authorization is created per
14/// order identifier when the order is created, starting in the `pending` state
15/// and carrying the challenges the client can satisfy to prove control of the
16/// identifier.
17///
18/// ## Storage Details
19///
20/// - `identifier` is persisted as a JSON `{type, value}` object.
21/// - Timestamps are epoch seconds (matching orders/accounts/nonces) and rendered
22///   as RFC3339 strings in [`Authorization::to_json`].
23/// - The authorization URL is **derived** from the id + base URL (like the
24///   order's `finalize`/`certificate` URLs), never stored.
25///
26/// ## Wildcards
27///
28/// A wildcard authorization stores its identifier in the **wildcard form**
29/// (`*.example.com`), while the ACME object shows the base name plus a separate
30/// `"wildcard": true` member (RFC 8555 §7.1.4). Storing the base name instead
31/// would collide: the canonical wildcard order `["example.com", "*.example.com"]`
32/// creates two authorizations, and `UNIQUE(order_id, identifier)` compares the
33/// serialized JSON — both rows would be identical and the order would fail to
34/// persist. Deriving with [`Authorization::base_identifier`] costs a
35/// `strip_prefix` and no migration.
36#[derive(Debug)]
37pub struct Authorization {
38    pub id: Uuid,
39    pub order_id: Uuid,
40    pub identifier: Identifier,
41    pub status: AuthzStatus,
42    pub expires: i64,
43    pub created_at: i64,
44}
45
46/// An ACME challenge (RFC 8555 §8): one proof the client may offer for its
47/// authorization's identifier.
48///
49/// Which types an authorization carries is decided by
50/// [`ChallengeRegistry::types_for`](crate::challenge::ChallengeRegistry::types_for)
51/// — `UNIQUE(authz_id, type)` allows several, one per type. Whether triggering
52/// one performs a real network check or is accepted outright is the registry's
53/// `bypass` setting, not this model's business.
54///
55/// A failed challenge stores the problem document that explains why in `error`,
56/// which the client reads back from the challenge object.
57#[derive(Debug)]
58pub struct Challenge {
59    pub id: Uuid,
60    pub authz_id: Uuid,
61    pub typ: String,
62    pub token: String,
63    pub status: ChallengeStatus,
64    pub validated: Option<i64>,
65    /// The RFC 8555 problem document of a failed validation. `None` until one
66    /// fails.
67    pub error: Option<Value>,
68    pub created_at: i64,
69}
70
71/// Every column of `authorizations`, in one place: each read must select the same set
72/// or `from_row` fails on whichever forgot one.
73///
74/// A `macro_rules!` rather than a `const` so the expansion is a string
75/// *literal*, which is what `sqlx::query`'s `SqlSafeStr` bound requires.
76macro_rules! authz_columns {
77    () => {
78        "id, order_id, identifier, status, expires, created_at"
79    };
80}
81
82impl Authorization {
83    fn from_row(row: SqliteRow) -> Result<Self, sqlx::Error> {
84        let identifier_json: String = row.try_get("identifier")?;
85        let identifier: Identifier =
86            serde_json::from_str(&identifier_json).map_err(|e| sqlx::Error::Decode(Box::new(e)))?;
87
88        Ok(Authorization {
89            id: row.try_get("id")?,
90            order_id: row.try_get("order_id")?,
91            identifier,
92            status: status::from_column(row.try_get::<&str, _>("status")?)?,
93            expires: row.try_get("expires")?,
94            created_at: row.try_get("created_at")?,
95        })
96    }
97
98    /// Builds a new `pending` authorization for `identifier`. Pure — nothing is
99    /// persisted until [`Authorization::insert`] runs.
100    pub(crate) fn new(order_id: Uuid, identifier: Identifier, expires: i64) -> Authorization {
101        Authorization {
102            id: crate::sqlite::id::mint(),
103            order_id,
104            identifier,
105            status: AuthzStatus::Pending,
106            expires,
107            created_at: now_secs(),
108        }
109    }
110
111    /// Inserts the authorization using any executor — a pool, or a transaction
112    /// (see [`crate::sqlite::order::Order::insert`] for why that matters).
113    pub(crate) async fn insert<'e, E>(&self, executor: E) -> Result<(), sqlx::Error>
114    where
115        E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
116    {
117        // `Identifier` derives `Serialize`, so this never fails in practice.
118        let identifier_json = serde_json::to_string(&self.identifier)
119            .map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
120
121        debug!(event = "db_authz_create_started", outcome = "progress", authz_id = ?self.id, order_id = ?self.order_id);
122        sqlx::query(
123            "INSERT INTO authorizations (id, order_id, identifier, status, expires, created_at) \
124             VALUES (?, ?, ?, ?, ?, ?);",
125        )
126        .bind(self.id)
127        .bind(self.order_id)
128        .bind(identifier_json)
129        .bind(self.status.as_str())
130        .bind(self.expires)
131        .bind(self.created_at)
132        .execute(executor)
133        .await?;
134
135        info!(event = "db_authz_created", outcome = "success", authz_id = ?self.id, order_id = ?self.order_id);
136        Ok(())
137    }
138
139    /// Creates a new authorization for `identifier` in the `pending` state.
140    pub async fn create(
141        order_id: Uuid,
142        identifier: Identifier,
143        expires: i64,
144        database: &Database,
145    ) -> Result<Authorization, sqlx::Error> {
146        let authz = Authorization::new(order_id, identifier, expires);
147        authz.insert(&database.pool).await?;
148        Ok(authz)
149    }
150
151    pub async fn find_by_id(
152        id: &str,
153        database: &Database,
154    ) -> Result<Option<Authorization>, sqlx::Error> {
155        debug!(event = "db_authz_find_by_id_started", outcome = "progress", authz_id = ?id);
156        let Some(id) = crate::sqlite::id::parse(id) else {
157            return Ok(None);
158        };
159        let row = sqlx::query(concat!(
160            "SELECT ",
161            authz_columns!(),
162            " FROM authorizations WHERE id = ?;"
163        ))
164        .bind(id)
165        .fetch_optional(&database.pool)
166        .await?;
167
168        row.map(Authorization::from_row).transpose()
169    }
170
171    /// Lists an order's authorizations, oldest first (creation order), for the
172    /// order object's `authorizations` array and the all-valid readiness check.
173    pub async fn find_by_order(
174        order_id: Uuid,
175        database: &Database,
176    ) -> Result<Vec<Authorization>, sqlx::Error> {
177        Self::find_by_order_with(order_id, &database.pool).await
178    }
179
180    /// How many authorizations an order has. [`crate::sqlite::order::Order::count_by_account`]'s
181    /// counterpart, and for the same reason.
182    pub async fn count_by_order(order_id: Uuid, database: &Database) -> Result<i64, sqlx::Error> {
183        let row = sqlx::query("SELECT COUNT(*) FROM authorizations WHERE order_id = ?;")
184            .bind(order_id)
185            .fetch_one(&database.pool)
186            .await?;
187        row.try_get::<i64, _>(0)
188    }
189
190    /// The authorization ids of several orders at once, keyed by order id.
191    ///
192    /// The listing paths need nothing but the ids — `Order::to_json` builds its
193    /// `authorizations` URLs from them — and were calling
194    /// [`Authorization::find_by_order`] once per row: 51 queries for a default
195    /// page of 50. One `IN (…)` instead.
196    ///
197    /// An order with no authorizations is simply absent from the map, which is
198    /// what a caller wants: `map.remove(id).unwrap_or_default()`.
199    pub async fn find_ids_by_orders(
200        order_ids: &[Uuid],
201        database: &Database,
202    ) -> Result<std::collections::HashMap<Uuid, Vec<Uuid>>, sqlx::Error> {
203        let mut grouped: std::collections::HashMap<Uuid, Vec<Uuid>> =
204            std::collections::HashMap::new();
205        if order_ids.is_empty() {
206            return Ok(grouped);
207        }
208
209        // `QueryBuilder` rather than a formatted `IN` list: `push_bind` is what
210        // keeps the ids parameters instead of interpolated SQL, the same rule
211        // `OrderQuery::push_predicates` follows.
212        let mut builder =
213            sqlx::QueryBuilder::new("SELECT id, order_id FROM authorizations WHERE order_id IN (");
214        let mut separated = builder.separated(", ");
215        for id in order_ids {
216            separated.push_bind(*id);
217        }
218        builder.push(") ORDER BY created_at ASC;");
219
220        debug!(
221            event = "db_authz_find_ids_by_orders",
222            outcome = "success",
223            orders = order_ids.len()
224        );
225        for row in builder.build().fetch_all(&database.pool).await? {
226            let order_id: Uuid = row.try_get("order_id")?;
227            let id: Uuid = row.try_get("id")?;
228            grouped.entry(order_id).or_default().push(id);
229        }
230        Ok(grouped)
231    }
232
233    /// [`Authorization::find_by_order`] over any executor — a pool, or a
234    /// transaction.
235    ///
236    /// Reading inside the transaction that just wrote is what makes
237    /// "is every authorization of this order valid now?" answerable at all: from
238    /// the pool, two concurrent validations of two authorizations of one order
239    /// can each read before the other's write commits, so neither sees a
240    /// complete set and neither promotes the order.
241    pub(crate) async fn find_by_order_with<'e, E>(
242        order_id: Uuid,
243        executor: E,
244    ) -> Result<Vec<Authorization>, sqlx::Error>
245    where
246        E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
247    {
248        debug!(event = "db_authz_find_by_order_started", outcome = "progress", order_id = ?order_id);
249        let rows = sqlx::query(concat!(
250            "SELECT ",
251            authz_columns!(),
252            " FROM authorizations WHERE order_id = ? ORDER BY created_at ASC;"
253        ))
254        .bind(order_id)
255        .fetch_all(executor)
256        .await?;
257
258        rows.into_iter().map(Authorization::from_row).collect()
259    }
260
261    /// The `valid` transition as a bare statement, over any executor.
262    ///
263    /// Split from [`Authorization::mark_valid`] so `post_challenge` can compose
264    /// the challenge, authorization and order transitions into one transaction.
265    /// The in-memory sync stays in `mark_valid`: it must not happen until the
266    /// transaction has committed, or a rollback leaves the object claiming a
267    /// status the database never took.
268    pub(crate) async fn set_valid<'e, E>(id: Uuid, executor: E) -> Result<(), sqlx::Error>
269    where
270        E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
271    {
272        sqlx::query("UPDATE authorizations SET status = 'valid' WHERE id = ?;")
273            .bind(id)
274            .execute(executor)
275            .await?;
276        Ok(())
277    }
278
279    /// The `invalid` transition as a bare statement; see [`Authorization::set_valid`].
280    pub(crate) async fn set_invalid<'e, E>(id: Uuid, executor: E) -> Result<(), sqlx::Error>
281    where
282        E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
283    {
284        sqlx::query("UPDATE authorizations SET status = 'invalid' WHERE id = ?;")
285            .bind(id)
286            .execute(executor)
287            .await?;
288        Ok(())
289    }
290
291    /// The `deactivated` transition as a bare statement; see [`Authorization::set_valid`].
292    ///
293    /// The terminal state a client asks for itself (RFC 8555 §7.5.2). Like
294    /// [`Authorization::mark_invalid`] it does not stamp `validated`: §8 defines
295    /// that as the time of a *successful* validation, and relinquishing an
296    /// authorization is the opposite.
297    ///
298    /// Bare-statement only: §7.5.2's deactivate-and-demote pair is committed in
299    /// one transaction (`handlers::authz`), so there is no persist-and-sync twin
300    /// to go with it.
301    pub(crate) async fn set_deactivated<'e, E>(id: Uuid, executor: E) -> Result<(), sqlx::Error>
302    where
303        E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
304    {
305        sqlx::query("UPDATE authorizations SET status = 'deactivated' WHERE id = ?;")
306            .bind(id)
307            .execute(executor)
308            .await?;
309        Ok(())
310    }
311
312    /// Moves the authorization to the `valid` state and keeps `self` in sync (the
313    /// same persist-and-sync pattern as [`crate::sqlite::order::Order::finalize`]).
314    pub async fn mark_valid(&mut self, database: &Database) -> Result<(), sqlx::Error> {
315        debug!(event = "db_authz_mark_valid_started", outcome = "progress", authz_id = ?self.id);
316        Self::set_valid(self.id, &database.pool).await?;
317
318        self.status = AuthzStatus::Valid;
319        info!(event = "db_authz_marked_valid", outcome = "success", authz_id = ?self.id);
320        Ok(())
321    }
322
323    /// Moves the authorization to the terminal `invalid` state, after one of its
324    /// challenges failed validation (RFC 8555 §7.1.6).
325    ///
326    /// No `error` is stored: the RFC puts the problem document on the
327    /// *challenge*, and the authorization object has no `error` member — a client
328    /// reads the reason from the challenge it triggered.
329    pub async fn mark_invalid(&mut self, database: &Database) -> Result<(), sqlx::Error> {
330        debug!(event = "db_authz_mark_invalid_started", outcome = "progress", authz_id = ?self.id);
331        Self::set_invalid(self.id, &database.pool).await?;
332
333        self.status = AuthzStatus::Invalid;
334        info!(event = "db_authz_marked_invalid", outcome = "failure", authz_id = ?self.id);
335        Ok(())
336    }
337
338    /// Whether this authorization covers the wildcard of its identifier.
339    ///
340    /// Derived from the stored value rather than stored separately — see the
341    /// type's doc comment for why the row keeps the `*.` prefix.
342    #[must_use]
343    pub fn is_wildcard(&self) -> bool {
344        self.identifier.value.starts_with("*.")
345    }
346
347    /// The name to actually prove control of: the identifier with any `*.`
348    /// stripped.
349    ///
350    /// A wildcard is proved by controlling the zone, so the DNS record lives at
351    /// `_acme-challenge.example.com`, not at `_acme-challenge.*.example.com`.
352    #[must_use]
353    pub fn base_identifier(&self) -> &str {
354        self.identifier
355            .value
356            .strip_prefix("*.")
357            .unwrap_or(&self.identifier.value)
358    }
359
360    /// The RFC 8555 authorization object: `identifier`, `status`, `expires`
361    /// (RFC3339), the `challenges` array (each rendered by
362    /// [`Challenge::to_json`]), and `wildcard` when the authorization covers one.
363    #[must_use]
364    pub fn to_json(&self, base_url: &str, challenges: &[Challenge]) -> Value {
365        let mut object = serde_json::Map::new();
366        // RFC 8555 §7.1.4: the identifier of a wildcard authorization is the
367        // *base* name, with the wildcard signalled by its own member. The stored
368        // row keeps the `*.` prefix; only this rendering strips it.
369        object.insert(
370            "identifier".to_string(),
371            serde_json::to_value(Identifier::new(
372                self.identifier.typ.clone(),
373                self.base_identifier().to_string(),
374            ))
375            .expect("Identifier is always serializable"),
376        );
377        object.insert(
378            "status".to_string(),
379            Value::String(self.status.as_str().to_string()),
380        );
381        object.insert("expires".to_string(), Value::String(rfc3339(self.expires)));
382        let challenges: Vec<Value> = challenges.iter().map(|c| c.to_json(base_url)).collect();
383        object.insert("challenges".to_string(), Value::Array(challenges));
384        if self.is_wildcard() {
385            object.insert("wildcard".to_string(), Value::Bool(true));
386        }
387        Value::Object(object)
388    }
389}
390
391/// Every column of `challenges`, in one place: each read must select the same set
392/// or `from_row` fails on whichever forgot one.
393///
394/// A `macro_rules!` rather than a `const` so the expansion is a string
395/// *literal*, which is what `sqlx::query`'s `SqlSafeStr` bound requires.
396macro_rules! challenge_columns {
397    () => {
398        "id, authz_id, type, token, status, validated, error, created_at"
399    };
400}
401
402impl Challenge {
403    fn from_row(row: SqliteRow) -> Result<Self, sqlx::Error> {
404        let error_json: Option<String> = row.try_get("error")?;
405        let error = error_json
406            .map(|json| serde_json::from_str(&json))
407            .transpose()
408            .map_err(|e| sqlx::Error::Decode(Box::new(e)))?;
409
410        Ok(Challenge {
411            id: row.try_get("id")?,
412            authz_id: row.try_get("authz_id")?,
413            typ: row.try_get("type")?,
414            token: row.try_get("token")?,
415            status: status::from_column(row.try_get::<&str, _>("status")?)?,
416            validated: row.try_get("validated")?,
417            error,
418            created_at: row.try_get("created_at")?,
419        })
420    }
421
422    /// Builds a new `pending` challenge of type `typ` with a fresh random token.
423    /// Pure — nothing is persisted until [`Challenge::insert`] runs.
424    ///
425    /// **Each challenge gets its own token**, even when several are offered for
426    /// one authorization: RFC 8555 §8 describes the token as a per-challenge
427    /// value, and every key authorization derives from it.
428    pub(crate) fn new(authz_id: Uuid, typ: &str) -> Challenge {
429        Challenge {
430            id: crate::sqlite::id::mint(),
431            authz_id,
432            typ: typ.to_string(),
433            token: random_token(),
434            status: ChallengeStatus::Pending,
435            validated: None,
436            error: None,
437            created_at: now_secs(),
438        }
439    }
440
441    /// Inserts the challenge using any executor — a pool, or a transaction
442    /// (see [`crate::sqlite::order::Order::insert`] for why that matters).
443    pub(crate) async fn insert<'e, E>(&self, executor: E) -> Result<(), sqlx::Error>
444    where
445        E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
446    {
447        debug!(event = "db_challenge_create_started", outcome = "progress", challenge_id = ?self.id, authz_id = ?self.authz_id);
448        sqlx::query(
449            "INSERT INTO challenges (id, authz_id, type, token, status, validated, created_at) \
450             VALUES (?, ?, ?, ?, ?, NULL, ?);",
451        )
452        .bind(self.id)
453        .bind(self.authz_id)
454        .bind(&self.typ)
455        .bind(&self.token)
456        .bind(self.status.as_str())
457        .bind(self.created_at)
458        .execute(executor)
459        .await?;
460
461        info!(event = "db_challenge_created", outcome = "success", challenge_id = ?self.id, authz_id = ?self.authz_id);
462        Ok(())
463    }
464
465    /// Creates a new challenge of type `typ`, with a fresh random token, in the
466    /// `pending` state.
467    pub async fn create(
468        authz_id: Uuid,
469        typ: &str,
470        database: &Database,
471    ) -> Result<Challenge, sqlx::Error> {
472        let challenge = Challenge::new(authz_id, typ);
473        challenge.insert(&database.pool).await?;
474        Ok(challenge)
475    }
476
477    pub async fn find_by_id(
478        id: &str,
479        database: &Database,
480    ) -> Result<Option<Challenge>, sqlx::Error> {
481        debug!(event = "db_challenge_find_by_id_started", outcome = "progress", challenge_id = ?id);
482        let Some(id) = crate::sqlite::id::parse(id) else {
483            return Ok(None);
484        };
485        let row = sqlx::query(concat!(
486            "SELECT ",
487            challenge_columns!(),
488            " FROM challenges WHERE id = ?;"
489        ))
490        .bind(id)
491        .fetch_optional(&database.pool)
492        .await?;
493
494        row.map(Challenge::from_row).transpose()
495    }
496
497    /// Lists an authorization's challenges (creation order), for the
498    /// authorization object's `challenges` array.
499    pub async fn find_by_authz(
500        authz_id: Uuid,
501        database: &Database,
502    ) -> Result<Vec<Challenge>, sqlx::Error> {
503        debug!(event = "db_challenge_find_by_authz_started", outcome = "progress", authz_id = ?authz_id);
504        let rows = sqlx::query(concat!(
505            "SELECT ",
506            challenge_columns!(),
507            " FROM challenges WHERE authz_id = ? ORDER BY created_at ASC;"
508        ))
509        .bind(authz_id)
510        .fetch_all(&database.pool)
511        .await?;
512
513        rows.into_iter().map(Challenge::from_row).collect()
514    }
515
516    /// Takes this challenge for validation, moving `pending` to `processing`.
517    ///
518    /// Returns whether the claim was won. `Order::claim_for_finalize`'s
519    /// primitive, applied one table down and for a sharper reason: the
520    /// validator reaches out to an address the *client* chose, so two triggers
521    /// that both pass a status check in memory become two probes of somebody
522    /// else's host. Deciding it in the `UPDATE` is what makes "exactly one
523    /// validation per challenge" a property of the row rather than of how the
524    /// handler happens to be scheduled.
525    ///
526    /// A losing caller is not an error: its challenge is already being decided,
527    /// and the answer it owes the client is the object as it stands. That
528    /// object now says `processing`, which is exactly what §7.1.6 asks for —
529    /// "they transition to the `processing` state when the client responds to
530    /// the challenge" — and §8.2 pairs it with a `Retry-After`, which
531    /// `handlers::authz::add_pending_retry_after` supplies. A retry request is
532    /// explicitly *not* a state change there, so the loser's answer is a
533    /// conformant one rather than a consolation.
534    ///
535    /// A claim that is never settled (the process dies mid-validation) leaves
536    /// the row `processing`, which is the same trade `claim_for_finalize`
537    /// makes: the authorization's own `expires` retires it, and
538    /// `post_challenge` refuses an expired authorization before looking at the
539    /// challenge at all.
540    pub async fn claim_for_validation(&mut self, database: &Database) -> Result<bool, sqlx::Error> {
541        debug!(event = "db_challenge_claim_started", outcome = "progress", challenge_id = ?self.id);
542        let claimed = sqlx::query(
543            "UPDATE challenges SET status = 'processing' WHERE id = ? AND status = 'pending';",
544        )
545        .bind(self.id)
546        .execute(&database.pool)
547        .await?
548        .rows_affected()
549            == 1;
550
551        if !claimed {
552            debug!(event = "db_challenge_claim_refused", outcome = "advisory", challenge_id = ?self.id);
553            return Ok(false);
554        }
555
556        self.status = ChallengeStatus::Processing;
557        debug!(event = "db_challenge_claimed", outcome = "success", challenge_id = ?self.id);
558        Ok(true)
559    }
560
561    /// Records a successful validation: moves the challenge to `valid`, stamps
562    /// `validated`, and keeps `self` in sync.
563    /// The `valid` transition as a bare statement, over any executor.
564    ///
565    /// `validated` is taken as an argument rather than read from the clock here,
566    /// so a caller composing this into a transaction stamps the challenge and
567    /// its in-memory copy with the same instant. See
568    /// [`Authorization::set_valid`] for why the sync is separate.
569    pub(crate) async fn set_valid<'e, E>(
570        id: Uuid,
571        validated: i64,
572        executor: E,
573    ) -> Result<(), sqlx::Error>
574    where
575        E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
576    {
577        sqlx::query("UPDATE challenges SET status = 'valid', validated = ? WHERE id = ?;")
578            .bind(validated)
579            .bind(id)
580            .execute(executor)
581            .await?;
582        Ok(())
583    }
584
585    /// The `invalid` transition as a bare statement; see [`Challenge::set_valid`].
586    pub(crate) async fn set_invalid<'e, E>(
587        id: Uuid,
588        error: &Value,
589        executor: E,
590    ) -> Result<(), sqlx::Error>
591    where
592        E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
593    {
594        let error_json =
595            serde_json::to_string(error).map_err(|e| sqlx::Error::Encode(Box::new(e)))?;
596        sqlx::query("UPDATE challenges SET status = 'invalid', error = ? WHERE id = ?;")
597            .bind(error_json)
598            .bind(id)
599            .execute(executor)
600            .await?;
601        Ok(())
602    }
603
604    pub async fn mark_valid(&mut self, database: &Database) -> Result<(), sqlx::Error> {
605        let validated = now_secs();
606        debug!(event = "db_challenge_mark_valid_started", outcome = "progress", challenge_id = ?self.id);
607        Self::set_valid(self.id, validated, &database.pool).await?;
608
609        self.status = ChallengeStatus::Valid;
610        self.validated = Some(validated);
611        info!(event = "db_challenge_marked_valid", outcome = "success", challenge_id = ?self.id);
612        Ok(())
613    }
614
615    /// Records a failed validation: moves the challenge to the terminal
616    /// `invalid` state, stores the problem document explaining why, and keeps
617    /// `self` in sync.
618    ///
619    /// `validated` is deliberately **not** stamped — RFC 8555 §8 defines it as
620    /// the time the challenge was *successfully* validated.
621    pub async fn mark_invalid(
622        &mut self,
623        error: Value,
624        database: &Database,
625    ) -> Result<(), sqlx::Error> {
626        debug!(event = "db_challenge_mark_invalid_started", outcome = "progress", challenge_id = ?self.id);
627        Self::set_invalid(self.id, &error, &database.pool).await?;
628
629        self.status = ChallengeStatus::Invalid;
630        self.error = Some(error);
631        info!(event = "db_challenge_marked_invalid", outcome = "failure", challenge_id = ?self.id);
632        Ok(())
633    }
634
635    /// The RFC 8555 challenge object: `type`, the derived challenge `url`,
636    /// `status`, `token`, plus `validated` (RFC3339) and `error` once set.
637    #[must_use]
638    pub fn to_json(&self, base_url: &str) -> Value {
639        let mut object = serde_json::Map::new();
640        object.insert("type".to_string(), Value::String(self.typ.clone()));
641        object.insert(
642            "url".to_string(),
643            Value::String(format!("{base_url}/chall/{}", self.id)),
644        );
645        object.insert(
646            "status".to_string(),
647            Value::String(self.status.as_str().to_string()),
648        );
649        object.insert("token".to_string(), Value::String(self.token.clone()));
650        if let Some(validated) = self.validated {
651            object.insert("validated".to_string(), Value::String(rfc3339(validated)));
652        }
653        if let Some(error) = &self.error {
654            object.insert("error".to_string(), error.clone());
655        }
656        Value::Object(object)
657    }
658}
659
660#[cfg(test)]
661mod tests {
662
663    use super::*;
664    use crate::audit::ClientContext;
665    use crate::sqlite::account::Account;
666    use crate::sqlite::order::Order;
667    use crate::sqlite::status::OrderStatus;
668    use crate::testutil::account_id;
669    use std::sync::Arc;
670
671    /// The listing paths' N+1 fix: one query for a whole page.
672    #[tokio::test]
673    async fn ids_for_several_orders_come_back_grouped_in_one_query() {
674        let db = Arc::new(Database::connect_in_memory().await.unwrap());
675        let account = account_id(&db).await;
676
677        let mut expected = Vec::new();
678        for name in ["a.example.com", "b.example.com"] {
679            let order = Order::create(
680                "default",
681                account,
682                vec![Identifier::dns(name)],
683                now_secs() + 3600,
684                None,
685                None,
686                &db,
687            )
688            .await
689            .unwrap();
690            let first =
691                Authorization::create(order.id, Identifier::dns(name), now_secs() + 3600, &db)
692                    .await
693                    .unwrap();
694            expected.push((order.id, first.id));
695        }
696
697        let ids: Vec<Uuid> = expected.iter().map(|(o, _)| *o).collect();
698        let grouped = Authorization::find_ids_by_orders(&ids, &db).await.unwrap();
699        assert_eq!(grouped.len(), 2);
700        for (order_id, authz_id) in &expected {
701            assert_eq!(grouped[order_id], vec![*authz_id]);
702        }
703
704        // An order with no authorizations is simply absent, which is what
705        // `remove(..).unwrap_or_default()` at the call site relies on.
706        let grouped = Authorization::find_ids_by_orders(&[crate::sqlite::id::mint()], &db)
707            .await
708            .unwrap();
709        assert!(grouped.is_empty());
710
711        // And an empty request does not build a `WHERE id IN ()`.
712        assert!(
713            Authorization::find_ids_by_orders(&[], &db)
714                .await
715                .unwrap()
716                .is_empty()
717        );
718    }
719
720    /// Builds account → order and returns the order id, so authorizations have a
721    /// real parent to reference.
722    async fn order_id(db: &Arc<Database>) -> String {
723        let (account, _) = Account::find_or_create(
724            "default",
725            &[1u8, 2, 3],
726            vec![],
727            &ClientContext::default(),
728            db,
729        )
730        .await
731        .unwrap();
732        let order = Order::create(
733            "default",
734            account.id,
735            vec![Identifier::dns("example.com")],
736            now_secs() + 3600,
737            None,
738            None,
739            db,
740        )
741        .await
742        .unwrap();
743        order.id.to_string()
744    }
745
746    /// The `set_*` twins exist so `post_challenge` can put the challenge,
747    /// authorization and order transitions in one transaction. This is the
748    /// property that buys: a failure part-way through leaves *nothing* applied.
749    ///
750    /// Without it the three were separate statements, and a stop between the
751    /// last two left an order `pending` with every authorization `valid` — a
752    /// state nothing re-derives, since the readiness check only ever ran from
753    /// the challenge trigger and the client has no challenge left to answer.
754    #[tokio::test]
755    async fn the_validation_transitions_roll_back_together() {
756        let db = Arc::new(Database::connect_in_memory().await.unwrap());
757        let oid = order_id(&db).await;
758        let authz = Authorization::create(
759            oid.parse().unwrap(),
760            Identifier::dns("example.com"),
761            now_secs() + 3600,
762            &db,
763        )
764        .await
765        .unwrap();
766        let challenge = Challenge::create(authz.id, "http-01", &db).await.unwrap();
767
768        // Everything the success path does, then abandoned rather than
769        // committed — standing in for a failure after the first statement.
770        let mut tx = db.pool.begin().await.unwrap();
771        Challenge::set_valid(challenge.id, now_secs(), &mut *tx)
772            .await
773            .unwrap();
774        Authorization::set_valid(authz.id, &mut *tx).await.unwrap();
775        Order::set_ready(oid.parse().unwrap(), &mut *tx)
776            .await
777            .unwrap();
778        tx.rollback().await.unwrap();
779
780        let reloaded_authz = Authorization::find_by_id(authz.id.to_string().as_str(), &db)
781            .await
782            .unwrap()
783            .unwrap();
784        let reloaded_challenge = Challenge::find_by_id(challenge.id.to_string().as_str(), &db)
785            .await
786            .unwrap()
787            .unwrap();
788        let reloaded_order = Order::find_by_id(&oid, &db).await.unwrap().unwrap();
789
790        assert_eq!(reloaded_challenge.status, ChallengeStatus::Pending);
791        assert_eq!(reloaded_authz.status, AuthzStatus::Pending);
792        assert_eq!(reloaded_order.status, OrderStatus::Pending);
793    }
794
795    /// And the same three, committed, do all land — so the test above is about
796    /// the rollback and not about the statements being no-ops.
797    #[tokio::test]
798    async fn the_validation_transitions_commit_together() {
799        let db = Arc::new(Database::connect_in_memory().await.unwrap());
800        let oid = order_id(&db).await;
801        let authz = Authorization::create(
802            oid.parse().unwrap(),
803            Identifier::dns("example.com"),
804            now_secs() + 3600,
805            &db,
806        )
807        .await
808        .unwrap();
809        let challenge = Challenge::create(authz.id, "http-01", &db).await.unwrap();
810
811        let mut tx = db.pool.begin().await.unwrap();
812        Challenge::set_valid(challenge.id, now_secs(), &mut *tx)
813            .await
814            .unwrap();
815        Authorization::set_valid(authz.id, &mut *tx).await.unwrap();
816        Order::set_ready(oid.parse().unwrap(), &mut *tx)
817            .await
818            .unwrap();
819        tx.commit().await.unwrap();
820
821        assert_eq!(
822            Challenge::find_by_id(challenge.id.to_string().as_str(), &db)
823                .await
824                .unwrap()
825                .unwrap()
826                .status,
827            ChallengeStatus::Valid
828        );
829        assert_eq!(
830            Authorization::find_by_id(authz.id.to_string().as_str(), &db)
831                .await
832                .unwrap()
833                .unwrap()
834                .status,
835            AuthzStatus::Valid
836        );
837        assert_eq!(
838            Order::find_by_id(&oid, &db).await.unwrap().unwrap().status,
839            OrderStatus::Ready
840        );
841    }
842
843    #[tokio::test]
844    async fn authz_create_find_round_trip() {
845        let db = Arc::new(Database::connect_in_memory().await.unwrap());
846        let oid = order_id(&db).await;
847
848        let authz = Authorization::create(
849            oid.parse().unwrap(),
850            Identifier::dns("example.com"),
851            now_secs() + 3600,
852            &db,
853        )
854        .await
855        .unwrap();
856        assert_eq!(authz.status, AuthzStatus::Pending);
857
858        let by_id = Authorization::find_by_id(authz.id.to_string().as_str(), &db)
859            .await
860            .unwrap()
861            .unwrap();
862        assert_eq!(by_id.identifier, Identifier::dns("example.com"));
863        assert_eq!(by_id.order_id.to_string(), oid);
864
865        let by_order = Authorization::find_by_order(oid.parse().unwrap(), &db)
866            .await
867            .unwrap();
868        assert_eq!(by_order.len(), 1);
869    }
870
871    #[tokio::test]
872    async fn authz_mark_valid_persists_and_syncs() {
873        let db = Arc::new(Database::connect_in_memory().await.unwrap());
874        let oid = order_id(&db).await;
875
876        let mut authz = Authorization::create(
877            oid.parse().unwrap(),
878            Identifier::dns("example.com"),
879            now_secs() + 3600,
880            &db,
881        )
882        .await
883        .unwrap();
884        authz.mark_valid(&db).await.unwrap();
885
886        assert_eq!(authz.status, AuthzStatus::Valid);
887        let reloaded = Authorization::find_by_id(authz.id.to_string().as_str(), &db)
888            .await
889            .unwrap()
890            .unwrap();
891        assert_eq!(reloaded.status, AuthzStatus::Valid);
892    }
893
894    #[tokio::test]
895    async fn authz_to_json_shape() {
896        let db = Arc::new(Database::connect_in_memory().await.unwrap());
897        let oid = order_id(&db).await;
898
899        let authz = Authorization::create(
900            oid.parse().unwrap(),
901            Identifier::dns("example.com"),
902            now_secs() + 3600,
903            &db,
904        )
905        .await
906        .unwrap();
907        let challenge = Challenge::create(authz.id, "http-01", &db).await.unwrap();
908
909        let json = authz.to_json("http://localhost:3000", std::slice::from_ref(&challenge));
910        assert_eq!(json["status"], "pending");
911        assert_eq!(
912            json["identifier"],
913            serde_json::json!({"type":"dns","value":"example.com"})
914        );
915        assert!(json["expires"].as_str().unwrap().ends_with('Z'));
916        assert_eq!(json["challenges"].as_array().unwrap().len(), 1);
917        assert_eq!(json["challenges"][0]["type"], "http-01");
918    }
919
920    #[tokio::test]
921    async fn challenge_create_find_round_trip() {
922        let db = Arc::new(Database::connect_in_memory().await.unwrap());
923        let oid = order_id(&db).await;
924        let authz = Authorization::create(
925            oid.parse().unwrap(),
926            Identifier::dns("example.com"),
927            now_secs() + 3600,
928            &db,
929        )
930        .await
931        .unwrap();
932
933        let challenge = Challenge::create(authz.id, "http-01", &db).await.unwrap();
934        assert_eq!(challenge.typ, "http-01");
935        assert_eq!(challenge.status, ChallengeStatus::Pending);
936        assert!(!challenge.token.is_empty());
937        assert!(challenge.validated.is_none());
938
939        let by_id = Challenge::find_by_id(challenge.id.to_string().as_str(), &db)
940            .await
941            .unwrap()
942            .unwrap();
943        assert_eq!(by_id.token, challenge.token);
944
945        let by_authz = Challenge::find_by_authz(authz.id, &db).await.unwrap();
946        assert_eq!(by_authz.len(), 1);
947    }
948
949    #[tokio::test]
950    async fn challenge_mark_valid_persists_and_syncs() {
951        let db = Arc::new(Database::connect_in_memory().await.unwrap());
952        let oid = order_id(&db).await;
953        let authz = Authorization::create(
954            oid.parse().unwrap(),
955            Identifier::dns("example.com"),
956            now_secs() + 3600,
957            &db,
958        )
959        .await
960        .unwrap();
961
962        let mut challenge = Challenge::create(authz.id, "http-01", &db).await.unwrap();
963        challenge.mark_valid(&db).await.unwrap();
964
965        assert_eq!(challenge.status, ChallengeStatus::Valid);
966        assert!(challenge.validated.is_some());
967
968        let reloaded = Challenge::find_by_id(challenge.id.to_string().as_str(), &db)
969            .await
970            .unwrap()
971            .unwrap();
972        assert_eq!(reloaded.status, ChallengeStatus::Valid);
973        let json = reloaded.to_json("http://localhost:3000");
974        assert_eq!(json["status"], "valid");
975        assert_eq!(
976            json["url"],
977            format!("http://localhost:3000/chall/{}", challenge.id)
978        );
979        assert!(json["validated"].as_str().unwrap().ends_with('Z'));
980    }
981
982    #[tokio::test]
983    async fn challenge_mark_invalid_persists_the_problem_document() {
984        let db = Arc::new(Database::connect_in_memory().await.unwrap());
985        let oid = order_id(&db).await;
986        let authz = Authorization::create(
987            oid.parse().unwrap(),
988            Identifier::dns("example.com"),
989            now_secs() + 3600,
990            &db,
991        )
992        .await
993        .unwrap();
994
995        let mut challenge = Challenge::create(authz.id, "http-01", &db).await.unwrap();
996        let problem = serde_json::json!({
997            "type": "urn:ietf:params:acme:error:incorrectResponse",
998            "detail": "response body does not match the key authorization",
999            "status": 403,
1000        });
1001        challenge.mark_invalid(problem.clone(), &db).await.unwrap();
1002
1003        assert_eq!(challenge.status, ChallengeStatus::Invalid);
1004        assert_eq!(challenge.error.as_ref(), Some(&problem));
1005        // RFC 8555 §8 stamps `validated` only on success.
1006        assert!(challenge.validated.is_none());
1007
1008        let reloaded = Challenge::find_by_id(challenge.id.to_string().as_str(), &db)
1009            .await
1010            .unwrap()
1011            .unwrap();
1012        assert_eq!(reloaded.status, ChallengeStatus::Invalid);
1013        assert_eq!(reloaded.error.as_ref(), Some(&problem));
1014        let json = reloaded.to_json("http://localhost:3000");
1015        assert_eq!(json["error"], problem);
1016        assert!(json.get("validated").is_none());
1017    }
1018
1019    #[tokio::test]
1020    async fn authz_mark_invalid_persists_and_syncs() {
1021        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1022        let oid = order_id(&db).await;
1023
1024        let mut authz = Authorization::create(
1025            oid.parse().unwrap(),
1026            Identifier::dns("example.com"),
1027            now_secs() + 3600,
1028            &db,
1029        )
1030        .await
1031        .unwrap();
1032        authz.mark_invalid(&db).await.unwrap();
1033
1034        assert_eq!(authz.status, AuthzStatus::Invalid);
1035        let reloaded = Authorization::find_by_id(authz.id.to_string().as_str(), &db)
1036            .await
1037            .unwrap()
1038            .unwrap();
1039        assert_eq!(reloaded.status, AuthzStatus::Invalid);
1040    }
1041
1042    /// `UNIQUE(authz_id, type)` is what lets one authorization offer several
1043    /// challenges — one per type, and no more.
1044    #[tokio::test]
1045    async fn an_authorization_holds_one_challenge_per_type() {
1046        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1047        let oid = order_id(&db).await;
1048        let authz = Authorization::create(
1049            oid.parse().unwrap(),
1050            Identifier::dns("example.com"),
1051            now_secs() + 3600,
1052            &db,
1053        )
1054        .await
1055        .unwrap();
1056
1057        let http = Challenge::create(authz.id, "http-01", &db).await.unwrap();
1058        let dns_01 = Challenge::create(authz.id, "dns-01", &db).await.unwrap();
1059        Challenge::create(authz.id, "tls-alpn-01", &db)
1060            .await
1061            .unwrap();
1062
1063        // Each carries its own token: a key authorization is per challenge.
1064        assert_ne!(http.token, dns_01.token);
1065
1066        let challenges = Challenge::find_by_authz(authz.id, &db).await.unwrap();
1067        assert_eq!(challenges.len(), 3);
1068
1069        // A second challenge of a type already offered is refused by the schema.
1070        assert!(Challenge::create(authz.id, "http-01", &db).await.is_err());
1071    }
1072
1073    /// A wildcard authorization stores the `*.` form but renders the base name
1074    /// plus a `wildcard` member (RFC 8555 §7.1.4), and proves control of the
1075    /// base name.
1076    #[tokio::test]
1077    async fn a_wildcard_authorization_stores_the_prefix_and_renders_the_base_name() {
1078        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1079        let oid = order_id(&db).await;
1080
1081        let authz = Authorization::create(
1082            oid.parse().unwrap(),
1083            Identifier::dns("*.example.com"),
1084            now_secs() + 3600,
1085            &db,
1086        )
1087        .await
1088        .unwrap();
1089        let challenge = Challenge::create(authz.id, "dns-01", &db).await.unwrap();
1090
1091        // The row keeps the wildcard form, so the canonical two-authorization
1092        // order does not collide on `UNIQUE(order_id, identifier)`.
1093        let reloaded = Authorization::find_by_id(authz.id.to_string().as_str(), &db)
1094            .await
1095            .unwrap()
1096            .unwrap();
1097        assert_eq!(reloaded.identifier.value, "*.example.com");
1098        assert!(reloaded.is_wildcard());
1099        assert_eq!(reloaded.base_identifier(), "example.com");
1100
1101        let json = reloaded.to_json("http://localhost:3000", std::slice::from_ref(&challenge));
1102        assert_eq!(
1103            json["identifier"],
1104            serde_json::json!({"type":"dns","value":"example.com"})
1105        );
1106        assert_eq!(json["wildcard"], true);
1107        assert_eq!(json["challenges"][0]["type"], "dns-01");
1108    }
1109
1110    /// The non-wildcard object has no `wildcard` member at all — RFC 8555 makes
1111    /// it optional and clients treat its absence as false.
1112    #[tokio::test]
1113    async fn a_plain_authorization_has_no_wildcard_member() {
1114        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1115        let oid = order_id(&db).await;
1116
1117        let authz = Authorization::create(
1118            oid.parse().unwrap(),
1119            Identifier::dns("example.com"),
1120            now_secs() + 3600,
1121            &db,
1122        )
1123        .await
1124        .unwrap();
1125        assert!(!authz.is_wildcard());
1126        assert_eq!(authz.base_identifier(), "example.com");
1127        assert!(
1128            authz
1129                .to_json("http://localhost:3000", &[])
1130                .get("wildcard")
1131                .is_none()
1132        );
1133    }
1134
1135    #[tokio::test]
1136    async fn absent_lookups_return_none() {
1137        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1138        assert!(
1139            Authorization::find_by_id("nope", &db)
1140                .await
1141                .unwrap()
1142                .is_none()
1143        );
1144        assert!(Challenge::find_by_id("nope", &db).await.unwrap().is_none());
1145    }
1146}