Skip to main content

acme_proxy/sqlite/
authz.rs

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