Skip to main content

acme_proxy/sqlite/
account.rs

1use serde_json::Value;
2use sqlx::Row;
3use sqlx::sqlite::SqliteRow;
4use tracing::{debug, info};
5use uuid::Uuid;
6
7use crate::audit::ClientContext;
8use crate::sqlite::db::Database;
9use crate::sqlite::nonce::now_secs;
10
11/// An ACME account (RFC 8555 §7.1.2), keyed by the client's public key stored as
12/// DER SPKI. `contact` is persisted as a JSON array of strings.
13///
14/// ## ACME Protocol Compliance
15///
16/// This struct represents the account object as defined in RFC 8555:
17/// - `id`: Unique identifier for the account (UUID)
18/// - `pubkey`: DER-encoded SPKI public key used for authentication
19/// - `contact`: Array of contact URIs (email, etc.) for the account holder
20/// - `status`: Account status (valid, deactivated, etc.)
21/// - `created_at`: Timestamp when the account was created
22///
23/// ## Storage Details
24///
25/// - The public key is stored in DER SPKI format for consistent hashing and lookup
26/// - Contact information is serialized as JSON for flexible storage
27/// - The ID is generated as a UUID v4 for uniqueness
28/// - Status is tracked to support account lifecycle management
29///
30/// ## Methods
31///
32/// - `find_by_pubkey`: Lookup account by public key
33/// - `find_by_id`: Lookup account by ID
34/// - `find_or_create`: Create new account or return existing one (RFC 8555 §7.3)
35/// - `delete`: Hard-delete an account, cascading to its orders (admin CLI)
36/// - `to_json`: Convert to RFC 8555 account JSON object format
37#[derive(Debug)]
38pub struct Account {
39    pub id: String,
40    /// The ACME endpoint (`[profiles.<name>]`) this account was registered at.
41    /// Accounts are keyed by `(profile, pubkey)`, so the same client key at two
42    /// endpoints is two accounts — see the schema comment in
43    /// `migrations/20260722210000_add_accounts.sql` for why that is a security
44    /// property and not just tidiness.
45    pub profile: String,
46    pub pubkey: Vec<u8>,
47    pub contact: Vec<String>,
48    pub status: String,
49    pub created_at: i64,
50    /// Which EAB credential (if any) created this account -- an audit trail
51    /// only, set once and never overwritten. See [`Account::set_eab_kid`].
52    pub eab_kid: Option<String>,
53    /// Whether this account agreed to the terms of service when it was created
54    /// (RFC 8555 §7.3.3). `None` for an account created at an endpoint that
55    /// advertised none — which is not the same as "declined", and renders as an
56    /// absent member rather than `false`. Set once, at creation; see
57    /// [`Account::set_terms_agreed`].
58    pub terms_of_service_agreed: Option<bool>,
59    /// Where `newAccount` was called from, and the reverse name that address
60    /// had at the time. Traceability only — see the schema comment in
61    /// `migrations/20260722210000_add_accounts.sql` for why nothing ever
62    /// compares against these.
63    pub created_ip: Option<String>,
64    pub created_ptr: Option<String>,
65    /// When this key last authenticated a request, and from where. Advanced by
66    /// [`Account::touch`] under the [`ACCOUNT_TOUCH_INTERVAL`] throttle.
67    pub last_seen_at: Option<i64>,
68    pub last_seen_ip: Option<String>,
69    pub last_seen_ptr: Option<String>,
70}
71
72/// How often `last_seen_*` is allowed to cost a write, in seconds.
73///
74/// Every ACME POST already writes a nonce row; an unthrottled `UPDATE` here
75/// would double that on the POST-as-GET polling that dominates a real
76/// deployment, for a field whose whole precision requirement is "roughly when".
77/// The web admin's `SESSION_TOUCH_INTERVAL` is the same trade at the same
78/// interval, made for the same reason.
79///
80/// [`Account::needs_touch`] overrides it when the *address* changed, which is
81/// the one case a minute of staleness would hide the interesting thing.
82pub const ACCOUNT_TOUCH_INTERVAL: i64 = 60;
83
84/// A short, stable fingerprint of a public key, for correlating log lines.
85///
86/// The field this feeds used to be `hex::encode(pubkey)` — the *entire* key, so
87/// a log line for an RSA account carried ~700 hex characters, and the name said
88/// "hash" while the value was the key itself. Public keys are not secret, but
89/// they are not log material either.
90pub(crate) fn pubkey_fingerprint(pubkey: &[u8]) -> String {
91    let digest = ring::digest::digest(&ring::digest::SHA256, pubkey);
92    hex::encode(&digest.as_ref()[..8])
93}
94
95/// Every column, in one place: each lookup, the listing and the paged search
96/// must select the same set or [`Account::from_row`] fails on whichever forgot
97/// one.
98///
99/// A `macro_rules!` rather than a `const` so the expansion is a string
100/// *literal*: `sqlx::query` takes `impl SqlSafeStr`, which a runtime `format!`
101/// does not satisfy, so `concat!("SELECT ", columns!(), " FROM …")` is what
102/// keeps a shared column list and a compile-time-checked query in the same
103/// design.
104macro_rules! columns {
105    () => {
106        "id, profile, pubkey, contact, status, created_at, eab_kid, \
107         terms_of_service_agreed, created_ip, created_ptr, last_seen_at, \
108         last_seen_ip, last_seen_ptr"
109    };
110}
111
112impl Account {
113    fn from_row(row: SqliteRow) -> Result<Self, sqlx::Error> {
114        let contact_json: String = row.try_get("contact")?;
115        let contact: Vec<String> =
116            serde_json::from_str(&contact_json).map_err(|e| sqlx::Error::Decode(Box::new(e)))?;
117
118        Ok(Account {
119            id: row.try_get("id")?,
120            profile: row.try_get("profile")?,
121            pubkey: row.try_get("pubkey")?,
122            contact,
123            status: row.try_get("status")?,
124            created_at: row.try_get("created_at")?,
125            eab_kid: row.try_get("eab_kid")?,
126            terms_of_service_agreed: row.try_get("terms_of_service_agreed")?,
127            created_ip: row.try_get("created_ip")?,
128            created_ptr: row.try_get("created_ptr")?,
129            last_seen_at: row.try_get("last_seen_at")?,
130            last_seen_ip: row.try_get("last_seen_ip")?,
131            last_seen_ptr: row.try_get("last_seen_ptr")?,
132        })
133    }
134
135    #[tracing::instrument(name = "Account::find_by_pubkey", skip(pubkey, database))]
136    pub async fn find_by_pubkey(
137        profile: &str,
138        pubkey: &[u8],
139        database: &Database,
140    ) -> Result<Option<Account>, sqlx::Error> {
141        debug!(event = "db_account_find_by_pubkey_started", outcome = "progress", profile = %profile, pubkey_fp = %pubkey_fingerprint(pubkey));
142        let row = sqlx::query(concat!(
143            "SELECT ",
144            columns!(),
145            " FROM accounts WHERE profile = ? AND pubkey = ?;"
146        ))
147        .bind(profile)
148        .bind(pubkey)
149        .fetch_optional(&database.pool)
150        .await?;
151
152        let result = row.map(Account::from_row).transpose()?;
153        if let Some(ref account) = result {
154            debug!(event = "db_account_found_by_pubkey", outcome = "success", account_id = %account.id, pubkey_fp = %pubkey_fingerprint(pubkey));
155        } else {
156            debug!(event = "db_account_not_found_by_pubkey", outcome = "failure", pubkey_fp = %pubkey_fingerprint(pubkey));
157        }
158        Ok(result)
159    }
160
161    /// Looks an account up by id **within one profile**. An id is a UUID and
162    /// therefore globally unique, so the `profile` predicate is not about
163    /// finding the row: it is what makes an account URL minted at one endpoint
164    /// unusable as a `kid` at another.
165    #[tracing::instrument(name = "Account::find_by_id", skip(database), fields(account_id = %id))]
166    pub async fn find_by_id(
167        profile: &str,
168        id: &str,
169        database: &Database,
170    ) -> Result<Option<Account>, sqlx::Error> {
171        debug!(event = "db_account_find_by_id_started", outcome = "progress", profile = %profile, account_id = %id);
172        let row = sqlx::query(concat!(
173            "SELECT ",
174            columns!(),
175            " FROM accounts WHERE profile = ? AND id = ?;"
176        ))
177        .bind(profile)
178        .bind(id)
179        .fetch_optional(&database.pool)
180        .await?;
181
182        let result = row.map(Account::from_row).transpose()?;
183        if let Some(ref account) = result {
184            debug!(event = "db_account_found_by_id", outcome = "success", account_id = %account.id);
185        } else {
186            debug!(event = "db_account_not_found_by_id", outcome = "failure", account_id = %id);
187        }
188        Ok(result)
189    }
190
191    /// Looks up the account for `pubkey`, creating it if absent. Returns the
192    /// account and whether it was newly created — RFC 8555 §7.3 find-or-create,
193    /// where a repeated key returns the existing account rather than a duplicate.
194    ///
195    /// `client` is stamped onto the row **only on the creating branch**: the
196    /// `created_*` columns mean "where this account was registered from", so a
197    /// later `newAccount` from elsewhere returning the same account must not
198    /// rewrite them. Where the key was last *used* from is `last_seen_*`, which
199    /// [`Account::touch`] keeps up to date.
200    #[tracing::instrument(name = "Account::find_or_create", skip(pubkey, client, database))]
201    pub async fn find_or_create(
202        profile: &str,
203        pubkey: &[u8],
204        contact: Vec<String>,
205        client: &ClientContext,
206        database: &Database,
207    ) -> Result<(Account, bool), sqlx::Error> {
208        debug!(event = "db_account_find_or_create_started", outcome = "progress", profile = %profile, pubkey_fp = %pubkey_fingerprint(pubkey));
209        if let Some(account) = Account::find_by_pubkey(profile, pubkey, database).await? {
210            debug!(event = "db_account_found_existing", outcome = "success", account_id = %account.id, pubkey_fp = %pubkey_fingerprint(pubkey));
211            return Ok((account, false));
212        }
213
214        let account = Account {
215            id: Uuid::new_v4().to_string(),
216            profile: profile.to_string(),
217            pubkey: pubkey.to_vec(),
218            contact,
219            status: "valid".to_string(),
220            created_at: now_secs(),
221            eab_kid: None,
222            terms_of_service_agreed: None,
223            created_ip: client.ip.clone(),
224            created_ptr: client.ptr.clone(),
225            // A brand-new account has been seen exactly once, right now, from
226            // here. Seeding these rather than leaving them NULL until the next
227            // request means "never used since registration" reads as a
228            // `last_seen_at` equal to `created_at`, not as a missing field a
229            // renderer has to special-case.
230            last_seen_at: Some(now_secs()),
231            last_seen_ip: client.ip.clone(),
232            last_seen_ptr: client.ptr.clone(),
233        };
234
235        // `contact` is a `Vec<String>`, so serialization is infallible.
236        let contact_json = Value::from(account.contact.clone()).to_string();
237
238        debug!(event = "db_account_create_started", outcome = "progress", account_id = %account.id);
239        let inserted = sqlx::query(
240            "INSERT INTO accounts (id, profile, pubkey, contact, status, created_at, created_ip, \
241             created_ptr, last_seen_at, last_seen_ip, last_seen_ptr) \
242             VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
243        )
244        .bind(&account.id)
245        .bind(&account.profile)
246        .bind(&account.pubkey)
247        .bind(contact_json)
248        .bind(&account.status)
249        .bind(account.created_at)
250        .bind(&account.created_ip)
251        .bind(&account.created_ptr)
252        .bind(account.last_seen_at)
253        .bind(&account.last_seen_ip)
254        .bind(&account.last_seen_ptr)
255        .execute(&database.pool)
256        .await;
257
258        // Another request registered this same key between the lookup above and
259        // this insert — two renewals starting together on first boot, or a
260        // client retrying a response it thought was slow. §7.3 makes
261        // find-or-create the contract, so the loser owes its caller the account
262        // that won rather than the constraint it tripped over: surfacing the
263        // violation reaches `post_new_account` as a bare `sqlx::Error` and
264        // leaves the client with a 500 where the RFC promises 200 and a
265        // `Location` — which typically aborts the whole renewal.
266        //
267        // The re-read cannot come back empty (the row the constraint named is
268        // committed by definition), but `find_by_pubkey` is fallible for its own
269        // reasons, and an unexpected `None` must surface as the original
270        // violation rather than as a second, less informative error.
271        if let Err(error) = inserted {
272            if is_pubkey_conflict(&error)
273                && let Some(existing) = Account::find_by_pubkey(profile, pubkey, database).await?
274            {
275                debug!(event = "db_account_create_lost_race", outcome = "advisory", account_id = %existing.id, pubkey_fp = %pubkey_fingerprint(pubkey));
276                return Ok((existing, false));
277            }
278            return Err(error);
279        }
280
281        debug!(event = "db_account_created", outcome = "success", account_id = %account.id, pubkey_fp = %pubkey_fingerprint(pubkey));
282        Ok((account, true))
283    }
284
285    /// Whether [`Account::touch`] is worth a write at `now`, for a request
286    /// arriving from `ip`.
287    ///
288    /// Two ways to say yes, and the second is the point of the method existing:
289    ///
290    /// - [`ACCOUNT_TOUCH_INTERVAL`] has elapsed (or nothing was ever recorded);
291    /// - **the address differs from the last one recorded**, whatever the
292    ///   interval says. A key that moves is the single most interesting thing
293    ///   these columns can show, and a throttle that swallowed the move for a
294    ///   minute would hide exactly the requests worth seeing — a stolen account
295    ///   key being used from somewhere new arrives as a burst, not a trickle.
296    ///
297    /// A pure function of the row and its arguments, so the policy is testable
298    /// without an HTTP request, and it lives beside the columns it governs
299    /// rather than in the extractor that calls it.
300    #[must_use]
301    pub fn needs_touch(&self, now: i64, ip: Option<&str>) -> bool {
302        match self.last_seen_at {
303            None => true,
304            Some(last) => {
305                now.saturating_sub(last) >= ACCOUNT_TOUCH_INTERVAL
306                    || self.last_seen_ip.as_deref() != ip
307            }
308        }
309    }
310
311    /// Records that this key just authenticated a request, from `client`.
312    ///
313    /// Called only when [`Account::needs_touch`] said so — which is also why
314    /// the reverse lookup belongs to the caller: resolving a PTR record for a
315    /// write that is about to be skipped would be the cost the throttle exists
316    /// to avoid. Keeps the in-memory fields in sync, so a `to_json` in the same
317    /// request reflects it without a re-read.
318    #[tracing::instrument(name = "Account::touch", skip(self, client, database), fields(account_id = %self.id))]
319    pub async fn touch(
320        &mut self,
321        client: &ClientContext,
322        database: &Database,
323    ) -> Result<(), sqlx::Error> {
324        let now = now_secs();
325        sqlx::query(
326            "UPDATE accounts SET last_seen_at = ?, last_seen_ip = ?, last_seen_ptr = ? \
327             WHERE id = ?;",
328        )
329        .bind(now)
330        .bind(&client.ip)
331        .bind(&client.ptr)
332        .bind(&self.id)
333        .execute(&database.pool)
334        .await?;
335
336        self.last_seen_at = Some(now);
337        self.last_seen_ip = client.ip.clone();
338        self.last_seen_ptr = client.ptr.clone();
339        debug!(event = "db_account_touched", outcome = "success", account_id = %self.id);
340        Ok(())
341    }
342
343    /// Replaces the account's contact list (RFC 8555 §7.3.2 account update). The
344    /// in-memory `self.contact` is kept in sync so a subsequent `to_json`
345    /// reflects the change without a re-read.
346    #[tracing::instrument(name = "Account::update_contact", skip(self, database), fields(account_id = %self.id))]
347    pub async fn update_contact(
348        &mut self,
349        contact: Vec<String>,
350        database: &Database,
351    ) -> Result<(), sqlx::Error> {
352        debug!(event = "db_account_contact_update_started", outcome = "progress", account_id = %self.id);
353        // `contact` is a `Vec<String>`, so serialization is infallible.
354        let contact_json = Value::from(contact.clone()).to_string();
355
356        sqlx::query("UPDATE accounts SET contact = ? WHERE id = ?;")
357            .bind(contact_json)
358            .bind(&self.id)
359            .execute(&database.pool)
360            .await?;
361
362        self.contact = contact;
363        debug!(event = "db_account_contact_updated", outcome = "success", account_id = %self.id);
364        Ok(())
365    }
366
367    /// Deactivates the account (RFC 8555 §7.3.6): sets `status` to `deactivated`,
368    /// a terminal state. Keeps `self.status` in sync.
369    #[tracing::instrument(name = "Account::deactivate", skip(self, database), fields(account_id = %self.id))]
370    pub async fn deactivate(&mut self, database: &Database) -> Result<(), sqlx::Error> {
371        debug!(event = "db_account_deactivation_started", outcome = "progress", account_id = %self.id);
372        sqlx::query("UPDATE accounts SET status = 'deactivated' WHERE id = ?;")
373            .bind(&self.id)
374            .execute(&database.pool)
375            .await?;
376
377        self.status = "deactivated".to_string();
378        debug!(event = "db_account_deactivated", outcome = "success", account_id = %self.id);
379        Ok(())
380    }
381
382    /// Replaces the account's key (RFC 8555 §7.3.5 account key rollover).
383    /// `pubkey` is DER SPKI, the same form every other lookup keys accounts
384    /// by. `pubkey` is `UNIQUE`, a backstop against the rare race the
385    /// caller's own pre-check (`Account::find_by_pubkey`) cannot fully close;
386    /// a violation here surfaces as a plain `sqlx::Error`.
387    pub async fn update_pubkey(
388        &mut self,
389        pubkey: &[u8],
390        database: &Database,
391    ) -> Result<(), sqlx::Error> {
392        debug!(event = "db_account_pubkey_update_started", outcome = "progress", account_id = ?self.id);
393        sqlx::query("UPDATE accounts SET pubkey = ? WHERE id = ?;")
394            .bind(pubkey)
395            .bind(&self.id)
396            .execute(&database.pool)
397            .await?;
398
399        self.pubkey = pubkey.to_vec();
400        info!(event = "db_account_pubkey_updated", outcome = "success", account_id = ?self.id);
401        Ok(())
402    }
403
404    /// Records which EAB credential created this account -- an audit trail
405    /// only (see the migration comment). Called once, right after
406    /// `Account::find_or_create` reports a freshly created row
407    /// (`post_new_account` in `lib.rs`): **never** overwritten afterwards, so
408    /// re-registering under an existing key does not change what is recorded,
409    /// even if a different (still valid) EAB credential is presented that time.
410    pub async fn set_eab_kid(
411        &mut self,
412        eab_kid: &str,
413        database: &Database,
414    ) -> Result<(), sqlx::Error> {
415        debug!(event = "db_account_eab_kid_set_started", outcome = "progress", account_id = ?self.id, eab_kid = ?eab_kid);
416        sqlx::query("UPDATE accounts SET eab_kid = ? WHERE id = ?;")
417            .bind(eab_kid)
418            .bind(&self.id)
419            .execute(&database.pool)
420            .await?;
421
422        self.eab_kid = Some(eab_kid.to_string());
423        info!(event = "db_account_eab_kid_set", outcome = "success", account_id = ?self.id, eab_kid = ?eab_kid);
424        Ok(())
425    }
426
427    /// Records that this account agreed to the terms of service
428    /// (RFC 8555 §7.3.3).
429    ///
430    /// Same lifecycle as [`Account::set_eab_kid`]: called once, right after
431    /// `find_or_create` reports a freshly created row, and never overwritten —
432    /// re-registering under an existing key does not restate the agreement,
433    /// and a ToS added to the configuration later does not retroactively make
434    /// old accounts look like they accepted it.
435    pub async fn set_terms_agreed(&mut self, database: &Database) -> Result<(), sqlx::Error> {
436        debug!(event = "db_account_terms_agreed_started", outcome = "progress", account_id = ?self.id);
437        sqlx::query("UPDATE accounts SET terms_of_service_agreed = 1 WHERE id = ?;")
438            .bind(&self.id)
439            .execute(&database.pool)
440            .await?;
441
442        self.terms_of_service_agreed = Some(true);
443        info!(event = "db_account_terms_agreed", outcome = "success", account_id = ?self.id);
444        Ok(())
445    }
446
447    /// Looks an account up by id across **every** profile — for the admin CLI,
448    /// where an operator holds an id and not necessarily the endpoint it came
449    /// from. Ids are UUIDs, so this is unambiguous.
450    ///
451    /// Never use it on a request path: profile scoping is what keeps an account
452    /// URL minted at one endpoint from being accepted at another.
453    pub async fn find_any_by_id(
454        id: &str,
455        database: &Database,
456    ) -> Result<Option<Account>, sqlx::Error> {
457        debug!(event = "db_account_find_any_by_id_started", outcome = "progress", account_id = %id);
458        let row = sqlx::query(concat!(
459            "SELECT ",
460            columns!(),
461            " FROM accounts WHERE id = ?;"
462        ))
463        .bind(id)
464        .fetch_optional(&database.pool)
465        .await?;
466
467        row.map(Account::from_row).transpose()
468    }
469
470    /// One page of accounts, newest first, plus the total the same filter
471    /// matches unpaged.
472    ///
473    /// The [`Account`] counterpart to [`crate::sqlite::order::Order::search`],
474    /// and the **only** listing this model offers: an unpaged `list_all` stood
475    /// beside it until `account list` grew a window, and a second listing whose
476    /// ordering disagreed with this one was a page control waiting to skip a
477    /// row. `profile` filters to one endpoint; `None` lists accounts of every
478    /// profile, which is what an operator asking "what is on this server?"
479    /// wants.
480    ///
481    /// Two literal statements per branch rather than a builder: with one
482    /// optional filter there are only two shapes, and `sqlx::query`'s
483    /// `&'static str` bound is a guarantee worth keeping where it is free.
484    pub async fn search(
485        profile: Option<&str>,
486        limit: i64,
487        offset: i64,
488        database: &Database,
489    ) -> Result<(Vec<Account>, i64), sqlx::Error> {
490        debug!(event = "db_account_search_started", outcome = "progress", profile = ?profile, limit = limit, offset = offset);
491
492        // `id` breaks the `created_at` tie for the same reason it does for
493        // orders: whole-second timestamps would otherwise let two rows swap
494        // between pages, and one of them would never be seen.
495        let (rows, total) = match profile {
496            Some(profile) => {
497                let rows = sqlx::query(concat!(
498                    "SELECT ",
499                    columns!(),
500                    " FROM accounts WHERE profile = ? \
501                     ORDER BY created_at DESC, id DESC LIMIT ? OFFSET ?;"
502                ))
503                .bind(profile)
504                .bind(limit)
505                .bind(offset)
506                .fetch_all(&database.pool)
507                .await?;
508                let total: i64 = sqlx::query("SELECT COUNT(*) FROM accounts WHERE profile = ?;")
509                    .bind(profile)
510                    .fetch_one(&database.pool)
511                    .await?
512                    .try_get(0)?;
513                (rows, total)
514            }
515            None => {
516                let rows = sqlx::query(concat!(
517                    "SELECT ",
518                    columns!(),
519                    " FROM accounts ORDER BY created_at DESC, id DESC LIMIT ? OFFSET ?;"
520                ))
521                .bind(limit)
522                .bind(offset)
523                .fetch_all(&database.pool)
524                .await?;
525                let total: i64 = sqlx::query("SELECT COUNT(*) FROM accounts;")
526                    .fetch_one(&database.pool)
527                    .await?
528                    .try_get(0)?;
529                (rows, total)
530            }
531        };
532
533        let accounts = rows
534            .into_iter()
535            .map(Account::from_row)
536            .collect::<Result<_, _>>()?;
537        Ok((accounts, total))
538    }
539
540    /// Hard-deletes the account row — cascading, via `ON DELETE CASCADE`, to
541    /// its orders, authorizations and challenges. Returns whether a row
542    /// existed to delete, so the caller can distinguish "gone" from "never
543    /// there".
544    pub async fn delete(id: &str, database: &Database) -> Result<bool, sqlx::Error> {
545        debug!(event = "db_account_delete_started", outcome = "progress", account_id = ?id);
546        let result = sqlx::query("DELETE FROM accounts WHERE id = ?;")
547            .bind(id)
548            .execute(&database.pool)
549            .await?;
550
551        let deleted = result.rows_affected() > 0;
552        if deleted {
553            info!(event = "db_account_deleted", outcome = "success", account_id = ?id);
554        } else {
555            debug!(event = "db_account_delete_missing", outcome = "success", account_id = ?id);
556        }
557        Ok(deleted)
558    }
559
560    /// The RFC 8555 account object: `status`, optional `contact`, and the
561    /// `orders` list URL (derived from the public `base_url`).
562    #[must_use]
563    pub fn to_json(&self, base_url: &str) -> Value {
564        let mut object = serde_json::Map::new();
565        object.insert("status".to_string(), Value::String(self.status.clone()));
566        if !self.contact.is_empty() {
567            object.insert("contact".to_string(), Value::from(self.contact.clone()));
568        }
569        object.insert(
570            "orders".to_string(),
571            Value::String(format!("{base_url}/acct/{}/orders", self.id)),
572        );
573        // RFC 8555 §7.1.2, optional: reflected only when it was actually
574        // recorded, so an account created at an endpoint with no terms of
575        // service says nothing rather than claiming to have declined.
576        if let Some(agreed) = self.terms_of_service_agreed {
577            object.insert("termsOfServiceAgreed".to_string(), Value::Bool(agreed));
578        }
579        Value::Object(object)
580    }
581}
582
583/// Whether a failed account INSERT was a concurrent `newAccount` for the same
584/// key winning the race.
585///
586/// Matched on the offending *columns* rather than on "any unique violation", the
587/// `handlers::order::is_replaces_conflict` treatment: `accounts` also carries a
588/// primary key on `id`, and a UUID collision there is a different event
589/// entirely — one that must not be quietly answered with somebody else's
590/// account.
591///
592/// The columns and not an index name: SQLite reports this as `UNIQUE constraint
593/// failed: accounts.profile, accounts.pubkey`, naming the columns of the table
594/// constraint. Pinned by
595/// `tests::concurrent_find_or_create_for_one_key_yields_one_account`, which
596/// reaches this branch by racing eight callers over one key.
597pub(crate) fn is_pubkey_conflict(error: &sqlx::Error) -> bool {
598    matches!(error, sqlx::Error::Database(db) if db.is_unique_violation()
599        && db.message().contains("accounts.pubkey"))
600}
601
602#[cfg(test)]
603mod tests {
604
605    /// The throttle's whole decision table. The second arm is the one worth
606    /// having: an account key that starts arriving from a new address is the
607    /// single most interesting thing these columns can show, and a minute of
608    /// staleness would hide exactly that.
609    #[test]
610    fn needs_touch_yields_to_the_interval_but_never_to_a_changed_address() {
611        let mut account = Account {
612            id: "a".to_string(),
613            profile: "default".to_string(),
614            pubkey: vec![1],
615            contact: vec![],
616            status: "valid".to_string(),
617            created_at: 0,
618            eab_kid: None,
619            terms_of_service_agreed: None,
620            created_ip: None,
621            created_ptr: None,
622            last_seen_at: None,
623            last_seen_ip: None,
624            last_seen_ptr: None,
625        };
626
627        // Never seen: always worth a write.
628        assert!(account.needs_touch(1_000, Some("203.0.113.7")));
629
630        account.last_seen_at = Some(1_000);
631        account.last_seen_ip = Some("203.0.113.7".to_string());
632
633        // Same address, inside the window: skipped.
634        assert!(!account.needs_touch(1_000, Some("203.0.113.7")));
635        assert!(!account.needs_touch(1_000 + ACCOUNT_TOUCH_INTERVAL - 1, Some("203.0.113.7")));
636        // Same address, at the boundary: written.
637        assert!(account.needs_touch(1_000 + ACCOUNT_TOUCH_INTERVAL, Some("203.0.113.7")));
638        // A different address beats the interval outright.
639        assert!(account.needs_touch(1_000, Some("198.51.100.4")));
640        // Including losing one entirely, which is not "absent from deny,
641        // therefore unchanged".
642        assert!(account.needs_touch(1_000, None));
643
644        // And a clock that went backwards must not underflow into a write per
645        // request; `saturating_sub` keeps the answer "not yet".
646        assert!(!account.needs_touch(0, Some("203.0.113.7")));
647    }
648
649    /// `created_*` mean "where this account was registered from" and must not
650    /// be rewritten by a later `newAccount` for the same key; `last_seen_*`
651    /// are what move.
652    #[tokio::test]
653    async fn creation_stamps_the_address_once_and_touch_moves_only_the_last_seen_columns() {
654        let db = Database::connect_in_memory().await.unwrap();
655        let first = ClientContext {
656            ip: Some("203.0.113.7".to_string()),
657            ptr: Some("first.example.com".to_string()),
658            user_agent: Some("certbot".to_string()),
659            request_id: Some("req-1".to_string()),
660        };
661        let (created, is_new) = Account::find_or_create("default", &[42u8], vec![], &first, &db)
662            .await
663            .unwrap();
664        assert!(is_new);
665        assert_eq!(created.created_ip.as_deref(), Some("203.0.113.7"));
666        assert_eq!(created.created_ptr.as_deref(), Some("first.example.com"));
667        // Seeded rather than left NULL: "never used since registration" reads
668        // as a `last_seen_at` equal to `created_at`, not a missing field.
669        assert_eq!(created.last_seen_ip.as_deref(), Some("203.0.113.7"));
670        assert!(created.last_seen_at.is_some());
671
672        // The same key arriving from somewhere else finds the account and
673        // leaves the creation columns exactly as they were.
674        let second = ClientContext {
675            ip: Some("198.51.100.4".to_string()),
676            ptr: Some("second.example.com".to_string()),
677            ..ClientContext::default()
678        };
679        let (mut found, is_new) = Account::find_or_create("default", &[42u8], vec![], &second, &db)
680            .await
681            .unwrap();
682        assert!(!is_new);
683        assert_eq!(found.created_ip.as_deref(), Some("203.0.113.7"));
684        assert_eq!(found.created_ptr.as_deref(), Some("first.example.com"));
685
686        found.touch(&second, &db).await.unwrap();
687        // In memory...
688        assert_eq!(found.last_seen_ip.as_deref(), Some("198.51.100.4"));
689        assert_eq!(found.last_seen_ptr.as_deref(), Some("second.example.com"));
690        // ...and on disk, with the creation columns untouched.
691        let reloaded = Account::find_by_id("default", &found.id, &db)
692            .await
693            .unwrap()
694            .unwrap();
695        assert_eq!(reloaded.created_ip.as_deref(), Some("203.0.113.7"));
696        assert_eq!(reloaded.last_seen_ip.as_deref(), Some("198.51.100.4"));
697        assert_eq!(
698            reloaded.last_seen_ptr.as_deref(),
699            Some("second.example.com")
700        );
701        assert!(reloaded.last_seen_at >= reloaded.created_at.into());
702
703        // A client with no resolvable name clears the stale one rather than
704        // leaving a name that no longer describes the address on the row.
705        let nameless = ClientContext {
706            ip: Some("198.51.100.4".to_string()),
707            ..ClientContext::default()
708        };
709        found.touch(&nameless, &db).await.unwrap();
710        let reloaded = Account::find_by_id("default", &found.id, &db)
711            .await
712            .unwrap()
713            .unwrap();
714        assert_eq!(reloaded.last_seen_ptr, None);
715    }
716
717    /// The traceability columns are admin-visible only: the ACME account object
718    /// is defined by RFC 8555 §7.1.2 and must not grow members naming where a
719    /// client connects from.
720    #[tokio::test]
721    async fn to_json_exposes_none_of_the_traceability_columns() {
722        let db = Database::connect_in_memory().await.unwrap();
723        let client = ClientContext {
724            ip: Some("203.0.113.7".to_string()),
725            ptr: Some("host.example.com".to_string()),
726            ..ClientContext::default()
727        };
728        let (account, _) = Account::find_or_create("default", &[7u8], vec![], &client, &db)
729            .await
730            .unwrap();
731        let json = account.to_json("http://localhost:3000");
732        let object = json.as_object().unwrap();
733        for absent in [
734            "createdIp",
735            "created_ip",
736            "createdPtr",
737            "lastSeenAt",
738            "lastSeenIp",
739            "lastSeenPtr",
740        ] {
741            assert!(!object.contains_key(absent), "{absent} leaked into to_json");
742        }
743        assert!(!json.to_string().contains("203.0.113.7"));
744    }
745
746    use super::*;
747    use std::sync::Arc;
748
749    #[tokio::test]
750    async fn find_or_create_creates_then_returns_existing() {
751        let db = Arc::new(Database::connect_in_memory().await.unwrap());
752        let pubkey = vec![1u8, 2, 3, 4];
753        let contact = vec!["mailto:a@example.com".to_string()];
754
755        let (created, is_new) = Account::find_or_create(
756            "default",
757            &pubkey,
758            contact.clone(),
759            &ClientContext::default(),
760            &db,
761        )
762        .await
763        .unwrap();
764        assert!(is_new);
765        assert_eq!(created.status, "valid");
766        assert_eq!(created.contact, contact);
767
768        // The same key returns the existing account (with its original contact),
769        // not a second row.
770        let (existing, is_new) =
771            Account::find_or_create("default", &pubkey, vec![], &ClientContext::default(), &db)
772                .await
773                .unwrap();
774        assert!(!is_new);
775        assert_eq!(existing.id, created.id);
776        assert_eq!(existing.contact, contact);
777    }
778
779    #[tokio::test]
780    async fn find_by_id_and_pubkey_round_trip() {
781        let db = Arc::new(Database::connect_in_memory().await.unwrap());
782        let pubkey = vec![9u8; 16];
783
784        let (account, _) =
785            Account::find_or_create("default", &pubkey, vec![], &ClientContext::default(), &db)
786                .await
787                .unwrap();
788
789        let by_id = Account::find_by_id("default", &account.id, &db)
790            .await
791            .unwrap()
792            .unwrap();
793        assert_eq!(by_id.pubkey, pubkey);
794
795        let by_key = Account::find_by_pubkey("default", &pubkey, &db)
796            .await
797            .unwrap()
798            .unwrap();
799        assert_eq!(by_key.id, account.id);
800    }
801
802    #[tokio::test]
803    async fn absent_lookups_return_none() {
804        let db = Arc::new(Database::connect_in_memory().await.unwrap());
805
806        assert!(
807            Account::find_by_id("default", "nope", &db)
808                .await
809                .unwrap()
810                .is_none()
811        );
812        assert!(
813            Account::find_by_pubkey("default", &[0u8; 4], &db)
814                .await
815                .unwrap()
816                .is_none()
817        );
818    }
819
820    #[tokio::test]
821    async fn update_contact_persists_and_syncs() {
822        let db = Arc::new(Database::connect_in_memory().await.unwrap());
823        let pubkey = vec![7u8; 8];
824
825        let (mut account, _) = Account::find_or_create(
826            "default",
827            &pubkey,
828            vec!["mailto:old@example.com".to_string()],
829            &ClientContext::default(),
830            &db,
831        )
832        .await
833        .unwrap();
834
835        let new_contact = vec!["mailto:new@example.com".to_string()];
836        account
837            .update_contact(new_contact.clone(), &db)
838            .await
839            .unwrap();
840
841        // In-memory struct is updated…
842        assert_eq!(account.contact, new_contact);
843        // …and so is the stored row.
844        let reloaded = Account::find_by_id("default", &account.id, &db)
845            .await
846            .unwrap()
847            .unwrap();
848        assert_eq!(reloaded.contact, new_contact);
849    }
850
851    #[tokio::test]
852    async fn deactivate_persists_and_syncs() {
853        let db = Arc::new(Database::connect_in_memory().await.unwrap());
854        let pubkey = vec![8u8; 8];
855
856        let (mut account, _) =
857            Account::find_or_create("default", &pubkey, vec![], &ClientContext::default(), &db)
858                .await
859                .unwrap();
860        assert_eq!(account.status, "valid");
861
862        account.deactivate(&db).await.unwrap();
863
864        assert_eq!(account.status, "deactivated");
865        let reloaded = Account::find_by_id("default", &account.id, &db)
866            .await
867            .unwrap()
868            .unwrap();
869        assert_eq!(reloaded.status, "deactivated");
870    }
871
872    #[tokio::test]
873    async fn update_pubkey_persists_and_syncs() {
874        let db = Arc::new(Database::connect_in_memory().await.unwrap());
875        let (mut account, _) =
876            Account::find_or_create("default", &[9u8; 8], vec![], &ClientContext::default(), &db)
877                .await
878                .unwrap();
879
880        let new_pubkey = vec![10u8; 8];
881        account.update_pubkey(&new_pubkey, &db).await.unwrap();
882
883        // In-memory struct is updated…
884        assert_eq!(account.pubkey, new_pubkey);
885        // …and so is the stored row, findable under the new key.
886        let reloaded = Account::find_by_id("default", &account.id, &db)
887            .await
888            .unwrap()
889            .unwrap();
890        assert_eq!(reloaded.pubkey, new_pubkey);
891        assert!(
892            Account::find_by_pubkey("default", &new_pubkey, &db)
893                .await
894                .unwrap()
895                .is_some()
896        );
897    }
898
899    /// `pubkey` is `UNIQUE`: rolling one account onto a key a *different*
900    /// account already owns must fail rather than silently letting two
901    /// accounts collide on one key. This is the DB-level backstop behind
902    /// `post_key_change`'s own `find_by_pubkey` pre-check.
903    #[tokio::test]
904    async fn update_pubkey_to_a_key_owned_by_another_account_is_rejected() {
905        let db = Arc::new(Database::connect_in_memory().await.unwrap());
906        let (_first, _) = Account::find_or_create(
907            "default",
908            &[11u8; 8],
909            vec![],
910            &ClientContext::default(),
911            &db,
912        )
913        .await
914        .unwrap();
915        let (mut second, _) = Account::find_or_create(
916            "default",
917            &[12u8; 8],
918            vec![],
919            &ClientContext::default(),
920            &db,
921        )
922        .await
923        .unwrap();
924
925        let error = second
926            .update_pubkey(&[11u8; 8], &db)
927            .await
928            .expect_err("taking another account's key must not succeed");
929        // Not merely "an error": `handlers::account::post_key_change` reads this
930        // exact violation to tell a lost rollover race from a real fault, and
931        // answers §7.3.5's `409` + `Location` on the strength of it. A message
932        // change that slipped past `is_pubkey_conflict` would turn that back
933        // into a `500` with nothing failing.
934        assert!(
935            is_pubkey_conflict(&error),
936            "the unique violation must be recognisable as a pubkey conflict: {error}"
937        );
938    }
939
940    #[tokio::test]
941    async fn set_eab_kid_persists_and_syncs() {
942        let db = Arc::new(Database::connect_in_memory().await.unwrap());
943        let (mut account, _) =
944            Account::find_or_create("default", &[5u8], vec![], &ClientContext::default(), &db)
945                .await
946                .unwrap();
947        assert!(account.eab_kid.is_none());
948
949        account.set_eab_kid("some-kid", &db).await.unwrap();
950        assert_eq!(account.eab_kid.as_deref(), Some("some-kid"));
951
952        let reloaded = Account::find_by_id("default", &account.id, &db)
953            .await
954            .unwrap()
955            .unwrap();
956        assert_eq!(reloaded.eab_kid.as_deref(), Some("some-kid"));
957    }
958
959    #[tokio::test]
960    async fn delete_removes_the_row_and_reports_true() {
961        let db = Arc::new(Database::connect_in_memory().await.unwrap());
962        let (account, _) =
963            Account::find_or_create("default", &[3u8], vec![], &ClientContext::default(), &db)
964                .await
965                .unwrap();
966
967        assert!(Account::delete(&account.id, &db).await.unwrap());
968        assert!(
969            Account::find_by_id("default", &account.id, &db)
970                .await
971                .unwrap()
972                .is_none()
973        );
974    }
975
976    #[tokio::test]
977    async fn delete_of_unknown_id_reports_false() {
978        let db = Arc::new(Database::connect_in_memory().await.unwrap());
979        assert!(!Account::delete("nope", &db).await.unwrap());
980    }
981
982    #[tokio::test]
983    async fn delete_cascades_to_the_accounts_orders() {
984        let db = Arc::new(Database::connect_in_memory().await.unwrap());
985        let (account, _) =
986            Account::find_or_create("default", &[4u8], vec![], &ClientContext::default(), &db)
987                .await
988                .unwrap();
989
990        crate::sqlite::order::Order::create(
991            "default",
992            &account.id,
993            vec![],
994            now_secs() + 3600,
995            None,
996            None,
997            &db,
998        )
999        .await
1000        .unwrap();
1001
1002        Account::delete(&account.id, &db).await.unwrap();
1003
1004        let remaining = crate::sqlite::order::Order::find_by_account(&account.id, &db)
1005            .await
1006            .unwrap();
1007        assert!(remaining.is_empty());
1008    }
1009
1010    /// Seeds `count` accounts under `profile`, backdated so `created_at DESC`
1011    /// is deterministic rather than resolved by the random-UUID tiebreak.
1012    async fn seed_accounts(db: &Arc<Database>, profile: &str, count: usize) -> Vec<String> {
1013        let base = now_secs();
1014        let mut ids = Vec::new();
1015        for index in 0..count {
1016            let (account, _) = Account::find_or_create(
1017                profile,
1018                &[profile.len() as u8, index as u8],
1019                vec![],
1020                &ClientContext::default(),
1021                db,
1022            )
1023            .await
1024            .unwrap();
1025            sqlx::query("UPDATE accounts SET created_at = ? WHERE id = ?;")
1026                .bind(base - index as i64)
1027                .bind(&account.id)
1028                .execute(&db.pool)
1029                .await
1030                .unwrap();
1031            ids.push(account.id);
1032        }
1033        ids
1034    }
1035
1036    #[tokio::test]
1037    async fn search_pages_newest_first_and_reports_the_unpaged_total() {
1038        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1039        let ids = seed_accounts(&db, "default", 5).await;
1040
1041        let (page, total) = Account::search(None, 2, 0, &db).await.unwrap();
1042        assert_eq!(total, 5, "the total must ignore the page window");
1043        assert_eq!(
1044            page.iter().map(|a| a.id.clone()).collect::<Vec<_>>(),
1045            ids[..2]
1046        );
1047
1048        let (second, _) = Account::search(None, 2, 2, &db).await.unwrap();
1049        assert_eq!(
1050            second.iter().map(|a| a.id.clone()).collect::<Vec<_>>(),
1051            ids[2..4]
1052        );
1053
1054        // Past the end: empty, but the total is still real.
1055        let (beyond, total) = Account::search(None, 2, 99, &db).await.unwrap();
1056        assert!(beyond.is_empty());
1057        assert_eq!(total, 5);
1058    }
1059
1060    #[tokio::test]
1061    async fn search_scopes_by_profile_and_counts_only_that_profile() {
1062        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1063        seed_accounts(&db, "default", 2).await;
1064        seed_accounts(&db, "other", 3).await;
1065
1066        let (rows, total) = Account::search(Some("other"), 50, 0, &db).await.unwrap();
1067        assert_eq!(total, 3);
1068        assert_eq!(rows.len(), 3);
1069        assert!(rows.iter().all(|a| a.profile == "other"));
1070
1071        let (_, total) = Account::search(None, 50, 0, &db).await.unwrap();
1072        assert_eq!(total, 5, "no profile means every endpoint");
1073
1074        let (rows, total) = Account::search(Some("nope"), 50, 0, &db).await.unwrap();
1075        assert!(rows.is_empty());
1076        assert_eq!(total, 0);
1077    }
1078
1079    #[tokio::test]
1080    async fn search_on_an_empty_table_is_empty_rather_than_an_error() {
1081        let db = Arc::new(Database::connect_in_memory().await.unwrap());
1082        let (rows, total) = Account::search(None, 50, 0, &db).await.unwrap();
1083        assert!(rows.is_empty());
1084        assert_eq!(total, 0);
1085    }
1086
1087    /// Two `newAccount` requests carrying the same account key must *both* come
1088    /// away with an account — RFC 8555 §7.3's find-or-create — even when they
1089    /// arrive close enough together that both find nothing and both insert.
1090    ///
1091    /// A file-backed database rather than `connect_in_memory`, which pins the
1092    /// pool to one connection and so serializes the callers out of the very
1093    /// interleaving this is about. The barrier is what makes the race
1094    /// deterministic rather than likely: every caller is released at the same
1095    /// point, and each one's lookup is an `.await`, so all eight `SELECT`s are
1096    /// issued before the first `INSERT` commits.
1097    ///
1098    /// `UNIQUE (profile, pubkey)` is what the losers then hit. Before the
1099    /// recovery in `find_or_create` that came back as a bare `sqlx::Error`,
1100    /// which `post_new_account` turns into `serverInternal` — a 500 for a
1101    /// request the RFC says must answer 200 with the existing account, leaving
1102    /// the client with no account at all.
1103    #[tokio::test]
1104    async fn concurrent_find_or_create_for_one_key_yields_one_account() {
1105        let file = std::env::temp_dir().join(format!("acme-proxy-test-{}.db", Uuid::new_v4()));
1106        let url = format!("sqlite://{}", file.display());
1107        let db = Arc::new(Database::connect(&url).await.unwrap());
1108
1109        const RACERS: usize = 8;
1110        let barrier = Arc::new(tokio::sync::Barrier::new(RACERS));
1111        let mut tasks = Vec::with_capacity(RACERS);
1112        for _ in 0..RACERS {
1113            let db = db.clone();
1114            let barrier = barrier.clone();
1115            tasks.push(tokio::spawn(async move {
1116                barrier.wait().await;
1117                Account::find_or_create(
1118                    "default",
1119                    &[7u8; 32],
1120                    vec![],
1121                    &ClientContext::default(),
1122                    &db,
1123                )
1124                .await
1125            }));
1126        }
1127
1128        let mut ids = Vec::with_capacity(RACERS);
1129        let mut created = 0;
1130        for task in tasks {
1131            let (account, is_new) = task
1132                .await
1133                .unwrap()
1134                .expect("losing the insert race is not an error");
1135            if is_new {
1136                created += 1;
1137            }
1138            ids.push(account.id);
1139        }
1140
1141        assert_eq!(created, 1, "exactly one caller may create the account");
1142        assert!(
1143            ids.windows(2).all(|pair| pair[0] == pair[1]),
1144            "every caller must be handed the same account: {ids:?}"
1145        );
1146
1147        db.pool.close().await;
1148        for suffix in ["", "-wal", "-shm"] {
1149            let _ = std::fs::remove_file(format!("{}{suffix}", file.display()));
1150        }
1151    }
1152}