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