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