Skip to main content

acme_proxy_protocol/acme/
issue.rs

1//! Certificate issuance as queued work.
2//!
3//! `finalize` checks the CSR, claims the order (`ready → processing`) and queues
4//! one of these rows **in the same transaction**; the job runner asks the
5//! backend to sign. RFC 8555 §7.4 already has the state this needs — an order is
6//! `processing` while "the certificate is being issued", and the client polls —
7//! and the `relay` backend has always answered that way.
8//!
9//! The point is where the key lives, not latency. Signing needs `ca.key`, a
10//! PKCS#11 login or a relay's upstream account, and only the `worker` role
11//! builds a backend, so the process parsing untrusted JWS and CSRs never holds
12//! one. This is the one place a backend is asked to issue.
13//!
14//! **One handler over every profile**, the [`SignerRevokeJob`] shape: a row names
15//! its order, the order names its profile, and the profile names the backend.
16//!
17//! How each answer settles the row:
18//!
19//! | Backend answer | Order | Job |
20//! |---|---|---|
21//! | a chain | `valid`, `certificate_issued`, notified | `Done` |
22//! | a chain this server cannot read | `invalid` via `abandon` | `Failed` |
23//! | a chain it could not store | unchanged | `Retry` |
24//! | `Processing` (relay) | unchanged — the relay job owns it | `Done` |
25//! | `BadCsr` | `invalid` with `badCSR` — a verdict | `Done` |
26//! | *(not asked)* account or an authorization deactivated since the claim | `invalid` with `unauthorized` | `Done` |
27//! | `Internal` | unchanged | `Retry`, then `invalid` via `abandon` |
28//!
29//! `BadCsr` makes the order `invalid` rather than `ready` again, although §7.4
30//! says a rejected CSR SHOULD leave it finalizable: the client was already told
31//! `processing`, and certbot, acme.sh and lego all poll for `valid` or
32//! `invalid` — an order back at `ready` would have them poll until they time
33//! out, with no error to show. `finalize` still runs the CSR/order match and the
34//! filter itself, so almost every bad CSR is still refused synchronously, with
35//! the order left `ready`.
36//!
37//! [`SignerRevokeJob`]: super::revoke::SignerRevokeJob
38
39use std::net::IpAddr;
40use std::sync::Arc;
41
42use base64::prelude::*;
43use tracing::{error, info, warn};
44
45use acme_proxy_core::audit::Actor;
46use acme_proxy_core::audit::AuditEvent;
47use acme_proxy_core::audit::AuditRecord;
48use acme_proxy_core::audit::ClientContext;
49use acme_proxy_core::error::Problem;
50use acme_proxy_jobs::auditor::Auditor;
51use acme_proxy_jobs::jobs::JobHandler;
52use acme_proxy_jobs::jobs::JobOutcome;
53use acme_proxy_jobs::jobs::JobSpec;
54use acme_proxy_signer::IssueOutcome;
55use acme_proxy_signer::RequestedValidity;
56use acme_proxy_signer::SignerBackend;
57use acme_proxy_signer::SignerError;
58use acme_proxy_signer::issuance::IssuanceError;
59use acme_proxy_signer::issuance::announce_issuance;
60use acme_proxy_signer::issuance::record_issuance;
61use acme_proxy_signer::issuance::record_issue_failure;
62use acme_proxy_store::account::Account;
63use acme_proxy_store::authz::Authorization;
64use acme_proxy_store::db::Database;
65use acme_proxy_store::job::Job;
66use acme_proxy_store::order::Order;
67use acme_proxy_store::status::AuthzStatus;
68use acme_proxy_store::status::OrderStatus;
69
70/// The `jobs.kind` one issuance is queued under.
71pub const SIGNER_ISSUE_KIND: &str = "signer_issue";
72
73/// The row asking a worker to issue the certificate `order` was just claimed
74/// for.
75///
76/// Keyed on the order, so the claim and the job identity are the same fact
77/// twice: the partial unique index refuses a second live row exactly as the
78/// `ready → processing` CAS refuses a second claim.
79///
80/// The CSR travels in the payload — it is public material, and the order row
81/// has no column for it — beside the two views of the client that asked:
82/// `client` is the audit row's context (address, reverse name, User-Agent,
83/// request id), `client_ip` the notification's rendering of the address, the
84/// same split `finalize` made when it answered inline. `deadline` is the
85/// order's own `expires`: past it the order is refused on read, so a
86/// certificate issued afterwards could never be collected.
87#[must_use]
88pub fn signer_issue_spec(
89    order: &Order,
90    csr_der: &[u8],
91    client: &ClientContext,
92    client_ip: Option<IpAddr>,
93) -> JobSpec {
94    JobSpec::now(SIGNER_ISSUE_KIND, order.id.to_string())
95        .with_payload(serde_json::json!({
96            "order_id": order.id.to_string(),
97            "profile": order.profile,
98            "csr": BASE64_URL_SAFE_NO_PAD.encode(csr_der),
99            "client": client.to_json(),
100            "client_ip": client_ip.map(|ip| acme_proxy_core::client::canonical(ip).to_string()),
101        }))
102        .with_deadline(Some(order.expires))
103}
104
105/// Issues the certificate a finalize request queued, at the backend of the
106/// order's profile.
107pub struct SignerIssueJob {
108    database: Arc<Database>,
109    audit: Arc<Auditor>,
110    signers: Vec<(String, Arc<dyn SignerBackend>)>,
111    notifiers: acme_proxy_jobs::notify::Notifiers,
112}
113
114impl SignerIssueJob {
115    /// `signers` is this generation's backend per profile name — empty in a
116    /// process that runs no worker, which never claims a row.
117    #[must_use]
118    pub fn new(
119        database: Arc<Database>,
120        audit: Arc<Auditor>,
121        signers: Vec<(String, Arc<dyn SignerBackend>)>,
122        notifiers: acme_proxy_jobs::notify::Notifiers,
123    ) -> Self {
124        Self {
125            database,
126            audit,
127            signers,
128            notifiers,
129        }
130    }
131
132    /// The backend this process mounts for `profile`, if any.
133    fn signer(&self, profile: &str) -> Option<&Arc<dyn SignerBackend>> {
134        super::mounted(&self.signers, profile)
135    }
136}
137
138/// Why `order` may no longer be issued, if it may not: its account or one of
139/// its authorizations has been deactivated since `finalize` claimed it.
140async fn authority_withdrawn(
141    order: &Order,
142    database: &Database,
143) -> Result<Option<&'static str>, sqlx::Error> {
144    let account =
145        Account::find_by_id(&order.profile, &order.account_id.to_string(), database).await?;
146    if account.is_none_or(|account| account.is_deactivated()) {
147        return Ok(Some(
148            "the account was deactivated before the certificate was issued",
149        ));
150    }
151    let authzs = Authorization::find_by_order(order.id, database).await?;
152    if authzs.len() != order.identifiers.len()
153        || authzs
154            .iter()
155            .any(|authz| authz.status != AuthzStatus::Valid)
156    {
157        return Ok(Some(
158            "an authorization was deactivated before the certificate was issued",
159        ));
160    }
161    Ok(None)
162}
163
164/// The client a row names, read back out of its payload.
165fn payload_client(job: &Job) -> ClientContext {
166    ClientContext::from_json(&job.payload["client"])
167}
168
169#[async_trait::async_trait]
170impl JobHandler for SignerIssueJob {
171    fn kind(&self) -> &'static str {
172        SIGNER_ISSUE_KIND
173    }
174
175    async fn run(&self, job: &Job) -> JobOutcome {
176        let payload = &job.payload;
177        let Some(order_id) = payload["order_id"].as_str() else {
178            return JobOutcome::Failed("the payload names no order".to_string());
179        };
180        let Some(csr_der) = payload["csr"]
181            .as_str()
182            .and_then(|csr| BASE64_URL_SAFE_NO_PAD.decode(csr).ok())
183        else {
184            return JobOutcome::Failed("the payload carries no readable CSR".to_string());
185        };
186        let mut order = match Order::find_by_id(order_id, &self.database).await {
187            Ok(Some(order)) => order,
188            Ok(None) => return JobOutcome::Failed("the order no longer exists".to_string()),
189            Err(error) => return JobOutcome::Retry(format!("reading the order failed: {error}")),
190        };
191        // Settled already: the row was redelivered after a lost lease, and
192        // there is nothing left for it to issue.
193        if order.status != OrderStatus::Processing {
194            return JobOutcome::Done;
195        }
196
197        // What `finalize` checked may have been withdrawn while the row
198        // waited: the account deactivated (§7.3.6, or an operator's
199        // `eab delete --deactivate-accounts`), or an authorization deactivated
200        // (§7.5.2) by a request that read the order just before it was
201        // claimed. Either one is a verdict on the order, recorded once.
202        match authority_withdrawn(&order, &self.database).await {
203            Ok(None) => {}
204            Ok(Some(detail)) => {
205                warn!(event = "order_finalize_authority_withdrawn", outcome = "failure", order_id = %order_id, detail = %detail);
206                let problem = Problem::unauthorized(detail);
207                if let Err(error) = order.mark_invalid(problem.to_value(), &self.database).await {
208                    error!(event = "order_mark_invalid_failed", outcome = "failure", order_id = %order_id, error = %error);
209                    return JobOutcome::Retry(format!("recording the refusal failed: {error}"));
210                }
211                self.audit
212                    .record(
213                        AuditRecord::new(
214                            AuditEvent::CertificateIssueFailed,
215                            &order.profile,
216                            Actor::acme(order.account_id),
217                        )
218                        .with_order(order.id, order.account_id, &order.identifiers)
219                        .with_client(payload_client(job))
220                        .with_reason("unauthorized")
221                        .with_detail(detail),
222                    )
223                    .await;
224                return JobOutcome::Done;
225            }
226            Err(error) => {
227                return JobOutcome::Retry(format!("reading the order's authority failed: {error}"));
228            }
229        }
230        let Some(signer) = self.signer(&order.profile) else {
231            return JobOutcome::Retry(format!(
232                "profile `{}` is not mounted by this process",
233                order.profile
234            ));
235        };
236
237        let client = payload_client(job);
238        // The order's own `notBefore`/`notAfter` (RFC 8555 §7.4), which the
239        // order object has always echoed back. The backend clamps or ignores
240        // them; see `RequestedValidity`.
241        let validity = RequestedValidity {
242            not_before: order.not_before,
243            not_after: order.not_after,
244        };
245        let issued = signer
246            .issue(order_id, &csr_der, &order.identifiers, validity)
247            .await;
248
249        match issued {
250            Ok(IssueOutcome::Issued(chain)) => {
251                match record_issuance(&mut order, chain, &self.database).await {
252                    Ok(serial) => {
253                        info!(event = "order_finalized", outcome = "success", order_id = %order_id, cert_serial = %serial);
254                        let dispatcher = self.notifiers.get(&order.profile);
255                        announce_issuance(
256                            &order,
257                            &serial,
258                            job.created_at,
259                            Actor::acme(order.account_id.to_string()),
260                            client,
261                            payload["client_ip"].as_str().map(str::to_string),
262                            &self.audit,
263                            dispatcher.as_deref(),
264                        )
265                        .await;
266                        JobOutcome::Done
267                    }
268                    // A certificate this server cannot read is one it cannot
269                    // revoke, and signing again produces another it cannot read:
270                    // the issuance failed, whoever retries it.
271                    Err(IssuanceError::Chain(error)) => {
272                        error!(event = "order_finalize_chain_unparsable", outcome = "failure", order_id = %order_id, error = %error);
273                        JobOutcome::Failed(format!("the issued chain is unparsable: {error}"))
274                    }
275                    Err(IssuanceError::Leaf(error)) => {
276                        error!(event = "order_finalize_leaf_unparsable", outcome = "failure", order_id = %order_id, error = %error);
277                        JobOutcome::Failed(format!("the issued certificate is unparsable: {error}"))
278                    }
279                    // A retry signs again, and the certificate this attempt
280                    // produced sits in no row — as it always did when this write
281                    // failed inline. The order is still `processing`, so the
282                    // retry is the only way it ever gets one.
283                    Err(IssuanceError::Persist(error)) => {
284                        error!(
285                            event = "order_finalize_persistence_failed",
286                            outcome = "failure",
287                            order_id = %order_id,
288                            error = %error
289                        );
290                        JobOutcome::Retry(format!("recording the certificate failed: {error}"))
291                    }
292                }
293            }
294            // A delegating backend took the CSR and resolves it elsewhere: the
295            // relay job owns the order from here, and writes the one audit row
296            // for this issuance when the upstream answers. It runs with no
297            // request in scope, so the client that asked is parked on the
298            // mapping row for it — see `UpstreamOrder::set_client`.
299            Ok(IssueOutcome::Processing) => {
300                if let Err(error) = acme_proxy_store::upstream_order::UpstreamOrder::set_client(
301                    order_id,
302                    &client,
303                    &self.database,
304                )
305                .await
306                {
307                    warn!(
308                        event = "upstream_order_client_context_failed",
309                        outcome = "failure",
310                        order_id = %order_id,
311                        error = %error
312                    );
313                }
314                info!(event = "order_finalize_delegated", outcome = "success", order_id = %order_id);
315                JobOutcome::Done
316            }
317            // The backend's verdict on the CSR: the order's answer, recorded
318            // once. Marked before the row is written, so a retry after a failed
319            // write does not audit the refusal twice.
320            Err(SignerError::BadCsr) => {
321                warn!(event = "order_finalize_bad_csr", outcome = "failure", order_id = %order_id);
322                let problem = Problem::bad_csr("CSR invalid or does not match order");
323                if let Err(error) = order.mark_invalid(problem.to_value(), &self.database).await {
324                    error!(event = "order_mark_invalid_failed", outcome = "failure", order_id = %order_id, error = %error);
325                    return JobOutcome::Retry(format!("recording the refusal failed: {error}"));
326                }
327                self.audit
328                    .record(
329                        AuditRecord::new(
330                            AuditEvent::CertificateIssueFailed,
331                            &order.profile,
332                            Actor::acme(order.account_id),
333                        )
334                        .with_order(order.id, order.account_id, &order.identifiers)
335                        .with_client(client)
336                        .with_reason("badCSR")
337                        .with_detail("the signer backend rejected the CSR"),
338                    )
339                    .await;
340                JobOutcome::Done
341            }
342            // Nothing decided anything about this CSR — a token, a script or
343            // an upstream that did not answer — so ask again, and let `abandon`
344            // retire the order once the budget is spent.
345            Err(SignerError::Internal(detail)) => {
346                error!(
347                    event = "order_finalize_issuance_failed",
348                    outcome = "failure",
349                    order_id = %order_id,
350                    detail = %detail
351                );
352                JobOutcome::Retry(detail)
353            }
354        }
355    }
356
357    /// The attempts ran out, the order expired under it, or the row could never
358    /// settle. An order left `processing` would be polled by its client until
359    /// it expired, saying nothing: record the failure instead, exactly once.
360    async fn abandon(&self, job: &Job, reason: &str) {
361        let Some(order_id) = job.payload["order_id"].as_str() else {
362            return;
363        };
364        warn!(
365            event = "order_finalize_abandoned",
366            outcome = "failure",
367            order_id = %order_id,
368            attempts = job.attempts,
369            reason = %reason,
370            "a queued issuance was given up; its order is marked invalid"
371        );
372        let mut order = match Order::find_by_id(order_id, &self.database).await {
373            Ok(Some(order)) if order.status == OrderStatus::Processing => order,
374            Ok(_) => return,
375            Err(error) => {
376                error!(event = "order_mark_invalid_failed", outcome = "failure", order_id = %order_id, error = %error);
377                return;
378            }
379        };
380        let account = order.account_id;
381        if let Err(error) = record_issue_failure(
382            &mut order,
383            &Problem::server_internal("Certificate issuance failed"),
384            reason,
385            Actor::acme(account),
386            payload_client(job),
387            &self.audit,
388            &self.database,
389        )
390        .await
391        {
392            error!(event = "order_mark_invalid_failed", outcome = "failure", order_id = %order_id, error = %error);
393        }
394    }
395}
396
397#[cfg(test)]
398mod tests {
399    use super::*;
400    use crate::acme::order::tests::{account, ready_order};
401    use acme_proxy_core::identifier::Identifier;
402
403    /// A signer answering `issue` with whatever the test set.
404    enum Answer {
405        BadCsr,
406        Internal,
407        Chain(&'static str),
408        Deferred,
409    }
410
411    struct Scripted(Answer);
412
413    #[async_trait::async_trait]
414    impl SignerBackend for Scripted {
415        async fn issue(
416            &self,
417            _order_id: &str,
418            _csr_der: &[u8],
419            _identifiers: &[Identifier],
420            _validity: RequestedValidity,
421        ) -> Result<IssueOutcome, SignerError> {
422            match &self.0 {
423                Answer::BadCsr => Err(SignerError::BadCsr),
424                Answer::Internal => Err(SignerError::Internal("the token is gone".into())),
425                Answer::Chain(chain) => Ok(IssueOutcome::Issued((*chain).to_string())),
426                Answer::Deferred => Ok(IssueOutcome::Processing),
427            }
428        }
429
430        async fn revoke(&self, _cert_der: &[u8], _reason: Option<u32>) -> Result<(), SignerError> {
431            Ok(())
432        }
433    }
434
435    fn client() -> ClientContext {
436        ClientContext {
437            ip: Some("203.0.113.9".to_string()),
438            ptr: Some("client.example.net".to_string()),
439            user_agent: Some("certbot/9".to_string()),
440            request_id: Some("req-1".to_string()),
441        }
442    }
443
444    /// A `processing` order, as `finalize` leaves it, and the row it queued.
445    async fn claimed(database: &Arc<Database>) -> (Order, Job) {
446        let account = account(database).await;
447        let (mut order, csr) = ready_order(database, &account).await;
448        assert!(order.claim_for_finalize(database).await.unwrap());
449        let spec = signer_issue_spec(
450            &order,
451            &BASE64_URL_SAFE_NO_PAD.decode(csr).unwrap(),
452            &client(),
453            Some("203.0.113.9".parse().unwrap()),
454        );
455        let job = Job {
456            kind: SIGNER_ISSUE_KIND.to_string(),
457            dedup_key: spec.key.clone(),
458            payload: spec.payload.clone(),
459            ..acme_proxy_store::testutil::job_fixture()
460        };
461        (order, job)
462    }
463
464    fn handler(database: &Arc<Database>, signer: Arc<dyn SignerBackend>) -> SignerIssueJob {
465        let (_tx, notifiers) = acme_proxy_jobs::notify::notifiers_channel(
466            acme_proxy_jobs::notify::DispatcherMap::new(),
467        );
468        SignerIssueJob::new(
469            database.clone(),
470            Arc::new(Auditor::offline(database.clone())),
471            vec![("default".to_string(), signer)],
472            notifiers,
473        )
474    }
475
476    async fn reload(database: &Database, order: &Order) -> Order {
477        Order::find_by_id(&order.id.to_string(), database)
478            .await
479            .unwrap()
480            .unwrap()
481    }
482
483    async fn audit_rows(database: &Database) -> Vec<acme_proxy_store::audit::AuditEntry> {
484        let query = acme_proxy_store::audit::AuditQuery {
485            limit: 50,
486            ..acme_proxy_store::audit::AuditQuery::default()
487        };
488        acme_proxy_store::audit::AuditEntry::search(&query, database)
489            .await
490            .unwrap()
491            .0
492    }
493
494    /// The worker signs: the order becomes `valid` with its certificate, and
495    /// the one `certificate_issued` row names the client that finalized — not
496    /// the worker, which has no request of its own.
497    #[tokio::test]
498    async fn a_signed_certificate_settles_the_order_and_names_the_client() {
499        let database = Arc::new(Database::connect_in_memory().await.unwrap());
500        let (order, job) = claimed(&database).await;
501        let ca = acme_proxy_signer::local_ca::LocalCa::generate_in_memory(
502            "ecdsa-p256",
503            90,
504            database.clone(),
505        )
506        .unwrap();
507
508        assert!(matches!(
509            handler(&database, Arc::new(ca)).run(&job).await,
510            JobOutcome::Done
511        ));
512        let stored = reload(&database, &order).await;
513        assert_eq!(stored.status, OrderStatus::Valid);
514        assert!(stored.certificate.is_some());
515
516        let rows = audit_rows(&database).await;
517        assert_eq!(rows.len(), 1, "{rows:?}");
518        assert_eq!(rows[0].event, "certificate_issued");
519        assert_eq!(rows[0].cert_serial, stored.cert_serial);
520        assert_eq!(rows[0].client_ip.as_deref(), Some("203.0.113.9"));
521        assert_eq!(rows[0].client_ptr.as_deref(), Some("client.example.net"));
522        assert_eq!(rows[0].request_id.as_deref(), Some("req-1"));
523    }
524
525    /// The backend's own CSR verdict is the order's answer: `invalid` with a
526    /// `badCSR` document the polling client reads, one row, and no retry.
527    #[tokio::test]
528    async fn a_csr_the_backend_rejects_invalidates_the_order_once() {
529        let database = Arc::new(Database::connect_in_memory().await.unwrap());
530        let (order, job) = claimed(&database).await;
531
532        assert!(matches!(
533            handler(&database, Arc::new(Scripted(Answer::BadCsr)))
534                .run(&job)
535                .await,
536            JobOutcome::Done
537        ));
538        let stored = reload(&database, &order).await;
539        assert_eq!(stored.status, OrderStatus::Invalid);
540        let error = stored.error.unwrap();
541        assert_eq!(error["type"], "urn:ietf:params:acme:error:badCSR");
542        assert_eq!(error["detail"], "CSR invalid or does not match order");
543
544        let rows = audit_rows(&database).await;
545        assert_eq!(rows.len(), 1);
546        assert_eq!(rows[0].event, "certificate_issue_failed");
547        assert_eq!(rows[0].reason.as_deref(), Some("badCSR"));
548        assert_eq!(rows[0].client_ip.as_deref(), Some("203.0.113.9"));
549    }
550
551    /// A backend that did not answer decided nothing: the job retries and the
552    /// order stays `processing`. Only `abandon` — once the budget is spent —
553    /// retires it, with one row carrying the backend's own reason.
554    #[tokio::test]
555    async fn a_backend_failure_retries_and_is_abandoned_once() {
556        let database = Arc::new(Database::connect_in_memory().await.unwrap());
557        let (order, job) = claimed(&database).await;
558        let handler = handler(&database, Arc::new(Scripted(Answer::Internal)));
559
560        let JobOutcome::Retry(reason) = handler.run(&job).await else {
561            panic!("an internal failure is retried")
562        };
563        assert_eq!(reason, "the token is gone");
564        assert_eq!(
565            reload(&database, &order).await.status,
566            OrderStatus::Processing
567        );
568        assert!(audit_rows(&database).await.is_empty());
569
570        handler.abandon(&job, &reason).await;
571        let stored = reload(&database, &order).await;
572        assert_eq!(stored.status, OrderStatus::Invalid);
573        assert_eq!(
574            stored.error.unwrap()["detail"],
575            "Certificate issuance failed"
576        );
577        let rows = audit_rows(&database).await;
578        assert_eq!(rows.len(), 1);
579        assert_eq!(rows[0].reason.as_deref(), Some("serverInternal"));
580        assert_eq!(rows[0].detail.as_deref(), Some("the token is gone"));
581
582        // A second retirement — a redelivered row — finds nothing to do.
583        handler.abandon(&job, &reason).await;
584        assert_eq!(audit_rows(&database).await.len(), 1);
585    }
586
587    /// A chain this server cannot read is a certificate it could never
588    /// revoke, and signing again would produce another: the row fails for good.
589    #[tokio::test]
590    async fn an_unreadable_chain_fails_the_row() {
591        let database = Arc::new(Database::connect_in_memory().await.unwrap());
592        let (order, job) = claimed(&database).await;
593
594        let outcome = handler(&database, Arc::new(Scripted(Answer::Chain("not a chain"))))
595            .run(&job)
596            .await;
597        let JobOutcome::Failed(reason) = outcome else {
598            panic!("an unreadable chain is permanent")
599        };
600        assert!(reason.contains("unparsable"), "{reason}");
601        let stored = reload(&database, &order).await;
602        assert!(stored.certificate.is_none());
603    }
604
605    /// A delegating backend owns the order from here: the row is done, the
606    /// order still `processing` for the relay to settle.
607    #[tokio::test]
608    async fn a_deferred_issuance_leaves_the_order_to_its_backend() {
609        let database = Arc::new(Database::connect_in_memory().await.unwrap());
610        let (order, job) = claimed(&database).await;
611
612        assert!(matches!(
613            handler(&database, Arc::new(Scripted(Answer::Deferred)))
614                .run(&job)
615                .await,
616            JobOutcome::Done
617        ));
618        assert_eq!(
619            reload(&database, &order).await.status,
620            OrderStatus::Processing
621        );
622        assert!(audit_rows(&database).await.is_empty());
623    }
624
625    /// A redelivered row whose order already settled signs nothing.
626    #[tokio::test]
627    async fn an_order_no_longer_processing_is_not_issued_again() {
628        let database = Arc::new(Database::connect_in_memory().await.unwrap());
629        let (mut order, job) = claimed(&database).await;
630        let settled = serde_json::json!({"type": "urn:ietf:params:acme:error:serverInternal"});
631        assert!(
632            order
633                .mark_invalid(settled.clone(), &database)
634                .await
635                .unwrap()
636        );
637
638        assert!(matches!(
639            handler(&database, Arc::new(Scripted(Answer::Internal)))
640                .run(&job)
641                .await,
642            JobOutcome::Done
643        ));
644        let stored = reload(&database, &order).await;
645        assert_eq!(stored.status, OrderStatus::Invalid);
646        assert_eq!(stored.error, Some(settled));
647    }
648
649    /// The account was deactivated while the row waited — by its client
650    /// (§7.3.6), or by `eab delete --deactivate-accounts`. The backend is never
651    /// asked: `Scripted(Internal)` would have answered `Retry`.
652    #[tokio::test]
653    async fn a_deactivated_account_is_not_issued_for() {
654        let database = Arc::new(Database::connect_in_memory().await.unwrap());
655        let (order, job) = claimed(&database).await;
656        Account::find_by_id("default", &order.account_id.to_string(), &database)
657            .await
658            .unwrap()
659            .unwrap()
660            .deactivate(&database)
661            .await
662            .unwrap();
663
664        assert!(matches!(
665            handler(&database, Arc::new(Scripted(Answer::Internal)))
666                .run(&job)
667                .await,
668            JobOutcome::Done
669        ));
670        let stored = reload(&database, &order).await;
671        assert_eq!(stored.status, OrderStatus::Invalid);
672        assert_eq!(
673            stored.error.unwrap()["type"],
674            "urn:ietf:params:acme:error:unauthorized"
675        );
676        let rows = audit_rows(&database).await;
677        assert_eq!(rows.len(), 1);
678        assert_eq!(rows[0].reason.as_deref(), Some("unauthorized"));
679    }
680
681    /// An authorization deactivated by a request that read the order just
682    /// before `finalize` claimed it (§7.5.2): the deactivation committed, so
683    /// the certificate must not follow.
684    #[tokio::test]
685    async fn an_authorization_deactivated_after_the_claim_is_not_issued_for() {
686        let database = Arc::new(Database::connect_in_memory().await.unwrap());
687        let (order, job) = claimed(&database).await;
688        let authz = &Authorization::find_by_order(order.id, &database)
689            .await
690            .unwrap()[0];
691        assert!(
692            Authorization::set_deactivated(authz.id, database.raw_pool())
693                .await
694                .unwrap()
695        );
696
697        assert!(matches!(
698            handler(&database, Arc::new(Scripted(Answer::Internal)))
699                .run(&job)
700                .await,
701            JobOutcome::Done
702        ));
703        assert_eq!(reload(&database, &order).await.status, OrderStatus::Invalid);
704    }
705
706    /// A profile this process does not mount is a question for another
707    /// process — or the next generation of this one — so it is asked again;
708    /// a row that cannot be read is not.
709    #[tokio::test]
710    async fn an_unmounted_profile_retries_and_a_broken_row_fails() {
711        let database = Arc::new(Database::connect_in_memory().await.unwrap());
712        let (_, job) = claimed(&database).await;
713        let (_tx, notifiers) = acme_proxy_jobs::notify::notifiers_channel(
714            acme_proxy_jobs::notify::DispatcherMap::new(),
715        );
716        let elsewhere = SignerIssueJob::new(
717            database.clone(),
718            Arc::new(Auditor::offline(database.clone())),
719            Vec::new(),
720            notifiers,
721        );
722        assert!(matches!(elsewhere.run(&job).await, JobOutcome::Retry(_)));
723
724        for payload in [
725            serde_json::json!({}),
726            serde_json::json!({ "order_id": job.payload["order_id"], "csr": "!!" }),
727            serde_json::json!({ "order_id": uuid::Uuid::nil().to_string(), "csr": "MAA" }),
728        ] {
729            let broken = Job {
730                payload,
731                ..job.clone()
732            };
733            assert!(matches!(
734                elsewhere.run(&broken).await,
735                JobOutcome::Failed(_)
736            ));
737        }
738        // `abandon` on a row naming no order has nothing to retire.
739        elsewhere
740            .abandon(
741                &Job {
742                    payload: serde_json::json!({}),
743                    ..job
744                },
745                "gone",
746            )
747            .await;
748    }
749}