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 fakecloud_aws::arn::Arn;
16use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
17
18use crate::state::{
19 AccountState, AcmAccounts, CertificateOptions, DomainValidation, RenewalSummary,
20 SharedAcmState, StoredCertificate,
21};
22
23const SUPPORTED_ACTIONS: &[&str] = &[
24 "RequestCertificate",
25 "DescribeCertificate",
26 "ListCertificates",
27 "DeleteCertificate",
28 "ImportCertificate",
29 "ExportCertificate",
30 "GetCertificate",
31 "RenewCertificate",
32 "RevokeCertificate",
33 "ResendValidationEmail",
34 "AddTagsToCertificate",
35 "RemoveTagsFromCertificate",
36 "ListTagsForCertificate",
37 "GetAccountConfiguration",
38 "PutAccountConfiguration",
39 "UpdateCertificateOptions",
40 "SearchCertificates",
41];
42
43pub struct AcmService {
44 state: SharedAcmState,
45 pending_validation_delay: std::time::Duration,
55}
56
57const DEFAULT_AUTO_ISSUE_SECS: u64 = 5;
61
62fn auto_issue_delay_from_env() -> std::time::Duration {
63 let secs = std::env::var("FAKECLOUD_ACM_AUTO_ISSUE_SECS")
64 .ok()
65 .and_then(|v| v.parse::<u64>().ok())
66 .unwrap_or(DEFAULT_AUTO_ISSUE_SECS);
67 std::time::Duration::from_secs(secs)
68}
69
70impl AcmService {
71 pub fn new(state: SharedAcmState) -> Self {
72 Self {
73 state,
74 pending_validation_delay: auto_issue_delay_from_env(),
75 }
76 }
77
78 pub fn shared_state(&self) -> SharedAcmState {
79 Arc::clone(&self.state)
80 }
81
82 pub fn with_pending_validation_delay(mut self, delay: std::time::Duration) -> Self {
86 self.pending_validation_delay = delay;
87 self
88 }
89
90 pub fn approve_certificate(&self, arn_or_id: &str) -> bool {
99 self.set_certificate_status(arn_or_id, "ISSUED", None)
100 }
101
102 pub fn chain_info(&self, arn_or_id: &str) -> Option<serde_json::Value> {
113 let state = self.state.read();
114 for account in state.accounts.values() {
115 let key = account
116 .certificates
117 .keys()
118 .find(|k| k.as_str() == arn_or_id || cert_id_from_arn(k) == arn_or_id)
119 .cloned()?;
120 if let Some(cert) = account.certificates.get(&key) {
121 let pem = cert.certificate_pem.as_deref().unwrap_or("");
122 let chain = cert.certificate_chain_pem.as_deref().unwrap_or("");
123 return Some(serde_json::json!({
124 "certificate_arn": key,
125 "certificate_pem_bytes": pem.len(),
126 "certificate_pem_blocks": pem.matches("-----BEGIN CERTIFICATE-----").count(),
127 "chain_pem_bytes": chain.len(),
128 "chain_pem_blocks": chain.matches("-----BEGIN CERTIFICATE-----").count(),
129 "external_ca_validated": false,
130 "status": cert.status,
131 "cert_type": cert.cert_type,
132 }));
133 }
134 }
135 None
136 }
137
138 pub fn set_certificate_status(
148 &self,
149 arn_or_id: &str,
150 status: &str,
151 reason: Option<String>,
152 ) -> bool {
153 let mut state = self.state.write();
154 for account in state.accounts.values_mut() {
155 let key = account
156 .certificates
157 .keys()
158 .find(|k| k.as_str() == arn_or_id || cert_id_from_arn(k) == arn_or_id)
159 .cloned();
160 if let Some(key) = key {
161 if let Some(cert) = account.certificates.get_mut(&key) {
162 cert.status = status.to_string();
163 match status {
164 "ISSUED" => {
165 let now = Utc::now();
166 cert.issued_at = Some(now);
167 for dv in cert.domain_validation.iter_mut() {
168 dv.validation_status = "SUCCESS".to_string();
169 }
170 cert.failure_reason = None;
171 if cert.cert_type == "AMAZON_ISSUED" {
175 cert.renewal_eligibility = "ELIGIBLE".to_string();
176 cert.renewal_summary = Some(RenewalSummary {
177 renewal_status: "PENDING_AUTO_RENEWAL".to_string(),
178 domain_validation: cert.domain_validation.clone(),
179 renewal_status_reason: None,
180 updated_at: now,
181 });
182 }
183 }
184 "FAILED" | "VALIDATION_TIMED_OUT" => {
185 for dv in cert.domain_validation.iter_mut() {
186 dv.validation_status = status.to_string();
187 }
188 if reason.is_some() {
189 cert.failure_reason = reason;
190 }
191 }
192 _ => {
193 if reason.is_some() {
194 cert.failure_reason = reason;
195 }
196 }
197 }
198 return true;
199 }
200 }
201 }
202 false
203 }
204}
205
206impl Default for AcmService {
207 fn default() -> Self {
208 Self::new(Arc::new(RwLock::new(AcmAccounts::new())))
209 }
210}
211
212#[async_trait]
213impl AwsService for AcmService {
214 fn service_name(&self) -> &str {
215 "acm"
216 }
217
218 fn supported_actions(&self) -> &[&str] {
219 SUPPORTED_ACTIONS
220 }
221
222 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
223 match req.action.as_str() {
224 "RequestCertificate" => self.request_certificate(&req),
225 "DescribeCertificate" => self.describe_certificate(&req),
226 "ListCertificates" => self.list_certificates(&req),
227 "DeleteCertificate" => self.delete_certificate(&req),
228 "ImportCertificate" => self.import_certificate(&req),
229 "ExportCertificate" => self.export_certificate(&req),
230 "GetCertificate" => self.get_certificate(&req),
231 "RenewCertificate" => self.renew_certificate(&req),
232 "RevokeCertificate" => self.revoke_certificate(&req),
233 "ResendValidationEmail" => self.resend_validation_email(&req),
234 "AddTagsToCertificate" => self.add_tags_to_certificate(&req),
235 "RemoveTagsFromCertificate" => self.remove_tags_from_certificate(&req),
236 "ListTagsForCertificate" => self.list_tags_for_certificate(&req),
237 "GetAccountConfiguration" => self.get_account_configuration(&req),
238 "PutAccountConfiguration" => self.put_account_configuration(&req),
239 "UpdateCertificateOptions" => self.update_certificate_options(&req),
240 "SearchCertificates" => self.search_certificates(&req),
241 other => Err(AwsServiceError::action_not_implemented("acm", other)),
242 }
243 }
244}
245
246impl AcmService {
249 fn request_certificate(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
250 let body = req.json_body();
251 let domain_name = body
252 .get("DomainName")
253 .and_then(Value::as_str)
254 .ok_or_else(|| invalid_param("DomainName is required"))?
255 .to_string();
256 if domain_name.is_empty() || domain_name.len() > 253 {
257 return Err(invalid_param("DomainName length must be between 1 and 253"));
258 }
259 let validation_method = body
260 .get("ValidationMethod")
261 .and_then(Value::as_str)
262 .unwrap_or("DNS")
263 .to_string();
264 if validation_method != "EMAIL" && validation_method != "DNS" && validation_method != "HTTP"
265 {
266 return Err(invalid_param("Invalid ValidationMethod"));
267 }
268 let sans: Vec<String> = body
269 .get("SubjectAlternativeNames")
270 .and_then(Value::as_array)
271 .map(|v| {
272 v.iter()
273 .filter_map(|s| s.as_str().map(|s| s.to_string()))
274 .collect()
275 })
276 .unwrap_or_default();
277 let key_algorithm = body
278 .get("KeyAlgorithm")
279 .and_then(Value::as_str)
280 .unwrap_or("RSA_2048")
281 .to_string();
282 const VALID_KEY_ALGORITHMS: [&str; 7] = [
283 "RSA_1024",
284 "RSA_2048",
285 "RSA_3072",
286 "RSA_4096",
287 "EC_prime256v1",
288 "EC_secp384r1",
289 "EC_secp521r1",
290 ];
291 if !VALID_KEY_ALGORITHMS.contains(&key_algorithm.as_str()) {
292 return Err(invalid_param("Invalid KeyAlgorithm"));
293 }
294 let idempotency_token = body
295 .get("IdempotencyToken")
296 .and_then(Value::as_str)
297 .map(|s| s.to_string());
298 if let Some(ref token) = idempotency_token {
299 if token.is_empty() || token.len() > 32 {
300 return Err(invalid_param(
301 "IdempotencyToken length must be between 1 and 32",
302 ));
303 }
304 if !token
305 .chars()
306 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
307 {
308 return Err(invalid_param(
309 "IdempotencyToken must match pattern ^[\\w-]+$",
310 ));
311 }
312 }
313 let managed_by = body
314 .get("ManagedBy")
315 .and_then(Value::as_str)
316 .map(|s| s.to_string());
317 if let Some(ref m) = managed_by {
318 if m != "CLOUDFRONT" {
319 return Err(invalid_param("Invalid ManagedBy"));
320 }
321 }
322 let ca_arn = body
323 .get("CertificateAuthorityArn")
324 .and_then(Value::as_str)
325 .map(|s| s.to_string());
326 if let Some(ref arn) = ca_arn {
327 if arn.len() < 20 || arn.len() > 2048 {
328 return Err(validation_error(
329 "CertificateAuthorityArn length must be between 20 and 2048",
330 ));
331 }
332 }
333 let tags = parse_tags(body.get("Tags"))?;
334 let options = parse_options(body.get("Options"));
335
336 let mut state = self.state.write();
337 let account = account_mut(&mut state, &req.account_id);
338
339 if let Some(token) = &idempotency_token {
343 if let Some(existing) = account.certificates.values().find(|c| {
344 c.idempotency_token.as_deref() == Some(token)
345 && c.domain_name == domain_name
346 && c.subject_alternative_names == effective_sans(&domain_name, &sans)
347 }) {
348 return Ok(AwsResponse::ok_json(
349 json!({ "CertificateArn": existing.arn }),
350 ));
351 }
352 }
353
354 let arn = synth_certificate_arn(&req.account_id, &req.region);
355 let now = Utc::now();
356 let effective = effective_sans(&domain_name, &sans);
357 let (cert_pem, key_pem) = generate_self_signed_cert(&domain_name, &effective)
358 .map(|(c, k)| (Some(c), Some(k)))
359 .unwrap_or((None, None));
360 let cert = StoredCertificate {
361 arn: arn.clone(),
362 domain_name: domain_name.clone(),
363 subject_alternative_names: effective,
364 status: "PENDING_VALIDATION".to_string(),
365 cert_type: "AMAZON_ISSUED".to_string(),
366 certificate_pem: cert_pem,
367 certificate_chain_pem: None,
368 private_key_pem: key_pem,
369 idempotency_token,
370 serial: synth_serial(&arn),
371 subject: format!("CN={domain_name}"),
372 issuer: "Amazon".to_string(),
373 key_algorithm,
374 signature_algorithm: "SHA256WITHRSA".to_string(),
375 created_at: now,
376 issued_at: None,
377 imported_at: None,
378 revoked_at: None,
379 revocation_reason: None,
380 not_before: now,
382 not_after: now + Duration::days(395),
383 validation_method: Some(validation_method.clone()),
384 domain_validation: synth_domain_validation(&domain_name, &sans, &validation_method),
385 options,
386 renewal_eligibility: "INELIGIBLE".to_string(),
387 managed_by,
388 certificate_authority_arn: ca_arn,
389 tags,
390 in_use_by: Vec::new(),
391 describe_read_count: 0,
392 failure_reason: None,
393 renewal_summary: None,
394 };
395 account.certificates.insert(arn.clone(), cert);
396 drop(state);
397
398 if validation_method == "DNS" {
410 let state_for_tick = Arc::clone(&self.state);
411 let arn_for_tick = arn.clone();
412 let account_for_tick = req.account_id.clone();
413 let delay = self.pending_validation_delay;
414 tokio::spawn(async move {
415 tokio::time::sleep(delay).await;
416 let mut state = state_for_tick.write();
417 if let Some(account) = state.accounts.get_mut(&account_for_tick) {
418 if let Some(cert) = account.certificates.get_mut(&arn_for_tick) {
419 if cert.status == "PENDING_VALIDATION" && cert.cert_type == "AMAZON_ISSUED"
420 {
421 let now = Utc::now();
422 cert.status = "ISSUED".to_string();
423 cert.issued_at = Some(now);
424 cert.renewal_eligibility = "ELIGIBLE".to_string();
425 for dv in cert.domain_validation.iter_mut() {
426 dv.validation_status = "SUCCESS".to_string();
427 }
428 cert.renewal_summary = Some(RenewalSummary {
429 renewal_status: "PENDING_AUTO_RENEWAL".to_string(),
430 domain_validation: cert.domain_validation.clone(),
431 renewal_status_reason: None,
432 updated_at: now,
433 });
434 }
435 }
436 }
437 });
438 }
439
440 Ok(AwsResponse::ok_json(json!({ "CertificateArn": arn })))
441 }
442
443 fn describe_certificate(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
444 let arn = require_certificate_arn(req)?;
445 let state = self.state.read();
446 let cert = state
447 .accounts
448 .get(&req.account_id)
449 .and_then(|a| a.certificates.get(&arn))
450 .ok_or_else(|| no_such_certificate(&arn))?
451 .clone();
452 Ok(AwsResponse::ok_json(json!({
459 "Certificate": certificate_detail_json(&cert),
460 })))
461 }
462
463 fn list_certificates(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
464 let body = req.json_body();
465 let max_items: usize = body
466 .get("MaxItems")
467 .and_then(Value::as_u64)
468 .map(|n| n as usize)
469 .unwrap_or(100);
470 if !(1..=1000).contains(&max_items) {
471 return Err(validation_error("MaxItems must be between 1 and 1000"));
472 }
473 let next_token = body
474 .get("NextToken")
475 .and_then(Value::as_str)
476 .map(|s| s.to_string());
477 if let Some(ref token) = next_token {
478 if token.is_empty() || token.len() > 10000 {
479 return Err(validation_error(
480 "NextToken length must be between 1 and 10000",
481 ));
482 }
483 }
484 if let Some(sort_by) = body.get("SortBy").and_then(Value::as_str) {
485 if sort_by != "CREATED_AT" {
486 return Err(validation_error("Invalid SortBy"));
487 }
488 }
489 if let Some(sort_order) = body.get("SortOrder").and_then(Value::as_str) {
490 if sort_order != "ASCENDING" && sort_order != "DESCENDING" {
491 return Err(validation_error("Invalid SortOrder"));
492 }
493 }
494 let statuses: Vec<String> = body
495 .get("CertificateStatuses")
496 .and_then(Value::as_array)
497 .map(|v| {
498 v.iter()
499 .filter_map(|s| s.as_str().map(|s| s.to_string()))
500 .collect()
501 })
502 .unwrap_or_default();
503 let includes = body.get("Includes");
504 let key_types: Vec<String> = includes
505 .and_then(|i| i.get("keyTypes"))
506 .and_then(Value::as_array)
507 .map(|v| {
508 v.iter()
509 .filter_map(|s| s.as_str().map(|s| s.to_string()))
510 .collect()
511 })
512 .unwrap_or_default();
513
514 let state = self.state.read();
515 let mut all: Vec<StoredCertificate> = state
516 .accounts
517 .get(&req.account_id)
518 .map(|a| a.certificates.values().cloned().collect())
519 .unwrap_or_default();
520 drop(state);
521 all.sort_by(|a, b| a.arn.cmp(&b.arn));
522 all.retain(|c| {
523 (statuses.is_empty() || statuses.contains(&c.status))
524 && (key_types.is_empty() || key_types.contains(&c.key_algorithm))
525 });
526
527 let start = next_token
528 .and_then(|t| t.parse::<usize>().ok())
529 .unwrap_or(0);
530 let end = (start + max_items).min(all.len());
531 let page: Vec<&StoredCertificate> = all.iter().skip(start).take(max_items).collect();
532 let next = if end < all.len() {
533 Some(end.to_string())
534 } else {
535 None
536 };
537 let mut response = json!({
538 "CertificateSummaryList": page
539 .iter()
540 .map(|c| certificate_summary_json(c))
541 .collect::<Vec<_>>(),
542 });
543 if let Some(t) = next {
544 response
545 .as_object_mut()
546 .unwrap()
547 .insert("NextToken".to_string(), Value::String(t));
548 }
549 Ok(AwsResponse::ok_json(response))
550 }
551
552 fn delete_certificate(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
553 let arn = require_certificate_arn(req)?;
554 let mut state = self.state.write();
555 let account = account_mut(&mut state, &req.account_id);
556 let cert = account
557 .certificates
558 .get(&arn)
559 .ok_or_else(|| no_such_certificate(&arn))?;
560 if !cert.in_use_by.is_empty() {
561 return Err(AwsServiceError::aws_error(
562 StatusCode::BAD_REQUEST,
563 "ResourceInUseException",
564 format!(
565 "Certificate {arn} is in use by {} resource(s)",
566 cert.in_use_by.len()
567 ),
568 ));
569 }
570 account.certificates.remove(&arn);
571 Ok(AwsResponse::ok_json(json!({})))
572 }
573
574 fn import_certificate(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
575 let body = req.json_body();
576 let cert_pem = decode_blob(body.get("Certificate"))
577 .ok_or_else(|| invalid_param("Certificate is required"))?;
578 let key_pem = decode_blob(body.get("PrivateKey"))
579 .ok_or_else(|| invalid_param("PrivateKey is required"))?;
580 let chain_pem = decode_blob(body.get("CertificateChain"));
581 let arn_in = body
582 .get("CertificateArn")
583 .and_then(Value::as_str)
584 .map(|s| s.to_string());
585 let tags = parse_tags(body.get("Tags"))?;
586
587 let domain_name = parse_cn_from_pem(&cert_pem).unwrap_or_else(|| "imported".to_string());
588 let now = Utc::now();
589 let mut state = self.state.write();
590 let account = account_mut(&mut state, &req.account_id);
591
592 let arn = match arn_in {
593 Some(existing) => {
594 let cert = account
595 .certificates
596 .get_mut(&existing)
597 .ok_or_else(|| no_such_certificate(&existing))?;
598 if cert.cert_type != "IMPORTED" {
599 return Err(invalid_param(
600 "Reimport is only supported for IMPORTED certificates",
601 ));
602 }
603 cert.certificate_pem = Some(cert_pem.clone());
604 cert.private_key_pem = Some(key_pem);
605 cert.certificate_chain_pem = chain_pem;
606 cert.imported_at = Some(now);
607 cert.not_before = now;
608 cert.not_after = now + Duration::days(395);
609 cert.subject = format!("CN={domain_name}");
610 cert.domain_name = domain_name.clone();
614 cert.subject_alternative_names = vec![domain_name.clone()];
615 if !tags.is_empty() {
616 for (k, v) in tags {
617 cert.tags.insert(k, v);
618 }
619 }
620 existing
621 }
622 None => {
623 let arn = synth_certificate_arn(&req.account_id, &req.region);
624 let cert = StoredCertificate {
625 arn: arn.clone(),
626 domain_name: domain_name.clone(),
627 subject_alternative_names: vec![domain_name.clone()],
628 status: "ISSUED".to_string(),
629 cert_type: "IMPORTED".to_string(),
630 certificate_pem: Some(cert_pem),
631 certificate_chain_pem: chain_pem,
632 private_key_pem: Some(key_pem),
633 idempotency_token: None,
634 serial: synth_serial(&arn),
635 subject: format!("CN={domain_name}"),
636 issuer: "fakecloud-imported".to_string(),
637 key_algorithm: "RSA_2048".to_string(),
638 signature_algorithm: "SHA256WITHRSA".to_string(),
639 created_at: now,
640 issued_at: Some(now),
641 imported_at: Some(now),
642 revoked_at: None,
643 revocation_reason: None,
644 not_before: now,
645 not_after: now + Duration::days(395),
646 validation_method: None,
647 domain_validation: Vec::new(),
648 options: CertificateOptions {
649 certificate_transparency_logging_preference: "ENABLED".to_string(),
650 export: "DISABLED".to_string(),
651 },
652 renewal_eligibility: "INELIGIBLE".to_string(),
653 managed_by: None,
654 certificate_authority_arn: None,
655 tags,
656 in_use_by: Vec::new(),
657 describe_read_count: 0,
658 failure_reason: None,
659 renewal_summary: None,
660 };
661 account.certificates.insert(arn.clone(), cert);
662 arn
663 }
664 };
665 Ok(AwsResponse::ok_json(json!({ "CertificateArn": arn })))
666 }
667
668 fn export_certificate(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
669 let body = req.json_body();
670 let arn = body
671 .get("CertificateArn")
672 .and_then(Value::as_str)
673 .ok_or_else(|| invalid_param("CertificateArn is required"))?
674 .to_string();
675 let passphrase_bytes = match body.get("Passphrase").and_then(Value::as_str) {
679 None | Some("") => None,
680 Some(s) => {
681 let decoded = base64::engine::general_purpose::STANDARD
682 .decode(s)
683 .map_err(|_| invalid_param("Passphrase must be valid base64"))?;
684 if decoded.is_empty() {
685 None
686 } else {
687 Some(decoded)
688 }
689 }
690 };
691 let state = self.state.read();
692 let cert = state
693 .accounts
694 .get(&req.account_id)
695 .and_then(|a| a.certificates.get(&arn))
696 .ok_or_else(|| no_such_certificate(&arn))?
697 .clone();
698 if cert.options.export != "ENABLED" && cert.cert_type != "IMPORTED" {
699 return Err(AwsServiceError::aws_error(
700 StatusCode::BAD_REQUEST,
701 "RequestInProgressException",
702 "Certificate is not exportable",
703 ));
704 }
705 let cert_pem = cert
706 .certificate_pem
707 .clone()
708 .unwrap_or_else(|| placeholder_cert_pem(&arn));
709 let chain_pem = cert
710 .certificate_chain_pem
711 .clone()
712 .unwrap_or_else(|| placeholder_chain_pem(&arn));
713 let key_pem = cert
714 .private_key_pem
715 .clone()
716 .unwrap_or_else(|| placeholder_key_pem(&arn));
717 let key_out = match passphrase_bytes {
718 Some(pp) => encrypt_private_key_pem(&key_pem, &pp)
719 .map_err(|e| invalid_param(format!("failed to encrypt private key: {e}")))?,
720 None => key_pem,
721 };
722 Ok(AwsResponse::ok_json(json!({
723 "Certificate": cert_pem,
724 "CertificateChain": chain_pem,
725 "PrivateKey": key_out,
726 })))
727 }
728
729 fn get_certificate(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
730 let arn = require_certificate_arn(req)?;
731 let state = self.state.read();
732 let cert = state
733 .accounts
734 .get(&req.account_id)
735 .and_then(|a| a.certificates.get(&arn))
736 .ok_or_else(|| no_such_certificate(&arn))?
737 .clone();
738 let cert_pem = cert
739 .certificate_pem
740 .clone()
741 .unwrap_or_else(|| placeholder_cert_pem(&arn));
742 let chain_pem = cert
743 .certificate_chain_pem
744 .clone()
745 .unwrap_or_else(|| placeholder_chain_pem(&arn));
746 Ok(AwsResponse::ok_json(json!({
747 "Certificate": cert_pem,
748 "CertificateChain": chain_pem,
749 })))
750 }
751
752 fn renew_certificate(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
753 let arn = require_certificate_arn(req)?;
754 let mut state = self.state.write();
755 let account = account_mut(&mut state, &req.account_id);
756 let cert = account
757 .certificates
758 .get_mut(&arn)
759 .ok_or_else(|| no_such_certificate(&arn))?;
760 if cert.cert_type == "IMPORTED" {
761 return Err(invalid_param(
762 "Imported certificates cannot be renewed via ACM",
763 ));
764 }
765 let now = Utc::now();
771 cert.not_before = now;
772 cert.not_after = now + Duration::days(395);
773 cert.issued_at = Some(now);
774 cert.status = "ISSUED".to_string();
775 cert.renewal_eligibility = "ELIGIBLE".to_string();
776 cert.failure_reason = None;
777 for dv in cert.domain_validation.iter_mut() {
778 dv.validation_status = "SUCCESS".to_string();
779 }
780 cert.renewal_summary = Some(RenewalSummary {
781 renewal_status: "SUCCESS".to_string(),
782 domain_validation: cert.domain_validation.clone(),
783 renewal_status_reason: None,
784 updated_at: now,
785 });
786 Ok(AwsResponse::ok_json(json!({})))
787 }
788
789 fn revoke_certificate(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
790 let body = req.json_body();
791 let arn = body
792 .get("CertificateArn")
793 .and_then(Value::as_str)
794 .ok_or_else(|| invalid_param("CertificateArn is required"))?
795 .to_string();
796 let reason = body
797 .get("RevocationReason")
798 .and_then(Value::as_str)
799 .ok_or_else(|| invalid_param("RevocationReason is required"))?
800 .to_string();
801 let mut state = self.state.write();
802 let account = account_mut(&mut state, &req.account_id);
803 let cert = account
804 .certificates
805 .get_mut(&arn)
806 .ok_or_else(|| no_such_certificate(&arn))?;
807 if cert.cert_type != "AMAZON_ISSUED" {
808 return Err(invalid_param(
809 "Only AMAZON_ISSUED certificates can be revoked",
810 ));
811 }
812 cert.status = "REVOKED".to_string();
813 cert.revoked_at = Some(Utc::now());
814 cert.revocation_reason = Some(reason);
815 Ok(AwsResponse::ok_json(json!({})))
816 }
817
818 fn resend_validation_email(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
819 let body = req.json_body();
820 let arn = body
821 .get("CertificateArn")
822 .and_then(Value::as_str)
823 .ok_or_else(|| invalid_param("CertificateArn is required"))?
824 .to_string();
825 let _ = body
826 .get("Domain")
827 .and_then(Value::as_str)
828 .ok_or_else(|| invalid_param("Domain is required"))?;
829 let _ = body
830 .get("ValidationDomain")
831 .and_then(Value::as_str)
832 .ok_or_else(|| invalid_param("ValidationDomain is required"))?;
833 let state = self.state.read();
834 let cert = state
835 .accounts
836 .get(&req.account_id)
837 .and_then(|a| a.certificates.get(&arn))
838 .ok_or_else(|| no_such_certificate(&arn))?;
839 if cert.validation_method.as_deref() != Some("EMAIL") {
840 return Err(invalid_param(
841 "Certificate is not configured for EMAIL validation",
842 ));
843 }
844 Ok(AwsResponse::ok_json(json!({})))
845 }
846
847 fn add_tags_to_certificate(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
848 let body = req.json_body();
849 let arn = body
850 .get("CertificateArn")
851 .and_then(Value::as_str)
852 .ok_or_else(|| invalid_param("CertificateArn is required"))?
853 .to_string();
854 let tags = parse_tags(body.get("Tags"))?;
855 if tags.is_empty() {
856 return Err(invalid_param("Tags must contain at least one entry"));
857 }
858 let mut state = self.state.write();
859 let account = account_mut(&mut state, &req.account_id);
860 let cert = account
861 .certificates
862 .get_mut(&arn)
863 .ok_or_else(|| no_such_certificate(&arn))?;
864 for (k, v) in tags {
865 cert.tags.insert(k, v);
866 }
867 Ok(AwsResponse::ok_json(json!({})))
868 }
869
870 fn remove_tags_from_certificate(
871 &self,
872 req: &AwsRequest,
873 ) -> Result<AwsResponse, AwsServiceError> {
874 let body = req.json_body();
875 let arn = body
876 .get("CertificateArn")
877 .and_then(Value::as_str)
878 .ok_or_else(|| invalid_param("CertificateArn is required"))?
879 .to_string();
880 let tags = parse_tags(body.get("Tags"))?;
881 if tags.is_empty() {
882 return Err(invalid_param("Tags must contain at least one entry"));
883 }
884 let mut state = self.state.write();
885 let account = account_mut(&mut state, &req.account_id);
886 let cert = account
887 .certificates
888 .get_mut(&arn)
889 .ok_or_else(|| no_such_certificate(&arn))?;
890 for (k, v) in tags {
893 if let Some(existing) = cert.tags.get(&k) {
894 if v.is_empty() || *existing == v {
895 cert.tags.remove(&k);
896 }
897 }
898 }
899 Ok(AwsResponse::ok_json(json!({})))
900 }
901
902 fn list_tags_for_certificate(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
903 let arn = require_certificate_arn(req)?;
904 let state = self.state.read();
905 let cert = state
906 .accounts
907 .get(&req.account_id)
908 .and_then(|a| a.certificates.get(&arn))
909 .ok_or_else(|| no_such_certificate(&arn))?;
910 let mut tags: Vec<(String, String)> = cert
911 .tags
912 .iter()
913 .map(|(k, v)| (k.clone(), v.clone()))
914 .collect();
915 tags.sort_by(|a, b| a.0.cmp(&b.0));
916 let tag_list: Vec<Value> = tags
917 .into_iter()
918 .map(|(k, v)| json!({ "Key": k, "Value": v }))
919 .collect();
920 Ok(AwsResponse::ok_json(json!({ "Tags": tag_list })))
921 }
922
923 fn get_account_configuration(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
924 let state = self.state.read();
925 let cfg = state
926 .accounts
927 .get(&req.account_id)
928 .map(|a| a.account_config.clone())
929 .unwrap_or_default();
930 let mut expiry = json!({});
931 if let Some(d) = cfg.expiry_events_days_before_expiry {
932 expiry
933 .as_object_mut()
934 .unwrap()
935 .insert("DaysBeforeExpiry".to_string(), json!(d));
936 }
937 Ok(AwsResponse::ok_json(json!({
938 "ExpiryEvents": expiry,
939 })))
940 }
941
942 fn put_account_configuration(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
943 let body = req.json_body();
944 let idempotency_token = body
945 .get("IdempotencyToken")
946 .and_then(Value::as_str)
947 .ok_or_else(|| invalid_param("IdempotencyToken is required"))?;
948 if idempotency_token.is_empty() || idempotency_token.len() > 32 {
949 return Err(validation_error(
950 "IdempotencyToken length must be between 1 and 32",
951 ));
952 }
953 if !idempotency_token
954 .chars()
955 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
956 {
957 return Err(validation_error(
958 "IdempotencyToken must match pattern ^[\\w-]+$",
959 ));
960 }
961 let days = body
962 .get("ExpiryEvents")
963 .and_then(|v| v.get("DaysBeforeExpiry"))
964 .and_then(Value::as_i64)
965 .map(|n| n as i32);
966 let mut state = self.state.write();
967 let account = account_mut(&mut state, &req.account_id);
968 account.account_config.expiry_events_days_before_expiry = days;
969 Ok(AwsResponse::ok_json(json!({})))
970 }
971
972 fn update_certificate_options(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
973 let body = req.json_body();
974 let arn = body
975 .get("CertificateArn")
976 .and_then(Value::as_str)
977 .ok_or_else(|| invalid_param("CertificateArn is required"))?
978 .to_string();
979 let options = body
980 .get("Options")
981 .ok_or_else(|| invalid_param("Options is required"))?;
982 let new_opts = CertificateOptions {
983 certificate_transparency_logging_preference: options
984 .get("CertificateTransparencyLoggingPreference")
985 .and_then(Value::as_str)
986 .unwrap_or("ENABLED")
987 .to_string(),
988 export: options
989 .get("Export")
990 .and_then(Value::as_str)
991 .unwrap_or("DISABLED")
992 .to_string(),
993 };
994 let mut state = self.state.write();
995 let account = account_mut(&mut state, &req.account_id);
996 let cert = account
997 .certificates
998 .get_mut(&arn)
999 .ok_or_else(|| no_such_certificate(&arn))?;
1000 cert.options = new_opts;
1001 Ok(AwsResponse::ok_json(json!({})))
1002 }
1003
1004 fn search_certificates(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1005 let body = req.json_body();
1012 let max_results: usize = body
1013 .get("MaxResults")
1014 .and_then(Value::as_u64)
1015 .map(|n| n as usize)
1016 .unwrap_or(100);
1017 if !(1..=500).contains(&max_results) {
1018 return Err(validation_error("MaxResults must be between 1 and 500"));
1019 }
1020 let next_token = body
1021 .get("NextToken")
1022 .and_then(Value::as_str)
1023 .map(|s| s.to_string());
1024 if let Some(ref token) = next_token {
1025 if token.is_empty() || token.len() > 10000 {
1026 return Err(validation_error(
1027 "NextToken length must be between 1 and 10000",
1028 ));
1029 }
1030 }
1031 if let Some(sort_by) = body.get("SortBy").and_then(Value::as_str) {
1032 const VALID_SORT_BY: [&str; 18] = [
1033 "CREATED_AT",
1034 "NOT_AFTER",
1035 "STATUS",
1036 "RENEWAL_STATUS",
1037 "EXPORTED",
1038 "IN_USE",
1039 "NOT_BEFORE",
1040 "KEY_ALGORITHM",
1041 "TYPE",
1042 "CERTIFICATE_ARN",
1043 "COMMON_NAME",
1044 "REVOKED_AT",
1045 "RENEWAL_ELIGIBILITY",
1046 "ISSUED_AT",
1047 "MANAGED_BY",
1048 "EXPORT_OPTION",
1049 "VALIDATION_METHOD",
1050 "IMPORTED_AT",
1051 ];
1052 if !VALID_SORT_BY.contains(&sort_by) {
1053 return Err(validation_error("Invalid SortBy"));
1054 }
1055 }
1056 if let Some(sort_order) = body.get("SortOrder").and_then(Value::as_str) {
1057 if sort_order != "ASCENDING" && sort_order != "DESCENDING" {
1058 return Err(validation_error("Invalid SortOrder"));
1059 }
1060 }
1061 let filter_values = |key: &str| -> Vec<String> {
1065 body.get("FilterStatement")
1066 .and_then(|f| f.get("Filter"))
1067 .and_then(|f| f.get(key))
1068 .and_then(Value::as_array)
1069 .map(|v| {
1070 v.iter()
1071 .filter_map(|s| s.as_str().map(|s| s.to_ascii_uppercase()))
1072 .collect()
1073 })
1074 .unwrap_or_default()
1075 };
1076 let key_types = filter_values("KeyTypes");
1077 let extended_key_usages = filter_values("ExtendedKeyUsages");
1083 let key_usage = filter_values("KeyUsage");
1084
1085 let statuses: Vec<String> = body
1087 .get("CertificateStatuses")
1088 .and_then(Value::as_array)
1089 .map(|v| {
1090 v.iter()
1091 .filter_map(|s| s.as_str().map(|s| s.to_ascii_uppercase()))
1092 .collect()
1093 })
1094 .unwrap_or_default();
1095
1096 let state = self.state.read();
1097 let mut all: Vec<StoredCertificate> = state
1098 .accounts
1099 .get(&req.account_id)
1100 .map(|a| a.certificates.values().cloned().collect())
1101 .unwrap_or_default();
1102 drop(state);
1103
1104 if !key_types.is_empty() {
1105 all.retain(|c| key_types.contains(&c.key_algorithm.to_ascii_uppercase()));
1106 }
1107 if !statuses.is_empty() {
1108 all.retain(|c| statuses.contains(&c.status.to_ascii_uppercase()));
1109 }
1110 const CERT_EKUS: [&str; 2] = [
1115 "TLS_WEB_SERVER_AUTHENTICATION",
1116 "TLS_WEB_CLIENT_AUTHENTICATION",
1117 ];
1118 const CERT_KEY_USAGES: [&str; 2] = ["DIGITAL_SIGNATURE", "KEY_ENCIPHERMENT"];
1119 if !extended_key_usages.is_empty() {
1120 all.retain(|_| {
1121 extended_key_usages
1122 .iter()
1123 .any(|f| f == "ANY" || CERT_EKUS.contains(&f.as_str()))
1124 });
1125 }
1126 if !key_usage.is_empty() {
1127 all.retain(|_| {
1128 key_usage
1129 .iter()
1130 .any(|f| f == "ANY" || CERT_KEY_USAGES.contains(&f.as_str()))
1131 });
1132 }
1133
1134 let sort_by = body
1137 .get("SortBy")
1138 .and_then(Value::as_str)
1139 .unwrap_or("CREATED_AT")
1140 .to_string();
1141 let descending = body
1142 .get("SortOrder")
1143 .and_then(Value::as_str)
1144 .map(|o| !o.eq_ignore_ascii_case("ASCENDING"))
1145 .unwrap_or(true);
1146 all.sort_by(|a, b| {
1147 let ord = match sort_by.as_str() {
1148 "NOT_AFTER" => a.not_after.cmp(&b.not_after),
1149 "NOT_BEFORE" => a.not_before.cmp(&b.not_before),
1150 "ISSUED_AT" => a.issued_at.cmp(&b.issued_at),
1151 "STATUS" => a.status.cmp(&b.status),
1152 "KEY_ALGORITHM" => a.key_algorithm.cmp(&b.key_algorithm),
1153 "TYPE" => a.cert_type.cmp(&b.cert_type),
1154 "CERTIFICATE_ARN" => a.arn.cmp(&b.arn),
1155 _ => a.created_at.cmp(&b.created_at),
1157 };
1158 let ord = ord.then_with(|| a.arn.cmp(&b.arn));
1160 if descending {
1161 ord.reverse()
1162 } else {
1163 ord
1164 }
1165 });
1166 let start = next_token
1167 .and_then(|t| t.parse::<usize>().ok())
1168 .unwrap_or(0);
1169 let end = (start + max_results).min(all.len());
1170 let page: Vec<&StoredCertificate> = all.iter().skip(start).take(max_results).collect();
1171 let next = if end < all.len() {
1172 Some(end.to_string())
1173 } else {
1174 None
1175 };
1176 let mut response = json!({
1177 "Results": page
1178 .iter()
1179 .map(|c| certificate_search_result_json(c))
1180 .collect::<Vec<_>>(),
1181 });
1182 if let Some(t) = next {
1183 response
1184 .as_object_mut()
1185 .unwrap()
1186 .insert("NextToken".to_string(), Value::String(t));
1187 }
1188 Ok(AwsResponse::ok_json(response))
1189 }
1190}
1191
1192fn account_mut<'a>(state: &'a mut AcmAccounts, account_id: &str) -> &'a mut AccountState {
1195 state.accounts.entry(account_id.to_string()).or_default()
1196}
1197
1198fn require_certificate_arn(req: &AwsRequest) -> Result<String, AwsServiceError> {
1199 req.json_body()
1200 .get("CertificateArn")
1201 .and_then(Value::as_str)
1202 .map(|s| s.to_string())
1203 .ok_or_else(|| invalid_param("CertificateArn is required"))
1204}
1205
1206fn invalid_param(msg: impl Into<String>) -> AwsServiceError {
1207 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "InvalidParameterException", msg)
1208}
1209
1210fn validation_error(msg: impl Into<String>) -> AwsServiceError {
1211 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "ValidationException", msg)
1212}
1213
1214fn no_such_certificate(arn: &str) -> AwsServiceError {
1215 AwsServiceError::aws_error(
1216 StatusCode::BAD_REQUEST,
1217 "ResourceNotFoundException",
1218 format!("Could not find certificate with arn {arn}"),
1219 )
1220}
1221
1222fn synth_certificate_arn(account_id: &str, region: &str) -> String {
1223 let region = if region.is_empty() {
1224 "us-east-1"
1225 } else {
1226 region
1227 };
1228 let id = Uuid::new_v4();
1229 Arn::new("acm", region, account_id, &format!("certificate/{id}")).to_string()
1230}
1231
1232fn cert_id_from_arn(arn: &str) -> &str {
1239 arn.rsplit_once("certificate/")
1240 .map(|(_, id)| id)
1241 .unwrap_or(arn)
1242}
1243
1244fn synth_serial(arn: &str) -> String {
1245 let mut hasher = Sha256::new();
1246 hasher.update(arn.as_bytes());
1247 let digest = hasher.finalize();
1248 hex::encode(&digest[..16])
1249}
1250
1251fn parse_tags(value: Option<&Value>) -> Result<BTreeMap<String, String>, AwsServiceError> {
1252 let mut out = BTreeMap::new();
1253 let Some(arr) = value.and_then(Value::as_array) else {
1254 return Ok(out);
1255 };
1256 for tag in arr {
1257 let key = tag
1258 .get("Key")
1259 .and_then(Value::as_str)
1260 .ok_or_else(|| invalid_param("Tag.Key is required"))?
1261 .to_string();
1262 let value = tag
1263 .get("Value")
1264 .and_then(Value::as_str)
1265 .unwrap_or_default()
1266 .to_string();
1267 out.insert(key, value);
1268 }
1269 Ok(out)
1270}
1271
1272fn parse_options(value: Option<&Value>) -> CertificateOptions {
1273 let v = match value {
1274 Some(v) => v,
1275 None => {
1276 return CertificateOptions {
1277 certificate_transparency_logging_preference: "ENABLED".to_string(),
1278 export: "DISABLED".to_string(),
1279 };
1280 }
1281 };
1282 CertificateOptions {
1283 certificate_transparency_logging_preference: v
1284 .get("CertificateTransparencyLoggingPreference")
1285 .and_then(Value::as_str)
1286 .unwrap_or("ENABLED")
1287 .to_string(),
1288 export: v
1289 .get("Export")
1290 .and_then(Value::as_str)
1291 .unwrap_or("DISABLED")
1292 .to_string(),
1293 }
1294}
1295
1296fn effective_sans(domain: &str, extras: &[String]) -> Vec<String> {
1300 let mut all = vec![domain.to_string()];
1301 for s in extras {
1302 if !all.contains(s) {
1303 all.push(s.clone());
1304 }
1305 }
1306 all
1307}
1308
1309fn synth_domain_validation(domain: &str, sans: &[String], method: &str) -> Vec<DomainValidation> {
1310 effective_sans(domain, sans)
1311 .iter()
1312 .map(|d| {
1313 if method == "DNS" {
1314 let token = synth_dns_token(d);
1315 DomainValidation {
1316 domain_name: d.clone(),
1317 validation_status: "PENDING_VALIDATION".to_string(),
1318 validation_method: "DNS".to_string(),
1319 resource_record_name: Some(format!("_{token}.{d}.")),
1320 resource_record_type: Some("CNAME".to_string()),
1321 resource_record_value: Some(format!("_{token}.acm-validations.aws.")),
1322 }
1323 } else {
1324 DomainValidation {
1325 domain_name: d.clone(),
1326 validation_status: "PENDING_VALIDATION".to_string(),
1327 validation_method: "EMAIL".to_string(),
1328 resource_record_name: None,
1329 resource_record_type: None,
1330 resource_record_value: None,
1331 }
1332 }
1333 })
1334 .collect()
1335}
1336
1337fn synth_dns_token(domain: &str) -> String {
1340 let mut hasher = Sha256::new();
1341 hasher.update(domain.as_bytes());
1342 let digest = hasher.finalize();
1343 hex::encode(&digest[..16])
1344}
1345
1346fn decode_blob(value: Option<&Value>) -> Option<String> {
1347 let v = value?;
1348 if let Some(s) = v.as_str() {
1349 if let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(s) {
1353 if let Ok(text) = String::from_utf8(bytes) {
1354 return Some(text);
1355 }
1356 }
1357 return Some(s.to_string());
1358 }
1359 None
1360}
1361
1362fn parse_cn_from_pem(pem: &str) -> Option<String> {
1367 pem.lines()
1368 .find_map(|line| line.split("CN=").nth(1))
1369 .map(|rest| {
1370 rest.split(['/', ',', '\n', ' '])
1371 .next()
1372 .unwrap_or("")
1373 .to_string()
1374 })
1375 .filter(|s| !s.is_empty())
1376}
1377
1378fn placeholder_cert_pem(arn: &str) -> String {
1379 let body = base64::engine::general_purpose::STANDARD.encode(arn.as_bytes());
1383 format!("-----BEGIN CERTIFICATE-----\n{body}\n-----END CERTIFICATE-----\n")
1384}
1385
1386fn placeholder_chain_pem(arn: &str) -> String {
1387 let body =
1388 base64::engine::general_purpose::STANDARD.encode(format!("chain-of-{arn}").as_bytes());
1389 format!("-----BEGIN CERTIFICATE-----\n{body}\n-----END CERTIFICATE-----\n")
1390}
1391
1392fn placeholder_key_pem(arn: &str) -> String {
1393 let body = base64::engine::general_purpose::STANDARD.encode(format!("key-of-{arn}").as_bytes());
1394 format!("-----BEGIN RSA PRIVATE KEY-----\n{body}\n-----END RSA PRIVATE KEY-----\n")
1395}
1396
1397fn generate_self_signed_cert(domain_name: &str, sans: &[String]) -> Option<(String, String)> {
1404 let mut all_names: Vec<String> = vec![domain_name.to_string()];
1405 for s in sans {
1406 if !all_names.contains(s) {
1407 all_names.push(s.clone());
1408 }
1409 }
1410 let cert = rcgen::generate_simple_self_signed(all_names).ok()?;
1411 Some((cert.cert.pem(), cert.key_pair.serialize_pem()))
1412}
1413
1414fn certificate_summary_json(c: &StoredCertificate) -> Value {
1415 let mut s = json!({
1416 "CertificateArn": c.arn,
1417 "DomainName": c.domain_name,
1418 "SubjectAlternativeNameSummaries": c.subject_alternative_names,
1419 "HasAdditionalSubjectAlternativeNames": false,
1420 "Status": c.status,
1421 "Type": c.cert_type,
1422 "KeyAlgorithm": c.key_algorithm,
1423 "KeyUsages": ["DIGITAL_SIGNATURE", "KEY_ENCIPHERMENT"],
1424 "ExtendedKeyUsages": ["TLS_WEB_SERVER_AUTHENTICATION", "TLS_WEB_CLIENT_AUTHENTICATION"],
1425 "InUse": !c.in_use_by.is_empty(),
1426 "Exported": false,
1427 "RenewalEligibility": c.renewal_eligibility,
1428 "NotBefore": c.not_before.timestamp() as f64,
1429 "NotAfter": c.not_after.timestamp() as f64,
1430 "CreatedAt": c.created_at.timestamp() as f64,
1431 });
1432 if let Some(t) = c.issued_at {
1433 s.as_object_mut()
1434 .unwrap()
1435 .insert("IssuedAt".to_string(), json!(t.timestamp() as f64));
1436 }
1437 if let Some(t) = c.imported_at {
1438 s.as_object_mut()
1439 .unwrap()
1440 .insert("ImportedAt".to_string(), json!(t.timestamp() as f64));
1441 }
1442 if let Some(t) = c.revoked_at {
1443 s.as_object_mut()
1444 .unwrap()
1445 .insert("RevokedAt".to_string(), json!(t.timestamp() as f64));
1446 if let Some(r) = &c.revocation_reason {
1447 s.as_object_mut()
1448 .unwrap()
1449 .insert("RevocationReason".to_string(), json!(r));
1450 }
1451 }
1452 if let Some(m) = &c.managed_by {
1453 s.as_object_mut()
1454 .unwrap()
1455 .insert("ManagedBy".to_string(), json!(m));
1456 }
1457 s
1458}
1459
1460fn certificate_detail_json(c: &StoredCertificate) -> Value {
1461 let mut d = json!({
1462 "CertificateArn": c.arn,
1463 "DomainName": c.domain_name,
1464 "SubjectAlternativeNames": c.subject_alternative_names,
1465 "Status": c.status,
1466 "Type": c.cert_type,
1467 "Serial": c.serial,
1468 "Subject": c.subject,
1469 "Issuer": c.issuer,
1470 "KeyAlgorithm": c.key_algorithm,
1471 "SignatureAlgorithm": c.signature_algorithm,
1472 "InUseBy": c.in_use_by,
1473 "RenewalEligibility": c.renewal_eligibility,
1474 "Options": {
1475 "CertificateTransparencyLoggingPreference":
1476 c.options.certificate_transparency_logging_preference,
1477 "Export": c.options.export,
1478 },
1479 "DomainValidationOptions": c
1480 .domain_validation
1481 .iter()
1482 .map(domain_validation_json)
1483 .collect::<Vec<_>>(),
1484 "NotBefore": c.not_before.timestamp() as f64,
1485 "NotAfter": c.not_after.timestamp() as f64,
1486 "CreatedAt": c.created_at.timestamp() as f64,
1487 "KeyUsages": [{"Name": "DIGITAL_SIGNATURE"}, {"Name": "KEY_ENCIPHERMENT"}],
1488 "ExtendedKeyUsages": [
1489 {"Name": "TLS_WEB_SERVER_AUTHENTICATION", "OID": "1.3.6.1.5.5.7.3.1"},
1490 {"Name": "TLS_WEB_CLIENT_AUTHENTICATION", "OID": "1.3.6.1.5.5.7.3.2"},
1491 ],
1492 });
1493 if let Some(t) = c.issued_at {
1494 d.as_object_mut()
1495 .unwrap()
1496 .insert("IssuedAt".to_string(), json!(t.timestamp() as f64));
1497 }
1498 if let Some(t) = c.imported_at {
1499 d.as_object_mut()
1500 .unwrap()
1501 .insert("ImportedAt".to_string(), json!(t.timestamp() as f64));
1502 }
1503 if let Some(t) = c.revoked_at {
1504 d.as_object_mut()
1505 .unwrap()
1506 .insert("RevokedAt".to_string(), json!(t.timestamp() as f64));
1507 }
1508 if let Some(r) = &c.revocation_reason {
1509 d.as_object_mut()
1510 .unwrap()
1511 .insert("RevocationReason".to_string(), json!(r));
1512 }
1513 if let Some(m) = &c.managed_by {
1514 d.as_object_mut()
1515 .unwrap()
1516 .insert("ManagedBy".to_string(), json!(m));
1517 }
1518 if let Some(ca) = &c.certificate_authority_arn {
1519 d.as_object_mut()
1520 .unwrap()
1521 .insert("CertificateAuthorityArn".to_string(), json!(ca));
1522 }
1523 if let Some(fr) = &c.failure_reason {
1524 d.as_object_mut()
1525 .unwrap()
1526 .insert("FailureReason".to_string(), json!(fr));
1527 }
1528 if let Some(rs) = &c.renewal_summary {
1529 let mut summary = json!({
1530 "RenewalStatus": rs.renewal_status,
1531 "DomainValidationOptions": rs
1532 .domain_validation
1533 .iter()
1534 .map(domain_validation_json)
1535 .collect::<Vec<_>>(),
1536 "UpdatedAt": rs.updated_at.timestamp() as f64,
1537 });
1538 if let Some(reason) = &rs.renewal_status_reason {
1539 summary
1540 .as_object_mut()
1541 .unwrap()
1542 .insert("RenewalStatusReason".to_string(), json!(reason));
1543 }
1544 d.as_object_mut()
1545 .unwrap()
1546 .insert("RenewalSummary".to_string(), summary);
1547 }
1548 d
1549}
1550
1551fn certificate_search_result_json(c: &StoredCertificate) -> Value {
1552 let san_objects: Vec<Value> = c
1553 .subject_alternative_names
1554 .iter()
1555 .map(|s| json!({ "DnsName": s }))
1556 .collect();
1557 let cn = c
1558 .subject
1559 .strip_prefix("CN=")
1560 .unwrap_or(c.subject.as_str())
1561 .to_string();
1562 json!({
1563 "CertificateArn": c.arn,
1564 "X509Attributes": {
1565 "Subject": { "CommonName": cn },
1566 "Issuer": { "CommonName": c.issuer },
1567 "SubjectAlternativeNames": san_objects,
1568 "KeyAlgorithm": c.key_algorithm,
1569 "KeyUsages": ["DIGITAL_SIGNATURE", "KEY_ENCIPHERMENT"],
1570 "ExtendedKeyUsages": ["TLS_WEB_SERVER_AUTHENTICATION", "TLS_WEB_CLIENT_AUTHENTICATION"],
1571 "SerialNumber": c.serial,
1572 "NotBefore": c.not_before.timestamp() as f64,
1573 "NotAfter": c.not_after.timestamp() as f64,
1574 },
1575 "CertificateMetadata": {
1576 "AcmCertificateMetadata": {
1577 "Status": c.status,
1578 "Type": c.cert_type,
1579 "InUse": !c.in_use_by.is_empty(),
1580 "Exported": false,
1581 "RenewalEligibility": c.renewal_eligibility,
1582 "CreatedAt": c.created_at.timestamp() as f64,
1583 "ManagedBy": c.managed_by.clone().unwrap_or_default(),
1584 "ValidationMethod": c.validation_method.clone().unwrap_or_default(),
1585 },
1586 },
1587 })
1588}
1589
1590fn domain_validation_json(v: &DomainValidation) -> Value {
1591 let mut out = json!({
1592 "DomainName": v.domain_name,
1593 "ValidationStatus": v.validation_status,
1594 "ValidationMethod": v.validation_method,
1595 });
1596 if let (Some(name), Some(rtype), Some(value)) = (
1597 &v.resource_record_name,
1598 &v.resource_record_type,
1599 &v.resource_record_value,
1600 ) {
1601 out.as_object_mut().unwrap().insert(
1602 "ResourceRecord".to_string(),
1603 json!({
1604 "Name": name,
1605 "Type": rtype,
1606 "Value": value,
1607 }),
1608 );
1609 }
1610 out
1611}
1612
1613fn encrypt_private_key_pem(key_pem: &str, passphrase: &[u8]) -> Result<String, String> {
1623 use pkcs8::{pkcs5::pbes2, LineEnding, PrivateKeyInfo};
1624
1625 let der = pem_decode_private_key(key_pem)?;
1630
1631 let mut salt = [0u8; 16];
1635 let mut iv = [0u8; 16];
1636 rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut salt);
1637 rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut iv);
1638 let params = pbes2::Parameters::pbkdf2_sha256_aes256cbc(2048, &salt, &iv)
1639 .map_err(|e| format!("invalid PBES2 parameters: {e}"))?;
1640
1641 let pki = PrivateKeyInfo::try_from(der.as_slice())
1642 .map_err(|e| format!("private key is not valid PKCS#8 DER: {e}"))?;
1643 let encrypted = pki
1644 .encrypt_with_params(params, passphrase)
1645 .map_err(|e| format!("PKCS#8 encryption failed: {e}"))?;
1646
1647 encrypted
1648 .to_pem("ENCRYPTED PRIVATE KEY", LineEnding::LF)
1649 .map(|s| s.to_string())
1650 .map_err(|e| format!("PEM encoding failed: {e}"))
1651}
1652
1653fn pem_decode_private_key(pem: &str) -> Result<Vec<u8>, String> {
1657 let begin = pem.find("-----BEGIN ").ok_or("missing BEGIN line")?;
1658 let after_begin = &pem[begin + 11..];
1659 let dash = after_begin.find("-----").ok_or("malformed BEGIN line")?;
1660 let label = after_begin[..dash].trim();
1661 if label != "PRIVATE KEY" {
1662 return Err(format!(
1663 "expected `BEGIN PRIVATE KEY` (PKCS#8), got `BEGIN {label}`"
1664 ));
1665 }
1666 let end = pem
1667 .find("-----END PRIVATE KEY-----")
1668 .ok_or("missing END line")?;
1669 let body_start = begin + 11 + dash + 5;
1670 let nl = pem[body_start..]
1671 .find('\n')
1672 .ok_or("missing newline after BEGIN")?;
1673 let body: String = pem[body_start + nl + 1..end]
1674 .chars()
1675 .filter(|c| !c.is_whitespace())
1676 .collect();
1677 base64::engine::general_purpose::STANDARD
1678 .decode(body.as_bytes())
1679 .map_err(|e| format!("PEM body is not valid base64: {e}"))
1680}
1681
1682#[cfg(test)]
1683mod tests {
1684 use super::*;
1685
1686 #[test]
1687 fn generate_self_signed_cert_returns_real_pem() {
1688 let (cert_pem, key_pem) =
1689 generate_self_signed_cert("example.com", &["www.example.com".to_string()])
1690 .expect("rcgen should produce a self-signed cert");
1691 assert!(
1692 cert_pem.starts_with("-----BEGIN CERTIFICATE-----"),
1693 "expected real PEM cert, got {cert_pem:.80}"
1694 );
1695 assert!(cert_pem.ends_with("-----END CERTIFICATE-----\n"));
1696 assert!(key_pem.contains("-----BEGIN PRIVATE KEY-----"));
1697 assert!(cert_pem.len() > 400, "real cert PEM should be >400 chars");
1699 }
1700
1701 #[tokio::test]
1702 async fn request_certificate_stores_real_pem_and_key() {
1703 let svc = AcmService::default();
1704 let req = AwsRequest {
1705 service: "acm".to_string(),
1706 action: "RequestCertificate".to_string(),
1707 region: "us-east-1".to_string(),
1708 account_id: "123456789012".to_string(),
1709 request_id: "rid".to_string(),
1710 headers: http::HeaderMap::new(),
1711 query_params: std::collections::HashMap::new(),
1712 body: bytes::Bytes::from(
1713 serde_json::to_vec(&json!({"DomainName": "example.com"})).unwrap(),
1714 ),
1715 body_stream: parking_lot::Mutex::new(None),
1716 path_segments: vec![],
1717 raw_path: "/".to_string(),
1718 raw_query: String::new(),
1719 method: http::Method::POST,
1720 is_query_protocol: false,
1721 access_key_id: None,
1722 principal: None,
1723 };
1724 let resp = svc.handle(req).await.unwrap();
1725 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
1726 let arn = body["CertificateArn"].as_str().unwrap();
1727 let st = svc.state.read();
1728 let cert = st
1729 .accounts
1730 .get("123456789012")
1731 .unwrap()
1732 .certificates
1733 .get(arn)
1734 .unwrap();
1735 let cert_pem = cert.certificate_pem.as_deref().unwrap();
1736 let key_pem = cert.private_key_pem.as_deref().unwrap();
1737 assert!(cert_pem.starts_with("-----BEGIN CERTIFICATE-----"));
1738 assert!(key_pem.contains("-----BEGIN PRIVATE KEY-----"));
1739 assert!(cert_pem.len() > 400);
1740 }
1741
1742 #[test]
1743 fn encrypt_private_key_pem_emits_pkcs8_v2_pem() {
1744 let (_cert, key_pem) =
1745 generate_self_signed_cert("example.com", &[]).expect("rcgen produces a key");
1746 let out = encrypt_private_key_pem(&key_pem, b"hunter2").expect("encryption succeeds");
1747 assert!(
1748 out.starts_with("-----BEGIN ENCRYPTED PRIVATE KEY-----\n"),
1749 "expected PKCS#8 v2 envelope, got {out:.80}"
1750 );
1751 assert!(out
1752 .trim_end()
1753 .ends_with("-----END ENCRYPTED PRIVATE KEY-----"));
1754 let plain_body: String = key_pem
1757 .lines()
1758 .filter(|l| !l.starts_with("-----"))
1759 .collect();
1760 assert!(!plain_body.is_empty());
1761 assert!(!out.contains(&plain_body));
1762 }
1763
1764 #[test]
1765 fn encrypt_private_key_pem_round_trips_via_pkcs8_decoder() {
1766 use pkcs8::{DecodePrivateKey, EncodePrivateKey, LineEnding};
1767
1768 let mut rng = rand::thread_rng();
1769 let original = rsa::RsaPrivateKey::new(&mut rng, 2048).expect("rsa keygen");
1770 let key_pem = original
1771 .to_pkcs8_pem(LineEnding::LF)
1772 .expect("pkcs8 pem encode")
1773 .to_string();
1774 let passphrase = b"correct horse battery staple";
1775
1776 let encrypted = encrypt_private_key_pem(&key_pem, passphrase).unwrap();
1777 assert!(encrypted.contains("BEGIN ENCRYPTED PRIVATE KEY"));
1778
1779 let recovered = rsa::RsaPrivateKey::from_pkcs8_encrypted_pem(&encrypted, passphrase)
1780 .expect("decryption + PKCS#8 parse");
1781 assert_eq!(original, recovered);
1782 }
1783
1784 fn make_req(action: &str, body: Value) -> AwsRequest {
1785 AwsRequest {
1786 service: "acm".to_string(),
1787 action: action.to_string(),
1788 region: "us-east-1".to_string(),
1789 account_id: "123456789012".to_string(),
1790 request_id: "rid".to_string(),
1791 headers: http::HeaderMap::new(),
1792 query_params: std::collections::HashMap::new(),
1793 body: bytes::Bytes::from(serde_json::to_vec(&body).unwrap()),
1794 body_stream: parking_lot::Mutex::new(None),
1795 path_segments: vec![],
1796 raw_path: "/".to_string(),
1797 raw_query: String::new(),
1798 method: http::Method::POST,
1799 is_query_protocol: false,
1800 access_key_id: None,
1801 principal: None,
1802 }
1803 }
1804
1805 async fn make_exportable_cert(svc: &AcmService) -> String {
1806 let req = make_req(
1807 "RequestCertificate",
1808 json!({
1809 "DomainName": "example.com",
1810 "Options": {"Export": "ENABLED"},
1811 }),
1812 );
1813 let resp = svc.handle(req).await.unwrap();
1814 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
1815 body["CertificateArn"].as_str().unwrap().to_string()
1816 }
1817
1818 #[tokio::test]
1819 async fn export_certificate_emits_passphrase_encrypted_pem() {
1820 let svc = AcmService::default();
1821 let arn = make_exportable_cert(&svc).await;
1822 let original_key_pem = svc
1823 .state
1824 .read()
1825 .accounts
1826 .get("123456789012")
1827 .unwrap()
1828 .certificates
1829 .get(&arn)
1830 .unwrap()
1831 .private_key_pem
1832 .clone()
1833 .unwrap();
1834
1835 let passphrase = b"hunter2";
1836 let passphrase_b64 = base64::engine::general_purpose::STANDARD.encode(passphrase);
1837 let export = make_req(
1838 "ExportCertificate",
1839 json!({"CertificateArn": arn, "Passphrase": passphrase_b64}),
1840 );
1841 let export_resp = svc.handle(export).await.unwrap();
1842 let export_json: Value = serde_json::from_slice(export_resp.body.expect_bytes()).unwrap();
1843 let private_key = export_json["PrivateKey"].as_str().unwrap();
1844 assert!(
1845 private_key.contains("BEGIN ENCRYPTED PRIVATE KEY"),
1846 "expected PKCS#8 v2 PEM, got {private_key}"
1847 );
1848 assert!(private_key.contains("END ENCRYPTED PRIVATE KEY"));
1849 let original_inner_der =
1852 pem_decode_private_key(&original_key_pem).expect("decode original PEM");
1853 let original_pki_bytes = pkcs8::PrivateKeyInfo::try_from(original_inner_der.as_slice())
1854 .map(|p| p.private_key.to_vec())
1855 .expect("parse original PKCS#8");
1856 let encrypted_der =
1857 pem_decode_encrypted_private_key(private_key).expect("decode encrypted PEM");
1858 let decrypted_doc = pkcs8::EncryptedPrivateKeyInfo::try_from(encrypted_der.as_slice())
1859 .expect("parse encrypted PKCS#8")
1860 .decrypt(passphrase)
1861 .expect("decrypt with passphrase");
1862 let decrypted_pki = pkcs8::PrivateKeyInfo::try_from(decrypted_doc.as_bytes())
1863 .expect("parse decrypted PKCS#8");
1864 assert_eq!(decrypted_pki.private_key, original_pki_bytes.as_slice());
1865 }
1866
1867 #[tokio::test]
1868 async fn export_certificate_without_passphrase_returns_plain_pem() {
1869 let svc = AcmService::default();
1870 let arn = make_exportable_cert(&svc).await;
1871 let export_resp = svc
1872 .handle(make_req(
1873 "ExportCertificate",
1874 json!({"CertificateArn": arn}),
1875 ))
1876 .await
1877 .unwrap();
1878 let export_json: Value = serde_json::from_slice(export_resp.body.expect_bytes()).unwrap();
1879 let private_key = export_json["PrivateKey"].as_str().unwrap();
1880 assert!(
1881 private_key.contains("-----BEGIN PRIVATE KEY-----"),
1882 "expected plain PKCS#8 PEM, got {private_key}"
1883 );
1884 assert!(!private_key.contains("ENCRYPTED PRIVATE KEY"));
1885 }
1886
1887 #[tokio::test]
1888 async fn export_certificate_with_empty_passphrase_returns_plain_pem() {
1889 let svc = AcmService::default();
1890 let arn = make_exportable_cert(&svc).await;
1891 let export_resp = svc
1892 .handle(make_req(
1893 "ExportCertificate",
1894 json!({"CertificateArn": arn, "Passphrase": ""}),
1895 ))
1896 .await
1897 .unwrap();
1898 let export_json: Value = serde_json::from_slice(export_resp.body.expect_bytes()).unwrap();
1899 let private_key = export_json["PrivateKey"].as_str().unwrap();
1900 assert!(private_key.contains("-----BEGIN PRIVATE KEY-----"));
1901 assert!(!private_key.contains("ENCRYPTED PRIVATE KEY"));
1902 }
1903
1904 #[tokio::test]
1905 async fn export_certificate_rejects_non_base64_passphrase() {
1906 let svc = AcmService::default();
1907 let resp = svc
1908 .handle(make_req(
1909 "RequestCertificate",
1910 json!({"DomainName": "example.com"}),
1911 ))
1912 .await
1913 .unwrap();
1914 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
1915 let arn = body["CertificateArn"].as_str().unwrap().to_string();
1916 let export = make_req(
1917 "ExportCertificate",
1918 json!({"CertificateArn": arn, "Passphrase": "not!base64!@#$"}),
1919 );
1920 let err = match svc.handle(export).await {
1921 Ok(_) => panic!("expected error for non-base64 passphrase"),
1922 Err(e) => e,
1923 };
1924 assert!(format!("{err:?}").contains("Passphrase"));
1925 }
1926
1927 fn pem_decode_encrypted_private_key(pem: &str) -> Result<Vec<u8>, String> {
1928 let begin = "-----BEGIN ENCRYPTED PRIVATE KEY-----";
1929 let end = "-----END ENCRYPTED PRIVATE KEY-----";
1930 let begin_idx = pem.find(begin).ok_or("missing BEGIN")?;
1931 let after = &pem[begin_idx + begin.len()..];
1932 let end_idx = after.find(end).ok_or("missing END")?;
1933 let body: String = after[..end_idx]
1934 .chars()
1935 .filter(|c| !c.is_whitespace())
1936 .collect();
1937 base64::engine::general_purpose::STANDARD
1938 .decode(body.as_bytes())
1939 .map_err(|e| format!("base64: {e}"))
1940 }
1941
1942 #[tokio::test]
1943 async fn describe_certificate_observes_auto_issue_transition() {
1944 let svc = AcmService::default()
1948 .with_pending_validation_delay(std::time::Duration::from_millis(50));
1949 let resp = svc
1950 .handle(make_req(
1951 "RequestCertificate",
1952 json!({"DomainName": "example.com", "ValidationMethod": "DNS"}),
1953 ))
1954 .await
1955 .unwrap();
1956 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
1957 let arn = body["CertificateArn"].as_str().unwrap().to_string();
1958
1959 let early = svc
1962 .handle(make_req(
1963 "DescribeCertificate",
1964 json!({"CertificateArn": arn}),
1965 ))
1966 .await
1967 .unwrap();
1968 let early: Value = serde_json::from_slice(early.body.expect_bytes()).unwrap();
1969 assert_eq!(early["Certificate"]["Status"], "PENDING_VALIDATION");
1970 assert_eq!(early["Certificate"]["RenewalEligibility"], "INELIGIBLE");
1971 assert!(early["Certificate"].get("RenewalSummary").is_none());
1972
1973 let mut last: Value = Value::Null;
1975 for _ in 0..40 {
1976 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1977 let resp = svc
1978 .handle(make_req(
1979 "DescribeCertificate",
1980 json!({"CertificateArn": arn}),
1981 ))
1982 .await
1983 .unwrap();
1984 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
1985 if body["Certificate"]["Status"] == "ISSUED" {
1986 last = body;
1987 break;
1988 }
1989 }
1990 assert_eq!(last["Certificate"]["Status"], "ISSUED");
1991 assert_eq!(last["Certificate"]["RenewalEligibility"], "ELIGIBLE");
1992 let rs = &last["Certificate"]["RenewalSummary"];
1993 assert_eq!(rs["RenewalStatus"], "PENDING_AUTO_RENEWAL");
1994 assert!(rs["UpdatedAt"].as_f64().unwrap() > 0.0);
1995 }
1996
1997 #[tokio::test]
1998 async fn request_certificate_emits_parseable_pem() {
1999 let svc = AcmService::default();
2000 let resp = svc
2001 .handle(make_req(
2002 "RequestCertificate",
2003 json!({"DomainName": "example.com"}),
2004 ))
2005 .await
2006 .unwrap();
2007 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2008 let arn = body["CertificateArn"].as_str().unwrap().to_string();
2009
2010 let resp = svc
2011 .handle(make_req("GetCertificate", json!({"CertificateArn": arn})))
2012 .await
2013 .unwrap();
2014 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2015 let cert_pem = body["Certificate"].as_str().unwrap();
2016 assert!(
2017 cert_pem.starts_with("-----BEGIN CERTIFICATE-----"),
2018 "expected real PEM cert header, got {cert_pem:.80}"
2019 );
2020 assert!(cert_pem.trim_end().ends_with("-----END CERTIFICATE-----"));
2021 assert!(
2024 cert_pem.len() > 400,
2025 "expected real X.509 PEM, got placeholder-sized blob"
2026 );
2027 assert!(!cert_pem.contains(arn.as_str()));
2030 }
2031
2032 #[tokio::test]
2033 async fn request_certificate_includes_san() {
2034 let svc = AcmService::default();
2035 let resp = svc
2036 .handle(make_req(
2037 "RequestCertificate",
2038 json!({
2039 "DomainName": "example.com",
2040 "SubjectAlternativeNames": ["api.example.com", "www.example.com"],
2041 }),
2042 ))
2043 .await
2044 .unwrap();
2045 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2046 let arn = body["CertificateArn"].as_str().unwrap().to_string();
2047
2048 let resp = svc
2049 .handle(make_req("GetCertificate", json!({"CertificateArn": arn})))
2050 .await
2051 .unwrap();
2052 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2053 let cert_pem = body["Certificate"].as_str().unwrap();
2054 assert!(cert_pem.starts_with("-----BEGIN CERTIFICATE-----"));
2055 assert!(cert_pem.trim_end().ends_with("-----END CERTIFICATE-----"));
2056
2057 let body_b64: String = cert_pem
2061 .lines()
2062 .filter(|l| !l.starts_with("-----"))
2063 .collect::<Vec<_>>()
2064 .join("");
2065 let der = base64::engine::general_purpose::STANDARD
2066 .decode(&body_b64)
2067 .expect("cert body is valid base64");
2068 for san in ["example.com", "api.example.com", "www.example.com"] {
2069 assert!(
2070 der.windows(san.len()).any(|w| w == san.as_bytes()),
2071 "expected SAN {san} embedded in DER"
2072 );
2073 }
2074 }
2075
2076 #[tokio::test]
2077 async fn request_certificate_starts_in_pending_validation() {
2078 let svc = AcmService::default();
2079 let resp = svc
2080 .handle(make_req(
2081 "RequestCertificate",
2082 json!({"DomainName": "example.com", "ValidationMethod": "DNS"}),
2083 ))
2084 .await
2085 .unwrap();
2086 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2087 let arn = body["CertificateArn"].as_str().unwrap().to_string();
2088 let st = svc.state.read();
2089 let cert = st
2090 .accounts
2091 .get("123456789012")
2092 .unwrap()
2093 .certificates
2094 .get(&arn)
2095 .unwrap();
2096 assert_eq!(cert.status, "PENDING_VALIDATION");
2097 assert!(cert.issued_at.is_none());
2098 }
2099
2100 #[tokio::test]
2101 async fn set_certificate_status_to_issued_via_admin() {
2102 let svc = AcmService::default();
2103 let resp = svc
2104 .handle(make_req(
2105 "RequestCertificate",
2106 json!({"DomainName": "admin.example.com"}),
2107 ))
2108 .await
2109 .unwrap();
2110 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2111 let arn = body["CertificateArn"].as_str().unwrap().to_string();
2112
2113 assert!(svc.set_certificate_status(&arn, "ISSUED", None));
2114 let st = svc.state.read();
2115 let cert = st
2116 .accounts
2117 .get("123456789012")
2118 .unwrap()
2119 .certificates
2120 .get(&arn)
2121 .unwrap();
2122 assert_eq!(cert.status, "ISSUED");
2123 assert!(cert.issued_at.is_some());
2124 for dv in &cert.domain_validation {
2125 assert_eq!(dv.validation_status, "SUCCESS");
2126 }
2127 }
2128
2129 #[tokio::test]
2130 async fn set_certificate_status_unknown_arn_returns_false() {
2131 let svc = AcmService::default();
2132 assert!(!svc.set_certificate_status("does-not-exist", "ISSUED", None));
2133 }
2134
2135 #[tokio::test]
2136 async fn auto_issue_tick_transitions_to_issued() {
2137 let svc = AcmService::default()
2138 .with_pending_validation_delay(std::time::Duration::from_millis(50));
2139 let resp = svc
2140 .handle(make_req(
2141 "RequestCertificate",
2142 json!({"DomainName": "auto.example.com", "ValidationMethod": "DNS"}),
2143 ))
2144 .await
2145 .unwrap();
2146 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2147 let arn = body["CertificateArn"].as_str().unwrap().to_string();
2148
2149 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
2150
2151 let st = svc.state.read();
2152 let cert = st
2153 .accounts
2154 .get("123456789012")
2155 .unwrap()
2156 .certificates
2157 .get(&arn)
2158 .unwrap();
2159 assert_eq!(cert.status, "ISSUED");
2160 assert!(cert.issued_at.is_some());
2161 assert_eq!(cert.renewal_eligibility, "ELIGIBLE");
2162 let rs = cert.renewal_summary.as_ref().expect("renewal summary set");
2163 assert_eq!(rs.renewal_status, "PENDING_AUTO_RENEWAL");
2164 }
2165
2166 #[tokio::test]
2167 async fn email_validation_stays_pending_until_approve() {
2168 let svc = AcmService::default()
2173 .with_pending_validation_delay(std::time::Duration::from_millis(20));
2174 let resp = svc
2175 .handle(make_req(
2176 "RequestCertificate",
2177 json!({
2178 "DomainName": "email.example.com",
2179 "ValidationMethod": "EMAIL",
2180 }),
2181 ))
2182 .await
2183 .unwrap();
2184 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2185 let arn = body["CertificateArn"].as_str().unwrap().to_string();
2186
2187 tokio::time::sleep(std::time::Duration::from_millis(150)).await;
2190 {
2191 let st = svc.state.read();
2192 let cert = st
2193 .accounts
2194 .get("123456789012")
2195 .unwrap()
2196 .certificates
2197 .get(&arn)
2198 .unwrap();
2199 assert_eq!(cert.status, "PENDING_VALIDATION");
2200 assert!(cert.issued_at.is_none());
2201 assert!(cert.renewal_summary.is_none());
2202 }
2203
2204 assert!(svc.approve_certificate(&arn));
2205 let st = svc.state.read();
2206 let cert = st
2207 .accounts
2208 .get("123456789012")
2209 .unwrap()
2210 .certificates
2211 .get(&arn)
2212 .unwrap();
2213 assert_eq!(cert.status, "ISSUED");
2214 assert_eq!(cert.renewal_eligibility, "ELIGIBLE");
2215 for dv in &cert.domain_validation {
2216 assert_eq!(dv.validation_status, "SUCCESS");
2217 }
2218 assert!(cert.renewal_summary.is_some());
2219 }
2220
2221 #[tokio::test]
2222 async fn renew_certificate_refreshes_renewal_summary() {
2223 let svc = AcmService::default()
2224 .with_pending_validation_delay(std::time::Duration::from_millis(20));
2225 let resp = svc
2226 .handle(make_req(
2227 "RequestCertificate",
2228 json!({"DomainName": "renew.example.com", "ValidationMethod": "DNS"}),
2229 ))
2230 .await
2231 .unwrap();
2232 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2233 let arn = body["CertificateArn"].as_str().unwrap().to_string();
2234
2235 tokio::time::sleep(std::time::Duration::from_millis(80)).await;
2236
2237 let resp = svc
2238 .handle(make_req("RenewCertificate", json!({"CertificateArn": arn})))
2239 .await
2240 .unwrap();
2241 assert_eq!(resp.status, http::StatusCode::OK);
2242
2243 let st = svc.state.read();
2244 let cert = st
2245 .accounts
2246 .get("123456789012")
2247 .unwrap()
2248 .certificates
2249 .get(&arn)
2250 .unwrap();
2251 assert_eq!(cert.status, "ISSUED");
2252 let rs = cert.renewal_summary.as_ref().expect("renewal summary set");
2253 assert_eq!(rs.renewal_status, "SUCCESS");
2254 assert_eq!(rs.domain_validation.len(), cert.domain_validation.len());
2255 }
2256
2257 #[test]
2258 fn auto_issue_delay_default_when_env_unset() {
2259 if std::env::var("FAKECLOUD_ACM_AUTO_ISSUE_SECS").is_err() {
2264 assert_eq!(
2265 auto_issue_delay_from_env(),
2266 std::time::Duration::from_secs(DEFAULT_AUTO_ISSUE_SECS),
2267 );
2268 }
2269 }
2270
2271 #[tokio::test]
2272 async fn set_certificate_status_failed_records_reason() {
2273 let svc = AcmService::default();
2274 let resp = svc
2275 .handle(make_req(
2276 "RequestCertificate",
2277 json!({"DomainName": "fail.example.com"}),
2278 ))
2279 .await
2280 .unwrap();
2281 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2282 let arn = body["CertificateArn"].as_str().unwrap().to_string();
2283 assert!(svc.set_certificate_status(&arn, "FAILED", Some("DNS lookup error".to_string())));
2284 let st = svc.state.read();
2285 let cert = st
2286 .accounts
2287 .get("123456789012")
2288 .unwrap()
2289 .certificates
2290 .get(&arn)
2291 .unwrap();
2292 assert_eq!(cert.status, "FAILED");
2293 assert_eq!(cert.failure_reason.as_deref(), Some("DNS lookup error"));
2294 }
2295
2296 #[tokio::test]
2297 async fn request_certificate_private_key_pem_is_valid() {
2298 let svc = AcmService::default();
2299 let resp = svc
2300 .handle(make_req(
2301 "RequestCertificate",
2302 json!({"DomainName": "key.example.com"}),
2303 ))
2304 .await
2305 .unwrap();
2306 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2307 let arn = body["CertificateArn"].as_str().unwrap().to_string();
2308
2309 let st = svc.state.read();
2310 let cert = st
2311 .accounts
2312 .get("123456789012")
2313 .unwrap()
2314 .certificates
2315 .get(&arn)
2316 .unwrap();
2317 let key_pem = cert
2318 .private_key_pem
2319 .as_deref()
2320 .expect("RequestCertificate should populate a real private key");
2321 let starts_pkcs8 = key_pem.starts_with("-----BEGIN PRIVATE KEY-----");
2322 let starts_rsa = key_pem.starts_with("-----BEGIN RSA PRIVATE KEY-----");
2323 assert!(
2324 starts_pkcs8 || starts_rsa,
2325 "expected PKCS#8 or RSA private key header, got {key_pem:.80}"
2326 );
2327 assert!(
2328 key_pem.trim_end().ends_with("-----END PRIVATE KEY-----")
2329 || key_pem
2330 .trim_end()
2331 .ends_with("-----END RSA PRIVATE KEY-----")
2332 );
2333 assert!(key_pem.len() > 200);
2334 }
2335
2336 #[tokio::test]
2340 async fn search_certificates_filters_by_status_and_sorts() {
2341 let svc = AcmService::default();
2342 let mut arn: std::collections::HashMap<&str, String> = std::collections::HashMap::new();
2343 for d in ["a.example.com", "b.example.com", "c.example.com"] {
2344 let resp = svc
2345 .handle(make_req("RequestCertificate", json!({ "DomainName": d })))
2346 .await
2347 .unwrap();
2348 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2349 arn.insert(d, body["CertificateArn"].as_str().unwrap().to_string());
2350 }
2351
2352 {
2354 let mut state = svc.state.write();
2355 let certs = &mut state.accounts.get_mut("123456789012").unwrap().certificates;
2356 let now = chrono::Utc::now();
2357 for c in certs.values_mut() {
2358 match c.domain_name.as_str() {
2359 "a.example.com" => {
2360 c.status = "ISSUED".to_string();
2361 c.created_at = now - chrono::Duration::hours(2);
2362 }
2363 "b.example.com" => {
2364 c.status = "PENDING_VALIDATION".to_string();
2365 c.created_at = now - chrono::Duration::hours(1);
2366 }
2367 _ => {
2368 c.status = "ISSUED".to_string();
2369 c.created_at = now;
2370 }
2371 }
2372 }
2373 }
2374
2375 let domains = |body: &Value| -> Vec<String> {
2376 body["Results"]
2377 .as_array()
2378 .unwrap()
2379 .iter()
2380 .map(|r| r["CertificateArn"].as_str().unwrap().to_string())
2381 .collect()
2382 };
2383
2384 let resp = svc
2386 .handle(make_req(
2387 "SearchCertificates",
2388 json!({ "CertificateStatuses": ["ISSUED"] }),
2389 ))
2390 .await
2391 .unwrap();
2392 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2393 let mut issued = domains(&body);
2394 issued.sort();
2395 let mut want = vec![arn["a.example.com"].clone(), arn["c.example.com"].clone()];
2396 want.sort();
2397 assert_eq!(issued, want);
2398
2399 let resp = svc
2401 .handle(make_req(
2402 "SearchCertificates",
2403 json!({ "SortBy": "CREATED_AT", "SortOrder": "ASCENDING" }),
2404 ))
2405 .await
2406 .unwrap();
2407 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2408 assert_eq!(
2409 domains(&body),
2410 vec![
2411 arn["a.example.com"].clone(),
2412 arn["b.example.com"].clone(),
2413 arn["c.example.com"].clone(),
2414 ]
2415 );
2416
2417 let resp = svc
2419 .handle(make_req(
2420 "SearchCertificates",
2421 json!({ "SortBy": "CREATED_AT", "SortOrder": "DESCENDING" }),
2422 ))
2423 .await
2424 .unwrap();
2425 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2426 assert_eq!(
2427 domains(&body),
2428 vec![
2429 arn["c.example.com"].clone(),
2430 arn["b.example.com"].clone(),
2431 arn["a.example.com"].clone(),
2432 ]
2433 );
2434 }
2435
2436 #[tokio::test]
2437 async fn search_certificates_extended_key_usage_filter() {
2438 let svc = AcmService::default();
2442 for d in ["a.example.com", "b.example.com"] {
2443 svc.handle(make_req("RequestCertificate", json!({ "DomainName": d })))
2444 .await
2445 .unwrap();
2446 }
2447
2448 let count = |body: &Value| body["Results"].as_array().unwrap().len();
2449
2450 let resp = svc
2452 .handle(make_req(
2453 "SearchCertificates",
2454 json!({
2455 "FilterStatement": {
2456 "Filter": { "ExtendedKeyUsages": ["TLS_WEB_SERVER_AUTHENTICATION"] }
2457 }
2458 }),
2459 ))
2460 .await
2461 .unwrap();
2462 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2463 assert_eq!(count(&body), 2, "matching EKU should return all certs");
2464
2465 let resp = svc
2467 .handle(make_req(
2468 "SearchCertificates",
2469 json!({
2470 "FilterStatement": {
2471 "Filter": { "ExtendedKeyUsages": ["CODE_SIGNING"] }
2472 }
2473 }),
2474 ))
2475 .await
2476 .unwrap();
2477 let body: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
2478 assert_eq!(count(&body), 0, "non-matching EKU should drop all certs");
2479 }
2480}