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 filter_values = |key: &str| -> Vec<String> {
1221 body.get("FilterStatement")
1222 .and_then(|f| f.get("Filter"))
1223 .and_then(|f| f.get(key))
1224 .and_then(Value::as_array)
1225 .map(|v| {
1226 v.iter()
1227 .filter_map(|s| s.as_str().map(|s| s.to_ascii_uppercase()))
1228 .collect()
1229 })
1230 .unwrap_or_default()
1231 };
1232 let key_types = filter_values("KeyTypes");
1233 let extended_key_usages = filter_values("ExtendedKeyUsages");
1239 let key_usage = filter_values("KeyUsage");
1240
1241 let statuses: Vec<String> = body
1243 .get("CertificateStatuses")
1244 .and_then(Value::as_array)
1245 .map(|v| {
1246 v.iter()
1247 .filter_map(|s| s.as_str().map(|s| s.to_ascii_uppercase()))
1248 .collect()
1249 })
1250 .unwrap_or_default();
1251
1252 let state = self.state.read();
1253 let mut all: Vec<StoredCertificate> = state
1254 .accounts
1255 .get(&req.account_id)
1256 .map(|a| a.certificates.values().cloned().collect())
1257 .unwrap_or_default();
1258 drop(state);
1259
1260 if !key_types.is_empty() {
1261 all.retain(|c| key_types.contains(&c.key_algorithm.to_ascii_uppercase()));
1262 }
1263 if !statuses.is_empty() {
1264 all.retain(|c| statuses.contains(&c.status.to_ascii_uppercase()));
1265 }
1266 const CERT_EKUS: [&str; 2] = [
1271 "TLS_WEB_SERVER_AUTHENTICATION",
1272 "TLS_WEB_CLIENT_AUTHENTICATION",
1273 ];
1274 const CERT_KEY_USAGES: [&str; 2] = ["DIGITAL_SIGNATURE", "KEY_ENCIPHERMENT"];
1275 if !extended_key_usages.is_empty() {
1276 all.retain(|_| {
1277 extended_key_usages
1278 .iter()
1279 .any(|f| f == "ANY" || CERT_EKUS.contains(&f.as_str()))
1280 });
1281 }
1282 if !key_usage.is_empty() {
1283 all.retain(|_| {
1284 key_usage
1285 .iter()
1286 .any(|f| f == "ANY" || CERT_KEY_USAGES.contains(&f.as_str()))
1287 });
1288 }
1289
1290 let sort_by = body
1293 .get("SortBy")
1294 .and_then(Value::as_str)
1295 .unwrap_or("CREATED_AT")
1296 .to_string();
1297 let descending = body
1298 .get("SortOrder")
1299 .and_then(Value::as_str)
1300 .map(|o| !o.eq_ignore_ascii_case("ASCENDING"))
1301 .unwrap_or(true);
1302 all.sort_by(|a, b| {
1303 let ord = match sort_by.as_str() {
1304 "NOT_AFTER" => a.not_after.cmp(&b.not_after),
1305 "NOT_BEFORE" => a.not_before.cmp(&b.not_before),
1306 "ISSUED_AT" => a.issued_at.cmp(&b.issued_at),
1307 "STATUS" => a.status.cmp(&b.status),
1308 "KEY_ALGORITHM" => a.key_algorithm.cmp(&b.key_algorithm),
1309 "TYPE" => a.cert_type.cmp(&b.cert_type),
1310 "CERTIFICATE_ARN" => a.arn.cmp(&b.arn),
1311 _ => a.created_at.cmp(&b.created_at),
1313 };
1314 let ord = ord.then_with(|| a.arn.cmp(&b.arn));
1316 if descending {
1317 ord.reverse()
1318 } else {
1319 ord
1320 }
1321 });
1322 let start = next_token
1323 .and_then(|t| t.parse::<usize>().ok())
1324 .unwrap_or(0);
1325 let end = (start + max_results).min(all.len());
1326 let page: Vec<&StoredCertificate> = all.iter().skip(start).take(max_results).collect();
1327 let next = if end < all.len() {
1328 Some(end.to_string())
1329 } else {
1330 None
1331 };
1332 let mut response = json!({
1333 "Results": page
1334 .iter()
1335 .map(|c| certificate_search_result_json(c))
1336 .collect::<Vec<_>>(),
1337 });
1338 if let Some(t) = next {
1339 response
1340 .as_object_mut()
1341 .unwrap()
1342 .insert("NextToken".to_string(), Value::String(t));
1343 }
1344 Ok(AwsResponse::ok_json(response))
1345 }
1346}
1347
1348fn account_mut<'a>(state: &'a mut AcmAccounts, account_id: &str) -> &'a mut AccountState {
1351 state.accounts.entry(account_id.to_string()).or_default()
1352}
1353
1354fn require_certificate_arn(req: &AwsRequest) -> Result<String, AwsServiceError> {
1355 req.json_body()
1356 .get("CertificateArn")
1357 .and_then(Value::as_str)
1358 .map(|s| s.to_string())
1359 .ok_or_else(|| invalid_param("CertificateArn is required"))
1360}
1361
1362fn invalid_param(msg: impl Into<String>) -> AwsServiceError {
1363 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "InvalidParameterException", msg)
1364}
1365
1366fn validation_error(msg: impl Into<String>) -> AwsServiceError {
1367 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "ValidationException", msg)
1368}
1369
1370fn no_such_certificate(arn: &str) -> AwsServiceError {
1371 AwsServiceError::aws_error(
1372 StatusCode::BAD_REQUEST,
1373 "ResourceNotFoundException",
1374 format!("Could not find certificate with arn {arn}"),
1375 )
1376}
1377
1378fn synth_certificate_arn(account_id: &str, region: &str) -> String {
1379 let region = if region.is_empty() {
1380 "us-east-1"
1381 } else {
1382 region
1383 };
1384 let id = Uuid::new_v4();
1385 Arn::new("acm", region, account_id, &format!("certificate/{id}")).to_string()
1386}
1387
1388fn cert_id_from_arn(arn: &str) -> &str {
1395 arn.rsplit_once("certificate/")
1396 .map(|(_, id)| id)
1397 .unwrap_or(arn)
1398}
1399
1400fn synth_serial(arn: &str) -> String {
1401 let mut hasher = Sha256::new();
1402 hasher.update(arn.as_bytes());
1403 let digest = hasher.finalize();
1404 hex::encode(&digest[..16])
1405}
1406
1407fn parse_tags(value: Option<&Value>) -> Result<BTreeMap<String, String>, AwsServiceError> {
1408 let mut out = BTreeMap::new();
1409 let Some(arr) = value.and_then(Value::as_array) else {
1410 return Ok(out);
1411 };
1412 for tag in arr {
1413 let key = tag
1414 .get("Key")
1415 .and_then(Value::as_str)
1416 .ok_or_else(|| invalid_param("Tag.Key is required"))?
1417 .to_string();
1418 let value = tag
1419 .get("Value")
1420 .and_then(Value::as_str)
1421 .unwrap_or_default()
1422 .to_string();
1423 out.insert(key, value);
1424 }
1425 Ok(out)
1426}
1427
1428fn parse_options(value: Option<&Value>) -> CertificateOptions {
1429 let v = match value {
1430 Some(v) => v,
1431 None => {
1432 return CertificateOptions {
1433 certificate_transparency_logging_preference: "ENABLED".to_string(),
1434 export: "DISABLED".to_string(),
1435 };
1436 }
1437 };
1438 CertificateOptions {
1439 certificate_transparency_logging_preference: v
1440 .get("CertificateTransparencyLoggingPreference")
1441 .and_then(Value::as_str)
1442 .unwrap_or("ENABLED")
1443 .to_string(),
1444 export: v
1445 .get("Export")
1446 .and_then(Value::as_str)
1447 .unwrap_or("DISABLED")
1448 .to_string(),
1449 }
1450}
1451
1452fn effective_sans(domain: &str, extras: &[String]) -> Vec<String> {
1456 let mut all = vec![domain.to_string()];
1457 for s in extras {
1458 if !all.contains(s) {
1459 all.push(s.clone());
1460 }
1461 }
1462 all
1463}
1464
1465fn synth_domain_validation(domain: &str, sans: &[String], method: &str) -> Vec<DomainValidation> {
1466 effective_sans(domain, sans)
1467 .iter()
1468 .map(|d| {
1469 if method == "DNS" {
1470 let token = synth_dns_token(d);
1471 DomainValidation {
1472 domain_name: d.clone(),
1473 validation_status: "PENDING_VALIDATION".to_string(),
1474 validation_method: "DNS".to_string(),
1475 resource_record_name: Some(format!("_{token}.{d}.")),
1476 resource_record_type: Some("CNAME".to_string()),
1477 resource_record_value: Some(format!("_{token}.acm-validations.aws.")),
1478 }
1479 } else {
1480 DomainValidation {
1481 domain_name: d.clone(),
1482 validation_status: "PENDING_VALIDATION".to_string(),
1483 validation_method: "EMAIL".to_string(),
1484 resource_record_name: None,
1485 resource_record_type: None,
1486 resource_record_value: None,
1487 }
1488 }
1489 })
1490 .collect()
1491}
1492
1493fn synth_dns_token(domain: &str) -> String {
1496 let mut hasher = Sha256::new();
1497 hasher.update(domain.as_bytes());
1498 let digest = hasher.finalize();
1499 hex::encode(&digest[..16])
1500}
1501
1502fn decode_blob(value: Option<&Value>) -> Option<String> {
1503 let v = value?;
1504 if let Some(s) = v.as_str() {
1505 if let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(s) {
1509 if let Ok(text) = String::from_utf8(bytes) {
1510 return Some(text);
1511 }
1512 }
1513 return Some(s.to_string());
1514 }
1515 None
1516}
1517
1518fn parse_cn_from_pem(pem: &str) -> Option<String> {
1523 pem.lines()
1524 .find_map(|line| line.split("CN=").nth(1))
1525 .map(|rest| {
1526 rest.split(['/', ',', '\n', ' '])
1527 .next()
1528 .unwrap_or("")
1529 .to_string()
1530 })
1531 .filter(|s| !s.is_empty())
1532}
1533
1534fn placeholder_cert_pem(arn: &str) -> String {
1535 let body = base64::engine::general_purpose::STANDARD.encode(arn.as_bytes());
1539 format!("-----BEGIN CERTIFICATE-----\n{body}\n-----END CERTIFICATE-----\n")
1540}
1541
1542fn placeholder_chain_pem(arn: &str) -> String {
1543 let body =
1544 base64::engine::general_purpose::STANDARD.encode(format!("chain-of-{arn}").as_bytes());
1545 format!("-----BEGIN CERTIFICATE-----\n{body}\n-----END CERTIFICATE-----\n")
1546}
1547
1548fn placeholder_key_pem(arn: &str) -> String {
1549 let body = base64::engine::general_purpose::STANDARD.encode(format!("key-of-{arn}").as_bytes());
1550 format!("-----BEGIN RSA PRIVATE KEY-----\n{body}\n-----END RSA PRIVATE KEY-----\n")
1551}
1552
1553fn generate_self_signed_cert(domain_name: &str, sans: &[String]) -> Option<(String, String)> {
1560 let mut all_names: Vec<String> = vec![domain_name.to_string()];
1561 for s in sans {
1562 if !all_names.contains(s) {
1563 all_names.push(s.clone());
1564 }
1565 }
1566 let cert = rcgen::generate_simple_self_signed(all_names).ok()?;
1567 Some((cert.cert.pem(), cert.key_pair.serialize_pem()))
1568}
1569
1570fn certificate_summary_json(c: &StoredCertificate) -> Value {
1571 let mut s = json!({
1572 "CertificateArn": c.arn,
1573 "DomainName": c.domain_name,
1574 "SubjectAlternativeNameSummaries": c.subject_alternative_names,
1575 "HasAdditionalSubjectAlternativeNames": false,
1576 "Status": c.status,
1577 "Type": c.cert_type,
1578 "KeyAlgorithm": c.key_algorithm,
1579 "KeyUsages": ["DIGITAL_SIGNATURE", "KEY_ENCIPHERMENT"],
1580 "ExtendedKeyUsages": ["TLS_WEB_SERVER_AUTHENTICATION", "TLS_WEB_CLIENT_AUTHENTICATION"],
1581 "InUse": !c.in_use_by.is_empty(),
1582 "Exported": false,
1583 "RenewalEligibility": c.renewal_eligibility,
1584 "NotBefore": c.not_before.timestamp() as f64,
1585 "NotAfter": c.not_after.timestamp() as f64,
1586 "CreatedAt": c.created_at.timestamp() as f64,
1587 });
1588 if let Some(t) = c.issued_at {
1589 s.as_object_mut()
1590 .unwrap()
1591 .insert("IssuedAt".to_string(), json!(t.timestamp() as f64));
1592 }
1593 if let Some(t) = c.imported_at {
1594 s.as_object_mut()
1595 .unwrap()
1596 .insert("ImportedAt".to_string(), json!(t.timestamp() as f64));
1597 }
1598 if let Some(t) = c.revoked_at {
1599 s.as_object_mut()
1600 .unwrap()
1601 .insert("RevokedAt".to_string(), json!(t.timestamp() as f64));
1602 if let Some(r) = &c.revocation_reason {
1603 s.as_object_mut()
1604 .unwrap()
1605 .insert("RevocationReason".to_string(), json!(r));
1606 }
1607 }
1608 if let Some(m) = &c.managed_by {
1609 s.as_object_mut()
1610 .unwrap()
1611 .insert("ManagedBy".to_string(), json!(m));
1612 }
1613 s
1614}
1615
1616fn certificate_detail_json(c: &StoredCertificate) -> Value {
1617 let mut d = json!({
1618 "CertificateArn": c.arn,
1619 "DomainName": c.domain_name,
1620 "SubjectAlternativeNames": c.subject_alternative_names,
1621 "Status": c.status,
1622 "Type": c.cert_type,
1623 "Serial": c.serial,
1624 "Subject": c.subject,
1625 "Issuer": c.issuer,
1626 "KeyAlgorithm": c.key_algorithm,
1627 "SignatureAlgorithm": c.signature_algorithm,
1628 "InUseBy": c.in_use_by,
1629 "RenewalEligibility": c.renewal_eligibility,
1630 "Options": {
1631 "CertificateTransparencyLoggingPreference":
1632 c.options.certificate_transparency_logging_preference,
1633 "Export": c.options.export,
1634 },
1635 "DomainValidationOptions": c
1636 .domain_validation
1637 .iter()
1638 .map(domain_validation_json)
1639 .collect::<Vec<_>>(),
1640 "NotBefore": c.not_before.timestamp() as f64,
1641 "NotAfter": c.not_after.timestamp() as f64,
1642 "CreatedAt": c.created_at.timestamp() as f64,
1643 "KeyUsages": [{"Name": "DIGITAL_SIGNATURE"}, {"Name": "KEY_ENCIPHERMENT"}],
1644 "ExtendedKeyUsages": [
1645 {"Name": "TLS_WEB_SERVER_AUTHENTICATION", "OID": "1.3.6.1.5.5.7.3.1"},
1646 {"Name": "TLS_WEB_CLIENT_AUTHENTICATION", "OID": "1.3.6.1.5.5.7.3.2"},
1647 ],
1648 });
1649 if let Some(t) = c.issued_at {
1650 d.as_object_mut()
1651 .unwrap()
1652 .insert("IssuedAt".to_string(), json!(t.timestamp() as f64));
1653 }
1654 if let Some(t) = c.imported_at {
1655 d.as_object_mut()
1656 .unwrap()
1657 .insert("ImportedAt".to_string(), json!(t.timestamp() as f64));
1658 }
1659 if let Some(t) = c.revoked_at {
1660 d.as_object_mut()
1661 .unwrap()
1662 .insert("RevokedAt".to_string(), json!(t.timestamp() as f64));
1663 }
1664 if let Some(r) = &c.revocation_reason {
1665 d.as_object_mut()
1666 .unwrap()
1667 .insert("RevocationReason".to_string(), json!(r));
1668 }
1669 if let Some(m) = &c.managed_by {
1670 d.as_object_mut()
1671 .unwrap()
1672 .insert("ManagedBy".to_string(), json!(m));
1673 }
1674 if let Some(ca) = &c.certificate_authority_arn {
1675 d.as_object_mut()
1676 .unwrap()
1677 .insert("CertificateAuthorityArn".to_string(), json!(ca));
1678 }
1679 if let Some(fr) = &c.failure_reason {
1680 d.as_object_mut()
1681 .unwrap()
1682 .insert("FailureReason".to_string(), json!(fr));
1683 }
1684 if let Some(rs) = &c.renewal_summary {
1685 let mut summary = json!({
1686 "RenewalStatus": rs.renewal_status,
1687 "DomainValidationOptions": rs
1688 .domain_validation
1689 .iter()
1690 .map(domain_validation_json)
1691 .collect::<Vec<_>>(),
1692 "UpdatedAt": rs.updated_at.timestamp() as f64,
1693 });
1694 if let Some(reason) = &rs.renewal_status_reason {
1695 summary
1696 .as_object_mut()
1697 .unwrap()
1698 .insert("RenewalStatusReason".to_string(), json!(reason));
1699 }
1700 d.as_object_mut()
1701 .unwrap()
1702 .insert("RenewalSummary".to_string(), summary);
1703 }
1704 d
1705}
1706
1707fn certificate_search_result_json(c: &StoredCertificate) -> Value {
1708 let san_objects: Vec<Value> = c
1709 .subject_alternative_names
1710 .iter()
1711 .map(|s| json!({ "DnsName": s }))
1712 .collect();
1713 let cn = c
1714 .subject
1715 .strip_prefix("CN=")
1716 .unwrap_or(c.subject.as_str())
1717 .to_string();
1718 json!({
1719 "CertificateArn": c.arn,
1720 "X509Attributes": {
1721 "Subject": { "CommonName": cn },
1722 "Issuer": { "CommonName": c.issuer },
1723 "SubjectAlternativeNames": san_objects,
1724 "KeyAlgorithm": c.key_algorithm,
1725 "KeyUsages": ["DIGITAL_SIGNATURE", "KEY_ENCIPHERMENT"],
1726 "ExtendedKeyUsages": ["TLS_WEB_SERVER_AUTHENTICATION", "TLS_WEB_CLIENT_AUTHENTICATION"],
1727 "SerialNumber": c.serial,
1728 "NotBefore": c.not_before.timestamp() as f64,
1729 "NotAfter": c.not_after.timestamp() as f64,
1730 },
1731 "CertificateMetadata": {
1732 "AcmCertificateMetadata": {
1733 "Status": c.status,
1734 "Type": c.cert_type,
1735 "InUse": !c.in_use_by.is_empty(),
1736 "Exported": false,
1737 "RenewalEligibility": c.renewal_eligibility,
1738 "CreatedAt": c.created_at.timestamp() as f64,
1739 "ManagedBy": c.managed_by.clone().unwrap_or_default(),
1740 "ValidationMethod": c.validation_method.clone().unwrap_or_default(),
1741 },
1742 },
1743 })
1744}
1745
1746fn domain_validation_json(v: &DomainValidation) -> Value {
1747 let mut out = json!({
1748 "DomainName": v.domain_name,
1749 "ValidationStatus": v.validation_status,
1750 "ValidationMethod": v.validation_method,
1751 });
1752 if let (Some(name), Some(rtype), Some(value)) = (
1753 &v.resource_record_name,
1754 &v.resource_record_type,
1755 &v.resource_record_value,
1756 ) {
1757 out.as_object_mut().unwrap().insert(
1758 "ResourceRecord".to_string(),
1759 json!({
1760 "Name": name,
1761 "Type": rtype,
1762 "Value": value,
1763 }),
1764 );
1765 }
1766 out
1767}
1768
1769fn encrypt_private_key_pem(key_pem: &str, passphrase: &[u8]) -> Result<String, String> {
1779 use pkcs8::{pkcs5::pbes2, LineEnding, PrivateKeyInfo};
1780
1781 let der = pem_decode_private_key(key_pem)?;
1786
1787 let mut salt = [0u8; 16];
1791 let mut iv = [0u8; 16];
1792 rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut salt);
1793 rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut iv);
1794 let params = pbes2::Parameters::pbkdf2_sha256_aes256cbc(2048, &salt, &iv)
1795 .map_err(|e| format!("invalid PBES2 parameters: {e}"))?;
1796
1797 let pki = PrivateKeyInfo::try_from(der.as_slice())
1798 .map_err(|e| format!("private key is not valid PKCS#8 DER: {e}"))?;
1799 let encrypted = pki
1800 .encrypt_with_params(params, passphrase)
1801 .map_err(|e| format!("PKCS#8 encryption failed: {e}"))?;
1802
1803 encrypted
1804 .to_pem("ENCRYPTED PRIVATE KEY", LineEnding::LF)
1805 .map(|s| s.to_string())
1806 .map_err(|e| format!("PEM encoding failed: {e}"))
1807}
1808
1809fn pem_decode_private_key(pem: &str) -> Result<Vec<u8>, String> {
1813 let begin = pem.find("-----BEGIN ").ok_or("missing BEGIN line")?;
1814 let after_begin = &pem[begin + 11..];
1815 let dash = after_begin.find("-----").ok_or("malformed BEGIN line")?;
1816 let label = after_begin[..dash].trim();
1817 if label != "PRIVATE KEY" {
1818 return Err(format!(
1819 "expected `BEGIN PRIVATE KEY` (PKCS#8), got `BEGIN {label}`"
1820 ));
1821 }
1822 let end = pem
1823 .find("-----END PRIVATE KEY-----")
1824 .ok_or("missing END line")?;
1825 let body_start = begin + 11 + dash + 5;
1826 let nl = pem[body_start..]
1827 .find('\n')
1828 .ok_or("missing newline after BEGIN")?;
1829 let body: String = pem[body_start + nl + 1..end]
1830 .chars()
1831 .filter(|c| !c.is_whitespace())
1832 .collect();
1833 base64::engine::general_purpose::STANDARD
1834 .decode(body.as_bytes())
1835 .map_err(|e| format!("PEM body is not valid base64: {e}"))
1836}
1837
1838#[cfg(test)]
1839mod tests {
1840 use super::*;
1841
1842 #[test]
1843 fn generate_self_signed_cert_returns_real_pem() {
1844 let (cert_pem, key_pem) =
1845 generate_self_signed_cert("example.com", &["www.example.com".to_string()])
1846 .expect("rcgen should produce a self-signed cert");
1847 assert!(
1848 cert_pem.starts_with("-----BEGIN CERTIFICATE-----"),
1849 "expected real PEM cert, got {cert_pem:.80}"
1850 );
1851 assert!(cert_pem.ends_with("-----END CERTIFICATE-----\n"));
1852 assert!(key_pem.contains("-----BEGIN PRIVATE KEY-----"));
1853 assert!(cert_pem.len() > 400, "real cert PEM should be >400 chars");
1855 }
1856
1857 #[tokio::test]
1858 async fn request_certificate_stores_real_pem_and_key() {
1859 let svc = AcmService::default();
1860 let req = AwsRequest {
1861 service: "acm".to_string(),
1862 action: "RequestCertificate".to_string(),
1863 region: "us-east-1".to_string(),
1864 account_id: "123456789012".to_string(),
1865 request_id: "rid".to_string(),
1866 headers: http::HeaderMap::new(),
1867 query_params: std::collections::HashMap::new(),
1868 body: bytes::Bytes::from(
1869 serde_json::to_vec(&json!({"DomainName": "example.com"})).unwrap(),
1870 ),
1871 body_stream: parking_lot::Mutex::new(None),
1872 path_segments: vec![],
1873 raw_path: "/".to_string(),
1874 raw_query: String::new(),
1875 method: http::Method::POST,
1876 is_query_protocol: false,
1877 access_key_id: None,
1878 principal: None,
1879 };
1880 let resp = svc.handle(req).await.unwrap();
1881 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
1882 let arn = body["CertificateArn"].as_str().unwrap();
1883 let st = svc.state.read();
1884 let cert = st
1885 .accounts
1886 .get("123456789012")
1887 .unwrap()
1888 .certificates
1889 .get(arn)
1890 .unwrap();
1891 let cert_pem = cert.certificate_pem.as_deref().unwrap();
1892 let key_pem = cert.private_key_pem.as_deref().unwrap();
1893 assert!(cert_pem.starts_with("-----BEGIN CERTIFICATE-----"));
1894 assert!(key_pem.contains("-----BEGIN PRIVATE KEY-----"));
1895 assert!(cert_pem.len() > 400);
1896 }
1897
1898 #[test]
1899 fn encrypt_private_key_pem_emits_pkcs8_v2_pem() {
1900 let (_cert, key_pem) =
1901 generate_self_signed_cert("example.com", &[]).expect("rcgen produces a key");
1902 let out = encrypt_private_key_pem(&key_pem, b"hunter2").expect("encryption succeeds");
1903 assert!(
1904 out.starts_with("-----BEGIN ENCRYPTED PRIVATE KEY-----\n"),
1905 "expected PKCS#8 v2 envelope, got {out:.80}"
1906 );
1907 assert!(out
1908 .trim_end()
1909 .ends_with("-----END ENCRYPTED PRIVATE KEY-----"));
1910 let plain_body: String = key_pem
1913 .lines()
1914 .filter(|l| !l.starts_with("-----"))
1915 .collect();
1916 assert!(!plain_body.is_empty());
1917 assert!(!out.contains(&plain_body));
1918 }
1919
1920 #[test]
1921 fn encrypt_private_key_pem_round_trips_via_pkcs8_decoder() {
1922 use pkcs8::{DecodePrivateKey, EncodePrivateKey, LineEnding};
1923
1924 let mut rng = rand::thread_rng();
1925 let original = rsa::RsaPrivateKey::new(&mut rng, 2048).expect("rsa keygen");
1926 let key_pem = original
1927 .to_pkcs8_pem(LineEnding::LF)
1928 .expect("pkcs8 pem encode")
1929 .to_string();
1930 let passphrase = b"correct horse battery staple";
1931
1932 let encrypted = encrypt_private_key_pem(&key_pem, passphrase).unwrap();
1933 assert!(encrypted.contains("BEGIN ENCRYPTED PRIVATE KEY"));
1934
1935 let recovered = rsa::RsaPrivateKey::from_pkcs8_encrypted_pem(&encrypted, passphrase)
1936 .expect("decryption + PKCS#8 parse");
1937 assert_eq!(original, recovered);
1938 }
1939
1940 fn make_req(action: &str, body: Value) -> AwsRequest {
1941 AwsRequest {
1942 service: "acm".to_string(),
1943 action: action.to_string(),
1944 region: "us-east-1".to_string(),
1945 account_id: "123456789012".to_string(),
1946 request_id: "rid".to_string(),
1947 headers: http::HeaderMap::new(),
1948 query_params: std::collections::HashMap::new(),
1949 body: bytes::Bytes::from(serde_json::to_vec(&body).unwrap()),
1950 body_stream: parking_lot::Mutex::new(None),
1951 path_segments: vec![],
1952 raw_path: "/".to_string(),
1953 raw_query: String::new(),
1954 method: http::Method::POST,
1955 is_query_protocol: false,
1956 access_key_id: None,
1957 principal: None,
1958 }
1959 }
1960
1961 async fn make_exportable_cert(svc: &AcmService) -> String {
1962 let req = make_req(
1963 "RequestCertificate",
1964 json!({
1965 "DomainName": "example.com",
1966 "Options": {"Export": "ENABLED"},
1967 }),
1968 );
1969 let resp = svc.handle(req).await.unwrap();
1970 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
1971 body["CertificateArn"].as_str().unwrap().to_string()
1972 }
1973
1974 #[tokio::test]
1975 async fn export_certificate_emits_passphrase_encrypted_pem() {
1976 let svc = AcmService::default();
1977 let arn = make_exportable_cert(&svc).await;
1978 let original_key_pem = svc
1979 .state
1980 .read()
1981 .accounts
1982 .get("123456789012")
1983 .unwrap()
1984 .certificates
1985 .get(&arn)
1986 .unwrap()
1987 .private_key_pem
1988 .clone()
1989 .unwrap();
1990
1991 let passphrase = b"hunter2";
1992 let passphrase_b64 = base64::engine::general_purpose::STANDARD.encode(passphrase);
1993 let export = make_req(
1994 "ExportCertificate",
1995 json!({"CertificateArn": arn, "Passphrase": passphrase_b64}),
1996 );
1997 let export_resp = svc.handle(export).await.unwrap();
1998 let export_json: Value = serde_json::from_slice(export_resp.body.expect_bytes()).unwrap();
1999 let private_key = export_json["PrivateKey"].as_str().unwrap();
2000 assert!(
2001 private_key.contains("BEGIN ENCRYPTED PRIVATE KEY"),
2002 "expected PKCS#8 v2 PEM, got {private_key}"
2003 );
2004 assert!(private_key.contains("END ENCRYPTED PRIVATE KEY"));
2005 let original_inner_der =
2008 pem_decode_private_key(&original_key_pem).expect("decode original PEM");
2009 let original_pki_bytes = pkcs8::PrivateKeyInfo::try_from(original_inner_der.as_slice())
2010 .map(|p| p.private_key.to_vec())
2011 .expect("parse original PKCS#8");
2012 let encrypted_der =
2013 pem_decode_encrypted_private_key(private_key).expect("decode encrypted PEM");
2014 let decrypted_doc = pkcs8::EncryptedPrivateKeyInfo::try_from(encrypted_der.as_slice())
2015 .expect("parse encrypted PKCS#8")
2016 .decrypt(passphrase)
2017 .expect("decrypt with passphrase");
2018 let decrypted_pki = pkcs8::PrivateKeyInfo::try_from(decrypted_doc.as_bytes())
2019 .expect("parse decrypted PKCS#8");
2020 assert_eq!(decrypted_pki.private_key, original_pki_bytes.as_slice());
2021 }
2022
2023 #[tokio::test]
2024 async fn export_certificate_without_passphrase_returns_plain_pem() {
2025 let svc = AcmService::default();
2026 let arn = make_exportable_cert(&svc).await;
2027 let export_resp = svc
2028 .handle(make_req(
2029 "ExportCertificate",
2030 json!({"CertificateArn": arn}),
2031 ))
2032 .await
2033 .unwrap();
2034 let export_json: Value = serde_json::from_slice(export_resp.body.expect_bytes()).unwrap();
2035 let private_key = export_json["PrivateKey"].as_str().unwrap();
2036 assert!(
2037 private_key.contains("-----BEGIN PRIVATE KEY-----"),
2038 "expected plain PKCS#8 PEM, got {private_key}"
2039 );
2040 assert!(!private_key.contains("ENCRYPTED PRIVATE KEY"));
2041 }
2042
2043 #[tokio::test]
2044 async fn export_certificate_with_empty_passphrase_returns_plain_pem() {
2045 let svc = AcmService::default();
2046 let arn = make_exportable_cert(&svc).await;
2047 let export_resp = svc
2048 .handle(make_req(
2049 "ExportCertificate",
2050 json!({"CertificateArn": arn, "Passphrase": ""}),
2051 ))
2052 .await
2053 .unwrap();
2054 let export_json: Value = serde_json::from_slice(export_resp.body.expect_bytes()).unwrap();
2055 let private_key = export_json["PrivateKey"].as_str().unwrap();
2056 assert!(private_key.contains("-----BEGIN PRIVATE KEY-----"));
2057 assert!(!private_key.contains("ENCRYPTED PRIVATE KEY"));
2058 }
2059
2060 #[tokio::test]
2061 async fn export_certificate_rejects_non_base64_passphrase() {
2062 let svc = AcmService::default();
2063 let resp = svc
2064 .handle(make_req(
2065 "RequestCertificate",
2066 json!({"DomainName": "example.com"}),
2067 ))
2068 .await
2069 .unwrap();
2070 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2071 let arn = body["CertificateArn"].as_str().unwrap().to_string();
2072 let export = make_req(
2073 "ExportCertificate",
2074 json!({"CertificateArn": arn, "Passphrase": "not!base64!@#$"}),
2075 );
2076 let err = match svc.handle(export).await {
2077 Ok(_) => panic!("expected error for non-base64 passphrase"),
2078 Err(e) => e,
2079 };
2080 assert!(format!("{err:?}").contains("Passphrase"));
2081 }
2082
2083 fn pem_decode_encrypted_private_key(pem: &str) -> Result<Vec<u8>, String> {
2084 let begin = "-----BEGIN ENCRYPTED PRIVATE KEY-----";
2085 let end = "-----END ENCRYPTED PRIVATE KEY-----";
2086 let begin_idx = pem.find(begin).ok_or("missing BEGIN")?;
2087 let after = &pem[begin_idx + begin.len()..];
2088 let end_idx = after.find(end).ok_or("missing END")?;
2089 let body: String = after[..end_idx]
2090 .chars()
2091 .filter(|c| !c.is_whitespace())
2092 .collect();
2093 base64::engine::general_purpose::STANDARD
2094 .decode(body.as_bytes())
2095 .map_err(|e| format!("base64: {e}"))
2096 }
2097
2098 #[tokio::test]
2099 async fn describe_certificate_observes_auto_issue_transition() {
2100 let svc = AcmService::default()
2104 .with_pending_validation_delay(std::time::Duration::from_millis(50));
2105 let resp = svc
2106 .handle(make_req(
2107 "RequestCertificate",
2108 json!({"DomainName": "example.com", "ValidationMethod": "DNS"}),
2109 ))
2110 .await
2111 .unwrap();
2112 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2113 let arn = body["CertificateArn"].as_str().unwrap().to_string();
2114
2115 let early = svc
2118 .handle(make_req(
2119 "DescribeCertificate",
2120 json!({"CertificateArn": arn}),
2121 ))
2122 .await
2123 .unwrap();
2124 let early: Value = serde_json::from_slice(early.body.expect_bytes()).unwrap();
2125 assert_eq!(early["Certificate"]["Status"], "PENDING_VALIDATION");
2126 assert_eq!(early["Certificate"]["RenewalEligibility"], "INELIGIBLE");
2127 assert!(early["Certificate"].get("RenewalSummary").is_none());
2128
2129 let mut last: Value = Value::Null;
2131 for _ in 0..40 {
2132 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
2133 let resp = svc
2134 .handle(make_req(
2135 "DescribeCertificate",
2136 json!({"CertificateArn": arn}),
2137 ))
2138 .await
2139 .unwrap();
2140 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2141 if body["Certificate"]["Status"] == "ISSUED" {
2142 last = body;
2143 break;
2144 }
2145 }
2146 assert_eq!(last["Certificate"]["Status"], "ISSUED");
2147 assert_eq!(last["Certificate"]["RenewalEligibility"], "ELIGIBLE");
2148 let rs = &last["Certificate"]["RenewalSummary"];
2149 assert_eq!(rs["RenewalStatus"], "PENDING_AUTO_RENEWAL");
2150 assert!(rs["UpdatedAt"].as_f64().unwrap() > 0.0);
2151 }
2152
2153 #[tokio::test]
2154 async fn request_certificate_emits_parseable_pem() {
2155 let svc = AcmService::default();
2156 let resp = svc
2157 .handle(make_req(
2158 "RequestCertificate",
2159 json!({"DomainName": "example.com"}),
2160 ))
2161 .await
2162 .unwrap();
2163 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2164 let arn = body["CertificateArn"].as_str().unwrap().to_string();
2165
2166 let resp = svc
2167 .handle(make_req("GetCertificate", json!({"CertificateArn": arn})))
2168 .await
2169 .unwrap();
2170 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2171 let cert_pem = body["Certificate"].as_str().unwrap();
2172 assert!(
2173 cert_pem.starts_with("-----BEGIN CERTIFICATE-----"),
2174 "expected real PEM cert header, got {cert_pem:.80}"
2175 );
2176 assert!(cert_pem.trim_end().ends_with("-----END CERTIFICATE-----"));
2177 assert!(
2180 cert_pem.len() > 400,
2181 "expected real X.509 PEM, got placeholder-sized blob"
2182 );
2183 assert!(!cert_pem.contains(arn.as_str()));
2186 }
2187
2188 #[tokio::test]
2189 async fn request_certificate_includes_san() {
2190 let svc = AcmService::default();
2191 let resp = svc
2192 .handle(make_req(
2193 "RequestCertificate",
2194 json!({
2195 "DomainName": "example.com",
2196 "SubjectAlternativeNames": ["api.example.com", "www.example.com"],
2197 }),
2198 ))
2199 .await
2200 .unwrap();
2201 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2202 let arn = body["CertificateArn"].as_str().unwrap().to_string();
2203
2204 let resp = svc
2205 .handle(make_req("GetCertificate", json!({"CertificateArn": arn})))
2206 .await
2207 .unwrap();
2208 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2209 let cert_pem = body["Certificate"].as_str().unwrap();
2210 assert!(cert_pem.starts_with("-----BEGIN CERTIFICATE-----"));
2211 assert!(cert_pem.trim_end().ends_with("-----END CERTIFICATE-----"));
2212
2213 let body_b64: String = cert_pem
2217 .lines()
2218 .filter(|l| !l.starts_with("-----"))
2219 .collect::<Vec<_>>()
2220 .join("");
2221 let der = base64::engine::general_purpose::STANDARD
2222 .decode(&body_b64)
2223 .expect("cert body is valid base64");
2224 for san in ["example.com", "api.example.com", "www.example.com"] {
2225 assert!(
2226 der.windows(san.len()).any(|w| w == san.as_bytes()),
2227 "expected SAN {san} embedded in DER"
2228 );
2229 }
2230 }
2231
2232 #[tokio::test]
2233 async fn request_certificate_starts_in_pending_validation() {
2234 let svc = AcmService::default();
2235 let resp = svc
2236 .handle(make_req(
2237 "RequestCertificate",
2238 json!({"DomainName": "example.com", "ValidationMethod": "DNS"}),
2239 ))
2240 .await
2241 .unwrap();
2242 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2243 let arn = body["CertificateArn"].as_str().unwrap().to_string();
2244 let st = svc.state.read();
2245 let cert = st
2246 .accounts
2247 .get("123456789012")
2248 .unwrap()
2249 .certificates
2250 .get(&arn)
2251 .unwrap();
2252 assert_eq!(cert.status, "PENDING_VALIDATION");
2253 assert!(cert.issued_at.is_none());
2254 }
2255
2256 #[tokio::test]
2257 async fn set_certificate_status_to_issued_via_admin() {
2258 let svc = AcmService::default();
2259 let resp = svc
2260 .handle(make_req(
2261 "RequestCertificate",
2262 json!({"DomainName": "admin.example.com"}),
2263 ))
2264 .await
2265 .unwrap();
2266 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2267 let arn = body["CertificateArn"].as_str().unwrap().to_string();
2268
2269 assert!(svc.set_certificate_status(&arn, "ISSUED", None));
2270 let st = svc.state.read();
2271 let cert = st
2272 .accounts
2273 .get("123456789012")
2274 .unwrap()
2275 .certificates
2276 .get(&arn)
2277 .unwrap();
2278 assert_eq!(cert.status, "ISSUED");
2279 assert!(cert.issued_at.is_some());
2280 for dv in &cert.domain_validation {
2281 assert_eq!(dv.validation_status, "SUCCESS");
2282 }
2283 }
2284
2285 #[tokio::test]
2286 async fn set_certificate_status_unknown_arn_returns_false() {
2287 let svc = AcmService::default();
2288 assert!(!svc.set_certificate_status("does-not-exist", "ISSUED", None));
2289 }
2290
2291 #[tokio::test]
2292 async fn auto_issue_tick_transitions_to_issued() {
2293 let svc = AcmService::default()
2294 .with_pending_validation_delay(std::time::Duration::from_millis(50));
2295 let resp = svc
2296 .handle(make_req(
2297 "RequestCertificate",
2298 json!({"DomainName": "auto.example.com", "ValidationMethod": "DNS"}),
2299 ))
2300 .await
2301 .unwrap();
2302 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2303 let arn = body["CertificateArn"].as_str().unwrap().to_string();
2304
2305 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
2306
2307 let st = svc.state.read();
2308 let cert = st
2309 .accounts
2310 .get("123456789012")
2311 .unwrap()
2312 .certificates
2313 .get(&arn)
2314 .unwrap();
2315 assert_eq!(cert.status, "ISSUED");
2316 assert!(cert.issued_at.is_some());
2317 assert_eq!(cert.renewal_eligibility, "ELIGIBLE");
2318 let rs = cert.renewal_summary.as_ref().expect("renewal summary set");
2319 assert_eq!(rs.renewal_status, "PENDING_AUTO_RENEWAL");
2320 }
2321
2322 #[tokio::test]
2323 async fn email_validation_stays_pending_until_approve() {
2324 let svc = AcmService::default()
2329 .with_pending_validation_delay(std::time::Duration::from_millis(20));
2330 let resp = svc
2331 .handle(make_req(
2332 "RequestCertificate",
2333 json!({
2334 "DomainName": "email.example.com",
2335 "ValidationMethod": "EMAIL",
2336 }),
2337 ))
2338 .await
2339 .unwrap();
2340 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2341 let arn = body["CertificateArn"].as_str().unwrap().to_string();
2342
2343 tokio::time::sleep(std::time::Duration::from_millis(150)).await;
2346 {
2347 let st = svc.state.read();
2348 let cert = st
2349 .accounts
2350 .get("123456789012")
2351 .unwrap()
2352 .certificates
2353 .get(&arn)
2354 .unwrap();
2355 assert_eq!(cert.status, "PENDING_VALIDATION");
2356 assert!(cert.issued_at.is_none());
2357 assert!(cert.renewal_summary.is_none());
2358 }
2359
2360 assert!(svc.approve_certificate(&arn));
2361 let st = svc.state.read();
2362 let cert = st
2363 .accounts
2364 .get("123456789012")
2365 .unwrap()
2366 .certificates
2367 .get(&arn)
2368 .unwrap();
2369 assert_eq!(cert.status, "ISSUED");
2370 assert_eq!(cert.renewal_eligibility, "ELIGIBLE");
2371 for dv in &cert.domain_validation {
2372 assert_eq!(dv.validation_status, "SUCCESS");
2373 }
2374 assert!(cert.renewal_summary.is_some());
2375 }
2376
2377 #[tokio::test]
2378 async fn renew_certificate_refreshes_renewal_summary() {
2379 let svc = AcmService::default()
2380 .with_pending_validation_delay(std::time::Duration::from_millis(20));
2381 let resp = svc
2382 .handle(make_req(
2383 "RequestCertificate",
2384 json!({"DomainName": "renew.example.com", "ValidationMethod": "DNS"}),
2385 ))
2386 .await
2387 .unwrap();
2388 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2389 let arn = body["CertificateArn"].as_str().unwrap().to_string();
2390
2391 tokio::time::sleep(std::time::Duration::from_millis(80)).await;
2392
2393 let resp = svc
2394 .handle(make_req("RenewCertificate", json!({"CertificateArn": arn})))
2395 .await
2396 .unwrap();
2397 assert_eq!(resp.status, http::StatusCode::OK);
2398
2399 let st = svc.state.read();
2400 let cert = st
2401 .accounts
2402 .get("123456789012")
2403 .unwrap()
2404 .certificates
2405 .get(&arn)
2406 .unwrap();
2407 assert_eq!(cert.status, "ISSUED");
2408 let rs = cert.renewal_summary.as_ref().expect("renewal summary set");
2409 assert_eq!(rs.renewal_status, "SUCCESS");
2410 assert_eq!(rs.domain_validation.len(), cert.domain_validation.len());
2411 }
2412
2413 #[test]
2414 fn auto_issue_delay_default_when_env_unset() {
2415 if std::env::var("FAKECLOUD_ACM_AUTO_ISSUE_SECS").is_err() {
2420 assert_eq!(
2421 auto_issue_delay_from_env(),
2422 std::time::Duration::from_secs(DEFAULT_AUTO_ISSUE_SECS),
2423 );
2424 }
2425 }
2426
2427 #[tokio::test]
2428 async fn set_certificate_status_failed_records_reason() {
2429 let svc = AcmService::default();
2430 let resp = svc
2431 .handle(make_req(
2432 "RequestCertificate",
2433 json!({"DomainName": "fail.example.com"}),
2434 ))
2435 .await
2436 .unwrap();
2437 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2438 let arn = body["CertificateArn"].as_str().unwrap().to_string();
2439 assert!(svc.set_certificate_status(&arn, "FAILED", Some("DNS lookup error".to_string())));
2440 let st = svc.state.read();
2441 let cert = st
2442 .accounts
2443 .get("123456789012")
2444 .unwrap()
2445 .certificates
2446 .get(&arn)
2447 .unwrap();
2448 assert_eq!(cert.status, "FAILED");
2449 assert_eq!(cert.failure_reason.as_deref(), Some("DNS lookup error"));
2450 }
2451
2452 #[tokio::test]
2453 async fn request_certificate_private_key_pem_is_valid() {
2454 let svc = AcmService::default();
2455 let resp = svc
2456 .handle(make_req(
2457 "RequestCertificate",
2458 json!({"DomainName": "key.example.com"}),
2459 ))
2460 .await
2461 .unwrap();
2462 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2463 let arn = body["CertificateArn"].as_str().unwrap().to_string();
2464
2465 let st = svc.state.read();
2466 let cert = st
2467 .accounts
2468 .get("123456789012")
2469 .unwrap()
2470 .certificates
2471 .get(&arn)
2472 .unwrap();
2473 let key_pem = cert
2474 .private_key_pem
2475 .as_deref()
2476 .expect("RequestCertificate should populate a real private key");
2477 let starts_pkcs8 = key_pem.starts_with("-----BEGIN PRIVATE KEY-----");
2478 let starts_rsa = key_pem.starts_with("-----BEGIN RSA PRIVATE KEY-----");
2479 assert!(
2480 starts_pkcs8 || starts_rsa,
2481 "expected PKCS#8 or RSA private key header, got {key_pem:.80}"
2482 );
2483 assert!(
2484 key_pem.trim_end().ends_with("-----END PRIVATE KEY-----")
2485 || key_pem
2486 .trim_end()
2487 .ends_with("-----END RSA PRIVATE KEY-----")
2488 );
2489 assert!(key_pem.len() > 200);
2490 }
2491
2492 #[tokio::test]
2496 async fn search_certificates_filters_by_status_and_sorts() {
2497 let svc = AcmService::default();
2498 let mut arn: std::collections::HashMap<&str, String> = std::collections::HashMap::new();
2499 for d in ["a.example.com", "b.example.com", "c.example.com"] {
2500 let resp = svc
2501 .handle(make_req("RequestCertificate", json!({ "DomainName": d })))
2502 .await
2503 .unwrap();
2504 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2505 arn.insert(d, body["CertificateArn"].as_str().unwrap().to_string());
2506 }
2507
2508 {
2510 let mut state = svc.state.write();
2511 let certs = &mut state.accounts.get_mut("123456789012").unwrap().certificates;
2512 let now = chrono::Utc::now();
2513 for c in certs.values_mut() {
2514 match c.domain_name.as_str() {
2515 "a.example.com" => {
2516 c.status = "ISSUED".to_string();
2517 c.created_at = now - chrono::Duration::hours(2);
2518 }
2519 "b.example.com" => {
2520 c.status = "PENDING_VALIDATION".to_string();
2521 c.created_at = now - chrono::Duration::hours(1);
2522 }
2523 _ => {
2524 c.status = "ISSUED".to_string();
2525 c.created_at = now;
2526 }
2527 }
2528 }
2529 }
2530
2531 let domains = |body: &Value| -> Vec<String> {
2532 body["Results"]
2533 .as_array()
2534 .unwrap()
2535 .iter()
2536 .map(|r| r["CertificateArn"].as_str().unwrap().to_string())
2537 .collect()
2538 };
2539
2540 let resp = svc
2542 .handle(make_req(
2543 "SearchCertificates",
2544 json!({ "CertificateStatuses": ["ISSUED"] }),
2545 ))
2546 .await
2547 .unwrap();
2548 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2549 let mut issued = domains(&body);
2550 issued.sort();
2551 let mut want = vec![arn["a.example.com"].clone(), arn["c.example.com"].clone()];
2552 want.sort();
2553 assert_eq!(issued, want);
2554
2555 let resp = svc
2557 .handle(make_req(
2558 "SearchCertificates",
2559 json!({ "SortBy": "CREATED_AT", "SortOrder": "ASCENDING" }),
2560 ))
2561 .await
2562 .unwrap();
2563 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2564 assert_eq!(
2565 domains(&body),
2566 vec![
2567 arn["a.example.com"].clone(),
2568 arn["b.example.com"].clone(),
2569 arn["c.example.com"].clone(),
2570 ]
2571 );
2572
2573 let resp = svc
2575 .handle(make_req(
2576 "SearchCertificates",
2577 json!({ "SortBy": "CREATED_AT", "SortOrder": "DESCENDING" }),
2578 ))
2579 .await
2580 .unwrap();
2581 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2582 assert_eq!(
2583 domains(&body),
2584 vec![
2585 arn["c.example.com"].clone(),
2586 arn["b.example.com"].clone(),
2587 arn["a.example.com"].clone(),
2588 ]
2589 );
2590 }
2591
2592 #[tokio::test]
2593 async fn search_certificates_extended_key_usage_filter() {
2594 let svc = AcmService::default();
2598 for d in ["a.example.com", "b.example.com"] {
2599 svc.handle(make_req("RequestCertificate", json!({ "DomainName": d })))
2600 .await
2601 .unwrap();
2602 }
2603
2604 let count = |body: &Value| body["Results"].as_array().unwrap().len();
2605
2606 let resp = svc
2608 .handle(make_req(
2609 "SearchCertificates",
2610 json!({
2611 "FilterStatement": {
2612 "Filter": { "ExtendedKeyUsages": ["TLS_WEB_SERVER_AUTHENTICATION"] }
2613 }
2614 }),
2615 ))
2616 .await
2617 .unwrap();
2618 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2619 assert_eq!(count(&body), 2, "matching EKU should return all certs");
2620
2621 let resp = svc
2623 .handle(make_req(
2624 "SearchCertificates",
2625 json!({
2626 "FilterStatement": {
2627 "Filter": { "ExtendedKeyUsages": ["CODE_SIGNING"] }
2628 }
2629 }),
2630 ))
2631 .await
2632 .unwrap();
2633 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2634 assert_eq!(count(&body), 0, "non-matching EKU should drop all certs");
2635 }
2636}