1use std::collections::BTreeMap;
4use std::sync::Arc;
5
6use async_trait::async_trait;
7use base64::Engine;
8use chrono::{Duration, Utc};
9use http::StatusCode;
10use parking_lot::RwLock;
11use serde_json::{json, Value};
12use sha2::{Digest, Sha256};
13use uuid::Uuid;
14
15use tokio::sync::Mutex as AsyncMutex;
16
17use fakecloud_aws::arn::Arn;
18use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
19use fakecloud_persistence::SnapshotStore;
20
21use crate::state::{
22 AccountState, AcmAccounts, AcmSnapshot, CertificateOptions, DomainValidation, RenewalSummary,
23 SharedAcmState, StoredCertificate, ACM_SNAPSHOT_SCHEMA_VERSION,
24};
25
26const SUPPORTED_ACTIONS: &[&str] = &[
27 "RequestCertificate",
28 "DescribeCertificate",
29 "ListCertificates",
30 "DeleteCertificate",
31 "ImportCertificate",
32 "ExportCertificate",
33 "GetCertificate",
34 "RenewCertificate",
35 "RevokeCertificate",
36 "ResendValidationEmail",
37 "AddTagsToCertificate",
38 "RemoveTagsFromCertificate",
39 "ListTagsForCertificate",
40 "GetAccountConfiguration",
41 "PutAccountConfiguration",
42 "UpdateCertificateOptions",
43 "SearchCertificates",
44];
45
46const MUTATING_ACTIONS: &[&str] = &[
50 "RequestCertificate",
51 "DeleteCertificate",
52 "ImportCertificate",
53 "RenewCertificate",
54 "RevokeCertificate",
55 "AddTagsToCertificate",
56 "RemoveTagsFromCertificate",
57 "PutAccountConfiguration",
58 "UpdateCertificateOptions",
59];
60
61pub struct AcmService {
62 state: SharedAcmState,
63 pending_validation_delay: std::time::Duration,
73 snapshot_store: Option<Arc<dyn SnapshotStore>>,
74 snapshot_lock: Arc<AsyncMutex<()>>,
75}
76
77const DEFAULT_AUTO_ISSUE_SECS: u64 = 5;
81
82fn auto_issue_delay_from_env() -> std::time::Duration {
83 let secs = std::env::var("FAKECLOUD_ACM_AUTO_ISSUE_SECS")
84 .ok()
85 .and_then(|v| v.parse::<u64>().ok())
86 .unwrap_or(DEFAULT_AUTO_ISSUE_SECS);
87 std::time::Duration::from_secs(secs)
88}
89
90impl AcmService {
91 pub fn new(state: SharedAcmState) -> Self {
92 Self {
93 state,
94 pending_validation_delay: auto_issue_delay_from_env(),
95 snapshot_store: None,
96 snapshot_lock: Arc::new(AsyncMutex::new(())),
97 }
98 }
99
100 pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
101 self.snapshot_store = Some(store);
102 self
103 }
104
105 pub fn shared_state(&self) -> SharedAcmState {
106 Arc::clone(&self.state)
107 }
108
109 async fn save_snapshot(&self) {
113 save_acm_snapshot(
114 &self.state,
115 self.snapshot_store.clone(),
116 &self.snapshot_lock,
117 )
118 .await;
119 }
120
121 pub fn snapshot_hook(&self) -> Option<fakecloud_persistence::SnapshotHook> {
126 let store = self.snapshot_store.clone()?;
127 let state = self.state.clone();
128 let lock = self.snapshot_lock.clone();
129 Some(Arc::new(move || {
130 let state = state.clone();
131 let store = store.clone();
132 let lock = lock.clone();
133 Box::pin(async move {
134 save_acm_snapshot(&state, Some(store), &lock).await;
135 })
136 }))
137 }
138
139 pub async fn set_certificate_status_persistent(
142 &self,
143 arn_or_id: &str,
144 status: &str,
145 reason: Option<String>,
146 ) -> bool {
147 let changed = self.set_certificate_status(arn_or_id, status, reason);
148 if changed {
149 self.save_snapshot().await;
150 }
151 changed
152 }
153
154 pub async fn approve_certificate_persistent(&self, arn_or_id: &str) -> bool {
157 self.set_certificate_status_persistent(arn_or_id, "ISSUED", None)
158 .await
159 }
160
161 pub fn rearm_pending_validations(&self) {
166 let pending: Vec<(String, String)> = {
167 let state = self.state.read();
168 let mut out = Vec::new();
169 for (account_id, account) in state.accounts.iter() {
170 for (arn, cert) in account.certificates.iter() {
171 if cert.status == "PENDING_VALIDATION"
172 && cert.cert_type == "AMAZON_ISSUED"
173 && cert.validation_method.as_deref() == Some("DNS")
174 {
175 out.push((account_id.clone(), arn.clone()));
176 }
177 }
178 }
179 out
180 };
181 for (account_id, arn) in pending {
182 self.spawn_auto_issue_tick(account_id, arn);
183 }
184 }
185
186 fn spawn_auto_issue_tick(&self, account_id: String, arn: String) {
191 let state_for_tick = Arc::clone(&self.state);
192 let delay = self.pending_validation_delay;
193 let store = self.snapshot_store.clone();
194 let lock = self.snapshot_lock.clone();
195 tokio::spawn(async move {
196 tokio::time::sleep(delay).await;
197 let flipped = {
198 let mut state = state_for_tick.write();
199 let mut flipped = false;
200 if let Some(account) = state.accounts.get_mut(&account_id) {
201 if let Some(cert) = account.certificates.get_mut(&arn) {
202 if cert.status == "PENDING_VALIDATION" && cert.cert_type == "AMAZON_ISSUED"
203 {
204 let now = Utc::now();
205 cert.status = "ISSUED".to_string();
206 cert.issued_at = Some(now);
207 cert.renewal_eligibility = "ELIGIBLE".to_string();
208 for dv in cert.domain_validation.iter_mut() {
209 dv.validation_status = "SUCCESS".to_string();
210 }
211 cert.renewal_summary = Some(RenewalSummary {
212 renewal_status: "PENDING_AUTO_RENEWAL".to_string(),
213 domain_validation: cert.domain_validation.clone(),
214 renewal_status_reason: None,
215 updated_at: now,
216 });
217 flipped = true;
218 }
219 }
220 }
221 flipped
222 };
223 if flipped {
224 save_acm_snapshot(&state_for_tick, store, &lock).await;
225 }
226 });
227 }
228
229 pub fn with_pending_validation_delay(mut self, delay: std::time::Duration) -> Self {
233 self.pending_validation_delay = delay;
234 self
235 }
236
237 pub fn approve_certificate(&self, arn_or_id: &str) -> bool {
246 self.set_certificate_status(arn_or_id, "ISSUED", None)
247 }
248
249 pub fn chain_info(&self, arn_or_id: &str) -> Option<serde_json::Value> {
260 let state = self.state.read();
261 for account in state.accounts.values() {
262 let key = account
263 .certificates
264 .keys()
265 .find(|k| k.as_str() == arn_or_id || cert_id_from_arn(k) == arn_or_id)
266 .cloned()?;
267 if let Some(cert) = account.certificates.get(&key) {
268 let pem = cert.certificate_pem.as_deref().unwrap_or("");
269 let chain = cert.certificate_chain_pem.as_deref().unwrap_or("");
270 return Some(serde_json::json!({
271 "certificate_arn": key,
272 "certificate_pem_bytes": pem.len(),
273 "certificate_pem_blocks": pem.matches("-----BEGIN CERTIFICATE-----").count(),
274 "chain_pem_bytes": chain.len(),
275 "chain_pem_blocks": chain.matches("-----BEGIN CERTIFICATE-----").count(),
276 "external_ca_validated": false,
277 "status": cert.status,
278 "cert_type": cert.cert_type,
279 }));
280 }
281 }
282 None
283 }
284
285 pub fn set_certificate_status(
295 &self,
296 arn_or_id: &str,
297 status: &str,
298 reason: Option<String>,
299 ) -> bool {
300 let mut state = self.state.write();
301 for account in state.accounts.values_mut() {
302 let key = account
303 .certificates
304 .keys()
305 .find(|k| k.as_str() == arn_or_id || cert_id_from_arn(k) == arn_or_id)
306 .cloned();
307 if let Some(key) = key {
308 if let Some(cert) = account.certificates.get_mut(&key) {
309 cert.status = status.to_string();
310 match status {
311 "ISSUED" => {
312 let now = Utc::now();
313 cert.issued_at = Some(now);
314 for dv in cert.domain_validation.iter_mut() {
315 dv.validation_status = "SUCCESS".to_string();
316 }
317 cert.failure_reason = None;
318 if cert.cert_type == "AMAZON_ISSUED" {
322 cert.renewal_eligibility = "ELIGIBLE".to_string();
323 cert.renewal_summary = Some(RenewalSummary {
324 renewal_status: "PENDING_AUTO_RENEWAL".to_string(),
325 domain_validation: cert.domain_validation.clone(),
326 renewal_status_reason: None,
327 updated_at: now,
328 });
329 }
330 }
331 "FAILED" | "VALIDATION_TIMED_OUT" => {
332 for dv in cert.domain_validation.iter_mut() {
333 dv.validation_status = status.to_string();
334 }
335 if reason.is_some() {
336 cert.failure_reason = reason;
337 }
338 }
339 _ => {
340 if reason.is_some() {
341 cert.failure_reason = reason;
342 }
343 }
344 }
345 return true;
346 }
347 }
348 }
349 false
350 }
351}
352
353pub async fn save_acm_snapshot(
359 state: &SharedAcmState,
360 store: Option<Arc<dyn SnapshotStore>>,
361 lock: &AsyncMutex<()>,
362) {
363 let Some(store) = store else {
364 return;
365 };
366 let _guard = lock.lock().await;
367 let snapshot = AcmSnapshot {
368 schema_version: ACM_SNAPSHOT_SCHEMA_VERSION,
369 accounts: Some(state.read().clone()),
370 };
371 let join = tokio::task::spawn_blocking(move || -> std::io::Result<()> {
372 let bytes = serde_json::to_vec(&snapshot)
373 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
374 store.save(&bytes)
375 })
376 .await;
377 match join {
378 Ok(Ok(())) => {}
379 Ok(Err(err)) => tracing::error!(%err, "failed to write acm snapshot"),
380 Err(err) => tracing::error!(%err, "acm snapshot task panicked"),
381 }
382}
383
384impl Default for AcmService {
385 fn default() -> Self {
386 Self::new(Arc::new(RwLock::new(AcmAccounts::new())))
387 }
388}
389
390#[async_trait]
391impl AwsService for AcmService {
392 fn service_name(&self) -> &str {
393 "acm"
394 }
395
396 fn supported_actions(&self) -> &[&str] {
397 SUPPORTED_ACTIONS
398 }
399
400 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
401 let mutates = MUTATING_ACTIONS.contains(&req.action.as_str());
402 let result = match req.action.as_str() {
403 "RequestCertificate" => self.request_certificate(&req),
404 "DescribeCertificate" => self.describe_certificate(&req),
405 "ListCertificates" => self.list_certificates(&req),
406 "DeleteCertificate" => self.delete_certificate(&req),
407 "ImportCertificate" => self.import_certificate(&req),
408 "ExportCertificate" => self.export_certificate(&req),
409 "GetCertificate" => self.get_certificate(&req),
410 "RenewCertificate" => self.renew_certificate(&req),
411 "RevokeCertificate" => self.revoke_certificate(&req),
412 "ResendValidationEmail" => self.resend_validation_email(&req),
413 "AddTagsToCertificate" => self.add_tags_to_certificate(&req),
414 "RemoveTagsFromCertificate" => self.remove_tags_from_certificate(&req),
415 "ListTagsForCertificate" => self.list_tags_for_certificate(&req),
416 "GetAccountConfiguration" => self.get_account_configuration(&req),
417 "PutAccountConfiguration" => self.put_account_configuration(&req),
418 "UpdateCertificateOptions" => self.update_certificate_options(&req),
419 "SearchCertificates" => self.search_certificates(&req),
420 other => Err(AwsServiceError::action_not_implemented("acm", other)),
421 };
422 if mutates && matches!(result.as_ref(), Ok(resp) if resp.status.is_success()) {
423 self.save_snapshot().await;
424 }
425 result
426 }
427}
428
429impl AcmService {
432 fn request_certificate(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
433 let body = req.json_body();
434 let domain_name = body
435 .get("DomainName")
436 .and_then(Value::as_str)
437 .ok_or_else(|| invalid_param("DomainName is required"))?
438 .to_string();
439 if domain_name.is_empty() || domain_name.len() > 253 {
440 return Err(invalid_param("DomainName length must be between 1 and 253"));
441 }
442 let validation_method = body
443 .get("ValidationMethod")
444 .and_then(Value::as_str)
445 .unwrap_or("DNS")
446 .to_string();
447 if validation_method != "EMAIL" && validation_method != "DNS" && validation_method != "HTTP"
448 {
449 return Err(invalid_param("Invalid ValidationMethod"));
450 }
451 let sans: Vec<String> = body
452 .get("SubjectAlternativeNames")
453 .and_then(Value::as_array)
454 .map(|v| {
455 v.iter()
456 .filter_map(|s| s.as_str().map(|s| s.to_string()))
457 .collect()
458 })
459 .unwrap_or_default();
460 let key_algorithm = body
461 .get("KeyAlgorithm")
462 .and_then(Value::as_str)
463 .unwrap_or("RSA_2048")
464 .to_string();
465 const VALID_KEY_ALGORITHMS: [&str; 7] = [
466 "RSA_1024",
467 "RSA_2048",
468 "RSA_3072",
469 "RSA_4096",
470 "EC_prime256v1",
471 "EC_secp384r1",
472 "EC_secp521r1",
473 ];
474 if !VALID_KEY_ALGORITHMS.contains(&key_algorithm.as_str()) {
475 return Err(invalid_param("Invalid KeyAlgorithm"));
476 }
477 let idempotency_token = body
478 .get("IdempotencyToken")
479 .and_then(Value::as_str)
480 .map(|s| s.to_string());
481 if let Some(ref token) = idempotency_token {
482 if token.is_empty() || token.len() > 32 {
483 return Err(invalid_param(
484 "IdempotencyToken length must be between 1 and 32",
485 ));
486 }
487 if !token
488 .chars()
489 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
490 {
491 return Err(invalid_param(
492 "IdempotencyToken must match pattern ^[\\w-]+$",
493 ));
494 }
495 }
496 let managed_by = body
497 .get("ManagedBy")
498 .and_then(Value::as_str)
499 .map(|s| s.to_string());
500 if let Some(ref m) = managed_by {
501 if m != "CLOUDFRONT" {
502 return Err(invalid_param("Invalid ManagedBy"));
503 }
504 }
505 let ca_arn = body
506 .get("CertificateAuthorityArn")
507 .and_then(Value::as_str)
508 .map(|s| s.to_string());
509 if let Some(ref arn) = ca_arn {
510 if arn.len() < 20 || arn.len() > 2048 {
511 return Err(validation_error(
512 "CertificateAuthorityArn length must be between 20 and 2048",
513 ));
514 }
515 }
516 let tags = parse_tags(body.get("Tags"))?;
517 let options = parse_options(body.get("Options"));
518
519 let mut state = self.state.write();
520 let account = account_mut(&mut state, &req.account_id);
521
522 if let Some(token) = &idempotency_token {
526 if let Some(existing) = account.certificates.values().find(|c| {
527 c.idempotency_token.as_deref() == Some(token)
528 && c.domain_name == domain_name
529 && c.subject_alternative_names == effective_sans(&domain_name, &sans)
530 }) {
531 return Ok(AwsResponse::ok_json(
532 json!({ "CertificateArn": existing.arn }),
533 ));
534 }
535 }
536
537 let arn = synth_certificate_arn(&req.account_id, &req.region);
538 let now = Utc::now();
539 let effective = effective_sans(&domain_name, &sans);
540 let (cert_pem, key_pem) = generate_self_signed_cert(&domain_name, &effective)
541 .map(|(c, k)| (Some(c), Some(k)))
542 .unwrap_or((None, None));
543 let cert = StoredCertificate {
544 arn: arn.clone(),
545 domain_name: domain_name.clone(),
546 subject_alternative_names: effective,
547 status: "PENDING_VALIDATION".to_string(),
548 cert_type: "AMAZON_ISSUED".to_string(),
549 certificate_pem: cert_pem,
550 certificate_chain_pem: None,
551 private_key_pem: key_pem,
552 idempotency_token,
553 serial: synth_serial(&arn),
554 subject: format!("CN={domain_name}"),
555 issuer: "Amazon".to_string(),
556 key_algorithm,
557 signature_algorithm: "SHA256WITHRSA".to_string(),
558 created_at: now,
559 issued_at: None,
560 imported_at: None,
561 revoked_at: None,
562 revocation_reason: None,
563 not_before: now,
565 not_after: now + Duration::days(395),
566 validation_method: Some(validation_method.clone()),
567 domain_validation: synth_domain_validation(&domain_name, &sans, &validation_method),
568 options,
569 renewal_eligibility: "INELIGIBLE".to_string(),
570 managed_by,
571 certificate_authority_arn: ca_arn,
572 tags,
573 in_use_by: Vec::new(),
574 describe_read_count: 0,
575 failure_reason: None,
576 renewal_summary: None,
577 };
578 account.certificates.insert(arn.clone(), cert);
579 drop(state);
580
581 if validation_method == "DNS" {
593 self.spawn_auto_issue_tick(req.account_id.clone(), arn.clone());
594 }
595
596 Ok(AwsResponse::ok_json(json!({ "CertificateArn": arn })))
597 }
598
599 fn describe_certificate(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
600 let arn = require_certificate_arn(req)?;
601 let state = self.state.read();
602 let cert = state
603 .accounts
604 .get(&req.account_id)
605 .and_then(|a| a.certificates.get(&arn))
606 .ok_or_else(|| no_such_certificate(&arn))?
607 .clone();
608 Ok(AwsResponse::ok_json(json!({
615 "Certificate": certificate_detail_json(&cert),
616 })))
617 }
618
619 fn list_certificates(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
620 let body = req.json_body();
621 let max_items: usize = body
622 .get("MaxItems")
623 .and_then(Value::as_u64)
624 .map(|n| n as usize)
625 .unwrap_or(100);
626 if !(1..=1000).contains(&max_items) {
627 return Err(validation_error("MaxItems must be between 1 and 1000"));
628 }
629 let next_token = body
630 .get("NextToken")
631 .and_then(Value::as_str)
632 .map(|s| s.to_string());
633 if let Some(ref token) = next_token {
634 if token.is_empty() || token.len() > 10000 {
635 return Err(validation_error(
636 "NextToken length must be between 1 and 10000",
637 ));
638 }
639 }
640 if let Some(sort_by) = body.get("SortBy").and_then(Value::as_str) {
641 if sort_by != "CREATED_AT" {
642 return Err(validation_error("Invalid SortBy"));
643 }
644 }
645 if let Some(sort_order) = body.get("SortOrder").and_then(Value::as_str) {
646 if sort_order != "ASCENDING" && sort_order != "DESCENDING" {
647 return Err(validation_error("Invalid SortOrder"));
648 }
649 }
650 let statuses: Vec<String> = body
651 .get("CertificateStatuses")
652 .and_then(Value::as_array)
653 .map(|v| {
654 v.iter()
655 .filter_map(|s| s.as_str().map(|s| s.to_string()))
656 .collect()
657 })
658 .unwrap_or_default();
659 let includes = body.get("Includes");
660 let key_types: Vec<String> = includes
661 .and_then(|i| i.get("keyTypes"))
662 .and_then(Value::as_array)
663 .map(|v| {
664 v.iter()
665 .filter_map(|s| s.as_str().map(|s| s.to_string()))
666 .collect()
667 })
668 .unwrap_or_default();
669
670 let state = self.state.read();
671 let mut all: Vec<StoredCertificate> = state
672 .accounts
673 .get(&req.account_id)
674 .map(|a| a.certificates.values().cloned().collect())
675 .unwrap_or_default();
676 drop(state);
677 all.sort_by(|a, b| a.arn.cmp(&b.arn));
678 all.retain(|c| {
679 (statuses.is_empty() || statuses.contains(&c.status))
680 && (key_types.is_empty() || key_types.contains(&c.key_algorithm))
681 });
682
683 let start = next_token
684 .and_then(|t| t.parse::<usize>().ok())
685 .unwrap_or(0);
686 let end = (start + max_items).min(all.len());
687 let page: Vec<&StoredCertificate> = all.iter().skip(start).take(max_items).collect();
688 let next = if end < all.len() {
689 Some(end.to_string())
690 } else {
691 None
692 };
693 let mut response = json!({
694 "CertificateSummaryList": page
695 .iter()
696 .map(|c| certificate_summary_json(c))
697 .collect::<Vec<_>>(),
698 });
699 if let Some(t) = next {
700 response
701 .as_object_mut()
702 .unwrap()
703 .insert("NextToken".to_string(), Value::String(t));
704 }
705 Ok(AwsResponse::ok_json(response))
706 }
707
708 fn delete_certificate(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
709 let arn = require_certificate_arn(req)?;
710 let mut state = self.state.write();
711 let account = account_mut(&mut state, &req.account_id);
712 let cert = account
713 .certificates
714 .get(&arn)
715 .ok_or_else(|| no_such_certificate(&arn))?;
716 if !cert.in_use_by.is_empty() {
717 return Err(AwsServiceError::aws_error(
718 StatusCode::BAD_REQUEST,
719 "ResourceInUseException",
720 format!(
721 "Certificate {arn} is in use by {} resource(s)",
722 cert.in_use_by.len()
723 ),
724 ));
725 }
726 account.certificates.remove(&arn);
727 Ok(AwsResponse::ok_json(json!({})))
728 }
729
730 fn import_certificate(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
731 let body = req.json_body();
732 let cert_pem = decode_blob(body.get("Certificate"))
733 .ok_or_else(|| invalid_param("Certificate is required"))?;
734 let key_pem = decode_blob(body.get("PrivateKey"))
735 .ok_or_else(|| invalid_param("PrivateKey is required"))?;
736 let chain_pem = decode_blob(body.get("CertificateChain"));
737 let arn_in = body
738 .get("CertificateArn")
739 .and_then(Value::as_str)
740 .map(|s| s.to_string());
741 let tags = parse_tags(body.get("Tags"))?;
742
743 let domain_name = parse_cn_from_pem(&cert_pem).unwrap_or_else(|| "imported".to_string());
744 let now = Utc::now();
745 let mut state = self.state.write();
746 let account = account_mut(&mut state, &req.account_id);
747
748 let arn = match arn_in {
749 Some(existing) => {
750 let cert = account
751 .certificates
752 .get_mut(&existing)
753 .ok_or_else(|| no_such_certificate(&existing))?;
754 if cert.cert_type != "IMPORTED" {
755 return Err(invalid_param(
756 "Reimport is only supported for IMPORTED certificates",
757 ));
758 }
759 cert.certificate_pem = Some(cert_pem.clone());
760 cert.private_key_pem = Some(key_pem);
761 cert.certificate_chain_pem = chain_pem;
762 cert.imported_at = Some(now);
763 cert.not_before = now;
764 cert.not_after = now + Duration::days(395);
765 cert.subject = format!("CN={domain_name}");
766 cert.domain_name = domain_name.clone();
770 cert.subject_alternative_names = vec![domain_name.clone()];
771 if !tags.is_empty() {
772 for (k, v) in tags {
773 cert.tags.insert(k, v);
774 }
775 }
776 existing
777 }
778 None => {
779 let arn = synth_certificate_arn(&req.account_id, &req.region);
780 let cert = StoredCertificate {
781 arn: arn.clone(),
782 domain_name: domain_name.clone(),
783 subject_alternative_names: vec![domain_name.clone()],
784 status: "ISSUED".to_string(),
785 cert_type: "IMPORTED".to_string(),
786 certificate_pem: Some(cert_pem),
787 certificate_chain_pem: chain_pem,
788 private_key_pem: Some(key_pem),
789 idempotency_token: None,
790 serial: synth_serial(&arn),
791 subject: format!("CN={domain_name}"),
792 issuer: "fakecloud-imported".to_string(),
793 key_algorithm: "RSA_2048".to_string(),
794 signature_algorithm: "SHA256WITHRSA".to_string(),
795 created_at: now,
796 issued_at: Some(now),
797 imported_at: Some(now),
798 revoked_at: None,
799 revocation_reason: None,
800 not_before: now,
801 not_after: now + Duration::days(395),
802 validation_method: None,
803 domain_validation: Vec::new(),
804 options: CertificateOptions {
805 certificate_transparency_logging_preference: "ENABLED".to_string(),
806 export: "DISABLED".to_string(),
807 },
808 renewal_eligibility: "INELIGIBLE".to_string(),
809 managed_by: None,
810 certificate_authority_arn: None,
811 tags,
812 in_use_by: Vec::new(),
813 describe_read_count: 0,
814 failure_reason: None,
815 renewal_summary: None,
816 };
817 account.certificates.insert(arn.clone(), cert);
818 arn
819 }
820 };
821 Ok(AwsResponse::ok_json(json!({ "CertificateArn": arn })))
822 }
823
824 fn export_certificate(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
825 let body = req.json_body();
826 let arn = body
827 .get("CertificateArn")
828 .and_then(Value::as_str)
829 .ok_or_else(|| invalid_param("CertificateArn is required"))?
830 .to_string();
831 let passphrase_bytes = match body.get("Passphrase").and_then(Value::as_str) {
835 None | Some("") => None,
836 Some(s) => {
837 let decoded = base64::engine::general_purpose::STANDARD
838 .decode(s)
839 .map_err(|_| invalid_param("Passphrase must be valid base64"))?;
840 if decoded.is_empty() {
841 None
842 } else {
843 Some(decoded)
844 }
845 }
846 };
847 let state = self.state.read();
848 let cert = state
849 .accounts
850 .get(&req.account_id)
851 .and_then(|a| a.certificates.get(&arn))
852 .ok_or_else(|| no_such_certificate(&arn))?
853 .clone();
854 if cert.options.export != "ENABLED" && cert.cert_type != "IMPORTED" {
855 return Err(AwsServiceError::aws_error(
856 StatusCode::BAD_REQUEST,
857 "RequestInProgressException",
858 "Certificate is not exportable",
859 ));
860 }
861 let cert_pem = cert
862 .certificate_pem
863 .clone()
864 .unwrap_or_else(|| placeholder_cert_pem(&arn));
865 let chain_pem = cert
866 .certificate_chain_pem
867 .clone()
868 .unwrap_or_else(|| placeholder_chain_pem(&arn));
869 let key_pem = cert
870 .private_key_pem
871 .clone()
872 .unwrap_or_else(|| placeholder_key_pem(&arn));
873 let key_out = match passphrase_bytes {
874 Some(pp) => encrypt_private_key_pem(&key_pem, &pp)
875 .map_err(|e| invalid_param(format!("failed to encrypt private key: {e}")))?,
876 None => key_pem,
877 };
878 Ok(AwsResponse::ok_json(json!({
879 "Certificate": cert_pem,
880 "CertificateChain": chain_pem,
881 "PrivateKey": key_out,
882 })))
883 }
884
885 fn get_certificate(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
886 let arn = require_certificate_arn(req)?;
887 let state = self.state.read();
888 let cert = state
889 .accounts
890 .get(&req.account_id)
891 .and_then(|a| a.certificates.get(&arn))
892 .ok_or_else(|| no_such_certificate(&arn))?
893 .clone();
894 let cert_pem = cert
895 .certificate_pem
896 .clone()
897 .unwrap_or_else(|| placeholder_cert_pem(&arn));
898 let chain_pem = cert
899 .certificate_chain_pem
900 .clone()
901 .unwrap_or_else(|| placeholder_chain_pem(&arn));
902 Ok(AwsResponse::ok_json(json!({
903 "Certificate": cert_pem,
904 "CertificateChain": chain_pem,
905 })))
906 }
907
908 fn renew_certificate(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
909 let arn = require_certificate_arn(req)?;
910 let mut state = self.state.write();
911 let account = account_mut(&mut state, &req.account_id);
912 let cert = account
913 .certificates
914 .get_mut(&arn)
915 .ok_or_else(|| no_such_certificate(&arn))?;
916 if cert.cert_type == "IMPORTED" {
917 return Err(invalid_param(
918 "Imported certificates cannot be renewed via ACM",
919 ));
920 }
921 let now = Utc::now();
927 cert.not_before = now;
928 cert.not_after = now + Duration::days(395);
929 cert.issued_at = Some(now);
930 cert.status = "ISSUED".to_string();
931 cert.renewal_eligibility = "ELIGIBLE".to_string();
932 cert.failure_reason = None;
933 for dv in cert.domain_validation.iter_mut() {
934 dv.validation_status = "SUCCESS".to_string();
935 }
936 cert.renewal_summary = Some(RenewalSummary {
937 renewal_status: "SUCCESS".to_string(),
938 domain_validation: cert.domain_validation.clone(),
939 renewal_status_reason: None,
940 updated_at: now,
941 });
942 Ok(AwsResponse::ok_json(json!({})))
943 }
944
945 fn revoke_certificate(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
946 let body = req.json_body();
947 let arn = body
948 .get("CertificateArn")
949 .and_then(Value::as_str)
950 .ok_or_else(|| invalid_param("CertificateArn is required"))?
951 .to_string();
952 let reason = body
953 .get("RevocationReason")
954 .and_then(Value::as_str)
955 .ok_or_else(|| invalid_param("RevocationReason is required"))?
956 .to_string();
957 let mut state = self.state.write();
958 let account = account_mut(&mut state, &req.account_id);
959 let cert = account
960 .certificates
961 .get_mut(&arn)
962 .ok_or_else(|| no_such_certificate(&arn))?;
963 if cert.cert_type != "AMAZON_ISSUED" {
964 return Err(invalid_param(
965 "Only AMAZON_ISSUED certificates can be revoked",
966 ));
967 }
968 cert.status = "REVOKED".to_string();
969 cert.revoked_at = Some(Utc::now());
970 cert.revocation_reason = Some(reason);
971 Ok(AwsResponse::ok_json(json!({})))
972 }
973
974 fn resend_validation_email(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
975 let body = req.json_body();
976 let arn = body
977 .get("CertificateArn")
978 .and_then(Value::as_str)
979 .ok_or_else(|| invalid_param("CertificateArn is required"))?
980 .to_string();
981 let _ = body
982 .get("Domain")
983 .and_then(Value::as_str)
984 .ok_or_else(|| invalid_param("Domain is required"))?;
985 let _ = body
986 .get("ValidationDomain")
987 .and_then(Value::as_str)
988 .ok_or_else(|| invalid_param("ValidationDomain is required"))?;
989 let state = self.state.read();
990 let cert = state
991 .accounts
992 .get(&req.account_id)
993 .and_then(|a| a.certificates.get(&arn))
994 .ok_or_else(|| no_such_certificate(&arn))?;
995 if cert.validation_method.as_deref() != Some("EMAIL") {
996 return Err(invalid_param(
997 "Certificate is not configured for EMAIL validation",
998 ));
999 }
1000 Ok(AwsResponse::ok_json(json!({})))
1001 }
1002
1003 fn add_tags_to_certificate(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1004 let body = req.json_body();
1005 let arn = body
1006 .get("CertificateArn")
1007 .and_then(Value::as_str)
1008 .ok_or_else(|| invalid_param("CertificateArn is required"))?
1009 .to_string();
1010 let tags = parse_tags(body.get("Tags"))?;
1011 if tags.is_empty() {
1012 return Err(invalid_param("Tags must contain at least one entry"));
1013 }
1014 let mut state = self.state.write();
1015 let account = account_mut(&mut state, &req.account_id);
1016 let cert = account
1017 .certificates
1018 .get_mut(&arn)
1019 .ok_or_else(|| no_such_certificate(&arn))?;
1020 for (k, v) in tags {
1021 cert.tags.insert(k, v);
1022 }
1023 Ok(AwsResponse::ok_json(json!({})))
1024 }
1025
1026 fn remove_tags_from_certificate(
1027 &self,
1028 req: &AwsRequest,
1029 ) -> Result<AwsResponse, AwsServiceError> {
1030 let body = req.json_body();
1031 let arn = body
1032 .get("CertificateArn")
1033 .and_then(Value::as_str)
1034 .ok_or_else(|| invalid_param("CertificateArn is required"))?
1035 .to_string();
1036 let tags = parse_tags(body.get("Tags"))?;
1037 if tags.is_empty() {
1038 return Err(invalid_param("Tags must contain at least one entry"));
1039 }
1040 let mut state = self.state.write();
1041 let account = account_mut(&mut state, &req.account_id);
1042 let cert = account
1043 .certificates
1044 .get_mut(&arn)
1045 .ok_or_else(|| no_such_certificate(&arn))?;
1046 for (k, v) in tags {
1049 if let Some(existing) = cert.tags.get(&k) {
1050 if v.is_empty() || *existing == v {
1051 cert.tags.remove(&k);
1052 }
1053 }
1054 }
1055 Ok(AwsResponse::ok_json(json!({})))
1056 }
1057
1058 fn list_tags_for_certificate(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1059 let arn = require_certificate_arn(req)?;
1060 let state = self.state.read();
1061 let cert = state
1062 .accounts
1063 .get(&req.account_id)
1064 .and_then(|a| a.certificates.get(&arn))
1065 .ok_or_else(|| no_such_certificate(&arn))?;
1066 let mut tags: Vec<(String, String)> = cert
1067 .tags
1068 .iter()
1069 .map(|(k, v)| (k.clone(), v.clone()))
1070 .collect();
1071 tags.sort_by(|a, b| a.0.cmp(&b.0));
1072 let tag_list: Vec<Value> = tags
1073 .into_iter()
1074 .map(|(k, v)| json!({ "Key": k, "Value": v }))
1075 .collect();
1076 Ok(AwsResponse::ok_json(json!({ "Tags": tag_list })))
1077 }
1078
1079 fn get_account_configuration(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1080 let state = self.state.read();
1081 let cfg = state
1082 .accounts
1083 .get(&req.account_id)
1084 .map(|a| a.account_config.clone())
1085 .unwrap_or_default();
1086 let mut expiry = json!({});
1087 if let Some(d) = cfg.expiry_events_days_before_expiry {
1088 expiry
1089 .as_object_mut()
1090 .unwrap()
1091 .insert("DaysBeforeExpiry".to_string(), json!(d));
1092 }
1093 Ok(AwsResponse::ok_json(json!({
1094 "ExpiryEvents": expiry,
1095 })))
1096 }
1097
1098 fn put_account_configuration(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1099 let body = req.json_body();
1100 let idempotency_token = body
1101 .get("IdempotencyToken")
1102 .and_then(Value::as_str)
1103 .ok_or_else(|| invalid_param("IdempotencyToken is required"))?;
1104 if idempotency_token.is_empty() || idempotency_token.len() > 32 {
1105 return Err(validation_error(
1106 "IdempotencyToken length must be between 1 and 32",
1107 ));
1108 }
1109 if !idempotency_token
1110 .chars()
1111 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
1112 {
1113 return Err(validation_error(
1114 "IdempotencyToken must match pattern ^[\\w-]+$",
1115 ));
1116 }
1117 let days = body
1118 .get("ExpiryEvents")
1119 .and_then(|v| v.get("DaysBeforeExpiry"))
1120 .and_then(Value::as_i64)
1121 .map(|n| n as i32);
1122 let mut state = self.state.write();
1123 let account = account_mut(&mut state, &req.account_id);
1124 account.account_config.expiry_events_days_before_expiry = days;
1125 Ok(AwsResponse::ok_json(json!({})))
1126 }
1127
1128 fn update_certificate_options(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1129 let body = req.json_body();
1130 let arn = body
1131 .get("CertificateArn")
1132 .and_then(Value::as_str)
1133 .ok_or_else(|| invalid_param("CertificateArn is required"))?
1134 .to_string();
1135 let options = body
1136 .get("Options")
1137 .ok_or_else(|| invalid_param("Options is required"))?;
1138 let new_opts = CertificateOptions {
1139 certificate_transparency_logging_preference: options
1140 .get("CertificateTransparencyLoggingPreference")
1141 .and_then(Value::as_str)
1142 .unwrap_or("ENABLED")
1143 .to_string(),
1144 export: options
1145 .get("Export")
1146 .and_then(Value::as_str)
1147 .unwrap_or("DISABLED")
1148 .to_string(),
1149 };
1150 let mut state = self.state.write();
1151 let account = account_mut(&mut state, &req.account_id);
1152 let cert = account
1153 .certificates
1154 .get_mut(&arn)
1155 .ok_or_else(|| no_such_certificate(&arn))?;
1156 cert.options = new_opts;
1157 Ok(AwsResponse::ok_json(json!({})))
1158 }
1159
1160 fn search_certificates(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1161 let body = req.json_body();
1168 let max_results: usize = body
1169 .get("MaxResults")
1170 .and_then(Value::as_u64)
1171 .map(|n| n as usize)
1172 .unwrap_or(100);
1173 if !(1..=500).contains(&max_results) {
1174 return Err(validation_error("MaxResults must be between 1 and 500"));
1175 }
1176 let next_token = body
1177 .get("NextToken")
1178 .and_then(Value::as_str)
1179 .map(|s| s.to_string());
1180 if let Some(ref token) = next_token {
1181 if token.is_empty() || token.len() > 10000 {
1182 return Err(validation_error(
1183 "NextToken length must be between 1 and 10000",
1184 ));
1185 }
1186 }
1187 if let Some(sort_by) = body.get("SortBy").and_then(Value::as_str) {
1188 const VALID_SORT_BY: [&str; 18] = [
1189 "CREATED_AT",
1190 "NOT_AFTER",
1191 "STATUS",
1192 "RENEWAL_STATUS",
1193 "EXPORTED",
1194 "IN_USE",
1195 "NOT_BEFORE",
1196 "KEY_ALGORITHM",
1197 "TYPE",
1198 "CERTIFICATE_ARN",
1199 "COMMON_NAME",
1200 "REVOKED_AT",
1201 "RENEWAL_ELIGIBILITY",
1202 "ISSUED_AT",
1203 "MANAGED_BY",
1204 "EXPORT_OPTION",
1205 "VALIDATION_METHOD",
1206 "IMPORTED_AT",
1207 ];
1208 if !VALID_SORT_BY.contains(&sort_by) {
1209 return Err(validation_error("Invalid SortBy"));
1210 }
1211 }
1212 if let Some(sort_order) = body.get("SortOrder").and_then(Value::as_str) {
1213 if sort_order != "ASCENDING" && sort_order != "DESCENDING" {
1214 return Err(validation_error("Invalid SortOrder"));
1215 }
1216 }
1217 let statuses: Vec<String> = body
1219 .get("CertificateStatuses")
1220 .and_then(Value::as_array)
1221 .map(|v| {
1222 v.iter()
1223 .filter_map(|s| s.as_str().map(|s| s.to_ascii_uppercase()))
1224 .collect()
1225 })
1226 .unwrap_or_default();
1227
1228 let state = self.state.read();
1229 let mut all: Vec<StoredCertificate> = state
1230 .accounts
1231 .get(&req.account_id)
1232 .map(|a| a.certificates.values().cloned().collect())
1233 .unwrap_or_default();
1234 drop(state);
1235
1236 if !statuses.is_empty() {
1237 all.retain(|c| statuses.contains(&c.status.to_ascii_uppercase()));
1238 }
1239 if let Some(stmt) = body.get("FilterStatement") {
1242 all.retain(|c| cert_matches_statement(c, stmt));
1243 }
1244
1245 let sort_by = body
1248 .get("SortBy")
1249 .and_then(Value::as_str)
1250 .unwrap_or("CREATED_AT")
1251 .to_string();
1252 let descending = body
1253 .get("SortOrder")
1254 .and_then(Value::as_str)
1255 .map(|o| !o.eq_ignore_ascii_case("ASCENDING"))
1256 .unwrap_or(true);
1257 all.sort_by(|a, b| {
1258 let ord = match sort_by.as_str() {
1259 "NOT_AFTER" => a.not_after.cmp(&b.not_after),
1260 "NOT_BEFORE" => a.not_before.cmp(&b.not_before),
1261 "ISSUED_AT" => a.issued_at.cmp(&b.issued_at),
1262 "STATUS" => a.status.cmp(&b.status),
1263 "KEY_ALGORITHM" => a.key_algorithm.cmp(&b.key_algorithm),
1264 "TYPE" => a.cert_type.cmp(&b.cert_type),
1265 "CERTIFICATE_ARN" => a.arn.cmp(&b.arn),
1266 _ => a.created_at.cmp(&b.created_at),
1268 };
1269 let ord = ord.then_with(|| a.arn.cmp(&b.arn));
1271 if descending {
1272 ord.reverse()
1273 } else {
1274 ord
1275 }
1276 });
1277 let start = next_token
1278 .and_then(|t| t.parse::<usize>().ok())
1279 .unwrap_or(0);
1280 let end = (start + max_results).min(all.len());
1281 let page: Vec<&StoredCertificate> = all.iter().skip(start).take(max_results).collect();
1282 let next = if end < all.len() {
1283 Some(end.to_string())
1284 } else {
1285 None
1286 };
1287 let mut response = json!({
1288 "Results": page
1289 .iter()
1290 .map(|c| certificate_search_result_json(c))
1291 .collect::<Vec<_>>(),
1292 });
1293 if let Some(t) = next {
1294 response
1295 .as_object_mut()
1296 .unwrap()
1297 .insert("NextToken".to_string(), Value::String(t));
1298 }
1299 Ok(AwsResponse::ok_json(response))
1300 }
1301}
1302
1303fn account_mut<'a>(state: &'a mut AcmAccounts, account_id: &str) -> &'a mut AccountState {
1306 state.accounts.entry(account_id.to_string()).or_default()
1307}
1308
1309const CERT_EKUS: [&str; 2] = [
1314 "TLS_WEB_SERVER_AUTHENTICATION",
1315 "TLS_WEB_CLIENT_AUTHENTICATION",
1316];
1317const CERT_KEY_USAGES: [&str; 2] = ["DIGITAL_SIGNATURE", "KEY_ENCIPHERMENT"];
1318
1319fn string_filter_matches(filter: &Value, value: &str) -> bool {
1323 let Some(target) = filter.get("Value").and_then(Value::as_str) else {
1324 return true;
1325 };
1326 match filter.get("ComparisonOperator").and_then(Value::as_str) {
1327 Some("CONTAINS") => value.contains(target),
1328 _ => value == target,
1330 }
1331}
1332
1333fn cert_common_name(cert: &StoredCertificate) -> &str {
1336 cert.subject
1337 .strip_prefix("CN=")
1338 .unwrap_or(cert.subject.as_str())
1339}
1340
1341fn ts_in_range(range: &Value, ts: chrono::DateTime<Utc>) -> bool {
1344 let bound = |key: &str| -> Option<i64> {
1345 range.get(key).and_then(|v| {
1346 v.as_f64()
1347 .map(|f| f as i64)
1348 .or_else(|| {
1349 v.as_str()
1350 .and_then(|s| s.parse::<f64>().ok().map(|f| f as i64))
1351 })
1352 .or_else(|| {
1353 v.as_str()
1354 .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
1355 .map(|d| d.timestamp())
1356 })
1357 })
1358 };
1359 let epoch = ts.timestamp();
1360 bound("Start").is_none_or(|s| epoch >= s) && bound("End").is_none_or(|e| epoch <= e)
1361}
1362
1363fn cert_matches_x509(cert: &StoredCertificate, x: &Value) -> bool {
1366 if let Some(cn) = x.get("Subject").and_then(|s| s.get("CommonName")) {
1367 return string_filter_matches(cn, cert_common_name(cert));
1368 }
1369 if let Some(dns) = x
1370 .get("SubjectAlternativeName")
1371 .and_then(|s| s.get("DnsName"))
1372 {
1373 return cert
1374 .subject_alternative_names
1375 .iter()
1376 .any(|san| string_filter_matches(dns, san));
1377 }
1378 if let Some(eku) = x.get("ExtendedKeyUsage").and_then(Value::as_str) {
1379 let eku = eku.to_ascii_uppercase();
1380 return match eku.as_str() {
1383 "ANY" => !CERT_EKUS.is_empty(),
1384 v => CERT_EKUS.contains(&v),
1385 };
1386 }
1387 if let Some(ku) = x.get("KeyUsage").and_then(Value::as_str) {
1388 let ku = ku.to_ascii_uppercase();
1389 return match ku.as_str() {
1390 "ANY" => !CERT_KEY_USAGES.is_empty(),
1391 v => CERT_KEY_USAGES.contains(&v),
1392 };
1393 }
1394 if let Some(alg) = x.get("KeyAlgorithm").and_then(Value::as_str) {
1395 return cert.key_algorithm.eq_ignore_ascii_case(alg);
1396 }
1397 if let Some(serial) = x.get("SerialNumber").and_then(Value::as_str) {
1398 return cert.serial == serial;
1399 }
1400 if let Some(range) = x.get("NotAfter") {
1401 return ts_in_range(range, cert.not_after);
1402 }
1403 if let Some(range) = x.get("NotBefore") {
1404 return ts_in_range(range, cert.not_before);
1405 }
1406 true
1408}
1409
1410fn cert_matches_metadata(cert: &StoredCertificate, md: &Value) -> bool {
1412 if let Some(status) = md.get("Status").and_then(Value::as_str) {
1413 return cert.status.eq_ignore_ascii_case(status);
1414 }
1415 if let Some(rs) = md.get("RenewalStatus").and_then(Value::as_str) {
1416 return cert
1417 .renewal_summary
1418 .as_ref()
1419 .is_some_and(|r| r.renewal_status.eq_ignore_ascii_case(rs));
1420 }
1421 if let Some(ty) = md.get("Type").and_then(Value::as_str) {
1422 return cert.cert_type.eq_ignore_ascii_case(ty);
1423 }
1424 if let Some(in_use) = md.get("InUse").and_then(Value::as_bool) {
1425 return cert.in_use_by.is_empty() != in_use;
1426 }
1427 if let Some(exported) = md.get("Exported").and_then(Value::as_bool) {
1428 return !exported;
1430 }
1431 if let Some(opt) = md.get("ExportOption").and_then(Value::as_str) {
1432 return cert.options.export.eq_ignore_ascii_case(opt);
1433 }
1434 if let Some(mb) = md.get("ManagedBy").and_then(Value::as_str) {
1435 return cert
1436 .managed_by
1437 .as_deref()
1438 .is_some_and(|m| m.eq_ignore_ascii_case(mb));
1439 }
1440 if let Some(vm) = md.get("ValidationMethod").and_then(Value::as_str) {
1441 return cert
1442 .validation_method
1443 .as_deref()
1444 .is_some_and(|m| m.eq_ignore_ascii_case(vm));
1445 }
1446 true
1447}
1448
1449fn cert_matches_leaf(cert: &StoredCertificate, filter: &Value) -> bool {
1453 if let Some(arn) = filter.get("CertificateArn").and_then(Value::as_str) {
1454 return cert.arn == arn;
1455 }
1456 if let Some(x) = filter.get("X509AttributeFilter") {
1457 return cert_matches_x509(cert, x);
1458 }
1459 if let Some(md) = filter.get("AcmCertificateMetadataFilter") {
1460 return cert_matches_metadata(cert, md);
1461 }
1462 true
1464}
1465
1466fn cert_matches_statement(cert: &StoredCertificate, stmt: &Value) -> bool {
1469 if let Some(and) = stmt.get("And").and_then(Value::as_array) {
1470 if !and.iter().all(|s| cert_matches_statement(cert, s)) {
1471 return false;
1472 }
1473 }
1474 if let Some(or) = stmt.get("Or").and_then(Value::as_array) {
1475 if !or.is_empty() && !or.iter().any(|s| cert_matches_statement(cert, s)) {
1476 return false;
1477 }
1478 }
1479 if let Some(not) = stmt.get("Not") {
1480 if cert_matches_statement(cert, not) {
1481 return false;
1482 }
1483 }
1484 if let Some(filter) = stmt.get("Filter") {
1485 if !cert_matches_leaf(cert, filter) {
1486 return false;
1487 }
1488 }
1489 true
1490}
1491
1492fn require_certificate_arn(req: &AwsRequest) -> Result<String, AwsServiceError> {
1493 req.json_body()
1494 .get("CertificateArn")
1495 .and_then(Value::as_str)
1496 .map(|s| s.to_string())
1497 .ok_or_else(|| invalid_param("CertificateArn is required"))
1498}
1499
1500fn invalid_param(msg: impl Into<String>) -> AwsServiceError {
1501 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "InvalidParameterException", msg)
1502}
1503
1504fn validation_error(msg: impl Into<String>) -> AwsServiceError {
1505 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "ValidationException", msg)
1506}
1507
1508fn no_such_certificate(arn: &str) -> AwsServiceError {
1509 AwsServiceError::aws_error(
1510 StatusCode::BAD_REQUEST,
1511 "ResourceNotFoundException",
1512 format!("Could not find certificate with arn {arn}"),
1513 )
1514}
1515
1516fn synth_certificate_arn(account_id: &str, region: &str) -> String {
1517 let region = if region.is_empty() {
1518 "us-east-1"
1519 } else {
1520 region
1521 };
1522 let id = Uuid::new_v4();
1523 Arn::new("acm", region, account_id, &format!("certificate/{id}")).to_string()
1524}
1525
1526fn cert_id_from_arn(arn: &str) -> &str {
1533 arn.rsplit_once("certificate/")
1534 .map(|(_, id)| id)
1535 .unwrap_or(arn)
1536}
1537
1538fn synth_serial(arn: &str) -> String {
1539 let mut hasher = Sha256::new();
1540 hasher.update(arn.as_bytes());
1541 let digest = hasher.finalize();
1542 hex::encode(&digest[..16])
1543}
1544
1545fn parse_tags(value: Option<&Value>) -> Result<BTreeMap<String, String>, AwsServiceError> {
1546 let mut out = BTreeMap::new();
1547 let Some(arr) = value.and_then(Value::as_array) else {
1548 return Ok(out);
1549 };
1550 for tag in arr {
1551 let key = tag
1552 .get("Key")
1553 .and_then(Value::as_str)
1554 .ok_or_else(|| invalid_param("Tag.Key is required"))?
1555 .to_string();
1556 let value = tag
1557 .get("Value")
1558 .and_then(Value::as_str)
1559 .unwrap_or_default()
1560 .to_string();
1561 out.insert(key, value);
1562 }
1563 Ok(out)
1564}
1565
1566fn parse_options(value: Option<&Value>) -> CertificateOptions {
1567 let v = match value {
1568 Some(v) => v,
1569 None => {
1570 return CertificateOptions {
1571 certificate_transparency_logging_preference: "ENABLED".to_string(),
1572 export: "DISABLED".to_string(),
1573 };
1574 }
1575 };
1576 CertificateOptions {
1577 certificate_transparency_logging_preference: v
1578 .get("CertificateTransparencyLoggingPreference")
1579 .and_then(Value::as_str)
1580 .unwrap_or("ENABLED")
1581 .to_string(),
1582 export: v
1583 .get("Export")
1584 .and_then(Value::as_str)
1585 .unwrap_or("DISABLED")
1586 .to_string(),
1587 }
1588}
1589
1590fn effective_sans(domain: &str, extras: &[String]) -> Vec<String> {
1594 let mut all = vec![domain.to_string()];
1595 for s in extras {
1596 if !all.contains(s) {
1597 all.push(s.clone());
1598 }
1599 }
1600 all
1601}
1602
1603fn synth_domain_validation(domain: &str, sans: &[String], method: &str) -> Vec<DomainValidation> {
1604 effective_sans(domain, sans)
1605 .iter()
1606 .map(|d| {
1607 if method == "DNS" {
1608 let token = synth_dns_token(d);
1609 DomainValidation {
1610 domain_name: d.clone(),
1611 validation_status: "PENDING_VALIDATION".to_string(),
1612 validation_method: "DNS".to_string(),
1613 resource_record_name: Some(format!("_{token}.{d}.")),
1614 resource_record_type: Some("CNAME".to_string()),
1615 resource_record_value: Some(format!("_{token}.acm-validations.aws.")),
1616 }
1617 } else {
1618 DomainValidation {
1619 domain_name: d.clone(),
1620 validation_status: "PENDING_VALIDATION".to_string(),
1621 validation_method: "EMAIL".to_string(),
1622 resource_record_name: None,
1623 resource_record_type: None,
1624 resource_record_value: None,
1625 }
1626 }
1627 })
1628 .collect()
1629}
1630
1631fn synth_dns_token(domain: &str) -> String {
1634 let mut hasher = Sha256::new();
1635 hasher.update(domain.as_bytes());
1636 let digest = hasher.finalize();
1637 hex::encode(&digest[..16])
1638}
1639
1640fn decode_blob(value: Option<&Value>) -> Option<String> {
1641 let v = value?;
1642 if let Some(s) = v.as_str() {
1643 if let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(s) {
1647 if let Ok(text) = String::from_utf8(bytes) {
1648 return Some(text);
1649 }
1650 }
1651 return Some(s.to_string());
1652 }
1653 None
1654}
1655
1656fn parse_cn_from_pem(pem: &str) -> Option<String> {
1661 pem.lines()
1662 .find_map(|line| line.split("CN=").nth(1))
1663 .map(|rest| {
1664 rest.split(['/', ',', '\n', ' '])
1665 .next()
1666 .unwrap_or("")
1667 .to_string()
1668 })
1669 .filter(|s| !s.is_empty())
1670}
1671
1672fn placeholder_cert_pem(arn: &str) -> String {
1673 let body = base64::engine::general_purpose::STANDARD.encode(arn.as_bytes());
1677 format!("-----BEGIN CERTIFICATE-----\n{body}\n-----END CERTIFICATE-----\n")
1678}
1679
1680fn placeholder_chain_pem(arn: &str) -> String {
1681 let body =
1682 base64::engine::general_purpose::STANDARD.encode(format!("chain-of-{arn}").as_bytes());
1683 format!("-----BEGIN CERTIFICATE-----\n{body}\n-----END CERTIFICATE-----\n")
1684}
1685
1686fn placeholder_key_pem(arn: &str) -> String {
1687 let body = base64::engine::general_purpose::STANDARD.encode(format!("key-of-{arn}").as_bytes());
1688 format!("-----BEGIN RSA PRIVATE KEY-----\n{body}\n-----END RSA PRIVATE KEY-----\n")
1689}
1690
1691fn generate_self_signed_cert(domain_name: &str, sans: &[String]) -> Option<(String, String)> {
1698 let mut all_names: Vec<String> = vec![domain_name.to_string()];
1699 for s in sans {
1700 if !all_names.contains(s) {
1701 all_names.push(s.clone());
1702 }
1703 }
1704 let cert = rcgen::generate_simple_self_signed(all_names).ok()?;
1705 Some((cert.cert.pem(), cert.key_pair.serialize_pem()))
1706}
1707
1708fn certificate_summary_json(c: &StoredCertificate) -> Value {
1709 let mut s = json!({
1710 "CertificateArn": c.arn,
1711 "DomainName": c.domain_name,
1712 "SubjectAlternativeNameSummaries": c.subject_alternative_names,
1713 "HasAdditionalSubjectAlternativeNames": false,
1714 "Status": c.status,
1715 "Type": c.cert_type,
1716 "KeyAlgorithm": c.key_algorithm,
1717 "KeyUsages": ["DIGITAL_SIGNATURE", "KEY_ENCIPHERMENT"],
1718 "ExtendedKeyUsages": ["TLS_WEB_SERVER_AUTHENTICATION", "TLS_WEB_CLIENT_AUTHENTICATION"],
1719 "InUse": !c.in_use_by.is_empty(),
1720 "Exported": false,
1721 "RenewalEligibility": c.renewal_eligibility,
1722 "NotBefore": c.not_before.timestamp() as f64,
1723 "NotAfter": c.not_after.timestamp() as f64,
1724 "CreatedAt": c.created_at.timestamp() as f64,
1725 });
1726 if let Some(t) = c.issued_at {
1727 s.as_object_mut()
1728 .unwrap()
1729 .insert("IssuedAt".to_string(), json!(t.timestamp() as f64));
1730 }
1731 if let Some(t) = c.imported_at {
1732 s.as_object_mut()
1733 .unwrap()
1734 .insert("ImportedAt".to_string(), json!(t.timestamp() as f64));
1735 }
1736 if let Some(t) = c.revoked_at {
1737 s.as_object_mut()
1738 .unwrap()
1739 .insert("RevokedAt".to_string(), json!(t.timestamp() as f64));
1740 if let Some(r) = &c.revocation_reason {
1741 s.as_object_mut()
1742 .unwrap()
1743 .insert("RevocationReason".to_string(), json!(r));
1744 }
1745 }
1746 if let Some(m) = &c.managed_by {
1747 s.as_object_mut()
1748 .unwrap()
1749 .insert("ManagedBy".to_string(), json!(m));
1750 }
1751 s
1752}
1753
1754fn certificate_detail_json(c: &StoredCertificate) -> Value {
1755 let mut d = json!({
1756 "CertificateArn": c.arn,
1757 "DomainName": c.domain_name,
1758 "SubjectAlternativeNames": c.subject_alternative_names,
1759 "Status": c.status,
1760 "Type": c.cert_type,
1761 "Serial": c.serial,
1762 "Subject": c.subject,
1763 "Issuer": c.issuer,
1764 "KeyAlgorithm": c.key_algorithm,
1765 "SignatureAlgorithm": c.signature_algorithm,
1766 "InUseBy": c.in_use_by,
1767 "RenewalEligibility": c.renewal_eligibility,
1768 "Options": {
1769 "CertificateTransparencyLoggingPreference":
1770 c.options.certificate_transparency_logging_preference,
1771 "Export": c.options.export,
1772 },
1773 "DomainValidationOptions": c
1774 .domain_validation
1775 .iter()
1776 .map(domain_validation_json)
1777 .collect::<Vec<_>>(),
1778 "NotBefore": c.not_before.timestamp() as f64,
1779 "NotAfter": c.not_after.timestamp() as f64,
1780 "CreatedAt": c.created_at.timestamp() as f64,
1781 "KeyUsages": [{"Name": "DIGITAL_SIGNATURE"}, {"Name": "KEY_ENCIPHERMENT"}],
1782 "ExtendedKeyUsages": [
1783 {"Name": "TLS_WEB_SERVER_AUTHENTICATION", "OID": "1.3.6.1.5.5.7.3.1"},
1784 {"Name": "TLS_WEB_CLIENT_AUTHENTICATION", "OID": "1.3.6.1.5.5.7.3.2"},
1785 ],
1786 });
1787 if let Some(t) = c.issued_at {
1788 d.as_object_mut()
1789 .unwrap()
1790 .insert("IssuedAt".to_string(), json!(t.timestamp() as f64));
1791 }
1792 if let Some(t) = c.imported_at {
1793 d.as_object_mut()
1794 .unwrap()
1795 .insert("ImportedAt".to_string(), json!(t.timestamp() as f64));
1796 }
1797 if let Some(t) = c.revoked_at {
1798 d.as_object_mut()
1799 .unwrap()
1800 .insert("RevokedAt".to_string(), json!(t.timestamp() as f64));
1801 }
1802 if let Some(r) = &c.revocation_reason {
1803 d.as_object_mut()
1804 .unwrap()
1805 .insert("RevocationReason".to_string(), json!(r));
1806 }
1807 if let Some(m) = &c.managed_by {
1808 d.as_object_mut()
1809 .unwrap()
1810 .insert("ManagedBy".to_string(), json!(m));
1811 }
1812 if let Some(ca) = &c.certificate_authority_arn {
1813 d.as_object_mut()
1814 .unwrap()
1815 .insert("CertificateAuthorityArn".to_string(), json!(ca));
1816 }
1817 if let Some(fr) = &c.failure_reason {
1818 d.as_object_mut()
1819 .unwrap()
1820 .insert("FailureReason".to_string(), json!(fr));
1821 }
1822 if let Some(rs) = &c.renewal_summary {
1823 let mut summary = json!({
1824 "RenewalStatus": rs.renewal_status,
1825 "DomainValidationOptions": rs
1826 .domain_validation
1827 .iter()
1828 .map(domain_validation_json)
1829 .collect::<Vec<_>>(),
1830 "UpdatedAt": rs.updated_at.timestamp() as f64,
1831 });
1832 if let Some(reason) = &rs.renewal_status_reason {
1833 summary
1834 .as_object_mut()
1835 .unwrap()
1836 .insert("RenewalStatusReason".to_string(), json!(reason));
1837 }
1838 d.as_object_mut()
1839 .unwrap()
1840 .insert("RenewalSummary".to_string(), summary);
1841 }
1842 d
1843}
1844
1845fn certificate_search_result_json(c: &StoredCertificate) -> Value {
1846 let san_objects: Vec<Value> = c
1847 .subject_alternative_names
1848 .iter()
1849 .map(|s| json!({ "DnsName": s }))
1850 .collect();
1851 let cn = c
1852 .subject
1853 .strip_prefix("CN=")
1854 .unwrap_or(c.subject.as_str())
1855 .to_string();
1856 json!({
1857 "CertificateArn": c.arn,
1858 "X509Attributes": {
1859 "Subject": { "CommonName": cn },
1860 "Issuer": { "CommonName": c.issuer },
1861 "SubjectAlternativeNames": san_objects,
1862 "KeyAlgorithm": c.key_algorithm,
1863 "KeyUsages": ["DIGITAL_SIGNATURE", "KEY_ENCIPHERMENT"],
1864 "ExtendedKeyUsages": ["TLS_WEB_SERVER_AUTHENTICATION", "TLS_WEB_CLIENT_AUTHENTICATION"],
1865 "SerialNumber": c.serial,
1866 "NotBefore": c.not_before.timestamp() as f64,
1867 "NotAfter": c.not_after.timestamp() as f64,
1868 },
1869 "CertificateMetadata": {
1870 "AcmCertificateMetadata": {
1871 "Status": c.status,
1872 "Type": c.cert_type,
1873 "InUse": !c.in_use_by.is_empty(),
1874 "Exported": false,
1875 "RenewalEligibility": c.renewal_eligibility,
1876 "CreatedAt": c.created_at.timestamp() as f64,
1877 "ManagedBy": c.managed_by.clone().unwrap_or_default(),
1878 "ValidationMethod": c.validation_method.clone().unwrap_or_default(),
1879 },
1880 },
1881 })
1882}
1883
1884fn domain_validation_json(v: &DomainValidation) -> Value {
1885 let mut out = json!({
1886 "DomainName": v.domain_name,
1887 "ValidationStatus": v.validation_status,
1888 "ValidationMethod": v.validation_method,
1889 });
1890 if let (Some(name), Some(rtype), Some(value)) = (
1891 &v.resource_record_name,
1892 &v.resource_record_type,
1893 &v.resource_record_value,
1894 ) {
1895 out.as_object_mut().unwrap().insert(
1896 "ResourceRecord".to_string(),
1897 json!({
1898 "Name": name,
1899 "Type": rtype,
1900 "Value": value,
1901 }),
1902 );
1903 }
1904 out
1905}
1906
1907fn encrypt_private_key_pem(key_pem: &str, passphrase: &[u8]) -> Result<String, String> {
1917 use pkcs8::{pkcs5::pbes2, LineEnding, PrivateKeyInfo};
1918
1919 let der = pem_decode_private_key(key_pem)?;
1924
1925 let mut salt = [0u8; 16];
1929 let mut iv = [0u8; 16];
1930 rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut salt);
1931 rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut iv);
1932 let params = pbes2::Parameters::pbkdf2_sha256_aes256cbc(2048, &salt, &iv)
1933 .map_err(|e| format!("invalid PBES2 parameters: {e}"))?;
1934
1935 let pki = PrivateKeyInfo::try_from(der.as_slice())
1936 .map_err(|e| format!("private key is not valid PKCS#8 DER: {e}"))?;
1937 let encrypted = pki
1938 .encrypt_with_params(params, passphrase)
1939 .map_err(|e| format!("PKCS#8 encryption failed: {e}"))?;
1940
1941 encrypted
1942 .to_pem("ENCRYPTED PRIVATE KEY", LineEnding::LF)
1943 .map(|s| s.to_string())
1944 .map_err(|e| format!("PEM encoding failed: {e}"))
1945}
1946
1947fn pem_decode_private_key(pem: &str) -> Result<Vec<u8>, String> {
1951 let begin = pem.find("-----BEGIN ").ok_or("missing BEGIN line")?;
1952 let after_begin = &pem[begin + 11..];
1953 let dash = after_begin.find("-----").ok_or("malformed BEGIN line")?;
1954 let label = after_begin[..dash].trim();
1955 if label != "PRIVATE KEY" {
1956 return Err(format!(
1957 "expected `BEGIN PRIVATE KEY` (PKCS#8), got `BEGIN {label}`"
1958 ));
1959 }
1960 let end = pem
1961 .find("-----END PRIVATE KEY-----")
1962 .ok_or("missing END line")?;
1963 let body_start = begin + 11 + dash + 5;
1964 let nl = pem[body_start..]
1965 .find('\n')
1966 .ok_or("missing newline after BEGIN")?;
1967 let body: String = pem[body_start + nl + 1..end]
1968 .chars()
1969 .filter(|c| !c.is_whitespace())
1970 .collect();
1971 base64::engine::general_purpose::STANDARD
1972 .decode(body.as_bytes())
1973 .map_err(|e| format!("PEM body is not valid base64: {e}"))
1974}
1975
1976#[cfg(test)]
1977mod tests {
1978 use super::*;
1979
1980 #[test]
1981 fn generate_self_signed_cert_returns_real_pem() {
1982 let (cert_pem, key_pem) =
1983 generate_self_signed_cert("example.com", &["www.example.com".to_string()])
1984 .expect("rcgen should produce a self-signed cert");
1985 assert!(
1986 cert_pem.starts_with("-----BEGIN CERTIFICATE-----"),
1987 "expected real PEM cert, got {cert_pem:.80}"
1988 );
1989 assert!(cert_pem.ends_with("-----END CERTIFICATE-----\n"));
1990 assert!(key_pem.contains("-----BEGIN PRIVATE KEY-----"));
1991 assert!(cert_pem.len() > 400, "real cert PEM should be >400 chars");
1993 }
1994
1995 #[tokio::test]
1996 async fn request_certificate_stores_real_pem_and_key() {
1997 let svc = AcmService::default();
1998 let req = AwsRequest {
1999 service: "acm".to_string(),
2000 action: "RequestCertificate".to_string(),
2001 region: "us-east-1".to_string(),
2002 account_id: "123456789012".to_string(),
2003 request_id: "rid".to_string(),
2004 headers: http::HeaderMap::new(),
2005 query_params: std::collections::HashMap::new(),
2006 body: bytes::Bytes::from(
2007 serde_json::to_vec(&json!({"DomainName": "example.com"})).unwrap(),
2008 ),
2009 body_stream: parking_lot::Mutex::new(None),
2010 path_segments: vec![],
2011 raw_path: "/".to_string(),
2012 raw_query: String::new(),
2013 method: http::Method::POST,
2014 is_query_protocol: false,
2015 access_key_id: None,
2016 principal: None,
2017 };
2018 let resp = svc.handle(req).await.unwrap();
2019 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2020 let arn = body["CertificateArn"].as_str().unwrap();
2021 let st = svc.state.read();
2022 let cert = st
2023 .accounts
2024 .get("123456789012")
2025 .unwrap()
2026 .certificates
2027 .get(arn)
2028 .unwrap();
2029 let cert_pem = cert.certificate_pem.as_deref().unwrap();
2030 let key_pem = cert.private_key_pem.as_deref().unwrap();
2031 assert!(cert_pem.starts_with("-----BEGIN CERTIFICATE-----"));
2032 assert!(key_pem.contains("-----BEGIN PRIVATE KEY-----"));
2033 assert!(cert_pem.len() > 400);
2034 }
2035
2036 #[test]
2037 fn encrypt_private_key_pem_emits_pkcs8_v2_pem() {
2038 let (_cert, key_pem) =
2039 generate_self_signed_cert("example.com", &[]).expect("rcgen produces a key");
2040 let out = encrypt_private_key_pem(&key_pem, b"hunter2").expect("encryption succeeds");
2041 assert!(
2042 out.starts_with("-----BEGIN ENCRYPTED PRIVATE KEY-----\n"),
2043 "expected PKCS#8 v2 envelope, got {out:.80}"
2044 );
2045 assert!(out
2046 .trim_end()
2047 .ends_with("-----END ENCRYPTED PRIVATE KEY-----"));
2048 let plain_body: String = key_pem
2051 .lines()
2052 .filter(|l| !l.starts_with("-----"))
2053 .collect();
2054 assert!(!plain_body.is_empty());
2055 assert!(!out.contains(&plain_body));
2056 }
2057
2058 #[test]
2059 fn encrypt_private_key_pem_round_trips_via_pkcs8_decoder() {
2060 use pkcs8::{DecodePrivateKey, EncodePrivateKey, LineEnding};
2061
2062 let mut rng = rand::thread_rng();
2063 let original = rsa::RsaPrivateKey::new(&mut rng, 2048).expect("rsa keygen");
2064 let key_pem = original
2065 .to_pkcs8_pem(LineEnding::LF)
2066 .expect("pkcs8 pem encode")
2067 .to_string();
2068 let passphrase = b"correct horse battery staple";
2069
2070 let encrypted = encrypt_private_key_pem(&key_pem, passphrase).unwrap();
2071 assert!(encrypted.contains("BEGIN ENCRYPTED PRIVATE KEY"));
2072
2073 let recovered = rsa::RsaPrivateKey::from_pkcs8_encrypted_pem(&encrypted, passphrase)
2074 .expect("decryption + PKCS#8 parse");
2075 assert_eq!(original, recovered);
2076 }
2077
2078 fn make_req(action: &str, body: Value) -> AwsRequest {
2079 AwsRequest {
2080 service: "acm".to_string(),
2081 action: action.to_string(),
2082 region: "us-east-1".to_string(),
2083 account_id: "123456789012".to_string(),
2084 request_id: "rid".to_string(),
2085 headers: http::HeaderMap::new(),
2086 query_params: std::collections::HashMap::new(),
2087 body: bytes::Bytes::from(serde_json::to_vec(&body).unwrap()),
2088 body_stream: parking_lot::Mutex::new(None),
2089 path_segments: vec![],
2090 raw_path: "/".to_string(),
2091 raw_query: String::new(),
2092 method: http::Method::POST,
2093 is_query_protocol: false,
2094 access_key_id: None,
2095 principal: None,
2096 }
2097 }
2098
2099 async fn make_exportable_cert(svc: &AcmService) -> String {
2100 let req = make_req(
2101 "RequestCertificate",
2102 json!({
2103 "DomainName": "example.com",
2104 "Options": {"Export": "ENABLED"},
2105 }),
2106 );
2107 let resp = svc.handle(req).await.unwrap();
2108 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2109 body["CertificateArn"].as_str().unwrap().to_string()
2110 }
2111
2112 #[tokio::test]
2113 async fn export_certificate_emits_passphrase_encrypted_pem() {
2114 let svc = AcmService::default();
2115 let arn = make_exportable_cert(&svc).await;
2116 let original_key_pem = svc
2117 .state
2118 .read()
2119 .accounts
2120 .get("123456789012")
2121 .unwrap()
2122 .certificates
2123 .get(&arn)
2124 .unwrap()
2125 .private_key_pem
2126 .clone()
2127 .unwrap();
2128
2129 let passphrase = b"hunter2";
2130 let passphrase_b64 = base64::engine::general_purpose::STANDARD.encode(passphrase);
2131 let export = make_req(
2132 "ExportCertificate",
2133 json!({"CertificateArn": arn, "Passphrase": passphrase_b64}),
2134 );
2135 let export_resp = svc.handle(export).await.unwrap();
2136 let export_json: Value = serde_json::from_slice(export_resp.body.expect_bytes()).unwrap();
2137 let private_key = export_json["PrivateKey"].as_str().unwrap();
2138 assert!(
2139 private_key.contains("BEGIN ENCRYPTED PRIVATE KEY"),
2140 "expected PKCS#8 v2 PEM, got {private_key}"
2141 );
2142 assert!(private_key.contains("END ENCRYPTED PRIVATE KEY"));
2143 let original_inner_der =
2146 pem_decode_private_key(&original_key_pem).expect("decode original PEM");
2147 let original_pki_bytes = pkcs8::PrivateKeyInfo::try_from(original_inner_der.as_slice())
2148 .map(|p| p.private_key.to_vec())
2149 .expect("parse original PKCS#8");
2150 let encrypted_der =
2151 pem_decode_encrypted_private_key(private_key).expect("decode encrypted PEM");
2152 let decrypted_doc = pkcs8::EncryptedPrivateKeyInfo::try_from(encrypted_der.as_slice())
2153 .expect("parse encrypted PKCS#8")
2154 .decrypt(passphrase)
2155 .expect("decrypt with passphrase");
2156 let decrypted_pki = pkcs8::PrivateKeyInfo::try_from(decrypted_doc.as_bytes())
2157 .expect("parse decrypted PKCS#8");
2158 assert_eq!(decrypted_pki.private_key, original_pki_bytes.as_slice());
2159 }
2160
2161 #[tokio::test]
2162 async fn export_certificate_without_passphrase_returns_plain_pem() {
2163 let svc = AcmService::default();
2164 let arn = make_exportable_cert(&svc).await;
2165 let export_resp = svc
2166 .handle(make_req(
2167 "ExportCertificate",
2168 json!({"CertificateArn": arn}),
2169 ))
2170 .await
2171 .unwrap();
2172 let export_json: Value = serde_json::from_slice(export_resp.body.expect_bytes()).unwrap();
2173 let private_key = export_json["PrivateKey"].as_str().unwrap();
2174 assert!(
2175 private_key.contains("-----BEGIN PRIVATE KEY-----"),
2176 "expected plain PKCS#8 PEM, got {private_key}"
2177 );
2178 assert!(!private_key.contains("ENCRYPTED PRIVATE KEY"));
2179 }
2180
2181 #[tokio::test]
2182 async fn export_certificate_with_empty_passphrase_returns_plain_pem() {
2183 let svc = AcmService::default();
2184 let arn = make_exportable_cert(&svc).await;
2185 let export_resp = svc
2186 .handle(make_req(
2187 "ExportCertificate",
2188 json!({"CertificateArn": arn, "Passphrase": ""}),
2189 ))
2190 .await
2191 .unwrap();
2192 let export_json: Value = serde_json::from_slice(export_resp.body.expect_bytes()).unwrap();
2193 let private_key = export_json["PrivateKey"].as_str().unwrap();
2194 assert!(private_key.contains("-----BEGIN PRIVATE KEY-----"));
2195 assert!(!private_key.contains("ENCRYPTED PRIVATE KEY"));
2196 }
2197
2198 #[tokio::test]
2199 async fn export_certificate_rejects_non_base64_passphrase() {
2200 let svc = AcmService::default();
2201 let resp = svc
2202 .handle(make_req(
2203 "RequestCertificate",
2204 json!({"DomainName": "example.com"}),
2205 ))
2206 .await
2207 .unwrap();
2208 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2209 let arn = body["CertificateArn"].as_str().unwrap().to_string();
2210 let export = make_req(
2211 "ExportCertificate",
2212 json!({"CertificateArn": arn, "Passphrase": "not!base64!@#$"}),
2213 );
2214 let err = match svc.handle(export).await {
2215 Ok(_) => panic!("expected error for non-base64 passphrase"),
2216 Err(e) => e,
2217 };
2218 assert!(format!("{err:?}").contains("Passphrase"));
2219 }
2220
2221 fn pem_decode_encrypted_private_key(pem: &str) -> Result<Vec<u8>, String> {
2222 let begin = "-----BEGIN ENCRYPTED PRIVATE KEY-----";
2223 let end = "-----END ENCRYPTED PRIVATE KEY-----";
2224 let begin_idx = pem.find(begin).ok_or("missing BEGIN")?;
2225 let after = &pem[begin_idx + begin.len()..];
2226 let end_idx = after.find(end).ok_or("missing END")?;
2227 let body: String = after[..end_idx]
2228 .chars()
2229 .filter(|c| !c.is_whitespace())
2230 .collect();
2231 base64::engine::general_purpose::STANDARD
2232 .decode(body.as_bytes())
2233 .map_err(|e| format!("base64: {e}"))
2234 }
2235
2236 #[tokio::test]
2237 async fn describe_certificate_observes_auto_issue_transition() {
2238 let svc = AcmService::default()
2242 .with_pending_validation_delay(std::time::Duration::from_millis(50));
2243 let resp = svc
2244 .handle(make_req(
2245 "RequestCertificate",
2246 json!({"DomainName": "example.com", "ValidationMethod": "DNS"}),
2247 ))
2248 .await
2249 .unwrap();
2250 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2251 let arn = body["CertificateArn"].as_str().unwrap().to_string();
2252
2253 let early = svc
2256 .handle(make_req(
2257 "DescribeCertificate",
2258 json!({"CertificateArn": arn}),
2259 ))
2260 .await
2261 .unwrap();
2262 let early: Value = serde_json::from_slice(early.body.expect_bytes()).unwrap();
2263 assert_eq!(early["Certificate"]["Status"], "PENDING_VALIDATION");
2264 assert_eq!(early["Certificate"]["RenewalEligibility"], "INELIGIBLE");
2265 assert!(early["Certificate"].get("RenewalSummary").is_none());
2266
2267 let mut last: Value = Value::Null;
2269 for _ in 0..40 {
2270 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
2271 let resp = svc
2272 .handle(make_req(
2273 "DescribeCertificate",
2274 json!({"CertificateArn": arn}),
2275 ))
2276 .await
2277 .unwrap();
2278 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2279 if body["Certificate"]["Status"] == "ISSUED" {
2280 last = body;
2281 break;
2282 }
2283 }
2284 assert_eq!(last["Certificate"]["Status"], "ISSUED");
2285 assert_eq!(last["Certificate"]["RenewalEligibility"], "ELIGIBLE");
2286 let rs = &last["Certificate"]["RenewalSummary"];
2287 assert_eq!(rs["RenewalStatus"], "PENDING_AUTO_RENEWAL");
2288 assert!(rs["UpdatedAt"].as_f64().unwrap() > 0.0);
2289 }
2290
2291 #[tokio::test]
2292 async fn request_certificate_emits_parseable_pem() {
2293 let svc = AcmService::default();
2294 let resp = svc
2295 .handle(make_req(
2296 "RequestCertificate",
2297 json!({"DomainName": "example.com"}),
2298 ))
2299 .await
2300 .unwrap();
2301 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2302 let arn = body["CertificateArn"].as_str().unwrap().to_string();
2303
2304 let resp = svc
2305 .handle(make_req("GetCertificate", json!({"CertificateArn": arn})))
2306 .await
2307 .unwrap();
2308 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2309 let cert_pem = body["Certificate"].as_str().unwrap();
2310 assert!(
2311 cert_pem.starts_with("-----BEGIN CERTIFICATE-----"),
2312 "expected real PEM cert header, got {cert_pem:.80}"
2313 );
2314 assert!(cert_pem.trim_end().ends_with("-----END CERTIFICATE-----"));
2315 assert!(
2318 cert_pem.len() > 400,
2319 "expected real X.509 PEM, got placeholder-sized blob"
2320 );
2321 assert!(!cert_pem.contains(arn.as_str()));
2324 }
2325
2326 #[tokio::test]
2327 async fn request_certificate_includes_san() {
2328 let svc = AcmService::default();
2329 let resp = svc
2330 .handle(make_req(
2331 "RequestCertificate",
2332 json!({
2333 "DomainName": "example.com",
2334 "SubjectAlternativeNames": ["api.example.com", "www.example.com"],
2335 }),
2336 ))
2337 .await
2338 .unwrap();
2339 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2340 let arn = body["CertificateArn"].as_str().unwrap().to_string();
2341
2342 let resp = svc
2343 .handle(make_req("GetCertificate", json!({"CertificateArn": arn})))
2344 .await
2345 .unwrap();
2346 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2347 let cert_pem = body["Certificate"].as_str().unwrap();
2348 assert!(cert_pem.starts_with("-----BEGIN CERTIFICATE-----"));
2349 assert!(cert_pem.trim_end().ends_with("-----END CERTIFICATE-----"));
2350
2351 let body_b64: String = cert_pem
2355 .lines()
2356 .filter(|l| !l.starts_with("-----"))
2357 .collect::<Vec<_>>()
2358 .join("");
2359 let der = base64::engine::general_purpose::STANDARD
2360 .decode(&body_b64)
2361 .expect("cert body is valid base64");
2362 for san in ["example.com", "api.example.com", "www.example.com"] {
2363 assert!(
2364 der.windows(san.len()).any(|w| w == san.as_bytes()),
2365 "expected SAN {san} embedded in DER"
2366 );
2367 }
2368 }
2369
2370 #[tokio::test]
2371 async fn request_certificate_starts_in_pending_validation() {
2372 let svc = AcmService::default();
2373 let resp = svc
2374 .handle(make_req(
2375 "RequestCertificate",
2376 json!({"DomainName": "example.com", "ValidationMethod": "DNS"}),
2377 ))
2378 .await
2379 .unwrap();
2380 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2381 let arn = body["CertificateArn"].as_str().unwrap().to_string();
2382 let st = svc.state.read();
2383 let cert = st
2384 .accounts
2385 .get("123456789012")
2386 .unwrap()
2387 .certificates
2388 .get(&arn)
2389 .unwrap();
2390 assert_eq!(cert.status, "PENDING_VALIDATION");
2391 assert!(cert.issued_at.is_none());
2392 }
2393
2394 #[tokio::test]
2395 async fn set_certificate_status_to_issued_via_admin() {
2396 let svc = AcmService::default();
2397 let resp = svc
2398 .handle(make_req(
2399 "RequestCertificate",
2400 json!({"DomainName": "admin.example.com"}),
2401 ))
2402 .await
2403 .unwrap();
2404 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2405 let arn = body["CertificateArn"].as_str().unwrap().to_string();
2406
2407 assert!(svc.set_certificate_status(&arn, "ISSUED", None));
2408 let st = svc.state.read();
2409 let cert = st
2410 .accounts
2411 .get("123456789012")
2412 .unwrap()
2413 .certificates
2414 .get(&arn)
2415 .unwrap();
2416 assert_eq!(cert.status, "ISSUED");
2417 assert!(cert.issued_at.is_some());
2418 for dv in &cert.domain_validation {
2419 assert_eq!(dv.validation_status, "SUCCESS");
2420 }
2421 }
2422
2423 #[tokio::test]
2424 async fn set_certificate_status_unknown_arn_returns_false() {
2425 let svc = AcmService::default();
2426 assert!(!svc.set_certificate_status("does-not-exist", "ISSUED", None));
2427 }
2428
2429 #[tokio::test]
2430 async fn auto_issue_tick_transitions_to_issued() {
2431 let svc = AcmService::default()
2432 .with_pending_validation_delay(std::time::Duration::from_millis(50));
2433 let resp = svc
2434 .handle(make_req(
2435 "RequestCertificate",
2436 json!({"DomainName": "auto.example.com", "ValidationMethod": "DNS"}),
2437 ))
2438 .await
2439 .unwrap();
2440 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2441 let arn = body["CertificateArn"].as_str().unwrap().to_string();
2442
2443 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
2444
2445 let st = svc.state.read();
2446 let cert = st
2447 .accounts
2448 .get("123456789012")
2449 .unwrap()
2450 .certificates
2451 .get(&arn)
2452 .unwrap();
2453 assert_eq!(cert.status, "ISSUED");
2454 assert!(cert.issued_at.is_some());
2455 assert_eq!(cert.renewal_eligibility, "ELIGIBLE");
2456 let rs = cert.renewal_summary.as_ref().expect("renewal summary set");
2457 assert_eq!(rs.renewal_status, "PENDING_AUTO_RENEWAL");
2458 }
2459
2460 #[tokio::test]
2461 async fn email_validation_stays_pending_until_approve() {
2462 let svc = AcmService::default()
2467 .with_pending_validation_delay(std::time::Duration::from_millis(20));
2468 let resp = svc
2469 .handle(make_req(
2470 "RequestCertificate",
2471 json!({
2472 "DomainName": "email.example.com",
2473 "ValidationMethod": "EMAIL",
2474 }),
2475 ))
2476 .await
2477 .unwrap();
2478 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2479 let arn = body["CertificateArn"].as_str().unwrap().to_string();
2480
2481 tokio::time::sleep(std::time::Duration::from_millis(150)).await;
2484 {
2485 let st = svc.state.read();
2486 let cert = st
2487 .accounts
2488 .get("123456789012")
2489 .unwrap()
2490 .certificates
2491 .get(&arn)
2492 .unwrap();
2493 assert_eq!(cert.status, "PENDING_VALIDATION");
2494 assert!(cert.issued_at.is_none());
2495 assert!(cert.renewal_summary.is_none());
2496 }
2497
2498 assert!(svc.approve_certificate(&arn));
2499 let st = svc.state.read();
2500 let cert = st
2501 .accounts
2502 .get("123456789012")
2503 .unwrap()
2504 .certificates
2505 .get(&arn)
2506 .unwrap();
2507 assert_eq!(cert.status, "ISSUED");
2508 assert_eq!(cert.renewal_eligibility, "ELIGIBLE");
2509 for dv in &cert.domain_validation {
2510 assert_eq!(dv.validation_status, "SUCCESS");
2511 }
2512 assert!(cert.renewal_summary.is_some());
2513 }
2514
2515 #[tokio::test]
2516 async fn renew_certificate_refreshes_renewal_summary() {
2517 let svc = AcmService::default()
2518 .with_pending_validation_delay(std::time::Duration::from_millis(20));
2519 let resp = svc
2520 .handle(make_req(
2521 "RequestCertificate",
2522 json!({"DomainName": "renew.example.com", "ValidationMethod": "DNS"}),
2523 ))
2524 .await
2525 .unwrap();
2526 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2527 let arn = body["CertificateArn"].as_str().unwrap().to_string();
2528
2529 tokio::time::sleep(std::time::Duration::from_millis(80)).await;
2530
2531 let resp = svc
2532 .handle(make_req("RenewCertificate", json!({"CertificateArn": arn})))
2533 .await
2534 .unwrap();
2535 assert_eq!(resp.status, http::StatusCode::OK);
2536
2537 let st = svc.state.read();
2538 let cert = st
2539 .accounts
2540 .get("123456789012")
2541 .unwrap()
2542 .certificates
2543 .get(&arn)
2544 .unwrap();
2545 assert_eq!(cert.status, "ISSUED");
2546 let rs = cert.renewal_summary.as_ref().expect("renewal summary set");
2547 assert_eq!(rs.renewal_status, "SUCCESS");
2548 assert_eq!(rs.domain_validation.len(), cert.domain_validation.len());
2549 }
2550
2551 #[test]
2552 fn auto_issue_delay_default_when_env_unset() {
2553 if std::env::var("FAKECLOUD_ACM_AUTO_ISSUE_SECS").is_err() {
2558 assert_eq!(
2559 auto_issue_delay_from_env(),
2560 std::time::Duration::from_secs(DEFAULT_AUTO_ISSUE_SECS),
2561 );
2562 }
2563 }
2564
2565 #[tokio::test]
2566 async fn set_certificate_status_failed_records_reason() {
2567 let svc = AcmService::default();
2568 let resp = svc
2569 .handle(make_req(
2570 "RequestCertificate",
2571 json!({"DomainName": "fail.example.com"}),
2572 ))
2573 .await
2574 .unwrap();
2575 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2576 let arn = body["CertificateArn"].as_str().unwrap().to_string();
2577 assert!(svc.set_certificate_status(&arn, "FAILED", Some("DNS lookup error".to_string())));
2578 let st = svc.state.read();
2579 let cert = st
2580 .accounts
2581 .get("123456789012")
2582 .unwrap()
2583 .certificates
2584 .get(&arn)
2585 .unwrap();
2586 assert_eq!(cert.status, "FAILED");
2587 assert_eq!(cert.failure_reason.as_deref(), Some("DNS lookup error"));
2588 }
2589
2590 #[tokio::test]
2591 async fn request_certificate_private_key_pem_is_valid() {
2592 let svc = AcmService::default();
2593 let resp = svc
2594 .handle(make_req(
2595 "RequestCertificate",
2596 json!({"DomainName": "key.example.com"}),
2597 ))
2598 .await
2599 .unwrap();
2600 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2601 let arn = body["CertificateArn"].as_str().unwrap().to_string();
2602
2603 let st = svc.state.read();
2604 let cert = st
2605 .accounts
2606 .get("123456789012")
2607 .unwrap()
2608 .certificates
2609 .get(&arn)
2610 .unwrap();
2611 let key_pem = cert
2612 .private_key_pem
2613 .as_deref()
2614 .expect("RequestCertificate should populate a real private key");
2615 let starts_pkcs8 = key_pem.starts_with("-----BEGIN PRIVATE KEY-----");
2616 let starts_rsa = key_pem.starts_with("-----BEGIN RSA PRIVATE KEY-----");
2617 assert!(
2618 starts_pkcs8 || starts_rsa,
2619 "expected PKCS#8 or RSA private key header, got {key_pem:.80}"
2620 );
2621 assert!(
2622 key_pem.trim_end().ends_with("-----END PRIVATE KEY-----")
2623 || key_pem
2624 .trim_end()
2625 .ends_with("-----END RSA PRIVATE KEY-----")
2626 );
2627 assert!(key_pem.len() > 200);
2628 }
2629
2630 #[tokio::test]
2634 async fn search_certificates_filters_by_status_and_sorts() {
2635 let svc = AcmService::default();
2636 let mut arn: std::collections::HashMap<&str, String> = std::collections::HashMap::new();
2637 for d in ["a.example.com", "b.example.com", "c.example.com"] {
2638 let resp = svc
2639 .handle(make_req("RequestCertificate", json!({ "DomainName": d })))
2640 .await
2641 .unwrap();
2642 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2643 arn.insert(d, body["CertificateArn"].as_str().unwrap().to_string());
2644 }
2645
2646 {
2648 let mut state = svc.state.write();
2649 let certs = &mut state.accounts.get_mut("123456789012").unwrap().certificates;
2650 let now = chrono::Utc::now();
2651 for c in certs.values_mut() {
2652 match c.domain_name.as_str() {
2653 "a.example.com" => {
2654 c.status = "ISSUED".to_string();
2655 c.created_at = now - chrono::Duration::hours(2);
2656 }
2657 "b.example.com" => {
2658 c.status = "PENDING_VALIDATION".to_string();
2659 c.created_at = now - chrono::Duration::hours(1);
2660 }
2661 _ => {
2662 c.status = "ISSUED".to_string();
2663 c.created_at = now;
2664 }
2665 }
2666 }
2667 }
2668
2669 let domains = |body: &Value| -> Vec<String> {
2670 body["Results"]
2671 .as_array()
2672 .unwrap()
2673 .iter()
2674 .map(|r| r["CertificateArn"].as_str().unwrap().to_string())
2675 .collect()
2676 };
2677
2678 let resp = svc
2680 .handle(make_req(
2681 "SearchCertificates",
2682 json!({ "CertificateStatuses": ["ISSUED"] }),
2683 ))
2684 .await
2685 .unwrap();
2686 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2687 let mut issued = domains(&body);
2688 issued.sort();
2689 let mut want = vec![arn["a.example.com"].clone(), arn["c.example.com"].clone()];
2690 want.sort();
2691 assert_eq!(issued, want);
2692
2693 let resp = svc
2695 .handle(make_req(
2696 "SearchCertificates",
2697 json!({ "SortBy": "CREATED_AT", "SortOrder": "ASCENDING" }),
2698 ))
2699 .await
2700 .unwrap();
2701 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2702 assert_eq!(
2703 domains(&body),
2704 vec![
2705 arn["a.example.com"].clone(),
2706 arn["b.example.com"].clone(),
2707 arn["c.example.com"].clone(),
2708 ]
2709 );
2710
2711 let resp = svc
2713 .handle(make_req(
2714 "SearchCertificates",
2715 json!({ "SortBy": "CREATED_AT", "SortOrder": "DESCENDING" }),
2716 ))
2717 .await
2718 .unwrap();
2719 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2720 assert_eq!(
2721 domains(&body),
2722 vec![
2723 arn["c.example.com"].clone(),
2724 arn["b.example.com"].clone(),
2725 arn["a.example.com"].clone(),
2726 ]
2727 );
2728 }
2729
2730 #[tokio::test]
2731 async fn search_certificates_extended_key_usage_filter() {
2732 let svc = AcmService::default();
2737 for d in ["a.example.com", "b.example.com"] {
2738 svc.handle(make_req("RequestCertificate", json!({ "DomainName": d })))
2739 .await
2740 .unwrap();
2741 }
2742
2743 let count = |body: &Value| body["Results"].as_array().unwrap().len();
2744
2745 let resp = svc
2747 .handle(make_req(
2748 "SearchCertificates",
2749 json!({
2750 "FilterStatement": {
2751 "Filter": {
2752 "X509AttributeFilter": {
2753 "ExtendedKeyUsage": "TLS_WEB_SERVER_AUTHENTICATION"
2754 }
2755 }
2756 }
2757 }),
2758 ))
2759 .await
2760 .unwrap();
2761 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2762 assert_eq!(count(&body), 2, "matching EKU should return all certs");
2763
2764 let resp = svc
2766 .handle(make_req(
2767 "SearchCertificates",
2768 json!({
2769 "FilterStatement": {
2770 "Filter": {
2771 "X509AttributeFilter": { "ExtendedKeyUsage": "CODE_SIGNING" }
2772 }
2773 }
2774 }),
2775 ))
2776 .await
2777 .unwrap();
2778 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2779 assert_eq!(count(&body), 0, "non-matching EKU should drop all certs");
2780
2781 let resp = svc
2783 .handle(make_req(
2784 "SearchCertificates",
2785 json!({
2786 "FilterStatement": {
2787 "Filter": { "X509AttributeFilter": { "ExtendedKeyUsage": "ANY" } }
2788 }
2789 }),
2790 ))
2791 .await
2792 .unwrap();
2793 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2794 assert_eq!(count(&body), 2, "ANY EKU should match all certs with EKUs");
2795
2796 let resp = svc
2798 .handle(make_req(
2799 "SearchCertificates",
2800 json!({
2801 "FilterStatement": {
2802 "Filter": { "X509AttributeFilter": { "KeyUsage": "ANY" } }
2803 }
2804 }),
2805 ))
2806 .await
2807 .unwrap();
2808 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2809 assert_eq!(
2810 count(&body),
2811 2,
2812 "ANY KeyUsage should match all certs with key usages"
2813 );
2814 }
2815
2816 #[tokio::test]
2817 async fn search_certificates_honors_and_or_not_composition() {
2818 let svc = AcmService::default();
2821 svc.handle(make_req(
2822 "RequestCertificate",
2823 json!({ "DomainName": "rsa.example.com", "KeyAlgorithm": "RSA_2048" }),
2824 ))
2825 .await
2826 .unwrap();
2827 svc.handle(make_req(
2828 "RequestCertificate",
2829 json!({ "DomainName": "ec.example.com", "KeyAlgorithm": "EC_prime256v1" }),
2830 ))
2831 .await
2832 .unwrap();
2833
2834 let search = |stmt: Value| {
2835 let svc = &svc;
2836 async move {
2837 let resp = svc
2838 .handle(make_req(
2839 "SearchCertificates",
2840 json!({ "FilterStatement": stmt }),
2841 ))
2842 .await
2843 .unwrap();
2844 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2845 body["Results"]
2846 .as_array()
2847 .unwrap()
2848 .iter()
2849 .map(|c| {
2850 c["X509Attributes"]["KeyAlgorithm"]
2851 .as_str()
2852 .unwrap()
2853 .to_string()
2854 })
2855 .collect::<Vec<_>>()
2856 }
2857 };
2858
2859 let key_alg =
2861 |alg: &str| json!({ "Filter": { "X509AttributeFilter": { "KeyAlgorithm": alg } } });
2862
2863 let got = search(json!({ "Not": key_alg("RSA_2048") })).await;
2865 assert_eq!(got, vec!["EC_prime256v1"]);
2866
2867 let mut got =
2869 search(json!({ "Or": [key_alg("RSA_2048"), key_alg("EC_prime256v1")] })).await;
2870 got.sort();
2871 assert_eq!(got, vec!["EC_prime256v1", "RSA_2048"]);
2872
2873 let got = search(json!({
2875 "And": [
2876 key_alg("RSA_2048"),
2877 { "Filter": { "X509AttributeFilter": { "ExtendedKeyUsage": "TLS_WEB_SERVER_AUTHENTICATION" } } },
2878 ]
2879 }))
2880 .await;
2881 assert_eq!(got, vec!["RSA_2048"]);
2882
2883 let got = search(json!({ "And": [key_alg("RSA_2048"), key_alg("EC_prime256v1")] })).await;
2885 assert!(got.is_empty());
2886 }
2887
2888 #[tokio::test]
2889 async fn search_certificates_leaf_filter_shapes_apply() {
2890 let svc = AcmService::default();
2894 let resp = svc
2895 .handle(make_req(
2896 "RequestCertificate",
2897 json!({
2898 "DomainName": "primary.example.com",
2899 "SubjectAlternativeNames": ["alt.example.org"],
2900 }),
2901 ))
2902 .await
2903 .unwrap();
2904 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2905 let arn = body["CertificateArn"].as_str().unwrap().to_string();
2906 svc.handle(make_req(
2907 "RequestCertificate",
2908 json!({ "DomainName": "other.example.com" }),
2909 ))
2910 .await
2911 .unwrap();
2912
2913 let arns = |stmt: Value| {
2914 let svc = &svc;
2915 async move {
2916 let resp = svc
2917 .handle(make_req(
2918 "SearchCertificates",
2919 json!({ "FilterStatement": stmt }),
2920 ))
2921 .await
2922 .unwrap();
2923 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2924 body["Results"]
2925 .as_array()
2926 .unwrap()
2927 .iter()
2928 .map(|c| c["CertificateArn"].as_str().unwrap().to_string())
2929 .collect::<Vec<_>>()
2930 }
2931 };
2932
2933 let got = arns(json!({ "Filter": { "CertificateArn": arn } })).await;
2935 assert_eq!(got.len(), 1);
2936
2937 let got = arns(json!({
2939 "Filter": { "X509AttributeFilter": { "Subject": {
2940 "CommonName": { "Value": "primary.example.com", "ComparisonOperator": "EQUALS" }
2941 } } }
2942 }))
2943 .await;
2944 assert_eq!(got.len(), 1);
2945
2946 let got = arns(json!({
2948 "Filter": { "X509AttributeFilter": { "SubjectAlternativeName": {
2949 "DnsName": { "Value": "example.org", "ComparisonOperator": "CONTAINS" }
2950 } } }
2951 }))
2952 .await;
2953 assert_eq!(got.len(), 1);
2954
2955 let got = arns(json!({
2958 "Filter": { "AcmCertificateMetadataFilter": { "Status": "PENDING_VALIDATION" } }
2959 }))
2960 .await;
2961 assert_eq!(got.len(), 2);
2962 let got = arns(json!({
2963 "Filter": { "AcmCertificateMetadataFilter": { "Status": "EXPIRED" } }
2964 }))
2965 .await;
2966 assert!(got.is_empty());
2967 }
2968}