1use std::sync::Arc;
25use std::time::Duration;
26
27use tracing::{error, info, warn};
28use uuid::Uuid;
29
30use acme_proxy_core::audit::Actor;
31use acme_proxy_core::audit::AuditEvent;
32use acme_proxy_core::audit::AuditRecord;
33use acme_proxy_core::audit::ClientContext;
34use acme_proxy_core::audit::RequestContext;
35use acme_proxy_core::error::Problem;
36use acme_proxy_jobs::auditor::Auditor;
37use acme_proxy_jobs::jobs::JobHandler;
38use acme_proxy_jobs::jobs::JobOutcome;
39use acme_proxy_jobs::jobs::JobQueue;
40use acme_proxy_jobs::jobs::JobSpec;
41use acme_proxy_jobs::notify::CertificateRevokedData;
42use acme_proxy_jobs::notify::NotifyDispatcher;
43use acme_proxy_jobs::notify::NotifyEvent;
44use acme_proxy_signer::RevocationRoute;
45use acme_proxy_signer::SignerBackend;
46use acme_proxy_signer::SignerError;
47use acme_proxy_store::account::Account;
48use acme_proxy_store::db::Database;
49use acme_proxy_store::job::Job;
50use acme_proxy_store::order::Order;
51
52pub enum Revoker<'a> {
54 Backend(&'a dyn SignerBackend),
57 Ledger {
65 issuer: &'a str,
67 jobs: &'a JobQueue,
68 },
69 Queued { jobs: &'a JobQueue, wait: Duration },
75}
76
77impl<'a> Revoker<'a> {
78 #[must_use]
81 pub fn for_route(route: &'a RevocationRoute, jobs: &'a JobQueue, wait: Duration) -> Self {
82 match route {
83 RevocationRoute::Ledger { issuer } => Revoker::Ledger { issuer, jobs },
84 RevocationRoute::Delegated => Revoker::Queued { jobs, wait },
85 }
86 }
87}
88
89pub enum Client<'a> {
91 Request(&'a RequestContext),
95 Resolved(ClientContext),
98}
99
100impl Client<'_> {
101 async fn resolve(self, audit: &Auditor) -> ClientContext {
102 match self {
103 Client::Request(request) => audit.client(request).await,
104 Client::Resolved(client) => client,
105 }
106 }
107}
108
109#[derive(Debug, thiserror::Error)]
111pub enum RevokeError {
112 #[error("{}", .0.detail())]
115 Refused(Problem),
116 #[error("no such order")]
118 NotFound,
119 #[error("order has no issued certificate")]
121 NotIssued,
122 #[error("certificate already revoked")]
123 AlreadyRevoked,
124 #[error("unsupported revocation reason code {0}")]
125 BadReason(u32),
126 #[error("signer error: {}", signer_detail(.0))]
129 Signer(SignerError),
130 #[error("database error: {0}")]
132 Database(sqlx::Error),
133 #[error("the revocation is queued as job {job} and has not completed yet")]
136 Pending { job: Uuid },
137 #[error("the revocation failed (job {job}): {reason}")]
140 Abandoned { job: Uuid, reason: String },
141 #[error("internal error: {0}")]
143 Internal(String),
144}
145
146pub fn signer_detail(error: &SignerError) -> String {
152 match error {
153 SignerError::Internal(detail) => detail.clone(),
154 SignerError::BadCsr => "unexpected badCsr from revoke".to_string(),
155 }
156}
157
158impl From<sqlx::Error> for RevokeError {
159 fn from(error: sqlx::Error) -> Self {
160 Self::Database(error)
161 }
162}
163
164impl From<RevokeError> for Problem {
166 fn from(error: RevokeError) -> Self {
167 match error {
168 RevokeError::Refused(problem) => problem,
169 RevokeError::NotFound | RevokeError::NotIssued => {
170 Problem::malformed("Unknown certificate")
171 }
172 RevokeError::AlreadyRevoked => Problem::already_revoked("Certificate already revoked"),
173 RevokeError::BadReason(reason) => Problem::bad_revocation_reason(format!(
174 "Unsupported revocation reason code {reason}"
175 )),
176 RevokeError::Pending { .. } => {
177 Problem::service_unavailable("The revocation is queued; retry to confirm it")
178 }
179 RevokeError::Signer(_)
180 | RevokeError::Database(_)
181 | RevokeError::Internal(_)
182 | RevokeError::Abandoned { .. } => Problem::server_internal("Revocation failed"),
183 }
184 }
185}
186
187pub struct Revocations<'a> {
189 pub database: &'a Arc<Database>,
190 pub audit: &'a Auditor,
191 pub notify: Option<&'a NotifyDispatcher>,
195 pub revoker: Revoker<'a>,
196}
197
198#[derive(Clone, Copy, PartialEq, Eq)]
200enum Refusals {
201 Audited,
204 Silent,
206}
207
208impl Revocations<'_> {
209 pub async fn revoke_certificate(
217 &self,
218 profile: &str,
219 cert_der: &[u8],
220 reason: Option<u32>,
221 pubkey: &[u8],
222 account: Option<Account>,
223 request: &RequestContext,
224 ) -> Result<Order, RevokeError> {
225 let database = self.database;
226 let (serial_hex, _) = acme_proxy_core::cert::cert_serial_and_spki(cert_der).map_err(|error| {
227 warn!(event = "certificate_revoke_parse_failed", outcome = "failure", error = %error);
228 RevokeError::Refused(Problem::malformed("certificate is unparsable"))
229 })?;
230
231 let order = Order::find_by_cert_serial(profile, &serial_hex, database)
232 .await
233 .map_err(|error| {
234 error!(event = "certificate_revoke_lookup_failed", outcome = "failure", cert_serial = %serial_hex, error = %error);
235 RevokeError::Refused(Problem::server_internal("Certificate lookup failed"))
236 })?
237 .filter(|order| {
238 order
239 .certificate
240 .as_deref()
241 .and_then(|chain| acme_proxy_core::cert::leaf_der_from_chain(chain).ok())
242 .is_some_and(|leaf| leaf == cert_der)
243 })
244 .ok_or(());
245
246 let actor = match &account {
252 Some(cached) => Actor::acme(cached.id.to_string()),
253 None => Actor::acme_certificate_key(),
254 };
255 let client = Client::Request(request).resolve(self.audit).await;
257 let revoke_failed = |reason: &'static str, detail: &str| {
258 refusal(
259 profile,
260 actor.clone(),
261 &serial_hex,
262 client.clone(),
263 reason,
264 detail,
265 )
266 };
267
268 let order = match order {
269 Ok(order) => order,
270 Err(()) => {
271 warn!(event = "certificate_revoke_unknown_certificate", outcome = "failure", cert_serial = %serial_hex);
278 self.audit
279 .record(revoke_failed(
280 "malformed",
281 "no certificate issued here matches",
282 ))
283 .await;
284 return Err(RevokeError::Refused(Problem::malformed(
285 "Unknown certificate",
286 )));
287 }
288 };
289
290 let cert_key_matches = order.cert_pubkey.as_deref() == Some(pubkey);
306 let authorized = if cert_key_matches {
307 true
308 } else {
309 match account {
310 Some(cached) => cached.id == order.account_id,
311 None => Account::find_by_pubkey(profile, pubkey, database)
312 .await
313 .map_err(|error| {
314 error!(event = "certificate_revoke_account_lookup_failed", outcome = "failure", error = %error);
315 RevokeError::Refused(Problem::server_internal("Account lookup failed"))
316 })?
317 .is_some_and(|found| found.id == order.account_id),
318 }
319 };
320 if !authorized {
321 warn!(event = "certificate_revoke_unauthorized", outcome = "failure", order_id = %order.id, cert_serial = %serial_hex);
322 self.audit
326 .record(
327 revoke_failed(
328 "unauthorized",
329 "signed by neither the order's account nor the certificate's own key",
330 )
331 .with_order(order.id, order.account_id, &order.identifiers),
332 )
333 .await;
334 return Err(RevokeError::Refused(Problem::unauthorized(
335 "Neither the order's account nor the certificate's own key signed this request",
336 )));
337 }
338
339 self.revoke(
340 order,
341 cert_der,
342 serial_hex,
343 reason,
344 actor,
345 client,
346 Refusals::Audited,
347 )
348 .await
349 }
350
351 pub async fn revoke_order(
360 &self,
361 id: &str,
362 reason: Option<u32>,
363 actor: Actor,
364 client: ClientContext,
365 ) -> Result<Order, RevokeError> {
366 let Some(order) = Order::find_by_id(id, self.database).await? else {
367 return Err(RevokeError::NotFound);
368 };
369 let Some(chain) = order.certificate.as_deref() else {
370 return Err(RevokeError::NotIssued);
371 };
372 let cert_der = acme_proxy_core::cert::leaf_der_from_chain(chain).map_err(|error| {
373 RevokeError::Internal(format!("stored certificate chain is unparsable: {error}"))
374 })?;
375 let serial = match order.cert_serial.clone() {
378 Some(serial) => serial,
379 None => acme_proxy_core::cert::cert_serial_and_spki(&cert_der)
380 .map(|(serial, _)| serial)
381 .map_err(|error| {
382 RevokeError::Internal(format!("stored certificate is unparsable: {error}"))
383 })?,
384 };
385 self.revoke(
386 order,
387 &cert_der,
388 serial,
389 reason,
390 actor,
391 client,
392 Refusals::Silent,
393 )
394 .await
395 }
396
397 #[allow(clippy::too_many_arguments)]
400 async fn revoke(
401 &self,
402 mut order: Order,
403 cert_der: &[u8],
404 serial_hex: String,
405 reason: Option<u32>,
406 actor: Actor,
407 client: ClientContext,
408 refusals: Refusals,
409 ) -> Result<Order, RevokeError> {
410 let refused = |order: &Order, reason: &'static str, detail: &str| {
411 refusal(
412 &order.profile,
413 actor.clone(),
414 &serial_hex,
415 client.clone(),
416 reason,
417 detail,
418 )
419 .with_order(order.id, order.account_id, &order.identifiers)
420 };
421 let audited = refusals == Refusals::Audited;
422
423 if order.revoked_at.is_some() {
426 if audited {
427 warn!(event = "certificate_revoke_already_revoked", outcome = "failure", order_id = %order.id, cert_serial = %serial_hex);
428 self.audit
429 .record(refused(&order, "alreadyRevoked", "already revoked"))
430 .await;
431 }
432 return Err(RevokeError::AlreadyRevoked);
433 }
434
435 if let Some(code) = reason
436 && !acme_proxy_core::cert::is_valid_revocation_reason(code)
437 {
438 if audited {
439 warn!(
440 event = "certificate_revoke_bad_reason",
441 outcome = "failure",
442 reason = code
443 );
444 self.audit
445 .record(refused(
446 &order,
447 "badRevocationReason",
448 &format!("reason code {code}"),
449 ))
450 .await;
451 }
452 return Err(RevokeError::BadReason(code));
453 }
454
455 match self.revoker {
456 Revoker::Queued { jobs, wait } => {
460 return self
461 .revoke_through_the_queue(&order, reason, &actor, &client, jobs, wait)
462 .await;
463 }
464 Revoker::Backend(signer) => {
468 if let Err(error) = signer.revoke(cert_der, reason).await {
469 error!(event = "certificate_revoke_signer_failed", outcome = "failure", order_id = %order.id, cert_serial = %serial_hex, error = %error);
470 self.audit
471 .record(refused(&order, "serverInternal", &error.to_string()))
472 .await;
473 return Err(RevokeError::Signer(error));
474 }
475 match order.revoke(reason.map(i64::from), self.database).await {
479 Ok(false) => {
480 if audited {
481 self.audit
482 .record(refused(&order, "alreadyRevoked", "already revoked"))
483 .await;
484 }
485 return Err(RevokeError::AlreadyRevoked);
486 }
487 Ok(true) => {}
488 Err(error) => {
489 error!(event = "certificate_revoke_persist_failed", outcome = "failure", order_id = %order.id, cert_serial = %serial_hex, error = %error);
490 self.audit
496 .record(refused(&order, "serverInternal", &error.to_string()))
497 .await;
498 return Err(RevokeError::Database(error));
499 }
500 }
501 }
502 Revoker::Ledger { issuer, jobs } => {
504 if let Err(error) = self
505 .record_in_ledger(&mut order, issuer, cert_der, &serial_hex, reason)
506 .await
507 {
508 self.audit
509 .record(refused(&order, "serverInternal", &error.to_string()))
510 .await;
511 return Err(error);
512 }
513 jobs.enqueue_or_log(acme_proxy_signer::local_ca::sweep::regenerate_spec(issuer))
516 .await;
517 }
518 }
519
520 info!(event = "certificate_revoked", outcome = "success", order_id = %order.id, cert_serial = %serial_hex);
521 let revoked = AuditRecord::new(AuditEvent::CertificateRevoked, &order.profile, actor)
522 .with_order(order.id, order.account_id, &order.identifiers)
523 .with_serial(&serial_hex)
524 .with_client(client.clone());
525 self.audit
530 .record(match reason {
531 Some(code) => revoked.with_reason(code.to_string()),
532 None => revoked,
533 })
534 .await;
535 if let Some(dispatcher) = self.notify {
536 dispatcher
537 .dispatch(NotifyEvent::CertificateRevoked(CertificateRevokedData {
538 profile: order.profile.clone(),
539 order_id: order.id.to_string(),
540 account_id: order.account_id.to_string(),
541 cert_serial: serial_hex,
542 reason,
543 client_ip: client.ip,
544 }))
545 .await;
546 }
547 Ok(order)
548 }
549}
550
551impl Revocations<'_> {
552 async fn revoke_through_the_queue(
559 &self,
560 order: &Order,
561 reason: Option<u32>,
562 actor: &Actor,
563 client: &ClientContext,
564 jobs: &JobQueue,
565 wait: Duration,
566 ) -> Result<Order, RevokeError> {
567 let id = order.id.to_string();
568 jobs.enqueue(signer_revoke_spec(&id, reason, actor, client))
569 .await
570 .inspect_err(|error| {
571 error!(event = "certificate_revoke_queue_failed", outcome = "failure", order_id = %id, error = %error);
572 })?;
573 let job = acme_proxy_store::job::Job::find_latest_by_dedup(
574 SIGNER_REVOKE_KIND,
575 &id,
576 self.database,
577 )
578 .await?
579 .ok_or_else(|| {
580 RevokeError::Internal(format!("the revocation of order {id} was not queued"))
581 })?;
582 info!(event = "certificate_revoke_queued", outcome = "progress", order_id = %id, job_id = %job.id);
583
584 match await_job(self.database, job.id, wait).await? {
585 JobSettled::Done => Order::find_by_id(&id, self.database)
586 .await?
587 .ok_or(RevokeError::NotFound),
588 JobSettled::Failed(reason) => Err(RevokeError::Abandoned {
589 job: job.id,
590 reason,
591 }),
592 JobSettled::Cancelled => Err(RevokeError::Abandoned {
593 job: job.id,
594 reason: "cancelled by an operator".to_string(),
595 }),
596 JobSettled::Pending => Err(RevokeError::Pending { job: job.id }),
597 }
598 }
599
600 async fn record_in_ledger(
610 &self,
611 order: &mut Order,
612 issuer: &str,
613 cert_der: &[u8],
614 serial_hex: &str,
615 reason: Option<u32>,
616 ) -> Result<(), RevokeError> {
617 let revoked_at = acme_proxy_store::nonce::now_secs();
618 let row = acme_proxy_store::revocation::Revocation {
619 issuer: issuer.to_string(),
620 serial: serial_hex.to_string(),
621 revoked_at,
622 reason,
623 not_after: acme_proxy_core::cert::cert_validity(cert_der)
626 .ok()
627 .map(|(_, not_after)| not_after),
628 };
629 let written = async {
630 let mut tx = self.database.write_transaction().await?;
636 if acme_proxy_store::crl::StoredCrl::find(issuer, tx.conn())
637 .await?
638 .is_none()
639 {
640 return Ok(Recorded::NoCrlYet);
641 }
642 if !Order::set_revoked(order.id, reason.map(i64::from), revoked_at, tx.conn()).await? {
648 return Ok(Recorded::Already);
649 }
650 row.insert_if_absent(tx.conn()).await?;
651 tx.commit().await?;
652 Ok::<Recorded, sqlx::Error>(Recorded::Yes)
653 }
654 .await;
655 let recorded = written.map_err(|error| {
656 error!(event = "certificate_revoke_persist_failed", outcome = "failure", order_id = %order.id, cert_serial = %serial_hex, error = %error);
657 RevokeError::Database(error)
658 })?;
659 if recorded == Recorded::Already {
660 warn!(event = "certificate_revoke_already_revoked", outcome = "failure", order_id = %order.id, cert_serial = %serial_hex);
661 return Err(RevokeError::AlreadyRevoked);
662 }
663 if recorded == Recorded::NoCrlYet {
664 warn!(event = "certificate_revoke_ca_uninitialized", outcome = "failure", order_id = %order.id, issuer = %issuer);
665 return Err(RevokeError::Internal(format!(
666 "the CA {issuer} has no stored CRL yet — start `acme-proxy serve` with this \
667 configuration once, so it can import its revocation ledger, then retry"
668 )));
669 }
670 order.revoked_at = Some(revoked_at);
671 order.revocation_reason = reason.map(i64::from);
672 Ok(())
673 }
674}
675
676#[derive(Debug, PartialEq, Eq)]
678enum Recorded {
679 Yes,
680 Already,
682 NoCrlYet,
684}
685
686fn refusal(
688 profile: &str,
689 actor: Actor,
690 serial: &str,
691 client: ClientContext,
692 reason: &'static str,
693 detail: &str,
694) -> AuditRecord {
695 AuditRecord::new(AuditEvent::CertificateRevokeFailed, profile, actor)
696 .with_serial(serial)
697 .with_client(client)
698 .with_reason(reason)
699 .with_detail(detail)
700}
701
702pub const SIGNER_REVOKE_KIND: &str = "signer_revoke";
704
705#[must_use]
714pub fn signer_revoke_spec(
715 order_id: &str,
716 reason: Option<u32>,
717 actor: &Actor,
718 client: &ClientContext,
719) -> JobSpec {
720 JobSpec::now(SIGNER_REVOKE_KIND, order_id).with_payload(serde_json::json!({
721 "order_id": order_id,
722 "reason": reason,
723 "actor_kind": actor.kind.as_str(),
724 "actor_id": actor.id,
725 "client": client.to_json(),
726 }))
727}
728
729#[must_use]
734pub fn request_wait(request_timeout_ms: u64) -> Duration {
735 Duration::from_millis(request_timeout_ms).saturating_sub(Duration::from_secs(1))
736}
737
738#[derive(Debug, Clone, PartialEq, Eq)]
740pub enum JobSettled {
741 Done,
742 Failed(String),
744 Cancelled,
745 Pending,
747}
748
749const AWAIT_PACE: Duration = Duration::from_millis(100);
753
754pub async fn await_job(
761 database: &Database,
762 id: Uuid,
763 wait: Duration,
764) -> Result<JobSettled, sqlx::Error> {
765 let deadline = tokio::time::Instant::now() + wait;
766 loop {
767 let Some(job) = acme_proxy_store::job::Job::find_by_id(id, database).await? else {
768 return Ok(JobSettled::Failed(format!("job {id} disappeared")));
769 };
770 match job.status.as_str() {
771 "done" => return Ok(JobSettled::Done),
772 "failed" => {
773 return Ok(JobSettled::Failed(
774 job.last_error
775 .unwrap_or_else(|| "no reason recorded".to_string()),
776 ));
777 }
778 "cancelled" => return Ok(JobSettled::Cancelled),
779 _ if tokio::time::Instant::now() >= deadline => return Ok(JobSettled::Pending),
780 _ => tokio::time::sleep(AWAIT_PACE).await,
781 }
782 }
783}
784
785pub struct SignerRevokeJob {
793 database: Arc<Database>,
794 audit: Arc<Auditor>,
795 signers: Vec<(String, Arc<dyn SignerBackend>)>,
796 notifiers: acme_proxy_jobs::notify::Notifiers,
797}
798
799impl SignerRevokeJob {
800 #[must_use]
802 pub fn new(
803 database: Arc<Database>,
804 audit: Arc<Auditor>,
805 signers: Vec<(String, Arc<dyn SignerBackend>)>,
806 notifiers: acme_proxy_jobs::notify::Notifiers,
807 ) -> Self {
808 Self {
809 database,
810 audit,
811 signers,
812 notifiers,
813 }
814 }
815}
816
817fn payload_actor(payload: &serde_json::Value) -> Actor {
819 let id = payload["actor_id"].as_str().map(str::to_string);
820 let kind = match payload["actor_kind"].as_str() {
821 Some("admin") => acme_proxy_core::audit::ActorKind::Admin,
822 Some("acme") => acme_proxy_core::audit::ActorKind::Acme,
823 Some("system") => acme_proxy_core::audit::ActorKind::System,
824 _ => acme_proxy_core::audit::ActorKind::Cli,
825 };
826 Actor { kind, id }
827}
828
829#[async_trait::async_trait]
830impl JobHandler for SignerRevokeJob {
831 fn kind(&self) -> &'static str {
832 SIGNER_REVOKE_KIND
833 }
834
835 async fn run(&self, job: &Job) -> JobOutcome {
841 let payload = &job.payload;
842 let Some(order_id) = payload["order_id"].as_str() else {
843 return JobOutcome::Failed("the payload names no order".to_string());
844 };
845 let reason = payload["reason"]
846 .as_u64()
847 .and_then(|code| u32::try_from(code).ok());
848
849 let order = match Order::find_by_id(order_id, &self.database).await {
850 Ok(Some(order)) => order,
851 Ok(None) => return JobOutcome::Failed("the order no longer exists".to_string()),
852 Err(error) => return JobOutcome::Retry(format!("reading the order failed: {error}")),
853 };
854 let Some(signer) = super::mounted(&self.signers, &order.profile) else {
855 return JobOutcome::Retry(format!(
856 "profile `{}` is not mounted by this process",
857 order.profile
858 ));
859 };
860 let dispatcher = self.notifiers.get(&order.profile);
861 let revocations = Revocations {
862 database: &self.database,
863 audit: &self.audit,
864 notify: dispatcher.as_deref(),
865 revoker: Revoker::Backend(signer.as_ref()),
866 };
867 match revocations
868 .revoke_order(
869 order_id,
870 reason,
871 payload_actor(payload),
872 ClientContext::from_json(&payload["client"]),
873 )
874 .await
875 {
876 Ok(_) | Err(RevokeError::AlreadyRevoked) => JobOutcome::Done,
877 Err(error @ (RevokeError::Signer(_) | RevokeError::Database(_))) => {
878 JobOutcome::Retry(error.to_string())
879 }
880 Err(error) => JobOutcome::Failed(error.to_string()),
881 }
882 }
883
884 async fn abandon(&self, job: &Job, reason: &str) {
885 error!(
886 event = "certificate_revoke_abandoned",
887 outcome = "failure",
888 order_id = %job.dedup_key,
889 reason = %reason,
890 "a queued revocation was given up; the certificate is still trusted"
891 );
892 }
893}
894
895#[cfg(test)]
896mod tests {
897 use super::*;
898 use acme_proxy_core::identifier::Identifier;
899 use acme_proxy_jobs::jobs::JobHandler;
900 use acme_proxy_signer::IssueOutcome;
901 use acme_proxy_signer::RequestedValidity;
902
903 struct Failing;
905
906 #[async_trait::async_trait]
907 impl SignerBackend for Failing {
908 async fn issue(
909 &self,
910 _order_id: &str,
911 _csr_der: &[u8],
912 _identifiers: &[Identifier],
913 _validity: RequestedValidity,
914 ) -> Result<IssueOutcome, SignerError> {
915 Err(SignerError::Internal("not here".into()))
916 }
917 async fn revoke(&self, _cert_der: &[u8], _reason: Option<u32>) -> Result<(), SignerError> {
918 Err(SignerError::Internal("upstream unreachable".into()))
919 }
920 }
921
922 fn handler(database: &Arc<Database>) -> SignerRevokeJob {
923 SignerRevokeJob::new(
924 database.clone(),
925 Arc::new(Auditor::offline(database.clone())),
926 vec![("default".to_string(), Arc::new(Failing))],
927 std::collections::HashMap::new().into(),
928 )
929 }
930
931 async fn queued(database: &Arc<Database>, order_id: &str) -> Job {
932 let queue = acme_proxy_jobs::testutil::idle_job_queue(database.clone());
933 queue
934 .enqueue(signer_revoke_spec(
935 order_id,
936 None,
937 &Actor::cli(),
938 &ClientContext::default(),
939 ))
940 .await
941 .unwrap();
942 Job::find_live(SIGNER_REVOKE_KIND, order_id, database)
943 .await
944 .unwrap()
945 .unwrap()
946 }
947
948 #[tokio::test]
950 async fn a_failing_backend_is_retried() {
951 let database = Arc::new(Database::connect_in_memory().await.unwrap());
952 let account = acme_proxy_store::testutil::account_id(&database).await;
953 let order = acme_proxy_store::testutil::issued_order(
954 &database,
955 "default",
956 account,
957 &["example.com"],
958 30,
959 )
960 .await;
961 let job = queued(&database, &order.id.to_string()).await;
962
963 assert!(matches!(
964 handler(&database).run(&job).await,
965 JobOutcome::Retry(_)
966 ));
967 let stored = Order::find_by_id(&order.id.to_string(), &database)
968 .await
969 .unwrap()
970 .unwrap();
971 assert!(stored.revoked_at.is_none());
972 }
973
974 struct Succeeding;
976
977 #[async_trait::async_trait]
978 impl SignerBackend for Succeeding {
979 async fn issue(
980 &self,
981 _order_id: &str,
982 _csr_der: &[u8],
983 _identifiers: &[Identifier],
984 _validity: RequestedValidity,
985 ) -> Result<IssueOutcome, SignerError> {
986 Err(SignerError::Internal("not here".into()))
987 }
988 async fn revoke(&self, _cert_der: &[u8], _reason: Option<u32>) -> Result<(), SignerError> {
989 Ok(())
990 }
991 }
992
993 fn worker(
997 database: &Arc<Database>,
998 backend: Arc<dyn SignerBackend>,
999 max_attempts: u32,
1000 ) -> (JobQueue, tokio::sync::watch::Sender<bool>) {
1001 let config = acme_proxy_core::config::JobsConfig {
1002 poll_interval_ms: 10,
1003 max_attempts,
1004 retry_base_seconds: 0,
1005 retry_max_seconds: 0,
1006 ..acme_proxy_core::config::JobsConfig::default()
1007 };
1008 let queue = JobQueue::new(database.clone(), &config);
1009 let mut registry = acme_proxy_jobs::jobs::JobRegistry::new();
1010 registry
1011 .register(Arc::new(SignerRevokeJob::new(
1012 database.clone(),
1013 Arc::new(Auditor::offline(database.clone())),
1014 vec![("default".to_string(), backend)],
1015 std::collections::HashMap::new().into(),
1016 )))
1017 .unwrap();
1018 let (shutdown, receiver) = tokio::sync::watch::channel(false);
1019 acme_proxy_jobs::jobs::spawn_runner(queue.clone(), Arc::new(registry), &config, receiver);
1020 (queue, shutdown)
1021 }
1022
1023 async fn issued(database: &Arc<Database>) -> Order {
1024 let account = acme_proxy_store::testutil::account_id(database).await;
1025 acme_proxy_store::testutil::issued_order(database, "default", account, &["example.com"], 30)
1026 .await
1027 }
1028
1029 fn operator_client() -> ClientContext {
1030 ClientContext {
1031 ip: Some("198.51.100.4".to_string()),
1032 ..ClientContext::default()
1033 }
1034 }
1035
1036 #[tokio::test]
1041 async fn a_queued_revocation_answers_once_the_worker_has_revoked() {
1042 let database = Arc::new(Database::connect_in_memory().await.unwrap());
1043 let order = issued(&database).await;
1044 let (jobs, _worker) = worker(&database, Arc::new(Succeeding), 5);
1045 let audit = Auditor::offline(database.clone());
1046 let revocations = Revocations {
1047 database: &database,
1048 audit: &audit,
1049 notify: None,
1050 revoker: Revoker::Queued {
1051 jobs: &jobs,
1052 wait: Duration::from_secs(10),
1053 },
1054 };
1055
1056 let revoked = revocations
1057 .revoke_order(
1058 &order.id.to_string(),
1059 Some(1),
1060 Actor::admin("root"),
1061 operator_client(),
1062 )
1063 .await
1064 .unwrap();
1065 assert!(revoked.revoked_at.is_some());
1066 assert_eq!(revoked.revocation_reason, Some(1));
1067
1068 let query = acme_proxy_store::audit::AuditQuery {
1069 limit: 50,
1070 ..acme_proxy_store::audit::AuditQuery::default()
1071 };
1072 let (rows, _) = acme_proxy_store::audit::AuditEntry::search(&query, &database)
1073 .await
1074 .unwrap();
1075 assert_eq!(rows.len(), 1, "{rows:?}");
1076 assert_eq!(rows[0].event, "certificate_revoked");
1077 assert_eq!(rows[0].actor_kind, "admin");
1078 assert_eq!(rows[0].client_ip.as_deref(), Some("198.51.100.4"));
1079 }
1080
1081 #[tokio::test]
1086 async fn concurrent_revocations_of_one_certificate_record_one() {
1087 let database = Arc::new(Database::connect_in_memory().await.unwrap());
1088 let order = issued(&database).await;
1089 let audit = Auditor::offline(database.clone());
1090 let backend = Succeeding;
1091 let revocations = Revocations {
1092 database: &database,
1093 audit: &audit,
1094 notify: None,
1095 revoker: Revoker::Backend(&backend),
1096 };
1097 let order_id = order.id.to_string();
1098 let revoke = |reason: u32| {
1099 revocations.revoke_order(
1100 &order_id,
1101 Some(reason),
1102 Actor::admin("root"),
1103 operator_client(),
1104 )
1105 };
1106
1107 let (first, second) = tokio::join!(revoke(1), revoke(4));
1108 let outcomes = [first, second];
1109 assert_eq!(
1110 outcomes.iter().filter(|outcome| outcome.is_ok()).count(),
1111 1,
1112 "exactly one revocation records the withdrawal: {outcomes:?}"
1113 );
1114 assert!(
1115 outcomes
1116 .iter()
1117 .any(|outcome| matches!(outcome, Err(RevokeError::AlreadyRevoked)))
1118 );
1119
1120 let query = acme_proxy_store::audit::AuditQuery {
1121 limit: 50,
1122 ..acme_proxy_store::audit::AuditQuery::default()
1123 };
1124 let (rows, _) = acme_proxy_store::audit::AuditEntry::search(&query, &database)
1125 .await
1126 .unwrap();
1127 assert_eq!(
1128 rows.iter()
1129 .filter(|row| row.event == "certificate_revoked")
1130 .count(),
1131 1,
1132 "{rows:?}"
1133 );
1134
1135 let winner = outcomes
1137 .iter()
1138 .find_map(|outcome| outcome.as_ref().ok())
1139 .unwrap();
1140 let stored = Order::find_by_id(&order.id.to_string(), &database)
1141 .await
1142 .unwrap()
1143 .unwrap();
1144 assert_eq!(stored.revocation_reason, winner.revocation_reason);
1145 assert_eq!(stored.revoked_at, winner.revoked_at);
1146 }
1147
1148 #[tokio::test]
1152 async fn an_unanswered_revocation_is_pending_and_asking_again_follows_it() {
1153 let database = Arc::new(Database::connect_in_memory().await.unwrap());
1154 let order = issued(&database).await;
1155 let jobs = acme_proxy_jobs::testutil::idle_job_queue(database.clone());
1156 let audit = Auditor::offline(database.clone());
1157 let revocations = Revocations {
1158 database: &database,
1159 audit: &audit,
1160 notify: None,
1161 revoker: Revoker::Queued {
1162 jobs: &jobs,
1163 wait: Duration::ZERO,
1164 },
1165 };
1166
1167 let mut seen = Vec::new();
1168 for _ in 0..2 {
1169 match revocations
1170 .revoke_order(
1171 &order.id.to_string(),
1172 None,
1173 Actor::cli(),
1174 ClientContext::default(),
1175 )
1176 .await
1177 {
1178 Err(RevokeError::Pending { job }) => seen.push(job),
1179 other => panic!("expected a pending revocation, got {other:?}"),
1180 }
1181 }
1182 assert_eq!(seen[0], seen[1], "the second ask follows the first job");
1183 assert_eq!(
1184 Job::count_live(SIGNER_REVOKE_KIND, &database)
1185 .await
1186 .unwrap(),
1187 1
1188 );
1189 let problem = Problem::from(RevokeError::Pending { job: seen[0] }).to_value();
1190 assert_eq!(problem["status"], 503);
1191 assert_eq!(problem["type"], "urn:ietf:params:acme:error:serverInternal");
1192 }
1193
1194 #[tokio::test]
1197 async fn a_revocation_the_worker_gave_up_on_is_abandoned() {
1198 let database = Arc::new(Database::connect_in_memory().await.unwrap());
1199 let order = issued(&database).await;
1200 let (jobs, _worker) = worker(&database, Arc::new(Failing), 1);
1201 let audit = Auditor::offline(database.clone());
1202 let revocations = Revocations {
1203 database: &database,
1204 audit: &audit,
1205 notify: None,
1206 revoker: Revoker::Queued {
1207 jobs: &jobs,
1208 wait: Duration::from_secs(10),
1209 },
1210 };
1211
1212 let error = revocations
1213 .revoke_order(
1214 &order.id.to_string(),
1215 None,
1216 Actor::cli(),
1217 ClientContext::default(),
1218 )
1219 .await
1220 .unwrap_err();
1221 let RevokeError::Abandoned { reason, .. } = &error else {
1222 panic!("expected an abandoned revocation, got {error:?}")
1223 };
1224 assert!(reason.contains("upstream unreachable"), "{reason}");
1225 assert_eq!(
1226 Problem::from(error).to_value()["detail"],
1227 "Revocation failed"
1228 );
1229 let stored = Order::find_by_id(&order.id.to_string(), &database)
1230 .await
1231 .unwrap()
1232 .unwrap();
1233 assert!(stored.revoked_at.is_none());
1234 }
1235
1236 #[tokio::test]
1239 async fn a_cancelled_or_vanished_job_settles_the_wait() {
1240 let database = Arc::new(Database::connect_in_memory().await.unwrap());
1241 let order = issued(&database).await;
1242 let job = queued(&database, &order.id.to_string()).await;
1243 Job::cancel_row(
1244 job.id,
1245 acme_proxy_store::status::JobStatus::Ready,
1246 &database,
1247 )
1248 .await
1249 .unwrap()
1250 .unwrap();
1251 assert_eq!(
1252 await_job(&database, job.id, Duration::from_secs(10))
1253 .await
1254 .unwrap(),
1255 JobSettled::Cancelled
1256 );
1257 let JobSettled::Failed(reason) = await_job(
1258 &database,
1259 acme_proxy_store::id::mint(),
1260 Duration::from_secs(10),
1261 )
1262 .await
1263 .unwrap() else {
1264 panic!("a job that is not there has failed")
1265 };
1266 assert!(reason.contains("disappeared"), "{reason}");
1267 }
1268
1269 #[tokio::test]
1272 async fn the_route_picks_the_revoker_and_the_wait_fits_the_deadline() {
1273 let database = Arc::new(Database::connect_in_memory().await.unwrap());
1274 let jobs = acme_proxy_jobs::testutil::idle_job_queue(database);
1275 let ledger = RevocationRoute::Ledger {
1276 issuer: "ab".to_string(),
1277 };
1278 assert!(matches!(
1279 Revoker::for_route(&ledger, &jobs, Duration::ZERO),
1280 Revoker::Ledger { issuer: "ab", .. }
1281 ));
1282 assert!(matches!(
1283 Revoker::for_route(&RevocationRoute::Delegated, &jobs, Duration::from_secs(3)),
1284 Revoker::Queued { wait, .. } if wait == Duration::from_secs(3)
1285 ));
1286 assert_eq!(request_wait(60_000), Duration::from_secs(59));
1287 assert_eq!(request_wait(500), Duration::ZERO);
1288 }
1289
1290 #[tokio::test]
1292 async fn a_vanished_order_fails_for_good() {
1293 let database = Arc::new(Database::connect_in_memory().await.unwrap());
1294 let job = queued(&database, &acme_proxy_store::id::mint().to_string()).await;
1295 assert!(matches!(
1296 handler(&database).run(&job).await,
1297 JobOutcome::Failed(_)
1298 ));
1299 }
1300}