1use 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
96impl 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 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 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 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 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 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
751fn 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
839fn 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
880fn 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 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
905fn 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}