Skip to main content

acme_proxy_protocol/acme/
account.rs

1//! Accounts (RFC 8555 §7.3): creation, update, deactivation and key rollover.
2//!
3//! [`AccountService`] holds the operations of one endpoint, as `OrderService`
4//! does for orders; the handlers are its HTTP edge. [`deactivate`] and
5//! [`update_contact`] are free functions because the operator front ends call
6//! them too, so a contact one surface would refuse cannot be stored through
7//! another.
8
9use std::net::IpAddr;
10use std::sync::Arc;
11
12use axum::http::StatusCode;
13use serde::Deserialize;
14use tracing::{error, info, warn};
15use uuid::Uuid;
16
17use super::access::signer_account;
18use super::error::Error;
19use super::rules::validate_contacts;
20use crate::profile::Profile;
21use acme_proxy_core::audit::RequestContext;
22use acme_proxy_core::eab;
23use acme_proxy_core::error::Problem;
24use acme_proxy_core::jws::ProtectedHeader;
25use acme_proxy_core::jws::signature::spki_to_jwk;
26use acme_proxy_core::key_change;
27use acme_proxy_jobs::auditor::Auditor;
28use acme_proxy_jobs::notify::AccountCreatedData;
29use acme_proxy_jobs::notify::AccountDeactivatedData;
30use acme_proxy_jobs::notify::NotifyDispatcher;
31use acme_proxy_jobs::notify::NotifyEvent;
32use acme_proxy_store::account::Account;
33use acme_proxy_store::account::pubkey_fingerprint;
34use acme_proxy_store::db::Database;
35use acme_proxy_store::eab::Eab;
36use acme_proxy_store::order::Order;
37
38/// Every field is optional: real clients may omit `contact`, and the two flags
39/// default to `false`.
40#[derive(Debug, Default, Deserialize)]
41#[serde(default)]
42pub struct NewAccountPayload {
43    pub contact: Vec<String>,
44    #[serde(alias = "termsOfServiceAgreed")]
45    pub terms_of_service_agreed: bool,
46    #[serde(alias = "onlyReturnExisting")]
47    pub only_return_existing: bool,
48    #[serde(alias = "externalAccountBinding")]
49    pub external_account_binding: Option<eab::EabJws>,
50}
51
52/// Fields an account-update request may carry.
53#[derive(Debug, Default, Deserialize)]
54#[serde(default)]
55pub struct UpdateAccountPayload {
56    pub contact: Option<Vec<String>>,
57    pub status: Option<String>,
58}
59
60/// The account-side operations of one endpoint — the
61/// [`OrderService`](super::OrderService) shape.
62pub struct AccountService<'a> {
63    pub database: &'a Arc<Database>,
64    pub audit: &'a Auditor,
65    pub profile: &'a Profile,
66}
67
68impl AccountService<'_> {
69    /// `newAccount` (RFC 8555 §7.3): finds the account `pubkey` holds at this
70    /// endpoint, or creates one. The `bool` is whether it was created — `201`
71    /// against `200` at the edge.
72    ///
73    /// `onlyReturnExisting` (§7.3.1) looks up and never creates. A refusal to
74    /// agree to configured terms is [`Error::TermsNotAgreed`], since the
75    /// response it owes carries a `Link` a problem document cannot.
76    pub async fn new_account(
77        &self,
78        payload: NewAccountPayload,
79        header: &ProtectedHeader,
80        pubkey: &[u8],
81        client_ip: Option<IpAddr>,
82        request: &RequestContext,
83    ) -> Result<(Account, bool), Error> {
84        let (database, profile, audit) = (self.database, self.profile, self.audit);
85
86        if payload.only_return_existing {
87            let account = Account::find_by_pubkey(&profile.name, pubkey, database)
88                .await
89                .map_err(|error| {
90                    // Distinct from `acme::access`'s `account_lookup_failed`: same
91                    // query, but this one is `newAccount`'s §7.3.1 lookup, not the
92                    // one that resolves the signer of an order-side request.
93                    error!(event = "account_only_return_existing_lookup_failed", outcome = "failure", error = %error);
94                    Problem::server_internal("Account lookup failed")
95                })?
96                .ok_or_else(|| {
97                    info!(event = "account_only_return_existing_miss", outcome = "failure");
98                    Problem::account_does_not_exist("No account for this key")
99                })?;
100
101            refuse_deactivated(&account, true)?;
102            return Ok((account, false));
103        }
104
105        // Checked before the EAB, so a client with a typo'd address hears about
106        // the typo rather than having its credential refused for a reason that
107        // is not the credential's.
108        validate_contacts(&payload.contact)?;
109
110        // RFC 8555 §7.3.3: a client agrees to the terms by setting
111        // `termsOfServiceAgreed`, and §6.7's `userActionRequired` is the refusal
112        // when it has not. Enforced only when `meta.termsOfService` is configured —
113        // §7.3.3 ties the requirement to the directory advertising a ToS, so an
114        // endpoint with none must not demand agreement to something it never named.
115        if !profile.meta.terms_of_service.is_empty() && !payload.terms_of_service_agreed {
116            warn!(event = "account_terms_not_agreed", outcome = "failure");
117            return Err(Error::TermsNotAgreed);
118        }
119
120        let eab_kid = if profile.eab.enabled {
121            Some(
122                verify_eab(
123                    payload.external_account_binding.as_ref(),
124                    header,
125                    &profile.name,
126                    database,
127                )
128                .await?,
129            )
130        } else {
131            None
132        };
133
134        // Resolved before the write, and only on this path: `onlyReturnExisting`
135        // returned above without ever creating anything, and `find_or_create`
136        // stamps these columns on the creating branch alone — so a PTR lookup for a
137        // request that turns out to find an existing account is wasted, but a
138        // lookup after the INSERT would need a second UPDATE to record it.
139        let client = audit.client(request).await;
140        // The credential that authorized this registration and the agreement to
141        // the terms are columns of the insert, not writes that follow it: an
142        // account bound to no credential escapes `eab delete
143        // --deactivate-accounts` and fails every `eab` filter rule closed.
144        // Recorded only where the endpoint has terms to agree to — the check
145        // above already refused a request that did not agree, so reaching here
146        // with a ToS configured means the client set the flag.
147        let registration = acme_proxy_store::account::Registration {
148            eab_kid,
149            terms_agreed: !profile.meta.terms_of_service.is_empty(),
150        };
151        let (account, created) = Account::find_or_register(
152            &profile.name,
153            pubkey,
154            payload.contact,
155            &registration,
156            &client,
157            database,
158        )
159        .await
160        .map_err(|error| {
161            error!(event = "account_creation_failed", outcome = "failure", error = %error);
162            Problem::server_internal("Account persistence failed")
163        })?;
164
165        // Only on the found branch: an account this request just created is never
166        // deactivated, and asking would be reading a column we wrote a line ago.
167        if !created {
168            refuse_deactivated(&account, false)?;
169        }
170
171        if created {
172            profile
173                .notify
174                .dispatch(NotifyEvent::AccountCreated(AccountCreatedData {
175                    profile: profile.name.clone(),
176                    account_id: account.id.to_string(),
177                    contact: account.contact.clone(),
178                    client_ip: client_ip
179                        .map(|ip| acme_proxy_core::client::canonical(ip).to_string()),
180                }))
181                .await;
182        }
183
184        let status = if created {
185            StatusCode::CREATED
186        } else {
187            StatusCode::OK
188        };
189        // Two names, because §7.3's find-or-create makes them two different
190        // events: one registered a key, the other recognised one. An operator
191        // counting registrations must not have to filter a field out of the
192        // count.
193        if created {
194            info!(event = "account_created", outcome = "success", account_id = %account.id, status = %status);
195        } else {
196            info!(event = "account_found", outcome = "success", account_id = %account.id, status = %status);
197        }
198        Ok((account, created))
199    }
200
201    /// `POST /acct/{id}` (RFC 8555 §7.3.2, §7.3.6): replaces the contact list, or
202    /// deactivates the account, for a request signed by the account's own key.
203    pub async fn update(
204        &self,
205        id: &str,
206        payload: UpdateAccountPayload,
207        pubkey: &[u8],
208        client_ip: Option<IpAddr>,
209    ) -> Result<Account, Error> {
210        let (database, profile) = (self.database, self.profile);
211
212        let mut account = match Account::find_by_id(&profile.name, id, database).await {
213            Ok(Some(account)) => account,
214            Ok(None) => {
215                warn!(event = "account_not_found", outcome = "failure", account_id = %id);
216                return Err(Problem::account_does_not_exist("Unknown account").into());
217            }
218            Err(error) => {
219                error!(
220                    event = "account_update_lookup_failed",
221                    outcome = "failure",
222                    account_id = %id,
223                    error = %error
224                );
225                return Err(Problem::server_internal("Account lookup failed").into());
226            }
227        };
228
229        if account.pubkey != pubkey {
230            warn!(
231                event = "account_key_mismatch",
232                outcome = "failure",
233                account_id = %id,
234                expected_pubkey_fp = %pubkey_fingerprint(&account.pubkey),
235                actual_pubkey_fp = %pubkey_fingerprint(pubkey)
236            );
237            return Err(Problem::unauthorized("Signed by a different account key").into());
238        }
239
240        if account.is_deactivated() {
241            warn!(
242                event = "account_deactivated_modify_refused",
243                outcome = "failure",
244                account_id = %id
245            );
246            return Err(Problem::unauthorized("Account deactivated").into());
247        }
248
249        if let Some(status) = payload.status {
250            if status != acme_proxy_store::account::DEACTIVATED {
251                warn!(event = "account_update_bad_status", outcome = "failure", account_id = %id, status = %status);
252                return Err(Problem::malformed("Only 'deactivated' status is accepted").into());
253            }
254            deactivate(
255                &mut account,
256                database,
257                Some(&profile.notify),
258                client_ip.map(|ip| acme_proxy_core::client::canonical(ip).to_string()),
259            )
260            .await
261            .map_err(|error| {
262                error!(
263                    event = "account_deactivation_failed",
264                    outcome = "failure",
265                    account_id = %id,
266                    error = %error
267                );
268                Problem::server_internal("Account update failed")
269            })?;
270            info!(
271                event = "account_deactivated",
272                outcome = "success",
273                account_id = %id
274            );
275        } else if let Some(contact) = payload.contact {
276            update_contact(&mut account, contact, database)
277                .await
278                .map_err(|error| match error {
279                    ContactUpdateError::Refused(problem) => problem,
280                    ContactUpdateError::Database(error) => {
281                        error!(
282                            event = "account_contact_update_failed",
283                            outcome = "failure",
284                            account_id = %id,
285                            error = %error
286                        );
287                        Problem::server_internal("Account update failed")
288                    }
289                })?;
290            info!(
291                event = "account_contact_updated",
292                outcome = "success",
293                account_id = %id
294            );
295        }
296
297        info!(
298            event = "account_updated",
299            outcome = "success",
300            account_id = %id
301        );
302        Ok(account)
303    }
304
305    /// `keyChange` (RFC 8555 §7.3.5): moves the signer's account onto the key the
306    /// nested JWS proves possession of.
307    ///
308    /// A new key already held by another account is
309    /// [`Error::KeyChangeConflict`], carrying the holder so the edge can answer
310    /// with its `Location`.
311    pub async fn key_change(
312        &self,
313        cached: Option<Account>,
314        old_pubkey: &[u8],
315        header: &ProtectedHeader,
316        inner_jws: &key_change::KeyChangeJws,
317    ) -> Result<Account, Error> {
318        let (database, profile) = (self.database, self.profile);
319        let base = &profile.base_url;
320
321        let mut old_account = signer_account(cached, &profile.name, old_pubkey, database).await?;
322
323        let inner_header = key_change::parse_header(inner_jws, &header.url)
324            .map_err(key_change::key_change_problem)?;
325        let new_pubkey = key_change::verify_signature(inner_jws, &inner_header)
326            .map_err(key_change::key_change_problem)?;
327
328        let account_url = format!("{base}/acct/{}", old_account.id);
329        let old_key_jwk = spki_to_jwk(&old_account.pubkey).map_err(|error| {
330            error!(event = "key_change_old_key_decode_failed", outcome = "failure", account_id = %old_account.id, error = %error);
331            Problem::server_internal("Stored account key could not be decoded")
332        })?;
333        key_change::verify_payload(inner_jws, &account_url, &old_key_jwk)
334            .map_err(key_change::key_change_problem)?;
335
336        let conflicting_account = Account::find_by_pubkey(&profile.name, &new_pubkey, database)
337            .await
338            .map_err(|error| {
339                error!(event = "key_change_conflict_lookup_failed", outcome = "failure", account_id = %old_account.id, error = %error);
340                Problem::server_internal("Account lookup failed")
341            })?;
342        if let Some(existing) = conflicting_account {
343            warn!(event = "key_change_conflict", outcome = "failure", account_id = %old_account.id, conflicting_account_id = %existing.id);
344            return Err(Error::KeyChangeConflict {
345                holder: existing.id,
346            });
347        }
348
349        if let Err(error) = old_account.update_pubkey(&new_pubkey, database).await {
350            // The check above and this write are two statements, and another
351            // rollover onto the same key can land between them — at which point
352            // `UNIQUE (profile, pubkey)` is what says so. §7.3.5 gives that case a
353            // status and a `Location`, so answering `serverInternal` here would
354            // report "something went wrong" for a condition the RFC describes
355            // exactly, and deny the client the one field it needs to recover.
356            //
357            // Re-read rather than reuse `new_pubkey`'s earlier (empty) lookup: the
358            // account that won is by definition committed now.
359            if acme_proxy_store::account::is_pubkey_conflict(&error)
360                && let Ok(Some(winner)) =
361                    Account::find_by_pubkey(&profile.name, &new_pubkey, database).await
362            {
363                warn!(event = "key_change_conflict", outcome = "failure", account_id = %old_account.id, conflicting_account_id = %winner.id);
364                return Err(Error::KeyChangeConflict { holder: winner.id });
365            }
366            error!(event = "key_change_persist_failed", outcome = "failure", account_id = %old_account.id, error = %error);
367            return Err(Problem::server_internal("Account key update failed").into());
368        }
369
370        info!(event = "account_key_changed", outcome = "success", account_id = %old_account.id);
371        Ok(old_account)
372    }
373
374    /// The orders-list resource (RFC 8555 §7.1.2.1) of account `id`, for a request
375    /// signed by that account: its orders still worth handing back.
376    pub async fn orders(
377        &self,
378        cached: Option<Account>,
379        pubkey: &[u8],
380        id: &str,
381    ) -> Result<Vec<Order>, Error> {
382        let (database, profile) = (self.database, self.profile);
383        let account = signer_account(cached, &profile.name, pubkey, database).await?;
384        if account.id.to_string() != id {
385            warn!(
386                event = "account_orders_ownership_mismatch",
387                outcome = "failure",
388                requested = %id,
389                signer = %account.id
390            );
391            return Err(Problem::unauthorized("Not your account").into());
392        }
393
394        // RFC 8555 §7.1.2.1's filtered view — expired and `invalid` orders are not
395        // URLs worth handing back (see `find_active_by_account`).
396        Ok(Order::find_active_by_account(account.id, database)
397            .await
398            .map_err(|error| {
399                error!(
400                    event = "account_orders_lookup_failed",
401                    outcome = "failure",
402                    account_id = %id,
403                    error = %error
404                );
405                Problem::server_internal("Order list failed")
406            })?)
407    }
408}
409
410/// Refuses a request signed by the key of a deactivated account.
411///
412/// RFC 8555 §7.3.6: "If a server receives a POST or POST-as-GET from a
413/// deactivated account, it MUST return an error response with status code 401
414/// (Unauthorized) and type `urn:ietf:params:acme:error:unauthorized`." Every
415/// order-side endpoint and `keyChange` get this through `signer_account`, and
416/// `update` checks it directly — `newAccount` was the one path that did not, on
417/// either of its branches, so a deactivated key could still confirm its account
418/// existed and read its own `contact` list back out of the `Location` response.
419///
420/// The wording matches `signer_account`'s byte for byte, so a deactivated key
421/// gets one answer wherever it knocks.
422fn refuse_deactivated(account: &Account, only_return_existing: bool) -> Result<(), Problem> {
423    if !account.is_deactivated() {
424        return Ok(());
425    }
426    warn!(
427        event = "account_deactivated_registration_refused",
428        outcome = "failure",
429        account_id = %account.id,
430        only_return_existing = only_return_existing
431    );
432    Err(Problem::unauthorized("Account is deactivated"))
433}
434
435/// Verifies the RFC 8555 §7.3.4 External Account Binding.
436pub async fn verify_eab(
437    eab_jws: Option<&eab::EabJws>,
438    header: &ProtectedHeader,
439    profile: &str,
440    database: &Arc<Database>,
441) -> Result<Uuid, Problem> {
442    let eab_jws = eab_jws.ok_or_else(|| {
443        warn!(event = "eab_required", outcome = "failure", profile);
444        Problem::external_account_required("This server requires External Account Binding")
445    })?;
446
447    let outer_jwk = header.jwk.as_ref().ok_or_else(|| {
448        warn!(event = "eab_missing_jwk", outcome = "failure", profile);
449        Problem::malformed("newAccount requires an embedded jwk for External Account Binding")
450    })?;
451
452    let eab_header = eab::parse_header(eab_jws, &header.url).map_err(eab::eab_problem)?;
453
454    let key = Eab::find_by_kid(&eab_header.kid, profile, database)
455        .await
456        .map_err(|error| {
457            error!(event = "eab_lookup_failed", outcome = "failure", kid = %eab_header.kid, error = %error);
458            Problem::server_internal("External Account Binding lookup failed")
459        })?
460        .filter(Eab::is_active)
461        .ok_or_else(|| {
462            warn!(event = "eab_unknown_or_revoked_kid", outcome = "failure", kid = %eab_header.kid);
463            Problem::unauthorized("Unknown or revoked External Account Binding key")
464        })?;
465
466    eab::verify_payload_and_signature(eab_jws, &key.secret, outer_jwk).map_err(eab::eab_problem)?;
467
468    info!(event = "eab_verified", outcome = "success", kid = %key.kid);
469    Ok(key.kid)
470}
471
472/// Deactivates `account` (RFC 8555 §7.3.6), then queues `account_deactivated`.
473///
474/// Shared by the account's own request and an operator's: the notification is
475/// about the account, whoever shut it. `notify` is the account's profile's
476/// dispatcher where this process has one, and `client_ip` whoever asked — the
477/// client, the operator, or nobody. Logs nothing; each caller names its own
478/// events.
479pub async fn deactivate(
480    account: &mut Account,
481    database: &Database,
482    notify: Option<&NotifyDispatcher>,
483    client_ip: Option<String>,
484) -> Result<(), sqlx::Error> {
485    account.deactivate(database).await?;
486    if let Some(dispatcher) = notify {
487        dispatcher
488            .dispatch(NotifyEvent::AccountDeactivated(AccountDeactivatedData {
489                profile: account.profile.clone(),
490                account_id: account.id.to_string(),
491                client_ip,
492            }))
493            .await;
494    }
495    Ok(())
496}
497
498/// Why [`update_contact`] did not write.
499#[derive(Debug, thiserror::Error)]
500pub enum ContactUpdateError {
501    /// A contact RFC 8555 §7.3 refuses, as the problem `newAccount` would answer.
502    #[error("{0}")]
503    Refused(Problem),
504    #[error("the contact list could not be stored")]
505    Database(#[from] sqlx::Error),
506}
507
508/// Replaces `account`'s contact list, refusing one `newAccount` would refuse.
509///
510/// One check for every surface — the ACME update, the admin API, the panel and
511/// the CLI — so no front end can store a contact another would reject.
512pub async fn update_contact(
513    account: &mut Account,
514    contact: Vec<String>,
515    database: &Database,
516) -> Result<(), ContactUpdateError> {
517    validate_contacts(&contact).map_err(ContactUpdateError::Refused)?;
518    account
519        .update_contact(contact, database)
520        .await
521        .map_err(ContactUpdateError::Database)
522}