Skip to main content

fakecloud_acm/
service.rs

1//! ACM (Certificate Manager) JSON 1.1 service.
2
3use 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    /// How long the auto-issue tick sleeps before flipping a freshly
46    /// requested DNS cert from `PENDING_VALIDATION` to `ISSUED`. Real
47    /// ACM takes minutes; the default of 5s keeps SDK-driven integration
48    /// tests fast while still simulating the async transition. EMAIL
49    /// certs do **not** auto-issue — they wait for the admin
50    /// `/_fakecloud/acm/certificates/{arn}/approve` endpoint. Tests can
51    /// shrink the delay via the env var `FAKECLOUD_ACM_AUTO_ISSUE_SECS`
52    /// (read at construction time) or via
53    /// [`AcmService::with_pending_validation_delay`].
54    pending_validation_delay: std::time::Duration,
55}
56
57/// Default DNS auto-issue delay when `FAKECLOUD_ACM_AUTO_ISSUE_SECS` is
58/// unset. Five seconds is short enough to keep most real-world SDK
59/// flows snappy without making race-prone tests pass by accident.
60const 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    /// Override the auto-issue delay. Used by unit tests so they don't
83    /// have to wait wall-clock seconds for the tick to flip
84    /// `PENDING_VALIDATION` -> `ISSUED`.
85    pub fn with_pending_validation_delay(mut self, delay: std::time::Duration) -> Self {
86        self.pending_validation_delay = delay;
87        self
88    }
89
90    /// Synchronously approve a `PENDING_VALIDATION` certificate.
91    /// Mirrors the admin endpoint
92    /// `POST /_fakecloud/acm/certificates/{arn-or-id}/approve` — equivalent
93    /// to the user clicking the approval link in the validation email
94    /// (or in the case of DNS, the validation record landing). Returns
95    /// `false` if no certificate matches `arn_or_id` across any
96    /// account. Already-issued certs are left alone (returns `true`)
97    /// so the endpoint is idempotent.
98    pub fn approve_certificate(&self, arn_or_id: &str) -> bool {
99        self.set_certificate_status(arn_or_id, "ISSUED", None)
100    }
101
102    /// Return parsed-on-best-effort structural info about an imported
103    /// certificate + chain, surfaced via
104    /// `/_fakecloud/acm/certificates/{arn-or-id}/chain-info` so tests can
105    /// assert what fakecloud actually accepted at ImportCertificate.
106    ///
107    /// We deliberately do **not** anchor the chain to a real root —
108    /// fakecloud is not a PKI. The returned JSON exposes the PEM block
109    /// counts and byte lengths so callers can confirm we received the
110    /// chain they expected, plus an `external_ca_validated: false`
111    /// marker that documents the emulator gap.
112    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    /// Flip a stored certificate's status (and optionally a failure
139    /// reason). Returns `false` if no certificate matches `arn_or_id`
140    /// across any account. The admin endpoint `POST
141    /// /_fakecloud/acm/certificates/{arn-or-id}/status` calls this so
142    /// tests can synchronously force a cert into `ISSUED`, `FAILED`, or
143    /// `VALIDATION_TIMED_OUT` without waiting on the auto-issue tick.
144    ///
145    /// `arn_or_id` accepts either a full ACM ARN or just the
146    /// trailing UUID portion (everything after `certificate/`).
147    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                            // AMAZON_ISSUED certs become renewal-eligible
172                            // and gain a fresh RenewalSummary the moment
173                            // they reach ISSUED, matching real ACM.
174                            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
246// ─── Request handlers ────────────────────────────────────────────────
247
248impl 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        // Idempotency: a same-token + same-DomainName + same-SANs request returns
340        // the prior cert. Real ACM keys this on a 1-hour window; fakecloud uses
341        // exact match for determinism.
342        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            // Issued certs from real ACM are valid 13 months. Match.
381            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        // Auto-issue tick: real ACM transitions DNS-validated certs
399        // from PENDING_VALIDATION to ISSUED asynchronously once the
400        // validation record lands. fakecloud fires the same flip after
401        // `pending_validation_delay` (default 5s, configurable via
402        // `FAKECLOUD_ACM_AUTO_ISSUE_SECS`) so SDK-driven tests can
403        // observe the transition without standing up a real DNS
404        // resolver. EMAIL certs intentionally stay PENDING — real AWS
405        // sends a confirmation email; tests drive the approval via
406        // `POST /_fakecloud/acm/certificates/{arn}/approve`.
407        // ImportCertificate stays ISSUED-on-arrival (its own code path
408        // never enters this branch).
409        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        // Status / NotBefore / NotAfter / RenewalEligibility /
453        // RenewalSummary / DomainValidationOptions[].ValidationStatus
454        // all come straight from the stored cert; the auto-issue tick
455        // (DNS) and admin `/approve` endpoint (EMAIL) mutate them
456        // out-of-band so successive describes naturally observe the
457        // transition.
458        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                // Reimport must overwrite the domain identity too —
611                // otherwise Describe / List / Search keep returning the
612                // previous DomainName + SANs after a successful import.
613                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        // AWS encodes Passphrase as a Blob. Over awsJson1_1 (the wire
676        // protocol used by ACM) blobs are base64 strings. Missing/empty
677        // means the caller wants the plain PEM back.
678        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        // Renewal: flip to ISSUED from any prior state, refresh the
766        // validity window, mark domain validation SUCCESS, and refresh
767        // RenewalSummary. Real ACM kicks off a managed-renewal
768        // background job; fakecloud collapses that to an immediate
769        // success since there's nothing to actually validate.
770        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        // Real ACM: a tag removes if Key matches; if Value is supplied it
891        // also has to match. Otherwise it's a no-op (not an error).
892        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        // SearchCertificates is effectively ListCertificates with a
1006        // recursive `FilterStatement` (And/Or/Not/Filter union) plus
1007        // sort knobs. fakecloud honors the leaf-`Filter` cases
1008        // (KeyTypes, ExtendedKeyUsages match by passing through) and
1009        // ignores the And/Or/Not composition for now — enough to keep
1010        // SDK callers and the conformance probe happy.
1011        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        // Filter values live under FilterStatement.Filter.<Key>; AWS ANDs the
1062        // leaf filters and, within each leaf, keeps certs that match ANY listed
1063        // value. Only KeyTypes was applied before (bug-audit 2026-05-28, 1.5).
1064        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        // ExtendedKeyUsages / KeyUsage were silently dropped before (the
1078        // comment claimed pass-through but nothing applied them — bug-audit
1079        // 2026-06-13, 1.11). fakecloud-issued certs carry the canonical ACM
1080        // EKU/KeyUsage sets (mirrored in the response JSON below), so a cert
1081        // matches a leaf filter when its set intersects the requested values.
1082        let extended_key_usages = filter_values("ExtendedKeyUsages");
1083        let key_usage = filter_values("KeyUsage");
1084
1085        // CertificateStatuses is a top-level filter on the certificate status.
1086        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        // Canonical EKU/KeyUsage sets carried by every fakecloud-issued
1111        // cert (kept in sync with certificate_search_result_json /
1112        // describe output). A cert matches when the requested filter
1113        // intersects its set; `ANY` is the AWS wildcard sentinel.
1114        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        // Apply SortBy/SortOrder (validated above but previously never applied,
1135        // 1.5). Default order is descending; the default key is CREATED_AT.
1136        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                // CREATED_AT and any other validated-but-unmodeled key.
1156                _ => a.created_at.cmp(&b.created_at),
1157            };
1158            // Stable tiebreak by ARN keeps paging deterministic.
1159            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
1192// ─── Helpers ────────────────────────────────────────────────────────
1193
1194fn 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
1232/// Extract the trailing UUID portion of a certificate ARN
1233/// (`arn:aws:acm:region:account:certificate/<id>` -> `<id>`). Returns
1234/// the input unchanged if it doesn't match the ACM ARN shape — callers
1235/// only use this to compare against shorthand identifiers passed to the
1236/// admin endpoint, where a partial match against the full ARN is also
1237/// acceptable.
1238fn 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
1296/// Real ACM always carries the apex `DomainName` as the first entry of
1297/// `SubjectAlternativeNames`; replicate that so SDK tests that read SANs
1298/// don't have to special-case its absence.
1299fn 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
1337/// Deterministic 32-char hex token derived from the domain so test
1338/// assertions on the validation record stay stable across runs.
1339fn 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        // Real SDKs base64-encode blob shapes over the wire. Decode the
1350        // outer encoding back to the underlying PEM text; if it isn't
1351        // base64 (which happens with ad-hoc curl tests), pass through.
1352        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
1362/// Cheap CN scan for an imported PEM. Real ACM parses the X.509 cert
1363/// to extract the subject; fakecloud just looks for a `CN=` substring
1364/// or falls back to the PEM hash so the returned `DomainName` is at
1365/// least stable per input.
1366fn 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    // Fallback used only when an actually-issued cert was somehow
1380    // dropped. Kept distinguishable so callers don't silently treat
1381    // these as real X.509.
1382    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
1397/// Generate a real self-signed X.509 certificate + private key pair
1398/// for `domain_name` covering `sans`. Returns
1399/// `(certificate_pem, private_key_pem)`. Used by RequestCertificate
1400/// so the cert that GetCertificate / ExportCertificate hands back
1401/// is actually parseable as a real PEM-encoded X.509 (matching real
1402/// ACM's output format), not a base64-of-the-ARN placeholder.
1403fn 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
1613/// Encrypt a PEM-encoded PKCS#8 private key with a passphrase,
1614/// producing a PEM file with `BEGIN ENCRYPTED PRIVATE KEY` headers.
1615/// This matches what real ACM `ExportCertificate` returns: a PKCS#8 v2
1616/// `EncryptedPrivateKeyInfo` using PBES2 (PBKDF2 + AES-256-CBC).
1617///
1618/// The resulting PEM is decryptable with anything that handles modern
1619/// PKCS#8 encrypted keys: `openssl pkcs8 -in key.pem -passin pass:...`,
1620/// Python's `cryptography.hazmat.primitives.serialization`,
1621/// `rsa::pkcs8::DecodePrivateKey::from_pkcs8_encrypted_pem`, etc.
1622fn encrypt_private_key_pem(key_pem: &str, passphrase: &[u8]) -> Result<String, String> {
1623    use pkcs8::{pkcs5::pbes2, LineEnding, PrivateKeyInfo};
1624
1625    // Strip the PEM headers and base64-decode the inner DER. Real keys
1626    // (rcgen, openssl) emit `BEGIN PRIVATE KEY` for PKCS#8 v1; we don't
1627    // support encrypting `BEGIN RSA PRIVATE KEY` (PKCS#1) or other
1628    // legacy SEC1 forms here because rcgen always produces PKCS#8.
1629    let der = pem_decode_private_key(key_pem)?;
1630
1631    // PBES2 parameters: 16-byte random salt, 2048 PBKDF2-SHA256
1632    // iterations (matches OpenSSL defaults for `openssl pkcs8 -topk8`),
1633    // AES-256-CBC with a random 16-byte IV.
1634    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
1653/// Decode a PEM `BEGIN PRIVATE KEY` block to its inner DER bytes.
1654/// Returns an error if the block is missing, has the wrong label, or
1655/// the body is not valid base64.
1656fn 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        // Substantially longer than the placeholder (= base64-of-domain).
1698        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        // The plaintext PEM body should not appear in the encrypted
1755        // output — sanity check that we actually encrypted.
1756        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        // Round-trip: decrypt with the same passphrase and confirm we
1850        // recover the same private key bytes that ACM stored.
1851        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        // After the auto-issue tick fires, successive describes should
1945        // pick up Status=ISSUED, IssuedAt, RenewalEligibility=ELIGIBLE,
1946        // and a populated RenewalSummary without any extra prodding.
1947        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        // Immediate describe sees PENDING_VALIDATION + ELIGIBILITY
1960        // INELIGIBLE.
1961        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        // Poll up to 2s for the auto-issue tick.
1974        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        // Real X.509 base64 body is much larger than the legacy
2022        // base64-of-ARN placeholder (which was ~80 chars total).
2023        assert!(
2024            cert_pem.len() > 400,
2025            "expected real X.509 PEM, got placeholder-sized blob"
2026        );
2027        // Sanity-check we didn't smuggle the ARN body back in via the
2028        // old placeholder code path.
2029        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        // Decode the PEM body and search the DER bytes for the SAN dNSName
2058        // octets. rcgen emits SANs as IA5String entries inside the
2059        // SubjectAltName extension, so the raw domain bytes are present.
2060        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        // EMAIL certs intentionally do NOT trigger the auto-issue tick:
2169        // real ACM only flips them once the user clicks the approval
2170        // link. fakecloud routes the approval through
2171        // `approve_certificate`.
2172        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        // Wait well past the DNS auto-issue delay; EMAIL must stay
2188        // PENDING_VALIDATION because no tick was scheduled.
2189        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        // Other tests don't touch FAKECLOUD_ACM_AUTO_ISSUE_SECS, so
2260        // reading it here while running serially with `cargo test`
2261        // returns the documented default. We use a constant to guard
2262        // against accidental drift in `DEFAULT_AUTO_ISSUE_SECS`.
2263        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    // bug-audit 2026-05-28, 1.5: SearchCertificates must honor the
2337    // CertificateStatuses filter and apply SortBy/SortOrder (previously only
2338    // KeyTypes was applied and the order was hardcoded).
2339    #[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        // Stamp deterministic statuses + creation times: a oldest, c newest.
2353        {
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        // CertificateStatuses filter keeps only ISSUED (a + c).
2385        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        // SortBy CREATED_AT ASCENDING -> oldest first.
2400        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        // DESCENDING -> newest first (reverse).
2418        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        // EKU filtering was silently dropped (1.11). fakecloud certs carry
2439        // the canonical TLS server/client auth EKUs, so a matching filter
2440        // narrows to all certs and a non-matching one drops them all.
2441        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        // Matching EKU -> both certs returned.
2451        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        // Non-matching EKU -> filtered out entirely.
2466        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}