1use 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
70pub const SIGNER_ISSUE_KIND: &str = "signer_issue";
72
73#[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
105pub 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 #[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 fn signer(&self, profile: &str) -> Option<&Arc<dyn SignerBackend>> {
134 super::mounted(&self.signers, profile)
135 }
136}
137
138async 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
164fn 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 if order.status != OrderStatus::Processing {
194 return JobOutcome::Done;
195 }
196
197 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 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 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 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 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 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 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 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 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 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 #[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 #[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 #[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 handler.abandon(&job, &reason).await;
584 assert_eq!(audit_rows(&database).await.len(), 1);
585 }
586
587 #[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 #[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 #[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 #[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 #[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 #[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 elsewhere
740 .abandon(
741 &Job {
742 payload: serde_json::json!({}),
743 ..job
744 },
745 "gone",
746 )
747 .await;
748 }
749}