Skip to main content

fakecloud_acm/
service.rs

1//! ACM (Certificate Manager) JSON 1.1 service.
2
3use std::collections::HashMap;
4use std::sync::Arc;
5
6use async_trait::async_trait;
7use base64::Engine;
8use chrono::{Duration, Utc};
9use http::StatusCode;
10use parking_lot::RwLock;
11use serde_json::{json, Value};
12use sha2::{Digest, Sha256};
13use uuid::Uuid;
14
15use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
16
17use crate::state::{
18    AccountState, AcmAccounts, CertificateOptions, DomainValidation, SharedAcmState,
19    StoredCertificate,
20};
21
22const SUPPORTED_ACTIONS: &[&str] = &[
23    "RequestCertificate",
24    "DescribeCertificate",
25    "ListCertificates",
26    "DeleteCertificate",
27    "ImportCertificate",
28    "ExportCertificate",
29    "GetCertificate",
30    "RenewCertificate",
31    "RevokeCertificate",
32    "ResendValidationEmail",
33    "AddTagsToCertificate",
34    "RemoveTagsFromCertificate",
35    "ListTagsForCertificate",
36    "GetAccountConfiguration",
37    "PutAccountConfiguration",
38    "UpdateCertificateOptions",
39    "SearchCertificates",
40];
41
42pub struct AcmService {
43    state: SharedAcmState,
44}
45
46impl AcmService {
47    pub fn new(state: SharedAcmState) -> Self {
48        Self { state }
49    }
50
51    pub fn shared_state(&self) -> SharedAcmState {
52        Arc::clone(&self.state)
53    }
54}
55
56impl Default for AcmService {
57    fn default() -> Self {
58        Self::new(Arc::new(RwLock::new(AcmAccounts::new())))
59    }
60}
61
62#[async_trait]
63impl AwsService for AcmService {
64    fn service_name(&self) -> &str {
65        "acm"
66    }
67
68    fn supported_actions(&self) -> &[&str] {
69        SUPPORTED_ACTIONS
70    }
71
72    async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
73        match req.action.as_str() {
74            "RequestCertificate" => self.request_certificate(&req),
75            "DescribeCertificate" => self.describe_certificate(&req),
76            "ListCertificates" => self.list_certificates(&req),
77            "DeleteCertificate" => self.delete_certificate(&req),
78            "ImportCertificate" => self.import_certificate(&req),
79            "ExportCertificate" => self.export_certificate(&req),
80            "GetCertificate" => self.get_certificate(&req),
81            "RenewCertificate" => self.renew_certificate(&req),
82            "RevokeCertificate" => self.revoke_certificate(&req),
83            "ResendValidationEmail" => self.resend_validation_email(&req),
84            "AddTagsToCertificate" => self.add_tags_to_certificate(&req),
85            "RemoveTagsFromCertificate" => self.remove_tags_from_certificate(&req),
86            "ListTagsForCertificate" => self.list_tags_for_certificate(&req),
87            "GetAccountConfiguration" => self.get_account_configuration(&req),
88            "PutAccountConfiguration" => self.put_account_configuration(&req),
89            "UpdateCertificateOptions" => self.update_certificate_options(&req),
90            "SearchCertificates" => self.search_certificates(&req),
91            other => Err(AwsServiceError::action_not_implemented("acm", other)),
92        }
93    }
94}
95
96// ─── Request handlers ────────────────────────────────────────────────
97
98impl AcmService {
99    fn request_certificate(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
100        let body = req.json_body();
101        let domain_name = body
102            .get("DomainName")
103            .and_then(Value::as_str)
104            .ok_or_else(|| invalid_param("DomainName is required"))?
105            .to_string();
106        let validation_method = body
107            .get("ValidationMethod")
108            .and_then(Value::as_str)
109            .unwrap_or("DNS")
110            .to_string();
111        let sans: Vec<String> = body
112            .get("SubjectAlternativeNames")
113            .and_then(Value::as_array)
114            .map(|v| {
115                v.iter()
116                    .filter_map(|s| s.as_str().map(|s| s.to_string()))
117                    .collect()
118            })
119            .unwrap_or_default();
120        let key_algorithm = body
121            .get("KeyAlgorithm")
122            .and_then(Value::as_str)
123            .unwrap_or("RSA_2048")
124            .to_string();
125        let idempotency_token = body
126            .get("IdempotencyToken")
127            .and_then(Value::as_str)
128            .map(|s| s.to_string());
129        let managed_by = body
130            .get("ManagedBy")
131            .and_then(Value::as_str)
132            .map(|s| s.to_string());
133        let ca_arn = body
134            .get("CertificateAuthorityArn")
135            .and_then(Value::as_str)
136            .map(|s| s.to_string());
137        let tags = parse_tags(body.get("Tags"))?;
138        let options = parse_options(body.get("Options"));
139
140        let mut state = self.state.write();
141        let account = account_mut(&mut state, &req.account_id);
142
143        // Idempotency: a same-token + same-DomainName + same-SANs request returns
144        // the prior cert. Real ACM keys this on a 1-hour window; fakecloud uses
145        // exact match for determinism.
146        if let Some(token) = &idempotency_token {
147            if let Some(existing) = account.certificates.values().find(|c| {
148                c.idempotency_token.as_deref() == Some(token)
149                    && c.domain_name == domain_name
150                    && c.subject_alternative_names == effective_sans(&domain_name, &sans)
151            }) {
152                return Ok(AwsResponse::ok_json(
153                    json!({ "CertificateArn": existing.arn }),
154                ));
155            }
156        }
157
158        let arn = synth_certificate_arn(&req.account_id, &req.region);
159        let now = Utc::now();
160        let cert = StoredCertificate {
161            arn: arn.clone(),
162            domain_name: domain_name.clone(),
163            subject_alternative_names: effective_sans(&domain_name, &sans),
164            status: "PENDING_VALIDATION".to_string(),
165            cert_type: "AMAZON_ISSUED".to_string(),
166            certificate_pem: None,
167            certificate_chain_pem: None,
168            private_key_pem: None,
169            idempotency_token,
170            serial: synth_serial(&arn),
171            subject: format!("CN={domain_name}"),
172            issuer: "Amazon".to_string(),
173            key_algorithm,
174            signature_algorithm: "SHA256WITHRSA".to_string(),
175            created_at: now,
176            issued_at: None,
177            imported_at: None,
178            revoked_at: None,
179            revocation_reason: None,
180            // Issued certs from real ACM are valid 13 months. Match.
181            not_before: now,
182            not_after: now + Duration::days(395),
183            validation_method: Some(validation_method.clone()),
184            domain_validation: synth_domain_validation(&domain_name, &sans, &validation_method),
185            options,
186            renewal_eligibility: "INELIGIBLE".to_string(),
187            managed_by,
188            certificate_authority_arn: ca_arn,
189            tags,
190            in_use_by: Vec::new(),
191        };
192        account.certificates.insert(arn.clone(), cert);
193        Ok(AwsResponse::ok_json(json!({ "CertificateArn": arn })))
194    }
195
196    fn describe_certificate(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
197        let arn = require_certificate_arn(req)?;
198        let state = self.state.read();
199        let cert = state
200            .accounts
201            .get(&req.account_id)
202            .and_then(|a| a.certificates.get(&arn))
203            .ok_or_else(|| no_such_certificate(&arn))?
204            .clone();
205        Ok(AwsResponse::ok_json(json!({
206            "Certificate": certificate_detail_json(&cert),
207        })))
208    }
209
210    fn list_certificates(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
211        let body = req.json_body();
212        let max_items: usize = body
213            .get("MaxItems")
214            .and_then(Value::as_u64)
215            .map(|n| n as usize)
216            .unwrap_or(100);
217        let next_token = body
218            .get("NextToken")
219            .and_then(Value::as_str)
220            .map(|s| s.to_string());
221        let statuses: Vec<String> = body
222            .get("CertificateStatuses")
223            .and_then(Value::as_array)
224            .map(|v| {
225                v.iter()
226                    .filter_map(|s| s.as_str().map(|s| s.to_string()))
227                    .collect()
228            })
229            .unwrap_or_default();
230        let includes = body.get("Includes");
231        let key_types: Vec<String> = includes
232            .and_then(|i| i.get("keyTypes"))
233            .and_then(Value::as_array)
234            .map(|v| {
235                v.iter()
236                    .filter_map(|s| s.as_str().map(|s| s.to_string()))
237                    .collect()
238            })
239            .unwrap_or_default();
240
241        let state = self.state.read();
242        let mut all: Vec<StoredCertificate> = state
243            .accounts
244            .get(&req.account_id)
245            .map(|a| a.certificates.values().cloned().collect())
246            .unwrap_or_default();
247        drop(state);
248        all.sort_by(|a, b| a.arn.cmp(&b.arn));
249        all.retain(|c| {
250            (statuses.is_empty() || statuses.contains(&c.status))
251                && (key_types.is_empty() || key_types.contains(&c.key_algorithm))
252        });
253
254        let start = next_token
255            .and_then(|t| t.parse::<usize>().ok())
256            .unwrap_or(0);
257        let end = (start + max_items).min(all.len());
258        let page: Vec<&StoredCertificate> = all.iter().skip(start).take(max_items).collect();
259        let next = if end < all.len() {
260            Some(end.to_string())
261        } else {
262            None
263        };
264        let mut response = json!({
265            "CertificateSummaryList": page
266                .iter()
267                .map(|c| certificate_summary_json(c))
268                .collect::<Vec<_>>(),
269        });
270        if let Some(t) = next {
271            response
272                .as_object_mut()
273                .unwrap()
274                .insert("NextToken".to_string(), Value::String(t));
275        }
276        Ok(AwsResponse::ok_json(response))
277    }
278
279    fn delete_certificate(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
280        let arn = require_certificate_arn(req)?;
281        let mut state = self.state.write();
282        let account = account_mut(&mut state, &req.account_id);
283        let cert = account
284            .certificates
285            .get(&arn)
286            .ok_or_else(|| no_such_certificate(&arn))?;
287        if !cert.in_use_by.is_empty() {
288            return Err(AwsServiceError::aws_error(
289                StatusCode::BAD_REQUEST,
290                "ResourceInUseException",
291                format!(
292                    "Certificate {arn} is in use by {} resource(s)",
293                    cert.in_use_by.len()
294                ),
295            ));
296        }
297        account.certificates.remove(&arn);
298        Ok(AwsResponse::ok_json(json!({})))
299    }
300
301    fn import_certificate(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
302        let body = req.json_body();
303        let cert_pem = decode_blob(body.get("Certificate"))
304            .ok_or_else(|| invalid_param("Certificate is required"))?;
305        let key_pem = decode_blob(body.get("PrivateKey"))
306            .ok_or_else(|| invalid_param("PrivateKey is required"))?;
307        let chain_pem = decode_blob(body.get("CertificateChain"));
308        let arn_in = body
309            .get("CertificateArn")
310            .and_then(Value::as_str)
311            .map(|s| s.to_string());
312        let tags = parse_tags(body.get("Tags"))?;
313
314        let domain_name = parse_cn_from_pem(&cert_pem).unwrap_or_else(|| "imported".to_string());
315        let now = Utc::now();
316        let mut state = self.state.write();
317        let account = account_mut(&mut state, &req.account_id);
318
319        let arn = match arn_in {
320            Some(existing) => {
321                let cert = account
322                    .certificates
323                    .get_mut(&existing)
324                    .ok_or_else(|| no_such_certificate(&existing))?;
325                if cert.cert_type != "IMPORTED" {
326                    return Err(invalid_param(
327                        "Reimport is only supported for IMPORTED certificates",
328                    ));
329                }
330                cert.certificate_pem = Some(cert_pem.clone());
331                cert.private_key_pem = Some(key_pem);
332                cert.certificate_chain_pem = chain_pem;
333                cert.imported_at = Some(now);
334                cert.not_before = now;
335                cert.not_after = now + Duration::days(395);
336                cert.subject = format!("CN={domain_name}");
337                // Reimport must overwrite the domain identity too —
338                // otherwise Describe / List / Search keep returning the
339                // previous DomainName + SANs after a successful import.
340                cert.domain_name = domain_name.clone();
341                cert.subject_alternative_names = vec![domain_name.clone()];
342                if !tags.is_empty() {
343                    for (k, v) in tags {
344                        cert.tags.insert(k, v);
345                    }
346                }
347                existing
348            }
349            None => {
350                let arn = synth_certificate_arn(&req.account_id, &req.region);
351                let cert = StoredCertificate {
352                    arn: arn.clone(),
353                    domain_name: domain_name.clone(),
354                    subject_alternative_names: vec![domain_name.clone()],
355                    status: "ISSUED".to_string(),
356                    cert_type: "IMPORTED".to_string(),
357                    certificate_pem: Some(cert_pem),
358                    certificate_chain_pem: chain_pem,
359                    private_key_pem: Some(key_pem),
360                    idempotency_token: None,
361                    serial: synth_serial(&arn),
362                    subject: format!("CN={domain_name}"),
363                    issuer: "fakecloud-imported".to_string(),
364                    key_algorithm: "RSA_2048".to_string(),
365                    signature_algorithm: "SHA256WITHRSA".to_string(),
366                    created_at: now,
367                    issued_at: Some(now),
368                    imported_at: Some(now),
369                    revoked_at: None,
370                    revocation_reason: None,
371                    not_before: now,
372                    not_after: now + Duration::days(395),
373                    validation_method: None,
374                    domain_validation: Vec::new(),
375                    options: CertificateOptions {
376                        certificate_transparency_logging_preference: "ENABLED".to_string(),
377                        export: "DISABLED".to_string(),
378                    },
379                    renewal_eligibility: "INELIGIBLE".to_string(),
380                    managed_by: None,
381                    certificate_authority_arn: None,
382                    tags,
383                    in_use_by: Vec::new(),
384                };
385                account.certificates.insert(arn.clone(), cert);
386                arn
387            }
388        };
389        Ok(AwsResponse::ok_json(json!({ "CertificateArn": arn })))
390    }
391
392    fn export_certificate(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
393        let body = req.json_body();
394        let arn = body
395            .get("CertificateArn")
396            .and_then(Value::as_str)
397            .ok_or_else(|| invalid_param("CertificateArn is required"))?
398            .to_string();
399        let passphrase_b64 = body
400            .get("Passphrase")
401            .and_then(Value::as_str)
402            .ok_or_else(|| invalid_param("Passphrase is required"))?;
403        if passphrase_b64.is_empty() {
404            return Err(invalid_param("Passphrase must not be empty"));
405        }
406        let state = self.state.read();
407        let cert = state
408            .accounts
409            .get(&req.account_id)
410            .and_then(|a| a.certificates.get(&arn))
411            .ok_or_else(|| no_such_certificate(&arn))?
412            .clone();
413        if cert.options.export != "ENABLED" && cert.cert_type != "IMPORTED" {
414            return Err(AwsServiceError::aws_error(
415                StatusCode::BAD_REQUEST,
416                "RequestInProgressException",
417                "Certificate is not exportable",
418            ));
419        }
420        let cert_pem = cert
421            .certificate_pem
422            .clone()
423            .unwrap_or_else(|| placeholder_cert_pem(&arn));
424        let chain_pem = cert
425            .certificate_chain_pem
426            .clone()
427            .unwrap_or_else(|| placeholder_chain_pem(&arn));
428        let key_pem = cert
429            .private_key_pem
430            .clone()
431            .unwrap_or_else(|| placeholder_key_pem(&arn));
432        Ok(AwsResponse::ok_json(json!({
433            "Certificate": cert_pem,
434            "CertificateChain": chain_pem,
435            "PrivateKey": key_pem,
436        })))
437    }
438
439    fn get_certificate(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
440        let arn = require_certificate_arn(req)?;
441        let state = self.state.read();
442        let cert = state
443            .accounts
444            .get(&req.account_id)
445            .and_then(|a| a.certificates.get(&arn))
446            .ok_or_else(|| no_such_certificate(&arn))?
447            .clone();
448        let cert_pem = cert
449            .certificate_pem
450            .clone()
451            .unwrap_or_else(|| placeholder_cert_pem(&arn));
452        let chain_pem = cert
453            .certificate_chain_pem
454            .clone()
455            .unwrap_or_else(|| placeholder_chain_pem(&arn));
456        Ok(AwsResponse::ok_json(json!({
457            "Certificate": cert_pem,
458            "CertificateChain": chain_pem,
459        })))
460    }
461
462    fn renew_certificate(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
463        let arn = require_certificate_arn(req)?;
464        let mut state = self.state.write();
465        let account = account_mut(&mut state, &req.account_id);
466        let cert = account
467            .certificates
468            .get_mut(&arn)
469            .ok_or_else(|| no_such_certificate(&arn))?;
470        if cert.cert_type == "IMPORTED" {
471            return Err(invalid_param(
472                "Imported certificates cannot be renewed via ACM",
473            ));
474        }
475        let now = Utc::now();
476        cert.not_before = now;
477        cert.not_after = now + Duration::days(395);
478        cert.issued_at = Some(now);
479        cert.status = "ISSUED".to_string();
480        Ok(AwsResponse::ok_json(json!({})))
481    }
482
483    fn revoke_certificate(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
484        let body = req.json_body();
485        let arn = body
486            .get("CertificateArn")
487            .and_then(Value::as_str)
488            .ok_or_else(|| invalid_param("CertificateArn is required"))?
489            .to_string();
490        let reason = body
491            .get("RevocationReason")
492            .and_then(Value::as_str)
493            .ok_or_else(|| invalid_param("RevocationReason is required"))?
494            .to_string();
495        let mut state = self.state.write();
496        let account = account_mut(&mut state, &req.account_id);
497        let cert = account
498            .certificates
499            .get_mut(&arn)
500            .ok_or_else(|| no_such_certificate(&arn))?;
501        if cert.cert_type != "AMAZON_ISSUED" {
502            return Err(invalid_param(
503                "Only AMAZON_ISSUED certificates can be revoked",
504            ));
505        }
506        cert.status = "REVOKED".to_string();
507        cert.revoked_at = Some(Utc::now());
508        cert.revocation_reason = Some(reason);
509        Ok(AwsResponse::ok_json(json!({})))
510    }
511
512    fn resend_validation_email(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
513        let body = req.json_body();
514        let arn = body
515            .get("CertificateArn")
516            .and_then(Value::as_str)
517            .ok_or_else(|| invalid_param("CertificateArn is required"))?
518            .to_string();
519        let _ = body
520            .get("Domain")
521            .and_then(Value::as_str)
522            .ok_or_else(|| invalid_param("Domain is required"))?;
523        let _ = body
524            .get("ValidationDomain")
525            .and_then(Value::as_str)
526            .ok_or_else(|| invalid_param("ValidationDomain is required"))?;
527        let state = self.state.read();
528        let cert = state
529            .accounts
530            .get(&req.account_id)
531            .and_then(|a| a.certificates.get(&arn))
532            .ok_or_else(|| no_such_certificate(&arn))?;
533        if cert.validation_method.as_deref() != Some("EMAIL") {
534            return Err(invalid_param(
535                "Certificate is not configured for EMAIL validation",
536            ));
537        }
538        Ok(AwsResponse::ok_json(json!({})))
539    }
540
541    fn add_tags_to_certificate(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
542        let body = req.json_body();
543        let arn = body
544            .get("CertificateArn")
545            .and_then(Value::as_str)
546            .ok_or_else(|| invalid_param("CertificateArn is required"))?
547            .to_string();
548        let tags = parse_tags(body.get("Tags"))?;
549        if tags.is_empty() {
550            return Err(invalid_param("Tags must contain at least one entry"));
551        }
552        let mut state = self.state.write();
553        let account = account_mut(&mut state, &req.account_id);
554        let cert = account
555            .certificates
556            .get_mut(&arn)
557            .ok_or_else(|| no_such_certificate(&arn))?;
558        for (k, v) in tags {
559            cert.tags.insert(k, v);
560        }
561        Ok(AwsResponse::ok_json(json!({})))
562    }
563
564    fn remove_tags_from_certificate(
565        &self,
566        req: &AwsRequest,
567    ) -> Result<AwsResponse, AwsServiceError> {
568        let body = req.json_body();
569        let arn = body
570            .get("CertificateArn")
571            .and_then(Value::as_str)
572            .ok_or_else(|| invalid_param("CertificateArn is required"))?
573            .to_string();
574        let tags = parse_tags(body.get("Tags"))?;
575        if tags.is_empty() {
576            return Err(invalid_param("Tags must contain at least one entry"));
577        }
578        let mut state = self.state.write();
579        let account = account_mut(&mut state, &req.account_id);
580        let cert = account
581            .certificates
582            .get_mut(&arn)
583            .ok_or_else(|| no_such_certificate(&arn))?;
584        // Real ACM: a tag removes if Key matches; if Value is supplied it
585        // also has to match. Otherwise it's a no-op (not an error).
586        for (k, v) in tags {
587            if let Some(existing) = cert.tags.get(&k) {
588                if v.is_empty() || *existing == v {
589                    cert.tags.remove(&k);
590                }
591            }
592        }
593        Ok(AwsResponse::ok_json(json!({})))
594    }
595
596    fn list_tags_for_certificate(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
597        let arn = require_certificate_arn(req)?;
598        let state = self.state.read();
599        let cert = state
600            .accounts
601            .get(&req.account_id)
602            .and_then(|a| a.certificates.get(&arn))
603            .ok_or_else(|| no_such_certificate(&arn))?;
604        let mut tags: Vec<(String, String)> = cert
605            .tags
606            .iter()
607            .map(|(k, v)| (k.clone(), v.clone()))
608            .collect();
609        tags.sort_by(|a, b| a.0.cmp(&b.0));
610        let tag_list: Vec<Value> = tags
611            .into_iter()
612            .map(|(k, v)| json!({ "Key": k, "Value": v }))
613            .collect();
614        Ok(AwsResponse::ok_json(json!({ "Tags": tag_list })))
615    }
616
617    fn get_account_configuration(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
618        let state = self.state.read();
619        let cfg = state
620            .accounts
621            .get(&req.account_id)
622            .map(|a| a.account_config.clone())
623            .unwrap_or_default();
624        let mut expiry = json!({});
625        if let Some(d) = cfg.expiry_events_days_before_expiry {
626            expiry
627                .as_object_mut()
628                .unwrap()
629                .insert("DaysBeforeExpiry".to_string(), json!(d));
630        }
631        Ok(AwsResponse::ok_json(json!({
632            "ExpiryEvents": expiry,
633        })))
634    }
635
636    fn put_account_configuration(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
637        let body = req.json_body();
638        let _ = body
639            .get("IdempotencyToken")
640            .and_then(Value::as_str)
641            .ok_or_else(|| invalid_param("IdempotencyToken is required"))?;
642        let days = body
643            .get("ExpiryEvents")
644            .and_then(|v| v.get("DaysBeforeExpiry"))
645            .and_then(Value::as_i64)
646            .map(|n| n as i32);
647        let mut state = self.state.write();
648        let account = account_mut(&mut state, &req.account_id);
649        account.account_config.expiry_events_days_before_expiry = days;
650        Ok(AwsResponse::ok_json(json!({})))
651    }
652
653    fn update_certificate_options(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
654        let body = req.json_body();
655        let arn = body
656            .get("CertificateArn")
657            .and_then(Value::as_str)
658            .ok_or_else(|| invalid_param("CertificateArn is required"))?
659            .to_string();
660        let options = body
661            .get("Options")
662            .ok_or_else(|| invalid_param("Options is required"))?;
663        let new_opts = CertificateOptions {
664            certificate_transparency_logging_preference: options
665                .get("CertificateTransparencyLoggingPreference")
666                .and_then(Value::as_str)
667                .unwrap_or("ENABLED")
668                .to_string(),
669            export: options
670                .get("Export")
671                .and_then(Value::as_str)
672                .unwrap_or("DISABLED")
673                .to_string(),
674        };
675        let mut state = self.state.write();
676        let account = account_mut(&mut state, &req.account_id);
677        let cert = account
678            .certificates
679            .get_mut(&arn)
680            .ok_or_else(|| no_such_certificate(&arn))?;
681        cert.options = new_opts;
682        Ok(AwsResponse::ok_json(json!({})))
683    }
684
685    fn search_certificates(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
686        // SearchCertificates is effectively ListCertificates with a
687        // recursive `FilterStatement` (And/Or/Not/Filter union) plus
688        // sort knobs. fakecloud honors the leaf-`Filter` cases
689        // (KeyTypes, ExtendedKeyUsages match by passing through) and
690        // ignores the And/Or/Not composition for now — enough to keep
691        // SDK callers and the conformance probe happy.
692        let body = req.json_body();
693        let max_results: usize = body
694            .get("MaxResults")
695            .and_then(Value::as_u64)
696            .map(|n| n as usize)
697            .unwrap_or(100);
698        let next_token = body
699            .get("NextToken")
700            .and_then(Value::as_str)
701            .map(|s| s.to_string());
702        let key_types: Vec<String> = body
703            .get("FilterStatement")
704            .and_then(|f| f.get("Filter"))
705            .and_then(|f| f.get("KeyTypes"))
706            .and_then(Value::as_array)
707            .map(|v| {
708                v.iter()
709                    .filter_map(|s| s.as_str().map(|s| s.to_string()))
710                    .collect()
711            })
712            .unwrap_or_default();
713
714        let state = self.state.read();
715        let mut all: Vec<StoredCertificate> = state
716            .accounts
717            .get(&req.account_id)
718            .map(|a| a.certificates.values().cloned().collect())
719            .unwrap_or_default();
720        drop(state);
721        all.sort_by(|a, b| a.arn.cmp(&b.arn));
722        if !key_types.is_empty() {
723            all.retain(|c| key_types.contains(&c.key_algorithm));
724        }
725        let start = next_token
726            .and_then(|t| t.parse::<usize>().ok())
727            .unwrap_or(0);
728        let end = (start + max_results).min(all.len());
729        let page: Vec<&StoredCertificate> = all.iter().skip(start).take(max_results).collect();
730        let next = if end < all.len() {
731            Some(end.to_string())
732        } else {
733            None
734        };
735        let mut response = json!({
736            "Results": page
737                .iter()
738                .map(|c| certificate_search_result_json(c))
739                .collect::<Vec<_>>(),
740        });
741        if let Some(t) = next {
742            response
743                .as_object_mut()
744                .unwrap()
745                .insert("NextToken".to_string(), Value::String(t));
746        }
747        Ok(AwsResponse::ok_json(response))
748    }
749}
750
751// ─── Helpers ────────────────────────────────────────────────────────
752
753fn account_mut<'a>(state: &'a mut AcmAccounts, account_id: &str) -> &'a mut AccountState {
754    state.accounts.entry(account_id.to_string()).or_default()
755}
756
757fn require_certificate_arn(req: &AwsRequest) -> Result<String, AwsServiceError> {
758    req.json_body()
759        .get("CertificateArn")
760        .and_then(Value::as_str)
761        .map(|s| s.to_string())
762        .ok_or_else(|| invalid_param("CertificateArn is required"))
763}
764
765fn invalid_param(msg: impl Into<String>) -> AwsServiceError {
766    AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "InvalidParameterException", msg)
767}
768
769fn no_such_certificate(arn: &str) -> AwsServiceError {
770    AwsServiceError::aws_error(
771        StatusCode::BAD_REQUEST,
772        "ResourceNotFoundException",
773        format!("Could not find certificate with arn {arn}"),
774    )
775}
776
777fn synth_certificate_arn(account_id: &str, region: &str) -> String {
778    let region = if region.is_empty() {
779        "us-east-1"
780    } else {
781        region
782    };
783    let id = Uuid::new_v4();
784    format!("arn:aws:acm:{region}:{account_id}:certificate/{id}")
785}
786
787fn synth_serial(arn: &str) -> String {
788    let mut hasher = Sha256::new();
789    hasher.update(arn.as_bytes());
790    let digest = hasher.finalize();
791    hex::encode(&digest[..16])
792}
793
794fn parse_tags(value: Option<&Value>) -> Result<HashMap<String, String>, AwsServiceError> {
795    let mut out = HashMap::new();
796    let Some(arr) = value.and_then(Value::as_array) else {
797        return Ok(out);
798    };
799    for tag in arr {
800        let key = tag
801            .get("Key")
802            .and_then(Value::as_str)
803            .ok_or_else(|| invalid_param("Tag.Key is required"))?
804            .to_string();
805        let value = tag
806            .get("Value")
807            .and_then(Value::as_str)
808            .unwrap_or_default()
809            .to_string();
810        out.insert(key, value);
811    }
812    Ok(out)
813}
814
815fn parse_options(value: Option<&Value>) -> CertificateOptions {
816    let v = match value {
817        Some(v) => v,
818        None => {
819            return CertificateOptions {
820                certificate_transparency_logging_preference: "ENABLED".to_string(),
821                export: "DISABLED".to_string(),
822            };
823        }
824    };
825    CertificateOptions {
826        certificate_transparency_logging_preference: v
827            .get("CertificateTransparencyLoggingPreference")
828            .and_then(Value::as_str)
829            .unwrap_or("ENABLED")
830            .to_string(),
831        export: v
832            .get("Export")
833            .and_then(Value::as_str)
834            .unwrap_or("DISABLED")
835            .to_string(),
836    }
837}
838
839/// Real ACM always carries the apex `DomainName` as the first entry of
840/// `SubjectAlternativeNames`; replicate that so SDK tests that read SANs
841/// don't have to special-case its absence.
842fn effective_sans(domain: &str, extras: &[String]) -> Vec<String> {
843    let mut all = vec![domain.to_string()];
844    for s in extras {
845        if !all.contains(s) {
846            all.push(s.clone());
847        }
848    }
849    all
850}
851
852fn synth_domain_validation(domain: &str, sans: &[String], method: &str) -> Vec<DomainValidation> {
853    effective_sans(domain, sans)
854        .iter()
855        .map(|d| {
856            if method == "DNS" {
857                let token = synth_dns_token(d);
858                DomainValidation {
859                    domain_name: d.clone(),
860                    validation_status: "PENDING_VALIDATION".to_string(),
861                    validation_method: "DNS".to_string(),
862                    resource_record_name: Some(format!("_{token}.{d}.")),
863                    resource_record_type: Some("CNAME".to_string()),
864                    resource_record_value: Some(format!("_{token}.acm-validations.aws.")),
865                }
866            } else {
867                DomainValidation {
868                    domain_name: d.clone(),
869                    validation_status: "PENDING_VALIDATION".to_string(),
870                    validation_method: "EMAIL".to_string(),
871                    resource_record_name: None,
872                    resource_record_type: None,
873                    resource_record_value: None,
874                }
875            }
876        })
877        .collect()
878}
879
880/// Deterministic 32-char hex token derived from the domain so test
881/// assertions on the validation record stay stable across runs.
882fn synth_dns_token(domain: &str) -> String {
883    let mut hasher = Sha256::new();
884    hasher.update(domain.as_bytes());
885    let digest = hasher.finalize();
886    hex::encode(&digest[..16])
887}
888
889fn decode_blob(value: Option<&Value>) -> Option<String> {
890    let v = value?;
891    if let Some(s) = v.as_str() {
892        // Real SDKs base64-encode blob shapes over the wire. Decode the
893        // outer encoding back to the underlying PEM text; if it isn't
894        // base64 (which happens with ad-hoc curl tests), pass through.
895        if let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(s) {
896            if let Ok(text) = String::from_utf8(bytes) {
897                return Some(text);
898            }
899        }
900        return Some(s.to_string());
901    }
902    None
903}
904
905/// Cheap CN scan for an imported PEM. Real ACM parses the X.509 cert
906/// to extract the subject; fakecloud just looks for a `CN=` substring
907/// or falls back to the PEM hash so the returned `DomainName` is at
908/// least stable per input.
909fn parse_cn_from_pem(pem: &str) -> Option<String> {
910    pem.lines()
911        .find_map(|line| line.split("CN=").nth(1))
912        .map(|rest| {
913            rest.split(['/', ',', '\n', ' '])
914                .next()
915                .unwrap_or("")
916                .to_string()
917        })
918        .filter(|s| !s.is_empty())
919}
920
921fn placeholder_cert_pem(arn: &str) -> String {
922    let body = base64::engine::general_purpose::STANDARD.encode(arn.as_bytes());
923    format!("-----BEGIN CERTIFICATE-----\n{body}\n-----END CERTIFICATE-----\n")
924}
925
926fn placeholder_chain_pem(arn: &str) -> String {
927    let body =
928        base64::engine::general_purpose::STANDARD.encode(format!("chain-of-{arn}").as_bytes());
929    format!("-----BEGIN CERTIFICATE-----\n{body}\n-----END CERTIFICATE-----\n")
930}
931
932fn placeholder_key_pem(arn: &str) -> String {
933    let body = base64::engine::general_purpose::STANDARD.encode(format!("key-of-{arn}").as_bytes());
934    format!("-----BEGIN RSA PRIVATE KEY-----\n{body}\n-----END RSA PRIVATE KEY-----\n")
935}
936
937fn certificate_summary_json(c: &StoredCertificate) -> Value {
938    let mut s = json!({
939        "CertificateArn": c.arn,
940        "DomainName": c.domain_name,
941        "SubjectAlternativeNameSummaries": c.subject_alternative_names,
942        "HasAdditionalSubjectAlternativeNames": false,
943        "Status": c.status,
944        "Type": c.cert_type,
945        "KeyAlgorithm": c.key_algorithm,
946        "KeyUsages": ["DIGITAL_SIGNATURE", "KEY_ENCIPHERMENT"],
947        "ExtendedKeyUsages": ["TLS_WEB_SERVER_AUTHENTICATION", "TLS_WEB_CLIENT_AUTHENTICATION"],
948        "InUse": !c.in_use_by.is_empty(),
949        "Exported": false,
950        "RenewalEligibility": c.renewal_eligibility,
951        "NotBefore": c.not_before.timestamp() as f64,
952        "NotAfter": c.not_after.timestamp() as f64,
953        "CreatedAt": c.created_at.timestamp() as f64,
954    });
955    if let Some(t) = c.issued_at {
956        s.as_object_mut()
957            .unwrap()
958            .insert("IssuedAt".to_string(), json!(t.timestamp() as f64));
959    }
960    if let Some(t) = c.imported_at {
961        s.as_object_mut()
962            .unwrap()
963            .insert("ImportedAt".to_string(), json!(t.timestamp() as f64));
964    }
965    if let Some(t) = c.revoked_at {
966        s.as_object_mut()
967            .unwrap()
968            .insert("RevokedAt".to_string(), json!(t.timestamp() as f64));
969        if let Some(r) = &c.revocation_reason {
970            s.as_object_mut()
971                .unwrap()
972                .insert("RevocationReason".to_string(), json!(r));
973        }
974    }
975    if let Some(m) = &c.managed_by {
976        s.as_object_mut()
977            .unwrap()
978            .insert("ManagedBy".to_string(), json!(m));
979    }
980    s
981}
982
983fn certificate_detail_json(c: &StoredCertificate) -> Value {
984    let mut d = json!({
985        "CertificateArn": c.arn,
986        "DomainName": c.domain_name,
987        "SubjectAlternativeNames": c.subject_alternative_names,
988        "Status": c.status,
989        "Type": c.cert_type,
990        "Serial": c.serial,
991        "Subject": c.subject,
992        "Issuer": c.issuer,
993        "KeyAlgorithm": c.key_algorithm,
994        "SignatureAlgorithm": c.signature_algorithm,
995        "InUseBy": c.in_use_by,
996        "RenewalEligibility": c.renewal_eligibility,
997        "Options": {
998            "CertificateTransparencyLoggingPreference":
999                c.options.certificate_transparency_logging_preference,
1000            "Export": c.options.export,
1001        },
1002        "DomainValidationOptions": c
1003            .domain_validation
1004            .iter()
1005            .map(domain_validation_json)
1006            .collect::<Vec<_>>(),
1007        "NotBefore": c.not_before.timestamp() as f64,
1008        "NotAfter": c.not_after.timestamp() as f64,
1009        "CreatedAt": c.created_at.timestamp() as f64,
1010        "KeyUsages": [{"Name": "DIGITAL_SIGNATURE"}, {"Name": "KEY_ENCIPHERMENT"}],
1011        "ExtendedKeyUsages": [
1012            {"Name": "TLS_WEB_SERVER_AUTHENTICATION", "OID": "1.3.6.1.5.5.7.3.1"},
1013            {"Name": "TLS_WEB_CLIENT_AUTHENTICATION", "OID": "1.3.6.1.5.5.7.3.2"},
1014        ],
1015    });
1016    if let Some(t) = c.issued_at {
1017        d.as_object_mut()
1018            .unwrap()
1019            .insert("IssuedAt".to_string(), json!(t.timestamp() as f64));
1020    }
1021    if let Some(t) = c.imported_at {
1022        d.as_object_mut()
1023            .unwrap()
1024            .insert("ImportedAt".to_string(), json!(t.timestamp() as f64));
1025    }
1026    if let Some(t) = c.revoked_at {
1027        d.as_object_mut()
1028            .unwrap()
1029            .insert("RevokedAt".to_string(), json!(t.timestamp() as f64));
1030    }
1031    if let Some(r) = &c.revocation_reason {
1032        d.as_object_mut()
1033            .unwrap()
1034            .insert("RevocationReason".to_string(), json!(r));
1035    }
1036    if let Some(m) = &c.managed_by {
1037        d.as_object_mut()
1038            .unwrap()
1039            .insert("ManagedBy".to_string(), json!(m));
1040    }
1041    if let Some(ca) = &c.certificate_authority_arn {
1042        d.as_object_mut()
1043            .unwrap()
1044            .insert("CertificateAuthorityArn".to_string(), json!(ca));
1045    }
1046    d
1047}
1048
1049fn certificate_search_result_json(c: &StoredCertificate) -> Value {
1050    let san_objects: Vec<Value> = c
1051        .subject_alternative_names
1052        .iter()
1053        .map(|s| json!({ "DnsName": s }))
1054        .collect();
1055    let cn = c
1056        .subject
1057        .strip_prefix("CN=")
1058        .unwrap_or(c.subject.as_str())
1059        .to_string();
1060    json!({
1061        "CertificateArn": c.arn,
1062        "X509Attributes": {
1063            "Subject": { "CommonName": cn },
1064            "Issuer": { "CommonName": c.issuer },
1065            "SubjectAlternativeNames": san_objects,
1066            "KeyAlgorithm": c.key_algorithm,
1067            "KeyUsages": ["DIGITAL_SIGNATURE", "KEY_ENCIPHERMENT"],
1068            "ExtendedKeyUsages": ["TLS_WEB_SERVER_AUTHENTICATION", "TLS_WEB_CLIENT_AUTHENTICATION"],
1069            "SerialNumber": c.serial,
1070            "NotBefore": c.not_before.timestamp() as f64,
1071            "NotAfter": c.not_after.timestamp() as f64,
1072        },
1073        "CertificateMetadata": {
1074            "AcmCertificateMetadata": {
1075                "DomainName": c.domain_name,
1076                "Status": c.status,
1077                "Type": c.cert_type,
1078                "InUse": !c.in_use_by.is_empty(),
1079                "Exported": false,
1080                "RenewalEligibility": c.renewal_eligibility,
1081                "CreatedAt": c.created_at.timestamp() as f64,
1082                "ManagedBy": c.managed_by.clone().unwrap_or_default(),
1083                "ValidationMethod": c.validation_method.clone().unwrap_or_default(),
1084            },
1085        },
1086    })
1087}
1088
1089fn domain_validation_json(v: &DomainValidation) -> Value {
1090    let mut out = json!({
1091        "DomainName": v.domain_name,
1092        "ValidationStatus": v.validation_status,
1093        "ValidationMethod": v.validation_method,
1094    });
1095    if let (Some(name), Some(rtype), Some(value)) = (
1096        &v.resource_record_name,
1097        &v.resource_record_type,
1098        &v.resource_record_value,
1099    ) {
1100        out.as_object_mut().unwrap().insert(
1101            "ResourceRecord".to_string(),
1102            json!({
1103                "Name": name,
1104                "Type": rtype,
1105                "Value": value,
1106            }),
1107        );
1108    }
1109    out
1110}