Skip to main content

fakecloud_acm/
service.rs

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