1use std::net::IpAddr;
9use std::sync::Arc;
10
11use axum::http::StatusCode;
12use base64::prelude::*;
13use serde::Deserialize;
14use serde_json::Value;
15use tracing::{error, info, warn};
16use uuid::Uuid;
17
18use super::access::signer_account;
19use super::error::Error;
20use super::policy::{challenge_problem, check_identifiers};
21use super::rules::{
22 check_csr_matches_order, csr_identifiers, is_wildcard, names_an_ip_address, normalize_dns_name,
23 parse_csr, parse_rfc3339, well_formed_name,
24};
25use crate::profile::Profile;
26use acme_proxy_core::audit::RequestContext;
27use acme_proxy_core::error::Problem;
28use acme_proxy_core::identifier::Identifier;
29use acme_proxy_core::jws::signature::jwk_thumbprint;
30use acme_proxy_jobs::auditor::Auditor;
31use acme_proxy_jobs::jobs::JobQueue;
32use acme_proxy_jobs::notify::ChallengeFailedData;
33use acme_proxy_jobs::notify::NotifyEvent;
34use acme_proxy_net::challenge::ValidationContext;
35use acme_proxy_policy::filter::IdentifierStage;
36use acme_proxy_policy::filter::Stage as FilterStage;
37use acme_proxy_store::account::Account;
38use acme_proxy_store::authz::Authorization;
39use acme_proxy_store::authz::Challenge;
40use acme_proxy_store::authz::ValidationClaim;
41use acme_proxy_store::db::Database;
42use acme_proxy_store::nonce::now_secs;
43use acme_proxy_store::order::Order;
44use acme_proxy_store::status::AuthzStatus;
45use acme_proxy_store::status::ChallengeStatus;
46use acme_proxy_store::status::OrderStatus;
47
48#[derive(Debug, Default, Deserialize)]
50#[serde(default)]
51pub struct NewOrderPayload {
52 pub identifiers: Vec<Identifier>,
53 #[serde(rename = "notBefore")]
54 pub not_before: Option<String>,
55 #[serde(rename = "notAfter")]
56 pub not_after: Option<String>,
57 pub replaces: Option<String>,
61}
62
63#[derive(Debug, Deserialize)]
65pub struct FinalizePayload {
66 pub csr: String,
67}
68
69fn compound_identifier_problem(mut rejections: Vec<Problem>) -> Problem {
78 if rejections.len() == 1 {
79 return rejections.remove(0);
80 }
81
82 let status = rejections
83 .iter()
84 .map(Problem::status)
85 .max()
86 .unwrap_or(StatusCode::BAD_REQUEST);
87
88 Problem::compound(status, "Some of the identifiers requested were rejected")
89 .with_subproblems(rejections)
90}
91
92async fn check_replaces(
107 cert_id: &str,
108 profile: &str,
109 account_id: Uuid,
110 identifiers: &[Identifier],
111 database: &Arc<Database>,
112) -> Result<String, Problem> {
113 let parsed = acme_proxy_core::cert::parse_ari_cert_id(cert_id).map_err(|error| {
117 warn!(event = "replaces_malformed", outcome = "failure", replaces = %cert_id, error = %error);
118 Problem::malformed(format!("Invalid `replaces` certID: {error}"))
119 })?;
120
121 let predecessor = Order::find_by_cert_serial(profile, &parsed.serial_hex(), database)
122 .await
123 .map_err(|error| {
124 error!(event = "replaces_lookup_failed", outcome = "failure", error = %error);
125 Problem::server_internal("Predecessor lookup failed")
126 })?
127 .ok_or_else(|| {
128 warn!(event = "replaces_unknown", outcome = "failure", replaces = %cert_id);
129 Problem::malformed("`replaces` names no certificate issued here")
130 })?;
131
132 if let Some(certificate) = predecessor.certificate.as_ref()
137 && let Ok(leaf_der) = acme_proxy_core::cert::leaf_der_from_chain(certificate)
138 && let Ok((aki, _)) = acme_proxy_core::cert::ari_cert_id_parts(&leaf_der)
139 && aki != parsed.aki
140 {
141 warn!(event = "replaces_aki_mismatch", outcome = "failure", replaces = %cert_id);
142 return Err(Problem::malformed(
143 "`replaces` key identifier does not match the certificate",
144 ));
145 }
146
147 if predecessor.account_id != account_id {
150 warn!(event = "replaces_wrong_account", outcome = "failure", replaces = %cert_id, order_id = %predecessor.id);
151 return Err(Problem::malformed(
152 "`replaces` names a certificate belonging to another account",
153 ));
154 }
155
156 let shares_identifier = identifiers.iter().any(|wanted| {
158 predecessor
159 .identifiers
160 .iter()
161 .any(|had| had.typ == wanted.typ && had.value == wanted.value)
162 });
163 if !shares_identifier {
164 warn!(event = "replaces_no_shared_identifier", outcome = "failure", replaces = %cert_id);
165 return Err(Problem::malformed(
166 "`replaces` names a certificate sharing no identifier with this order",
167 ));
168 }
169
170 if let Some(existing) = Order::find_by_replaces(profile, cert_id, database)
173 .await
174 .map_err(|error| {
175 error!(event = "replaces_conflict_lookup_failed", outcome = "failure", error = %error);
176 Problem::server_internal("Replacement lookup failed")
177 })?
178 {
179 warn!(
180 event = "replaces_already_claimed",
181 outcome = "failure",
182 replaces = %cert_id,
183 existing_order_id = %existing.id,
184 );
185 return Err(Problem::already_replaced(
186 "This certificate has already been marked as replaced by another order",
187 ));
188 }
189
190 info!(event = "replaces_accepted", outcome = "success", replaces = %cert_id, predecessor_order_id = %predecessor.id);
191 Ok(cert_id.to_string())
192}
193
194fn is_replaces_conflict(error: &sqlx::Error) -> bool {
213 acme_proxy_store::sql::is_unique_violation_on(
214 error,
215 "orders.replaces",
216 "idx_orders_replaces_claim",
217 )
218}
219
220fn issue_failed(
226 profile: &str,
227 account_id: Uuid,
228 order: &Order,
229 client: &acme_proxy_core::audit::ClientContext,
230 reason: &'static str,
231 detail: &str,
232) -> acme_proxy_core::audit::AuditRecord {
233 acme_proxy_core::audit::AuditRecord::new(
234 acme_proxy_core::audit::AuditEvent::CertificateIssueFailed,
235 profile,
236 acme_proxy_core::audit::Actor::acme(account_id),
237 )
238 .with_order(order.id, order.account_id, &order.identifiers)
239 .with_client(client.clone())
240 .with_reason(reason)
241 .with_detail(detail)
242}
243
244pub struct OrderService<'a> {
253 pub database: &'a Arc<Database>,
254 pub audit: &'a Auditor,
255 pub profile: &'a Profile,
256}
257
258fn validated_identifiers(
264 mut identifiers: Vec<Identifier>,
265 profile: &Profile,
266) -> Result<Vec<Identifier>, Problem> {
267 let challenges = &profile.challenges;
268
269 if identifiers.is_empty() {
270 warn!(event = "order_no_identifiers", outcome = "failure");
271 return Err(Problem::malformed("No identifiers"));
272 }
273 if identifiers.len() > profile.order.max_identifiers {
276 warn!(
277 event = "order_too_many_identifiers",
278 outcome = "failure",
279 identifiers_count = identifiers.len(),
280 limit = profile.order.max_identifiers
281 );
282 return Err(Problem::malformed(format!(
283 "An order may name at most {} identifiers; this one names {}",
284 profile.order.max_identifiers,
285 identifiers.len()
286 )));
287 }
288 if let Some(bad) = identifiers.iter().find(|id| id.typ != "dns") {
289 warn!(event = "order_identifier_type_unsupported", outcome = "failure", typ = %bad.typ);
290 return Err(Problem::unsupported_identifier(
291 "Only dns identifiers supported",
292 ));
293 }
294
295 for identifier in &mut identifiers {
296 identifier.value = normalize_dns_name(&identifier.value);
297 }
298
299 let mut seen = std::collections::HashSet::new();
305 identifiers.retain(|identifier| seen.insert(identifier.value.clone()));
306
307 let rejections: Vec<Problem> = identifiers
312 .iter()
313 .filter_map(|identifier| {
314 if !well_formed_name(&identifier.value) {
315 warn!(event = "order_identifier_malformed", outcome = "failure", value = %identifier.value);
316 Some(
317 Problem::malformed(format!(
318 "Malformed identifier {}: not a DNS name (a `*` is only legal as a single leading `*.`)",
319 identifier.value
320 ))
321 .with_identifier(identifier),
322 )
323 } else if names_an_ip_address(&identifier.value) {
324 warn!(event = "order_identifier_is_address", outcome = "failure", value = %identifier.value);
325 Some(
326 Problem::rejected_identifier(format!(
327 "Identifier {} is an IP address, which a dns identifier cannot name",
328 identifier.value
329 ))
330 .with_identifier(identifier),
331 )
332 } else if challenges
333 .types_for(is_wildcard(&identifier.value))
334 .is_empty()
335 {
336 warn!(event = "order_identifier_wildcard_rejected", outcome = "failure", value = %identifier.value);
337 Some(
338 Problem::rejected_identifier(format!(
339 "Wildcard identifier {} requires the dns-01 challenge, which is not enabled",
340 identifier.value
341 ))
342 .with_identifier(identifier),
343 )
344 } else {
345 None
346 }
347 })
348 .collect();
349
350 if !rejections.is_empty() {
351 return Err(compound_identifier_problem(rejections));
352 }
353 Ok(identifiers)
354}
355
356impl OrderService<'_> {
357 pub async fn new_order(
366 &self,
367 payload: NewOrderPayload,
368 cached: Option<Account>,
369 pubkey: &[u8],
370 client_ip: Option<IpAddr>,
371 request: &RequestContext,
372 ) -> Result<(Order, Vec<Uuid>), Error> {
373 let (database, profile, audit) = (self.database, self.profile, self.audit);
374 let identifiers = validated_identifiers(payload.identifiers, profile)?;
375
376 let not_before = match payload.not_before {
377 Some(ref s) => Some(parse_rfc3339("notBefore", s)?),
378 None => None,
379 };
380 let not_after = match payload.not_after {
381 Some(ref s) => Some(parse_rfc3339("notAfter", s)?),
382 None => None,
383 };
384
385 let account = signer_account(cached, &profile.name, pubkey, database).await?;
386
387 check_identifiers(
388 &profile.filter,
389 client_ip,
390 &account.id.to_string(),
391 &profile.name,
392 IdentifierStage::NewOrder,
393 &identifiers,
394 database,
395 )
396 .await?;
397
398 let replaces = match payload.replaces {
401 Some(ref cert_id) => Some(
402 check_replaces(cert_id, &profile.name, account.id, &identifiers, database).await?,
403 ),
404 None => None,
405 };
406
407 let expires = now_secs() + profile.order.validity_seconds as i64;
408
409 let client = audit.client(request).await;
414 let mut order = Order::new(
415 &profile.name,
416 account.id,
417 identifiers,
418 expires,
419 not_before,
420 not_after,
421 )
422 .with_client(&client);
423 order.replaces = replaces;
424 let mut authz_ids = Vec::with_capacity(order.identifiers.len());
425
426 let persisted = async {
427 let mut tx = database.transaction().await?;
428 order.insert(tx.conn()).await?;
429
430 for identifier in &order.identifiers {
431 let authz = Authorization::new(order.id, identifier.clone(), order.expires);
432 authz.insert(tx.conn()).await?;
433 for typ in profile.challenges.types_for(is_wildcard(&identifier.value)) {
434 Challenge::new(authz.id, typ).insert(tx.conn()).await?;
435 }
436 authz_ids.push(authz.id);
437 }
438
439 tx.commit().await
440 }
441 .await;
442
443 persisted.map_err(|error| {
444 if is_replaces_conflict(&error) {
450 warn!(event = "replaces_claim_race_lost", outcome = "failure", account_id = %account.id);
451 return Problem::already_replaced(
452 "This certificate has already been marked as replaced by another order",
453 );
454 }
455 error!(
456 event = "order_creation_failed",
457 outcome = "failure",
458 error = %error,
459 account_id = %account.id
460 );
461 Problem::server_internal("Order persistence failed")
462 })?;
463
464 info!(
465 event = "order_created",
466 outcome = "success",
467 order_id = %order.id,
468 account_id = %account.id,
469 identifiers_count = order.identifiers.len()
470 );
471
472 Ok((order, authz_ids))
473 }
474
475 pub async fn deactivate_authz(
482 &self,
483 authz: &mut Authorization,
484 order: &mut Order,
485 ) -> Result<(), Error> {
486 let database = self.database;
487 if authz.status == AuthzStatus::Deactivated {
488 return Ok(());
489 }
490
491 if order.status == OrderStatus::Valid {
496 warn!(event = "authz_deactivate_refused_order_valid", outcome = "failure", authz_id = %authz.id, order_id = %order.id);
497 return Err(Problem::malformed(
498 "Cannot deactivate an authorization whose order has already been issued; revoke the certificate instead",
499 )
500 .into());
501 }
502
503 if order.status == OrderStatus::Processing {
508 warn!(event = "authz_deactivate_refused_order_processing", outcome = "failure", authz_id = %authz.id, order_id = %order.id);
509 return Err(Problem::malformed(
510 "Cannot deactivate an authorization whose order is being issued",
511 )
512 .into());
513 }
514
515 if authz.status != AuthzStatus::Pending && authz.status != AuthzStatus::Valid {
516 warn!(event = "authz_deactivate_refused_terminal", outcome = "failure", authz_id = %authz.id, status = %authz.status);
517 return Err(Problem::malformed(
518 "Authorization is in a terminal state and cannot be deactivated",
519 )
520 .into());
521 }
522
523 let outcome = async {
536 let mut tx = database.transaction().await?;
537 let deactivated = Authorization::set_deactivated(authz.id, tx.conn()).await?;
538 let demoted = deactivated && Order::set_pending(order.id, tx.conn()).await?;
539 tx.commit().await?;
540 Ok::<_, sqlx::Error>((deactivated, demoted))
541 }
542 .await;
543
544 let (deactivated, demoted) = outcome.map_err(|error| {
545 error!(event = "authz_deactivate_failed", outcome = "failure", authz_id = %authz.id, error = %error);
546 Problem::server_internal("Authorization deactivation failed")
547 })?;
548 if !deactivated {
549 warn!(event = "authz_deactivate_refused_terminal", outcome = "failure", authz_id = %authz.id, status = %authz.status);
550 return Err(Problem::malformed(
551 "Authorization is in a terminal state and cannot be deactivated",
552 )
553 .into());
554 }
555
556 authz.status = AuthzStatus::Deactivated;
559 if demoted {
560 order.status = OrderStatus::Pending;
561 }
562
563 info!(event = "authz_deactivated", outcome = "success", authz_id = %authz.id, order_id = %order.id);
564 Ok(())
565 }
566
567 pub async fn claim_challenge(
580 &self,
581 challenge: &mut Challenge,
582 authz: &Authorization,
583 order: &Order,
584 ) -> Result<ValidationClaim, Error> {
585 if authz.status != AuthzStatus::Valid && authz.expires <= now_secs() {
586 warn!(event = "authz_expired", outcome = "failure", authz_id = %authz.id, expires = authz.expires);
587 return Err(Problem::malformed("Authorization has expired").into());
588 }
589
590 if authz.status == AuthzStatus::Deactivated {
594 warn!(event = "authz_already_deactivated", outcome = "failure", authz_id = %authz.id);
595 return Err(Problem::malformed("Authorization has been deactivated").into());
596 }
597
598 let decided = challenge.status == ChallengeStatus::Valid
601 || challenge.status == ChallengeStatus::Invalid
602 || authz.status == AuthzStatus::Valid;
603 if decided {
604 return Ok(ValidationClaim::Decided);
605 }
606
607 if authz.status == AuthzStatus::Invalid || order.status == OrderStatus::Invalid {
613 let fresh = Challenge::find_by_id(challenge.id.to_string().as_str(), self.database)
622 .await
623 .map_err(|error| {
624 error!(event = "challenge_lookup_failed", outcome = "failure", challenge_id = %challenge.id, error = %error);
625 Problem::server_internal("Challenge lookup failed")
626 })?;
627 if let Some(fresh) = fresh
628 && (fresh.status == ChallengeStatus::Valid
629 || fresh.status == ChallengeStatus::Invalid)
630 {
631 *challenge = fresh;
632 return Ok(ValidationClaim::Decided);
633 }
634
635 warn!(event = "challenge_trigger_refused_invalid", outcome = "failure", authz_id = %authz.id, order_id = %order.id);
636 return Err(Problem::malformed(
637 "The authorization or its order is already invalid; create a new order",
638 )
639 .into());
640 }
641
642 let claimed = challenge
654 .claim_for_validation(self.profile.challenges.max_in_flight_per_account(), self.database)
655 .await
656 .map_err(|error| {
657 error!(event = "challenge_claim_failed", outcome = "failure", challenge_id = %challenge.id, error = %error);
658 Problem::server_internal("Challenge could not be claimed for validation")
659 })?;
660 if claimed == ValidationClaim::Limited {
661 warn!(event = "challenge_trigger_rate_limited", outcome = "failure", account_id = %order.account_id, challenge_id = %challenge.id);
662 }
663 Ok(claimed)
664 }
665
666 pub async fn run_validation(
677 &self,
678 account: &Account,
679 challenge: &mut Challenge,
680 authz: &mut Authorization,
681 order: &mut Order,
682 client_ip: Option<IpAddr>,
683 ) -> Result<(), Error> {
684 let (database, profile) = (self.database, self.profile);
685 let thumbprint = jwk_thumbprint(&account.pubkey).map_err(|error| {
686 error!(event = "authz_thumbprint_failed", outcome = "failure", account_id = %account.id, error = %error);
687 Problem::server_internal("Key authorization could not be computed")
688 })?;
689 let key_authorization = format!("{}.{}", challenge.token, thumbprint);
690 let challenge_id = challenge.id.to_string();
691
692 let context = ValidationContext {
693 identifier: authz.base_identifier(),
694 wildcard: authz.is_wildcard(),
695 token: &challenge.token,
696 key_authorization: &key_authorization,
697 challenge_id: &challenge_id,
698 };
699
700 match profile.challenges.validate(&challenge.typ, &context).await {
701 Ok(()) => {
702 commit_validation(challenge, authz, order, database).await?;
703 }
704 Err(error) => {
705 let problem =
706 challenge_problem(&error, &challenge.typ, authz.base_identifier()).to_value();
707 warn!(
708 event = "challenge_failed",
709 outcome = "failure",
710 challenge_id = %challenge_id,
711 typ = %challenge.typ,
712 kind = error.kind()
713 );
714
715 let recorded =
716 commit_validation_failure(challenge, authz, order, &problem, database).await?;
717 if !recorded {
718 return Ok(());
719 }
720
721 profile
726 .notify
727 .dispatch(NotifyEvent::ChallengeFailed(ChallengeFailedData {
728 profile: profile.name.clone(),
729 order_id: order.id.to_string(),
730 account_id: account.id.to_string(),
731 authz_id: authz.id.to_string(),
732 challenge_id: challenge.id.clone().to_string(),
733 challenge_type: challenge.typ.clone(),
734 identifier: authz.base_identifier().to_string(),
735 error: error.kind().to_string(),
736 client_ip: client_ip
737 .map(|ip| acme_proxy_core::client::canonical(ip).to_string()),
738 }))
739 .await;
740 }
741 }
742 Ok(())
743 }
744
745 pub async fn abandon_validation(
758 &self,
759 challenge: &mut Challenge,
760 authz: &mut Authorization,
761 order: &mut Order,
762 reason: &str,
763 ) -> Result<(), Error> {
764 let problem =
765 Problem::server_internal(format!("Challenge validation was not completed: {reason}"))
766 .to_value();
767 commit_validation_failure(challenge, authz, order, &problem, self.database).await?;
768 Ok(())
769 }
770
771 pub async fn finalize(
778 &self,
779 account: &Account,
780 mut order: Order,
781 csr: &str,
782 client_ip: Option<IpAddr>,
783 request: &RequestContext,
784 jobs: &JobQueue,
785 ) -> Result<Order, Error> {
786 let (database, profile, audit) = (self.database, self.profile, self.audit);
787 let filter = &profile.filter;
788
789 let id = order.id.to_string();
790 if order.status != OrderStatus::Ready {
791 warn!(event = "order_finalize_not_ready", outcome = "failure", order_id = %id, status = %order.status);
792 return Err(Problem::order_not_ready("Order is not ready").into());
793 }
794
795 let client = audit.client(request).await;
803 let failed = |order: &Order, reason: &'static str, detail: &str| {
804 issue_failed(&profile.name, account.id, order, &client, reason, detail)
805 };
806
807 let csr_der = match BASE64_URL_SAFE_NO_PAD.decode(csr) {
808 Ok(der) => der,
809 Err(_) => {
810 audit
811 .record(failed(&order, "badCSR", "CSR base64 invalid"))
812 .await;
813 return Err(Problem::bad_csr("CSR base64 invalid").into());
814 }
815 };
816 let csr = match parse_csr(&csr_der) {
817 Ok(csr) => csr,
818 Err(problem) => {
819 audit
820 .record(failed(&order, "badCSR", "CSR is unparsable"))
821 .await;
822 return Err(problem.into());
823 }
824 };
825
826 if let Err(problem) = check_csr_matches_order(&csr, &csr_der, &order.identifiers) {
831 audit
832 .record(failed(
833 &order,
834 "badCSR",
835 "CSR identifiers do not match the order",
836 ))
837 .await;
838 return Err(problem.into());
839 }
840
841 if filter.has_rules_at(FilterStage::Identifiers) {
845 let requested = csr_identifiers(&csr);
846 if let Err(problem) = check_identifiers(
847 filter,
848 client_ip,
849 order.account_id.to_string().as_str(),
850 &profile.name,
851 IdentifierStage::Csr,
852 &requested,
853 database,
854 )
855 .await
856 {
857 let (reason, detail) = if problem.status() == StatusCode::BAD_REQUEST {
862 ("badCSR", "the filter policy refused the CSR identifiers")
863 } else {
864 (
865 "serverInternal",
866 "the filter policy could not be evaluated for the CSR identifiers",
867 )
868 };
869 audit.record(failed(&order, reason, detail)).await;
870 return Err(problem.into());
871 }
872 }
873
874 let spec = super::issue::signer_issue_spec(&order, &csr_der, &client, client_ip);
884 let claimed = async {
885 let mut tx = database.transaction().await?;
886 if !order.claim_for_finalize_on(tx.conn()).await? {
887 return Ok(false);
888 }
889 jobs.enqueue_in(&spec, tx.conn()).await?;
890 tx.commit().await?;
891 Ok::<bool, sqlx::Error>(true)
892 }
893 .await;
894 match claimed {
895 Ok(true) => {}
896 Ok(false) => {
897 warn!(
898 event = "order_finalize_claim_refused",
899 outcome = "failure",
900 order_id = %id
901 );
902 return Err(Problem::order_not_ready("Order is already being finalized").into());
903 }
904 Err(error) => {
905 error!(
906 event = "order_mark_processing_failed",
907 outcome = "failure",
908 order_id = %id,
909 error = %error
910 );
911 return Err(Problem::server_internal("Order finalize failed").into());
912 }
913 }
914 jobs.wake();
915
916 info!(event = "order_finalize_queued", outcome = "success", order_id = %id);
917 Ok(order)
918 }
919}
920
921async fn commit_validation(
944 challenge: &mut Challenge,
945 authz: &mut Authorization,
946 order: &mut Order,
947 database: &Arc<Database>,
948) -> Result<(), Problem> {
949 let validated = now_secs();
950 let outcome = async {
951 let mut tx = database.transaction().await?;
952 let challenge_written = Challenge::set_valid(challenge.id, validated, tx.conn()).await?;
959 let authz_written =
960 challenge_written && Authorization::set_valid(authz.id, tx.conn()).await?;
961
962 let promoted = authz_written && {
968 let authzs = Authorization::find_by_order_with(order.id, tx.conn()).await?;
969 authzs.len() == order.identifiers.len()
970 && authzs
971 .iter()
972 .all(|authz| authz.status == AuthzStatus::Valid)
973 && Order::set_ready(order.id, tx.conn()).await?
974 };
975 tx.commit().await?;
976 Ok::<_, sqlx::Error>((challenge_written, authz_written, promoted))
977 }
978 .await;
979
980 match outcome {
981 Ok((challenge_written, authz_written, promoted)) => {
982 if challenge_written {
985 challenge.status = ChallengeStatus::Valid;
986 challenge.validated = Some(validated);
987 }
988 if authz_written {
989 authz.status = AuthzStatus::Valid;
990 } else if challenge_written {
991 info!(event = "challenge_verdict_superseded", outcome = "advisory", challenge_id = %challenge.id, authz_id = %authz.id);
992 }
993 if promoted {
994 order.status = OrderStatus::Ready;
995 }
996 Ok(())
997 }
998 Err(error) => {
999 error!(
1000 event = "challenge_validation_persist_failed",
1001 outcome = "failure",
1002 challenge_id = %challenge.id,
1003 authz_id = %authz.id,
1004 order_id = %order.id,
1005 error = %error
1006 );
1007 Err(Problem::server_internal("Challenge validation failed"))
1008 }
1009 }
1010}
1011
1012async fn commit_validation_failure(
1024 challenge: &mut Challenge,
1025 authz: &mut Authorization,
1026 order: &mut Order,
1027 problem: &Value,
1028 database: &Arc<Database>,
1029) -> Result<bool, Problem> {
1030 let outcome = async {
1031 let mut tx = database.transaction().await?;
1032 let challenge_written = Challenge::set_invalid(challenge.id, problem, tx.conn()).await?;
1033 let authz_written =
1034 challenge_written && Authorization::set_invalid(authz.id, tx.conn()).await?;
1035 let order_written =
1036 authz_written && Order::set_invalid(order.id, problem, tx.conn()).await?;
1037 tx.commit().await?;
1038 Ok::<_, sqlx::Error>((challenge_written, authz_written, order_written))
1039 }
1040 .await;
1041
1042 match outcome {
1043 Ok((challenge_written, authz_written, order_written)) => {
1044 if challenge_written {
1045 challenge.status = ChallengeStatus::Invalid;
1046 challenge.error = Some(problem.clone());
1047 }
1048 if authz_written {
1049 authz.status = AuthzStatus::Invalid;
1050 } else if challenge_written {
1051 info!(event = "challenge_verdict_superseded", outcome = "advisory", challenge_id = %challenge.id, authz_id = %authz.id);
1052 }
1053 if order_written {
1054 order.status = OrderStatus::Invalid;
1055 order.error = Some(problem.clone());
1056 }
1057 Ok(challenge_written)
1058 }
1059 Err(error) => {
1060 error!(
1061 event = "challenge_failure_persist_failed",
1062 outcome = "failure",
1063 challenge_id = %challenge.id,
1064 authz_id = %authz.id,
1065 order_id = %order.id,
1066 error = %error
1067 );
1068 Err(Problem::server_internal("Challenge validation failed"))
1069 }
1070 }
1071}
1072
1073#[cfg(test)]
1078pub(crate) mod tests {
1079 use super::*;
1080 use crate::profile::ProfileParts;
1081 use acme_proxy_core::identifier::Identifier;
1082 use acme_proxy_jobs::notify::NotifyDispatcher;
1083 use acme_proxy_net::challenge::ChallengeError;
1084 use acme_proxy_net::challenge::ChallengeRegistry;
1085 use acme_proxy_net::challenge::ChallengeValidator;
1086 use std::time::Duration;
1087
1088 pub(crate) fn profile(database: &Arc<Database>, challenges: ChallengeRegistry) -> Profile {
1091 let ca = acme_proxy_signer::local_ca::LocalCa::generate_in_memory(
1092 "ecdsa-p256",
1093 90,
1094 database.clone(),
1095 )
1096 .unwrap();
1097 profile_with(database, challenges, Arc::new(ca))
1098 }
1099
1100 pub(crate) fn profile_with(
1102 database: &Arc<Database>,
1103 challenges: ChallengeRegistry,
1104 signer: Arc<dyn acme_proxy_signer::SignerBackend>,
1105 ) -> Profile {
1106 Profile::new(
1107 "default",
1108 "http://localhost:3000",
1109 ProfileParts {
1110 signer_info: signer.info(),
1111 filter: Arc::new(acme_proxy_policy::filter::FilterPolicy::default()),
1112 challenges: Arc::new(challenges),
1113 order: acme_proxy_core::config::OrderConfig::default(),
1114 eab: acme_proxy_core::config::EabConfig::default(),
1115 meta: acme_proxy_core::config::MetaConfig::default(),
1116 notify: Arc::new(NotifyDispatcher::disabled(
1117 acme_proxy_jobs::testutil::idle_job_queue(database.clone()),
1118 )),
1119 },
1120 )
1121 }
1122
1123 pub(crate) async fn account(database: &Arc<Database>) -> Account {
1126 use rcgen::PublicKeyData;
1127 let key = rcgen::KeyPair::generate().unwrap();
1128 Account::find_or_create(
1129 "default",
1130 &key.subject_public_key_info(),
1131 vec![],
1132 &acme_proxy_core::audit::ClientContext::default(),
1133 database,
1134 )
1135 .await
1136 .unwrap()
1137 .0
1138 }
1139
1140 async fn pending_order(
1143 database: &Arc<Database>,
1144 account: &Account,
1145 names: &[&str],
1146 ) -> (Order, Vec<(Authorization, Challenge)>) {
1147 let order = Order::create(
1148 "default",
1149 account.id,
1150 acme_proxy_store::testutil::dns_identifiers(names),
1151 now_secs() + 3600,
1152 None,
1153 None,
1154 database,
1155 )
1156 .await
1157 .unwrap();
1158 let mut authzs = Vec::new();
1159 for name in names {
1160 let authz =
1161 Authorization::create(order.id, Identifier::dns(*name), order.expires, database)
1162 .await
1163 .unwrap();
1164 let challenge = Challenge::create(authz.id, "http-01", database)
1165 .await
1166 .unwrap();
1167 authzs.push((authz, challenge));
1168 }
1169 (order, authzs)
1170 }
1171
1172 async fn reload(database: &Database, order: &Order) -> Order {
1173 Order::find_by_id(&order.id.to_string(), database)
1174 .await
1175 .unwrap()
1176 .unwrap()
1177 }
1178
1179 async fn reload_authz(database: &Database, authz: &Authorization) -> Authorization {
1180 Authorization::find_by_id(&authz.id.to_string(), database)
1181 .await
1182 .unwrap()
1183 .unwrap()
1184 }
1185
1186 struct Refusing;
1188
1189 #[async_trait::async_trait]
1190 impl ChallengeValidator for Refusing {
1191 fn typ(&self) -> &'static str {
1192 "http-01"
1193 }
1194 async fn validate(&self, _ctx: &ValidationContext<'_>) -> Result<(), ChallengeError> {
1195 Err(ChallengeError::IncorrectResponse("wrong body".into()))
1196 }
1197 }
1198
1199 #[tokio::test]
1200 async fn deactivating_under_a_ready_order_demotes_it_in_the_same_write() {
1201 let database = Arc::new(Database::connect_in_memory().await.unwrap());
1202 let profile = profile(&database, ChallengeRegistry::default());
1203 let audit = Auditor::offline(database.clone());
1204 let orders = OrderService {
1205 database: &database,
1206 audit: &audit,
1207 profile: &profile,
1208 };
1209 let account = account(&database).await;
1210 let (mut order, mut authzs) = pending_order(&database, &account, &["a.example.com"]).await;
1211 let (authz, challenge) = &mut authzs[0];
1212
1213 assert_eq!(
1214 orders
1215 .claim_challenge(challenge, authz, &order)
1216 .await
1217 .unwrap(),
1218 ValidationClaim::Claimed
1219 );
1220 orders
1221 .run_validation(&account, challenge, authz, &mut order, None)
1222 .await
1223 .unwrap();
1224 assert_eq!(reload(&database, &order).await.status, OrderStatus::Ready);
1225
1226 orders.deactivate_authz(authz, &mut order).await.unwrap();
1227 assert_eq!(authz.status, AuthzStatus::Deactivated);
1228 assert_eq!(reload(&database, &order).await.status, OrderStatus::Pending);
1229
1230 orders.deactivate_authz(authz, &mut order).await.unwrap();
1232
1233 let refused = orders
1235 .claim_challenge(challenge, authz, &order)
1236 .await
1237 .unwrap_err();
1238 assert_eq!(
1239 Problem::from(refused).to_value()["detail"],
1240 "Authorization has been deactivated"
1241 );
1242 }
1243
1244 #[tokio::test]
1245 async fn an_issued_order_refuses_deactivation() {
1246 let database = Arc::new(Database::connect_in_memory().await.unwrap());
1247 let profile = profile(&database, ChallengeRegistry::default());
1248 let audit = Auditor::offline(database.clone());
1249 let orders = OrderService {
1250 database: &database,
1251 audit: &audit,
1252 profile: &profile,
1253 };
1254 let account = account(&database).await;
1255 let (mut order, mut authzs) = pending_order(&database, &account, &["a.example.com"]).await;
1256 order.status = OrderStatus::Valid;
1257
1258 let refused = orders
1259 .deactivate_authz(&mut authzs[0].0, &mut order)
1260 .await
1261 .unwrap_err();
1262 assert_eq!(Problem::from(refused).status(), 400);
1263 assert_eq!(authzs[0].0.status, AuthzStatus::Pending);
1264 }
1265
1266 #[tokio::test]
1269 async fn a_challenge_is_claimed_once() {
1270 let database = Arc::new(Database::connect_in_memory().await.unwrap());
1271 let profile = profile(&database, ChallengeRegistry::default());
1272 let audit = Auditor::offline(database.clone());
1273 let orders = OrderService {
1274 database: &database,
1275 audit: &audit,
1276 profile: &profile,
1277 };
1278 let account = account(&database).await;
1279 let (order, mut authzs) = pending_order(&database, &account, &["a.example.com"]).await;
1280 let (authz, challenge) = &mut authzs[0];
1281 let mut twin = Challenge::find_by_id(&challenge.id.to_string(), &database)
1282 .await
1283 .unwrap()
1284 .unwrap();
1285
1286 assert_eq!(
1287 orders
1288 .claim_challenge(challenge, authz, &order)
1289 .await
1290 .unwrap(),
1291 ValidationClaim::Claimed
1292 );
1293 assert_eq!(
1294 orders
1295 .claim_challenge(&mut twin, authz, &order)
1296 .await
1297 .unwrap(),
1298 ValidationClaim::Decided
1299 );
1300 }
1301
1302 #[tokio::test]
1305 async fn concurrent_validations_of_one_order_promote_it() {
1306 let database = Arc::new(Database::connect_in_memory().await.unwrap());
1307 let profile = profile(&database, ChallengeRegistry::default());
1308 let audit = Auditor::offline(database.clone());
1309 let orders = OrderService {
1310 database: &database,
1311 audit: &audit,
1312 profile: &profile,
1313 };
1314 let account = account(&database).await;
1315 let (order, authzs) =
1316 pending_order(&database, &account, &["a.example.com", "b.example.com"]).await;
1317 let mut authzs = authzs.into_iter();
1318 let (mut authz_a, mut challenge_a) = authzs.next().unwrap();
1319 let (mut authz_b, mut challenge_b) = authzs.next().unwrap();
1320 let (mut order_a, mut order_b) = (
1321 reload(&database, &order).await,
1322 reload(&database, &order).await,
1323 );
1324
1325 let a = async {
1326 assert_eq!(
1327 orders
1328 .claim_challenge(&mut challenge_a, &authz_a, &order_a)
1329 .await
1330 .unwrap(),
1331 ValidationClaim::Claimed
1332 );
1333 orders
1334 .run_validation(&account, &mut challenge_a, &mut authz_a, &mut order_a, None)
1335 .await
1336 .unwrap();
1337 };
1338 let b = async {
1339 assert_eq!(
1340 orders
1341 .claim_challenge(&mut challenge_b, &authz_b, &order_b)
1342 .await
1343 .unwrap(),
1344 ValidationClaim::Claimed
1345 );
1346 orders
1347 .run_validation(&account, &mut challenge_b, &mut authz_b, &mut order_b, None)
1348 .await
1349 .unwrap();
1350 };
1351 tokio::join!(a, b);
1352
1353 assert_eq!(reload(&database, &order).await.status, OrderStatus::Ready);
1354 }
1355
1356 #[tokio::test]
1357 async fn a_failed_validation_invalidates_challenge_authorization_and_order_together() {
1358 let database = Arc::new(Database::connect_in_memory().await.unwrap());
1359 let profile = profile(
1360 &database,
1361 ChallengeRegistry::new(
1362 vec![Arc::new(Refusing)],
1363 vec!["http-01".to_string()],
1364 false,
1365 Duration::from_secs(5),
1366 ),
1367 );
1368 let audit = Auditor::offline(database.clone());
1369 let orders = OrderService {
1370 database: &database,
1371 audit: &audit,
1372 profile: &profile,
1373 };
1374 let account = account(&database).await;
1375 let (mut order, mut authzs) = pending_order(&database, &account, &["a.example.com"]).await;
1376 let (authz, challenge) = &mut authzs[0];
1377
1378 assert_eq!(
1379 orders
1380 .claim_challenge(challenge, authz, &order)
1381 .await
1382 .unwrap(),
1383 ValidationClaim::Claimed
1384 );
1385 orders
1386 .run_validation(&account, challenge, authz, &mut order, None)
1387 .await
1388 .expect("a refused validation is the challenge's answer, not an error");
1389
1390 let stored = Challenge::find_by_id(&challenge.id.to_string(), &database)
1391 .await
1392 .unwrap()
1393 .unwrap();
1394 assert_eq!(stored.status, ChallengeStatus::Invalid);
1395 assert_eq!(
1396 stored.error.unwrap()["type"],
1397 "urn:ietf:params:acme:error:incorrectResponse"
1398 );
1399 let reloaded = reload(&database, &order).await;
1400 assert_eq!(reloaded.status, OrderStatus::Invalid);
1401 assert_eq!(authz.status, AuthzStatus::Invalid);
1402 }
1403
1404 #[tokio::test]
1407 async fn duplicate_identifiers_become_one() {
1408 let database = Arc::new(Database::connect_in_memory().await.unwrap());
1409 let profile = profile(&database, ChallengeRegistry::default());
1410 let audit = Auditor::offline(database.clone());
1411 let orders = OrderService {
1412 database: &database,
1413 audit: &audit,
1414 profile: &profile,
1415 };
1416 let account = account(&database).await;
1417 let pubkey = account.pubkey.clone();
1418 let payload = NewOrderPayload {
1419 identifiers: vec![
1420 Identifier::dns("A.example.com"),
1421 Identifier::dns("a.example.com."),
1422 ],
1423 ..Default::default()
1424 };
1425
1426 let (order, authz_ids) = orders
1427 .new_order(
1428 payload,
1429 Some(account),
1430 &pubkey,
1431 None,
1432 &RequestContext::default(),
1433 )
1434 .await
1435 .unwrap();
1436
1437 assert_eq!(
1438 order.identifiers,
1439 acme_proxy_store::testutil::dns_identifiers(&["a.example.com"])
1440 );
1441 assert_eq!(authz_ids.len(), 1);
1442 }
1443
1444 fn bypassing() -> ChallengeRegistry {
1446 ChallengeRegistry::new(
1447 vec![],
1448 vec!["http-01".to_string(), "dns-01".to_string()],
1449 true,
1450 Duration::from_secs(5),
1451 )
1452 }
1453
1454 fn refusing() -> ChallengeRegistry {
1456 ChallengeRegistry::new(
1457 vec![Arc::new(Refusing)],
1458 vec!["http-01".to_string()],
1459 false,
1460 Duration::from_secs(5),
1461 )
1462 }
1463
1464 #[tokio::test]
1470 async fn a_late_sibling_failure_leaves_an_issued_order_valid() {
1471 let database = Arc::new(Database::connect_in_memory().await.unwrap());
1472 let passing = profile(&database, bypassing());
1473 let failing = profile(&database, refusing());
1474 let audit = Auditor::offline(database.clone());
1475 let account = account(&database).await;
1476 let (mut order, mut authzs) = pending_order(&database, &account, &["a.example.com"]).await;
1477 let (authz, http) = &mut authzs[0];
1478 let mut dns = Challenge::create(authz.id, "dns-01", &database)
1479 .await
1480 .unwrap();
1481
1482 let on = |profile| OrderService {
1483 database: &database,
1484 audit: &audit,
1485 profile,
1486 };
1487 assert_eq!(
1488 on(&passing)
1489 .claim_challenge(&mut dns, authz, &order)
1490 .await
1491 .unwrap(),
1492 ValidationClaim::Claimed
1493 );
1494 assert_eq!(
1495 on(&passing)
1496 .claim_challenge(http, authz, &order)
1497 .await
1498 .unwrap(),
1499 ValidationClaim::Claimed
1500 );
1501
1502 let mut authz_seen_by_second = reload_authz(&database, authz).await;
1503 let mut order_seen_by_second = reload(&database, &order).await;
1504 on(&passing)
1505 .run_validation(&account, &mut dns, authz, &mut order, None)
1506 .await
1507 .unwrap();
1508 assert_eq!(reload(&database, &order).await.status, OrderStatus::Ready);
1509 sqlx::query("UPDATE orders SET status = 'valid' WHERE id = ?;")
1510 .bind(order.id)
1511 .execute(database.raw_pool())
1512 .await
1513 .unwrap();
1514
1515 on(&failing)
1516 .run_validation(
1517 &account,
1518 http,
1519 &mut authz_seen_by_second,
1520 &mut order_seen_by_second,
1521 None,
1522 )
1523 .await
1524 .unwrap();
1525
1526 assert_eq!(http.status, ChallengeStatus::Invalid);
1527 assert_eq!(reload(&database, &order).await.status, OrderStatus::Valid);
1528 assert_eq!(
1529 reload_authz(&database, authz).await.status,
1530 AuthzStatus::Valid
1531 );
1532 }
1533
1534 #[tokio::test]
1538 async fn a_verdict_after_deactivation_leaves_the_authorization_deactivated() {
1539 let database = Arc::new(Database::connect_in_memory().await.unwrap());
1540 let profile = profile(&database, bypassing());
1541 let audit = Auditor::offline(database.clone());
1542 let orders = OrderService {
1543 database: &database,
1544 audit: &audit,
1545 profile: &profile,
1546 };
1547 let account = account(&database).await;
1548 let (mut order, mut authzs) = pending_order(&database, &account, &["a.example.com"]).await;
1549 let (authz, challenge) = &mut authzs[0];
1550
1551 assert_eq!(
1552 orders
1553 .claim_challenge(challenge, authz, &order)
1554 .await
1555 .unwrap(),
1556 ValidationClaim::Claimed
1557 );
1558 let mut authz_seen_by_job = reload_authz(&database, authz).await;
1559 orders.deactivate_authz(authz, &mut order).await.unwrap();
1560
1561 orders
1562 .run_validation(
1563 &account,
1564 challenge,
1565 &mut authz_seen_by_job,
1566 &mut order,
1567 None,
1568 )
1569 .await
1570 .unwrap();
1571
1572 assert_eq!(
1573 reload_authz(&database, authz).await.status,
1574 AuthzStatus::Deactivated
1575 );
1576 assert_eq!(reload(&database, &order).await.status, OrderStatus::Pending);
1577 }
1578
1579 #[tokio::test]
1583 async fn an_account_over_its_validation_cap_is_rate_limited() {
1584 let database = Arc::new(Database::connect_in_memory().await.unwrap());
1585 let profile = profile(&database, bypassing().with_max_in_flight_per_account(1));
1586 let audit = Auditor::offline(database.clone());
1587 let orders = OrderService {
1588 database: &database,
1589 audit: &audit,
1590 profile: &profile,
1591 };
1592 let account = account(&database).await;
1593 let (first_order, mut first) = pending_order(&database, &account, &["a.example.com"]).await;
1594 let (second_order, mut second) =
1595 pending_order(&database, &account, &["b.example.com"]).await;
1596 let (first_authz, first_challenge) = &mut first[0];
1597 let (second_authz, second_challenge) = &mut second[0];
1598
1599 assert_eq!(
1600 orders
1601 .claim_challenge(first_challenge, first_authz, &first_order)
1602 .await
1603 .unwrap(),
1604 ValidationClaim::Claimed
1605 );
1606 assert_eq!(
1607 orders
1608 .claim_challenge(second_challenge, second_authz, &second_order)
1609 .await
1610 .unwrap(),
1611 ValidationClaim::Limited
1612 );
1613 assert_eq!(second_challenge.status, ChallengeStatus::Pending);
1614
1615 let mut order = reload(&database, &first_order).await;
1617 orders
1618 .run_validation(&account, first_challenge, first_authz, &mut order, None)
1619 .await
1620 .unwrap();
1621 assert_eq!(
1622 orders
1623 .claim_challenge(second_challenge, second_authz, &second_order)
1624 .await
1625 .unwrap(),
1626 ValidationClaim::Claimed
1627 );
1628 }
1629
1630 #[tokio::test]
1634 async fn a_trigger_under_an_invalid_order_is_refused() {
1635 let database = Arc::new(Database::connect_in_memory().await.unwrap());
1636 let profile = profile(&database, refusing());
1637 let audit = Auditor::offline(database.clone());
1638 let orders = OrderService {
1639 database: &database,
1640 audit: &audit,
1641 profile: &profile,
1642 };
1643 let account = account(&database).await;
1644 let (mut order, mut authzs) =
1645 pending_order(&database, &account, &["a.example.com", "b.example.com"]).await;
1646 let (first, rest) = authzs.split_at_mut(1);
1647 let (authz_a, challenge_a) = &mut first[0];
1648 let (authz_b, challenge_b) = &mut rest[0];
1649
1650 assert_eq!(
1651 orders
1652 .claim_challenge(challenge_a, authz_a, &order)
1653 .await
1654 .unwrap(),
1655 ValidationClaim::Claimed
1656 );
1657 orders
1658 .run_validation(&account, challenge_a, authz_a, &mut order, None)
1659 .await
1660 .unwrap();
1661 assert_eq!(order.status, OrderStatus::Invalid);
1662
1663 let refused = orders
1664 .claim_challenge(challenge_b, authz_b, &order)
1665 .await
1666 .unwrap_err();
1667 assert_eq!(Problem::from(refused).status(), 400);
1668
1669 assert_eq!(
1672 challenge_b
1673 .claim_for_validation(0, &database)
1674 .await
1675 .unwrap(),
1676 ValidationClaim::Decided
1677 );
1678 }
1679
1680 #[tokio::test]
1691 async fn a_trigger_that_straddles_its_own_verdict_is_answered_with_the_challenge() {
1692 let database = Arc::new(Database::connect_in_memory().await.unwrap());
1693 let profile = profile(&database, refusing());
1694 let audit = Auditor::offline(database.clone());
1695 let orders = OrderService {
1696 database: &database,
1697 audit: &audit,
1698 profile: &profile,
1699 };
1700 let account = account(&database).await;
1701 let (mut order, mut authzs) = pending_order(&database, &account, &["a.example.com"]).await;
1702 let (authz, challenge) = &mut authzs[0];
1703
1704 assert_eq!(
1705 orders
1706 .claim_challenge(challenge, authz, &order)
1707 .await
1708 .unwrap(),
1709 ValidationClaim::Claimed
1710 );
1711
1712 let mut stale = Challenge::find_by_id(challenge.id.to_string().as_str(), &database)
1715 .await
1716 .unwrap()
1717 .unwrap();
1718 assert_eq!(stale.status, ChallengeStatus::Processing);
1719
1720 orders
1721 .run_validation(&account, challenge, authz, &mut order, None)
1722 .await
1723 .unwrap();
1724 assert_eq!(authz.status, AuthzStatus::Invalid);
1725 assert_eq!(order.status, OrderStatus::Invalid);
1726
1727 assert_eq!(
1730 orders
1731 .claim_challenge(&mut stale, authz, &order)
1732 .await
1733 .unwrap(),
1734 ValidationClaim::Decided
1735 );
1736 assert_eq!(stale.status, ChallengeStatus::Invalid);
1737 assert!(
1738 stale.error.is_some(),
1739 "the refreshed challenge carries the verdict the client came for"
1740 );
1741 }
1742
1743 #[tokio::test]
1751 async fn a_replaces_collision_is_told_apart_from_other_unique_violations() {
1752 let database = Database::connect_in_memory().await.unwrap();
1753 sqlx::query(
1754 "INSERT INTO accounts (id, profile, pubkey, contact, status, created_at) \
1755 VALUES ('acct', 'default', X'00', '[]', 'valid', 0);",
1756 )
1757 .execute(database.raw_pool())
1758 .await
1759 .unwrap();
1760
1761 let order = |id: &'static str, replaces: &'static str| {
1762 let pool = database.raw_pool().clone();
1763 async move {
1764 sqlx::query(
1765 "INSERT INTO orders (id, profile, account_id, status, identifiers, expires, \
1766 replaces, created_at) VALUES (?, 'default', 'acct', 'pending', '[]', 0, ?, 0);",
1767 )
1768 .bind(id)
1769 .bind(replaces)
1770 .execute(&pool)
1771 .await
1772 }
1773 };
1774
1775 order("first", "predecessor-cert-id").await.unwrap();
1776 let collision = order("second", "predecessor-cert-id").await.unwrap_err();
1777 assert!(is_replaces_conflict(&collision), "got {collision}");
1778
1779 let authz = |id: &'static str| {
1783 let pool = database.raw_pool().clone();
1784 async move {
1785 sqlx::query(
1786 "INSERT INTO authorizations (id, order_id, identifier, status, expires, \
1787 created_at) VALUES (?, 'first', '{\"type\":\"dns\",\"value\":\"a.example.com\"}', \
1788 'pending', 0, 0);",
1789 )
1790 .bind(id)
1791 .execute(&pool)
1792 .await
1793 }
1794 };
1795 authz("authz-one").await.unwrap();
1796 let other = authz("authz-two").await.unwrap_err();
1797 assert!(
1798 !is_replaces_conflict(&other),
1799 "an authorization collision must not read as alreadyReplaced: {other}"
1800 );
1801
1802 let missing = sqlx::query("INSERT INTO orders (id) VALUES ('x');")
1804 .execute(database.raw_pool())
1805 .await
1806 .unwrap_err();
1807 assert!(!is_replaces_conflict(&missing));
1808 }
1809
1810 pub(crate) async fn ready_order(
1812 database: &Arc<Database>,
1813 account: &Account,
1814 ) -> (Order, String) {
1815 let (order, authzs) = pending_order(database, account, &["a.example.com"]).await;
1816 for (authz, _) in &authzs {
1817 assert!(
1818 Authorization::set_valid(authz.id, database.raw_pool())
1819 .await
1820 .unwrap()
1821 );
1822 }
1823 assert!(
1824 Order::set_ready(order.id, database.raw_pool())
1825 .await
1826 .unwrap()
1827 );
1828 let key = rcgen::KeyPair::generate().unwrap();
1829 let csr = rcgen::CertificateParams::new(vec!["a.example.com".to_string()])
1830 .unwrap()
1831 .serialize_request(&key)
1832 .unwrap();
1833 (
1834 reload(database, &order).await,
1835 BASE64_URL_SAFE_NO_PAD.encode(csr.der()),
1836 )
1837 }
1838
1839 async fn finalize_ready() -> (Arc<Database>, Order, Result<Order, Error>) {
1842 let database = Arc::new(Database::connect_in_memory().await.unwrap());
1843 let profile = profile(&database, ChallengeRegistry::default());
1844 let audit = Auditor::offline(database.clone());
1845 let orders = OrderService {
1846 database: &database,
1847 audit: &audit,
1848 profile: &profile,
1849 };
1850 let account = account(&database).await;
1851 let (order, csr) = ready_order(&database, &account).await;
1852 let before = reload(&database, &order).await;
1853 let jobs = acme_proxy_jobs::testutil::idle_job_queue(database.clone());
1854 let outcome = orders
1855 .finalize(
1856 &account,
1857 order,
1858 &csr,
1859 None,
1860 &RequestContext::default(),
1861 &jobs,
1862 )
1863 .await;
1864 (database, before, outcome)
1865 }
1866
1867 #[tokio::test]
1871 async fn finalize_claims_the_order_and_queues_its_issuance() {
1872 let (database, order, outcome) = finalize_ready().await;
1873 let answered = outcome.unwrap();
1874 assert_eq!(answered.status, OrderStatus::Processing);
1875 let stored = reload(&database, &order).await;
1876 assert_eq!(stored.status, OrderStatus::Processing);
1877 assert!(stored.certificate.is_none(), "nothing was signed here");
1878
1879 let job = acme_proxy_store::job::Job::find_live(
1880 super::super::issue::SIGNER_ISSUE_KIND,
1881 &order.id.to_string(),
1882 &database,
1883 )
1884 .await
1885 .unwrap()
1886 .expect("the issuance is queued");
1887 assert_eq!(job.payload["order_id"], order.id.to_string());
1888 assert_eq!(job.payload["profile"], "default");
1889 assert!(
1890 job.payload["csr"]
1891 .as_str()
1892 .is_some_and(|csr| !csr.is_empty())
1893 );
1894 assert_eq!(job.deadline, Some(order.expires));
1895 }
1896
1897 #[tokio::test]
1900 async fn a_second_finalize_loses_the_claim() {
1901 let database = Arc::new(Database::connect_in_memory().await.unwrap());
1902 let profile = profile(&database, ChallengeRegistry::default());
1903 let audit = Auditor::offline(database.clone());
1904 let orders = OrderService {
1905 database: &database,
1906 audit: &audit,
1907 profile: &profile,
1908 };
1909 let account = account(&database).await;
1910 let (order, csr) = ready_order(&database, &account).await;
1911 let jobs = acme_proxy_jobs::testutil::idle_job_queue(database.clone());
1912
1913 let rival = reload(&database, &order).await;
1915 orders
1916 .finalize(
1917 &account,
1918 order,
1919 &csr,
1920 None,
1921 &RequestContext::default(),
1922 &jobs,
1923 )
1924 .await
1925 .unwrap();
1926 let error = orders
1927 .finalize(
1928 &account,
1929 rival,
1930 &csr,
1931 None,
1932 &RequestContext::default(),
1933 &jobs,
1934 )
1935 .await
1936 .unwrap_err();
1937 let problem = Problem::from(error).to_value();
1938 assert_eq!(problem["type"], "urn:ietf:params:acme:error:orderNotReady");
1939 assert_eq!(problem["detail"], "Order is already being finalized");
1940 }
1941
1942 #[tokio::test]
1946 async fn a_failed_enqueue_leaves_the_order_ready() {
1947 let database = Arc::new(Database::connect_in_memory().await.unwrap());
1948 let profile = profile(&database, ChallengeRegistry::default());
1949 let audit = Auditor::offline(database.clone());
1950 let orders = OrderService {
1951 database: &database,
1952 audit: &audit,
1953 profile: &profile,
1954 };
1955 let account = account(&database).await;
1956 let (order, csr) = ready_order(&database, &account).await;
1957 let jobs = acme_proxy_jobs::testutil::idle_job_queue(database.clone());
1958 sqlx::query("DROP TABLE jobs;")
1959 .execute(database.raw_pool())
1960 .await
1961 .unwrap();
1962
1963 let before = reload(&database, &order).await;
1964 let error = orders
1965 .finalize(
1966 &account,
1967 order,
1968 &csr,
1969 None,
1970 &RequestContext::default(),
1971 &jobs,
1972 )
1973 .await
1974 .unwrap_err();
1975 assert_eq!(
1976 Problem::from(error).to_value()["detail"],
1977 "Order finalize failed"
1978 );
1979 assert_eq!(reload(&database, &before).await.status, OrderStatus::Ready);
1980 }
1981}