1use crate::errors::{AuthError, Result};
26use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
27use chrono::{DateTime, Duration, Utc};
28use serde::{Deserialize, Serialize};
29use serde_json::Value;
30use std::collections::HashMap;
31use std::sync::Arc;
32use std::time::SystemTime;
33use tokio::sync::RwLock;
34use uuid::Uuid;
35use x509_parser::parse_x509_certificate;
36
37#[derive(Debug, Clone)]
39pub struct X509CertificateManager {
40 config: X509Config,
42
43 certificate_store: Arc<RwLock<HashMap<String, StoredCertificate>>>,
45
46 revocation_list: Arc<RwLock<HashMap<String, RevocationEntry>>>,
48
49 ca_certificates: Arc<RwLock<HashMap<String, CACertificate>>>,
51}
52
53#[derive(Debug, Clone)]
55pub struct X509Config {
56 pub default_validity_days: i64,
58
59 pub root_ca_cert_path: String,
61
62 pub root_ca_path: String,
64
65 pub root_ca_key_path: String,
67
68 pub intermediate_ca_cert_path: Option<String>,
70
71 pub intermediate_ca_path: Option<String>,
73
74 pub intermediate_ca_key_path: Option<String>,
76
77 pub default_rsa_key_size: u32,
79
80 pub default_ecdsa_curve: EcdsaCurve,
82
83 pub certificate_profiles: HashMap<String, CertificateProfile>,
85
86 pub enable_ocsp: bool,
88
89 pub ocsp_responder_url: Option<String>,
91
92 pub enable_crl: bool,
94
95 pub crl_distribution_url: Option<String>,
97}
98
99#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
101pub enum EcdsaCurve {
102 P256,
104 P384,
106 P521,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct CertificateProfile {
113 pub name: String,
115
116 pub cert_type: CertificateType,
118
119 pub key_usage: Vec<KeyUsage>,
121
122 pub extended_key_usage: Vec<ExtendedKeyUsage>,
124
125 pub subject_alt_names: Vec<SubjectAltName>,
127
128 pub validity_days: i64,
130
131 pub preferred_key_type: KeyType,
133
134 pub extensions: HashMap<String, Value>,
136}
137
138#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
140pub enum CertificateType {
141 RootCA,
143 IntermediateCA,
145 EndEntity,
147 CodeSigning,
149 Email,
151 TlsServer,
153 TlsClient,
155 DocumentSigning,
157}
158
159#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
161pub enum KeyUsage {
162 DigitalSignature,
164 NonRepudiation,
166 KeyEncipherment,
168 DataEncipherment,
170 KeyAgreement,
172 KeyCertSign,
174 CrlSign,
176 EncipherOnly,
178 DecipherOnly,
180}
181
182#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
184pub enum ExtendedKeyUsage {
185 ServerAuth,
187 ClientAuth,
189 CodeSigning,
191 EmailProtection,
193 TimeStamping,
195 OcspSigning,
197}
198
199#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
201pub enum SubjectAltName {
202 DnsName(String),
204 Email(String),
206 Uri(String),
208 IpAddress(String),
210}
211
212#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
214pub enum KeyType {
215 Rsa(u32), Ecdsa(EcdsaCurve),
219 Ed25519,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct StoredCertificate {
226 pub cert_id: String,
228
229 pub certificate_pem: String,
231
232 pub private_key_pem: Option<String>,
234
235 pub subject: String,
237
238 pub issuer: String,
240
241 pub serial_number: String,
243
244 pub not_before: DateTime<Utc>,
246
247 pub not_after: DateTime<Utc>,
249
250 pub profile: String,
252
253 pub status: CertificateStatus,
255
256 pub fingerprint: String,
258
259 pub created_at: DateTime<Utc>,
261
262 pub metadata: HashMap<String, Value>,
264}
265
266#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
268pub enum CertificateStatus {
269 Valid,
271 Expired,
273 Revoked,
275 Suspended,
277}
278
279#[derive(Debug, Clone)]
281pub struct CACertificate {
282 pub ca_id: String,
284
285 pub certificate: StoredCertificate,
287
288 pub subject: String,
290
291 pub private_key: Vec<u8>,
293
294 pub ca_type: CAType,
296
297 pub issued_count: u64,
299
300 pub next_serial: u64,
302}
303
304#[derive(Debug, Clone, PartialEq)]
306pub enum CAType {
307 Root,
309 Intermediate,
311}
312
313#[derive(Debug, Clone, Serialize, Deserialize)]
315pub struct RevocationEntry {
316 pub serial_number: String,
318
319 pub revocation_date: DateTime<Utc>,
321
322 pub reason: RevocationReason,
324
325 pub additional_info: Option<String>,
327}
328
329#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
331pub enum RevocationReason {
332 Unspecified,
334 KeyCompromise,
336 CaCompromise,
338 AffiliationChanged,
340 Superseded,
342 CessationOfOperation,
344 CertificateHold,
346 RemoveFromCrl,
348 PrivilegeWithdrawn,
350 AaCompromise,
352}
353
354#[derive(Debug, Clone, Serialize, Deserialize)]
356pub struct CertificateRequest {
357 pub request_id: String,
359
360 pub subject: CertificateSubject,
362
363 pub profile: String,
365
366 pub public_key_pem: String,
368
369 pub subject_alt_names: Vec<SubjectAltName>,
371
372 pub requested_at: DateTime<Utc>,
374
375 pub requester: String,
377
378 pub attributes: HashMap<String, Value>,
380}
381
382#[derive(Debug, Clone, Serialize, Deserialize)]
384pub struct CertificateSubject {
385 pub common_name: String,
387
388 pub organization: Option<String>,
390
391 pub organizational_unit: Option<String>,
393
394 pub country: Option<String>,
396
397 pub state: Option<String>,
399
400 pub locality: Option<String>,
402
403 pub email: Option<String>,
405}
406
407impl X509CertificateManager {
408 pub fn new(config: X509Config) -> Self {
410 Self {
411 config,
412 certificate_store: Arc::new(RwLock::new(HashMap::new())),
413 revocation_list: Arc::new(RwLock::new(HashMap::new())),
414 ca_certificates: Arc::new(RwLock::new(HashMap::new())),
415 }
416 }
417
418 pub async fn initialize(&self) -> Result<()> {
420 self.load_root_ca().await?;
422
423 if self.config.intermediate_ca_cert_path.is_some() {
425 self.load_intermediate_ca().await?;
426 }
427
428 Ok(())
429 }
430
431 async fn load_root_ca(&self) -> Result<()> {
433 #[cfg(feature = "hsm")]
436 if let Ok(hsm_config) = std::env::var("X509_HSM_CONFIG") {
437 tracing::info!("Loading CA certificate from HSM: {}", hsm_config);
438 return self.load_ca_from_hsm(&hsm_config).await;
440 }
441 #[cfg(not(feature = "hsm"))]
442 if std::env::var("X509_HSM_CONFIG").is_ok() {
443 tracing::warn!(
444 "X509_HSM_CONFIG is set but the 'hsm' feature is not enabled — ignoring"
445 );
446 }
447
448 if let Ok(vault_url) = std::env::var("X509_AZURE_VAULT_URL")
450 && let Ok(cert_name) = std::env::var("X509_AZURE_CERT_NAME")
451 {
452 tracing::info!("Loading CA certificate from Azure Key Vault: {}", vault_url);
453 return self.load_ca_from_azure_vault(&vault_url, &cert_name).await;
454 }
455
456 if let Ok(secret_id) = std::env::var("X509_AWS_SECRET_ID") {
458 tracing::info!(
459 "Loading CA certificate from AWS Secrets Manager: {}",
460 secret_id
461 );
462 return self.load_ca_from_aws_secrets(&secret_id).await;
463 }
464
465 let ca_cert_path = if self.config.root_ca_path.is_empty() {
467 "ca/root-ca.pem"
468 } else {
469 &self.config.root_ca_path
470 };
471
472 tracing::warn!(
473 "Loading CA certificate from file system - consider using HSM or secure vault for production"
474 );
475 self.load_ca_from_file(ca_cert_path).await
476 }
477
478 #[cfg(feature = "hsm")]
491 async fn load_ca_from_hsm(&self, hsm_config: &str) -> Result<()> {
492 let config: serde_json::Value = serde_json::from_str(hsm_config)
493 .map_err(|e| AuthError::config(format!("Invalid HSM JSON config: {}", e)))?;
494
495 let library = config["library"]
496 .as_str()
497 .ok_or_else(|| AuthError::config("HSM config missing 'library' path".to_string()))?;
498
499 let slot_id = config["slot"].as_u64().unwrap_or(0);
501 let pin = config["pin"].as_str().map(|s| s.to_string());
502 let _label = config["label"].as_str().unwrap_or("root-ca").to_string();
503
504 let library_path = library.to_string();
507
508 let handle = tokio::task::spawn_blocking(move || -> Result<Vec<u8>> {
509 let pkcs11 = cryptoki::context::Pkcs11::new(&library_path)
511 .map_err(|e| AuthError::config(format!("Failed to load PKCS#11 library: {}", e)))?;
512
513 pkcs11
514 .initialize(cryptoki::context::CInitializeArgs::new(
515 cryptoki::context::CInitializeFlags::OS_LOCKING_OK,
516 ))
517 .map_err(|e| {
518 AuthError::config(format!("Failed to initialize PKCS#11 context: {}", e))
519 })?;
520
521 let slots = pkcs11
523 .get_slots_with_token()
524 .map_err(|e| AuthError::config(format!("Failed to get PKCS#11 slots: {}", e)))?;
525
526 if slot_id as usize >= slots.len() {
527 return Err(AuthError::config(format!(
528 "HSM slot {} not found or has no token",
529 slot_id
530 )));
531 }
532 let slot = slots[slot_id as usize];
533
534 let session = pkcs11
536 .open_ro_session(slot)
537 .map_err(|e| AuthError::config(format!("Failed to open PKCS#11 session: {}", e)))?;
538
539 if let Some(p) = pin {
541 let auth_pin = cryptoki::types::AuthPin::new(p.into());
542 session
543 .login(cryptoki::session::UserType::User, Some(&auth_pin))
544 .map_err(|e| AuthError::config(format!("HSM login failed: {}", e)))?;
545 }
546
547 let mut search_template: Vec<cryptoki::object::Attribute> = Vec::new();
549 search_template.push(cryptoki::object::Attribute::Class(
550 cryptoki::object::ObjectClass::CERTIFICATE,
551 ));
552 search_template.push(cryptoki::object::Attribute::Label(
553 _label.clone().into_bytes(),
554 ));
555
556 let objects = session.find_objects(&search_template).map_err(|e| {
557 AuthError::config(format!("Failed to search PKCS#11 objects: {}", e))
558 })?;
559
560 if objects.is_empty() {
561 return Err(AuthError::config(format!(
562 "Certificate with label '{}' not found in HSM",
563 _label
564 )));
565 }
566
567 let cert_obj = objects[0];
569 let attrs = session
570 .get_attributes(cert_obj, &[cryptoki::object::AttributeType::Value])
571 .map_err(|e| {
572 AuthError::config(format!("Failed to get certificate value from HSM: {}", e))
573 })?;
574
575 if attrs.is_empty() {
576 return Err(AuthError::config(
577 "Certificate object has no value attribute".to_string(),
578 ));
579 }
580
581 let value = match &attrs[0] {
582 cryptoki::object::Attribute::Value(v) => v.clone(),
583 _ => {
584 return Err(AuthError::config(
585 "Invalid value attribute type".to_string(),
586 ));
587 }
588 };
589
590 Ok(value)
591 });
592
593 let cert_der = handle
594 .await
595 .map_err(|_| AuthError::config("HSM task panicked".to_string()))??;
596
597 let cert_pem = format!(
600 "-----BEGIN CERTIFICATE-----\n{}\n-----END CERTIFICATE-----",
601 BASE64_STANDARD.encode(&cert_der)
602 );
603
604 self.store_ca_certificate_from_pem(&cert_pem, &format!("hsm:slot{}", slot_id))
605 .await
606 }
607
608 async fn load_ca_from_azure_vault(&self, vault_url: &str, cert_name: &str) -> Result<()> {
609 let tenant_id = std::env::var("X509_AZURE_TENANT_ID").map_err(|_| {
610 AuthError::config(
611 "X509_AZURE_TENANT_ID environment variable required for Azure Key Vault authentication"
612 .to_string(),
613 )
614 })?;
615 let client_id = std::env::var("X509_AZURE_CLIENT_ID").map_err(|_| {
616 AuthError::config(
617 "X509_AZURE_CLIENT_ID environment variable required for Azure Key Vault authentication"
618 .to_string(),
619 )
620 })?;
621 let client_secret = std::env::var("X509_AZURE_CLIENT_SECRET").map_err(|_| {
622 AuthError::config(
623 "X509_AZURE_CLIENT_SECRET environment variable required for Azure Key Vault authentication"
624 .to_string(),
625 )
626 })?;
627
628 let http = reqwest::Client::new();
629
630 let token_url = format!(
632 "https://login.microsoftonline.com/{}/oauth2/v2.0/token",
633 tenant_id
634 );
635 let token_resp = http
636 .post(&token_url)
637 .form(&[
638 ("grant_type", "client_credentials"),
639 ("client_id", client_id.as_str()),
640 ("client_secret", client_secret.as_str()),
641 ("scope", "https://vault.azure.net/.default"),
642 ])
643 .send()
644 .await
645 .map_err(|e| AuthError::internal(format!("Azure AD token request failed: {}", e)))?;
646
647 if !token_resp.status().is_success() {
648 let status = token_resp.status();
649 let body = token_resp.text().await.unwrap_or_default();
650 return Err(AuthError::config(format!(
651 "Azure AD token request returned {}: {}",
652 status, body
653 )));
654 }
655
656 let token_json: serde_json::Value = token_resp.json().await.map_err(|e| {
657 AuthError::internal(format!("Failed to parse Azure AD token response: {}", e))
658 })?;
659 let access_token = token_json["access_token"]
660 .as_str()
661 .ok_or_else(|| AuthError::internal("Azure AD response missing 'access_token'"))?
662 .to_string();
663
664 let vault_base = vault_url.trim_end_matches('/');
666 let secret_url = format!("{}/secrets/{}?api-version=7.4", vault_base, cert_name);
667 let cert_resp = http
668 .get(&secret_url)
669 .header("Authorization", format!("Bearer {}", access_token))
670 .send()
671 .await
672 .map_err(|e| AuthError::internal(format!("Azure Key Vault request failed: {}", e)))?;
673
674 if !cert_resp.status().is_success() {
675 let status = cert_resp.status();
676 let body = cert_resp.text().await.unwrap_or_default();
677 return Err(AuthError::config(format!(
678 "Azure Key Vault secret fetch returned {}: {}",
679 status, body
680 )));
681 }
682
683 let cert_json: serde_json::Value = cert_resp.json().await.map_err(|e| {
684 AuthError::internal(format!("Failed to parse Azure Key Vault response: {}", e))
685 })?;
686
687 let raw_value = cert_json["value"]
688 .as_str()
689 .ok_or_else(|| AuthError::internal("Azure Key Vault response missing 'value' field"))?
690 .to_string();
691
692 let content_type = cert_json["contentType"]
693 .as_str()
694 .unwrap_or("application/x-pem-file");
695
696 let cert_pem = if content_type == "application/x-pem-file"
697 || raw_value.contains("-----BEGIN")
698 {
699 x509_extract_certificate_pem(&raw_value)
701 } else {
702 return Err(AuthError::config(format!(
703 "Azure Key Vault certificate '{}' uses content-type '{}'. \
704 Store the certificate as a PEM secret (application/x-pem-file) for automatic import.",
705 cert_name, content_type
706 )));
707 };
708
709 tracing::info!(
710 "Successfully loaded CA certificate from Azure Key Vault: {}/{}",
711 vault_base,
712 cert_name
713 );
714 self.store_ca_certificate_from_pem(
715 &cert_pem,
716 &format!("azure_kv:{}/{}", vault_base, cert_name),
717 )
718 .await
719 }
720
721 async fn load_ca_from_aws_secrets(&self, secret_id: &str) -> Result<()> {
732 let access_key = std::env::var("AWS_ACCESS_KEY_ID").map_err(|_| {
733 AuthError::config(
734 "AWS_ACCESS_KEY_ID environment variable required for Secrets Manager".to_string(),
735 )
736 })?;
737 let secret_key = std::env::var("AWS_SECRET_ACCESS_KEY").map_err(|_| {
738 AuthError::config(
739 "AWS_SECRET_ACCESS_KEY environment variable required for Secrets Manager"
740 .to_string(),
741 )
742 })?;
743 let region = std::env::var("AWS_REGION")
744 .or_else(|_| std::env::var("AWS_DEFAULT_REGION"))
745 .map_err(|_| {
746 AuthError::config(
747 "AWS_REGION (or AWS_DEFAULT_REGION) environment variable required for Secrets Manager"
748 .to_string(),
749 )
750 })?;
751 let session_token = std::env::var("AWS_SESSION_TOKEN").ok();
752
753 let service = "secretsmanager";
754 let host = format!("{}.{}.amazonaws.com", service, region);
755 let payload =
756 serde_json::to_vec(&serde_json::json!({ "SecretId": secret_id })).map_err(|e| {
757 AuthError::internal(format!(
758 "Failed to serialise Secrets Manager GetSecretValue request: {}",
759 e
760 ))
761 })?;
762
763 let now = chrono::Utc::now();
764 let amz_date = now.format("%Y%m%dT%H%M%SZ").to_string();
765 let date_stamp = now.format("%Y%m%d").to_string();
766
767 let authorization = AwsSigV4Request::new(&access_key, &secret_key)
768 .session_token(session_token.as_deref())
769 .region(®ion)
770 .service(service)
771 .method("POST")
772 .host(&host)
773 .payload(&payload)
774 .amz_date(&amz_date)
775 .date_stamp(&date_stamp)
776 .amz_target("secretsmanager.GetSecretValue")
777 .sign();
778
779 let url = format!("https://{}/", host);
780 let http = reqwest::Client::new();
781 let mut req_builder = http
782 .post(&url)
783 .header("Content-Type", "application/x-amz-json-1.1")
784 .header("X-Amz-Target", "secretsmanager.GetSecretValue")
785 .header("X-Amz-Date", &amz_date)
786 .header("Authorization", &authorization)
787 .body(payload);
788
789 if let Some(ref token) = session_token {
790 req_builder = req_builder.header("X-Amz-Security-Token", token.as_str());
791 }
792
793 let resp = req_builder.send().await.map_err(|e| {
794 AuthError::internal(format!("AWS Secrets Manager request failed: {}", e))
795 })?;
796
797 if !resp.status().is_success() {
798 let status = resp.status();
799 let body = resp.text().await.unwrap_or_default();
800 return Err(AuthError::config(format!(
801 "AWS Secrets Manager GetSecretValue returned {}: {}",
802 status, body
803 )));
804 }
805
806 let json: serde_json::Value = resp.json().await.map_err(|e| {
807 AuthError::internal(format!("Failed to parse Secrets Manager response: {}", e))
808 })?;
809
810 let raw_value = if let Some(s) = json["SecretString"].as_str() {
811 s.to_string()
812 } else if let Some(b64) = json["SecretBinary"].as_str() {
813 let bytes = BASE64_STANDARD.decode(b64).map_err(|e| {
814 AuthError::internal(format!("Failed to decode SecretBinary: {}", e))
815 })?;
816 String::from_utf8(bytes).map_err(|e| {
817 AuthError::internal(format!("SecretBinary is not valid UTF-8: {}", e))
818 })?
819 } else {
820 return Err(AuthError::config(format!(
821 "AWS Secrets Manager secret '{}' contains neither SecretString nor SecretBinary",
822 secret_id
823 )));
824 };
825
826 let cert_pem = if raw_value.contains("-----BEGIN CERTIFICATE-----") {
827 x509_extract_certificate_pem(&raw_value)
828 } else {
829 raw_value
830 };
831
832 tracing::info!(
833 "Successfully loaded CA certificate from AWS Secrets Manager: {}",
834 secret_id
835 );
836 self.store_ca_certificate_from_pem(&cert_pem, &format!("aws_secrets:{}", secret_id))
837 .await
838 }
839
840 async fn load_ca_from_file(&self, ca_cert_path: &str) -> Result<()> {
842 let (certificate_pem, subject, issuer, serial_number) = if std::path::Path::new(
843 ca_cert_path,
844 )
845 .exists()
846 {
847 let cert_content = tokio::fs::read_to_string(ca_cert_path).await.map_err(|e| {
849 AuthError::internal(format!("Failed to read CA certificate: {}", e))
850 })?;
851
852 let path = std::path::Path::new(ca_cert_path);
856 let subject = format!(
857 "CN=Loaded from {}",
858 path.file_name()
859 .map(|n| n.to_string_lossy())
860 .unwrap_or_else(|| path.to_string_lossy())
861 );
862 let issuer = subject.clone(); let serial_number = format!(
864 "{:x}",
865 cert_content
867 .bytes()
868 .fold(0u64, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u64))
869 );
870
871 (cert_content, subject, issuer, serial_number)
872 } else {
873 tracing::warn!(
875 "Root CA certificate not found at {}, generating self-signed root CA for development",
876 ca_cert_path
877 );
878
879 let (root_cert, root_key) = self.generate_self_signed_root_ca().await?;
881 let subject = "CN=AuthFramework Dev Root CA,O=Auth Framework,C=US".to_string();
882
883 if let Err(e) = tokio::fs::write(&ca_cert_path, &root_cert).await {
885 tracing::warn!("Failed to save generated root CA: {}", e);
886 }
887
888 let ca_dir = std::path::Path::new(&self.config.root_ca_cert_path)
890 .parent()
891 .map(|p| p.to_string_lossy().to_string())
892 .unwrap_or_else(|| ".".to_string());
893 let ca_key_path = format!("{}/ca.key", ca_dir);
894 if let Err(e) = tokio::fs::write(&ca_key_path, &root_key).await {
895 tracing::warn!("Failed to save generated root CA key: {}", e);
896 }
897
898 (root_cert, subject.clone(), subject, "1".to_string())
899 };
900
901 let ca_cert = StoredCertificate {
902 cert_id: "root_ca".to_string(),
903 certificate_pem: certificate_pem.clone(),
904 private_key_pem: None, subject: subject.clone(),
906 issuer,
907 serial_number,
908 not_before: Utc::now() - Duration::days(365),
909 not_after: Utc::now() + Duration::days(365 * 10), profile: "root_ca".to_string(),
911 status: CertificateStatus::Valid,
912 fingerprint: self.calculate_certificate_fingerprint(&certificate_pem)?,
913 created_at: Utc::now(),
914 metadata: HashMap::new(),
915 };
916
917 let ca = CACertificate {
918 ca_id: "root_ca".to_string(),
919 certificate: ca_cert,
920 subject: subject.clone(),
921 private_key: vec![], ca_type: CAType::Root,
923 issued_count: 0,
924 next_serial: 1000, };
926
927 let mut cas = self.ca_certificates.write().await;
928 cas.insert("root_ca".to_string(), ca);
929
930 Ok(())
931 }
932
933 async fn load_intermediate_ca(&self) -> Result<()> {
935 let intermediate_ca_path = self
937 .config
938 .intermediate_ca_path
939 .as_deref()
940 .unwrap_or("ca/intermediate-ca.pem");
941
942 if std::path::Path::new(intermediate_ca_path).exists() {
943 let cert_content = tokio::fs::read_to_string(intermediate_ca_path)
944 .await
945 .map_err(|e| {
946 AuthError::internal(format!("Failed to read intermediate CA: {}", e))
947 })?;
948
949 let intermediate_cert = StoredCertificate {
950 cert_id: "intermediate_ca".to_string(),
951 certificate_pem: cert_content.clone(),
952 private_key_pem: None,
953 subject: "CN=AuthFramework Intermediate CA, O=AuthFramework, C=US".to_string(),
954 issuer: "CN=AuthFramework Root CA, O=AuthFramework, C=US".to_string(),
955 serial_number: "2".to_string(),
956 not_before: Utc::now() - Duration::days(30),
957 not_after: Utc::now() + Duration::days(365 * 5), profile: "intermediate_ca".to_string(),
959 status: CertificateStatus::Valid,
960 fingerprint: self.calculate_fingerprint(&cert_content).await?,
961 created_at: Utc::now(),
962 metadata: HashMap::new(),
963 };
964
965 let intermediate_ca = CACertificate {
966 ca_id: "intermediate_ca".to_string(),
967 certificate: intermediate_cert,
968 subject: "CN=AuthFramework Intermediate CA".to_string(), private_key: vec![], ca_type: CAType::Intermediate,
971 issued_count: 0,
972 next_serial: 1,
973 };
974
975 let mut cas = self.ca_certificates.write().await;
976 cas.insert("intermediate_ca".to_string(), intermediate_ca);
977
978 tracing::info!("Loaded intermediate CA certificate");
979 } else {
980 tracing::info!("No intermediate CA certificate found, using root CA only");
981 }
982
983 Ok(())
984 }
985
986 pub async fn sign_certificate_request(
988 &self,
989 request: &CertificateRequest,
990 ca_id: &str,
991 ) -> Result<StoredCertificate> {
992 let ca = {
994 let cas = self.ca_certificates.read().await;
995 cas.get(ca_id)
996 .ok_or_else(|| AuthError::InvalidRequest(format!("CA not found: {}", ca_id)))?
997 .clone()
998 };
999
1000 let profile = self
1002 .config
1003 .certificate_profiles
1004 .get(&request.profile)
1005 .ok_or_else(|| {
1006 AuthError::InvalidRequest(format!(
1007 "Certificate profile not found: {}",
1008 request.profile
1009 ))
1010 })?;
1011
1012 let cert_id = Uuid::new_v4().to_string();
1014 let serial_number = self.get_next_serial_number(ca_id).await?;
1015
1016 let certificate = StoredCertificate {
1017 cert_id: cert_id.clone(),
1018 certificate_pem: self
1019 .generate_certificate_pem(request, profile, &serial_number)
1020 .await?,
1021 private_key_pem: None, subject: format!("CN={}", request.subject.common_name),
1023 issuer: ca.certificate.subject.clone(),
1024 serial_number: serial_number.clone(),
1025 not_before: Utc::now(),
1026 not_after: Utc::now() + Duration::days(profile.validity_days),
1027 profile: request.profile.clone(),
1028 status: CertificateStatus::Valid,
1029 fingerprint: self.calculate_fingerprint(&request.public_key_pem).await?,
1030 created_at: Utc::now(),
1031 metadata: HashMap::new(),
1032 };
1033
1034 let mut store = self.certificate_store.write().await;
1036 store.insert(cert_id.clone(), certificate.clone());
1037
1038 self.increment_ca_issued_count(ca_id).await?;
1040
1041 Ok(certificate)
1042 }
1043
1044 async fn generate_certificate_pem(
1049 &self,
1050 request: &CertificateRequest,
1051 profile: &CertificateProfile,
1052 serial_number: &str,
1053 ) -> Result<String> {
1054 use rcgen::{
1055 BasicConstraints, CertificateParams, DnType, ExtendedKeyUsagePurpose, IsCa,
1056 KeyUsagePurpose, SanType, SerialNumber,
1057 };
1058
1059 let mut params = CertificateParams::default();
1060
1061 params
1063 .distinguished_name
1064 .push(DnType::CommonName, &request.subject.common_name);
1065 if let Some(ref org) = request.subject.organization {
1066 params
1067 .distinguished_name
1068 .push(DnType::OrganizationName, org);
1069 }
1070 if let Some(ref ou) = request.subject.organizational_unit {
1071 params
1072 .distinguished_name
1073 .push(DnType::OrganizationalUnitName, ou);
1074 }
1075 if let Some(ref country) = request.subject.country {
1076 params.distinguished_name.push(DnType::CountryName, country);
1077 }
1078 if let Some(ref state) = request.subject.state {
1079 params
1080 .distinguished_name
1081 .push(DnType::StateOrProvinceName, state);
1082 }
1083 if let Some(ref locality) = request.subject.locality {
1084 params
1085 .distinguished_name
1086 .push(DnType::LocalityName, locality);
1087 }
1088
1089 let serial_num: u64 = serial_number.parse().unwrap_or(1);
1091 params.serial_number = Some(SerialNumber::from(serial_num.to_be_bytes().to_vec()));
1092
1093 params.not_before = time::OffsetDateTime::now_utc();
1095 params.not_after =
1096 time::OffsetDateTime::now_utc() + time::Duration::days(profile.validity_days);
1097
1098 params.key_usages = profile
1100 .key_usage
1101 .iter()
1102 .filter_map(|ku| match ku {
1103 KeyUsage::DigitalSignature => Some(KeyUsagePurpose::DigitalSignature),
1104 KeyUsage::KeyEncipherment => Some(KeyUsagePurpose::KeyEncipherment),
1105 KeyUsage::DataEncipherment => Some(KeyUsagePurpose::ContentCommitment),
1106 KeyUsage::KeyAgreement => Some(KeyUsagePurpose::KeyAgreement),
1107 KeyUsage::KeyCertSign => Some(KeyUsagePurpose::KeyCertSign),
1108 KeyUsage::CrlSign => Some(KeyUsagePurpose::CrlSign),
1109 _ => None,
1110 })
1111 .collect();
1112
1113 params.extended_key_usages = profile
1115 .extended_key_usage
1116 .iter()
1117 .map(|eku| match eku {
1118 ExtendedKeyUsage::ServerAuth => ExtendedKeyUsagePurpose::ServerAuth,
1119 ExtendedKeyUsage::ClientAuth => ExtendedKeyUsagePurpose::ClientAuth,
1120 ExtendedKeyUsage::CodeSigning => ExtendedKeyUsagePurpose::CodeSigning,
1121 ExtendedKeyUsage::EmailProtection => ExtendedKeyUsagePurpose::EmailProtection,
1122 ExtendedKeyUsage::TimeStamping => ExtendedKeyUsagePurpose::TimeStamping,
1123 ExtendedKeyUsage::OcspSigning => ExtendedKeyUsagePurpose::OcspSigning,
1124 })
1125 .collect();
1126
1127 params.subject_alt_names = request
1129 .subject_alt_names
1130 .iter()
1131 .filter_map(|san| match san {
1132 SubjectAltName::DnsName(name) => {
1133 Some(SanType::DnsName(name.clone().try_into().ok()?))
1134 }
1135 SubjectAltName::Email(email) => {
1136 Some(SanType::Rfc822Name(email.clone().try_into().ok()?))
1137 }
1138 SubjectAltName::IpAddress(ip) => ip.parse().ok().map(SanType::IpAddress),
1139 SubjectAltName::Uri(_) => None,
1140 })
1141 .collect();
1142
1143 match profile.cert_type {
1145 CertificateType::RootCA | CertificateType::IntermediateCA => {
1146 params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
1147 }
1148 _ => {
1149 params.is_ca = IsCa::NoCa;
1150 }
1151 }
1152
1153 let key_pair = rcgen::KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)
1155 .map_err(|e| AuthError::internal(format!("Key pair generation failed: {}", e)))?;
1156
1157 let ca_cert_pem = {
1159 let cas = self.ca_certificates.read().await;
1160 cas.get("root_ca")
1161 .map(|ca| ca.certificate.certificate_pem.clone())
1162 };
1163
1164 let cert = if let Some(_ca_pem) = ca_cert_pem {
1165 params
1167 .self_signed(&key_pair)
1168 .map_err(|e| AuthError::internal(format!("Certificate signing failed: {}", e)))?
1169 } else {
1170 params
1171 .self_signed(&key_pair)
1172 .map_err(|e| AuthError::internal(format!("Certificate self-sign failed: {}", e)))?
1173 };
1174
1175 Ok(cert.pem())
1176 }
1177
1178 async fn get_next_serial_number(&self, ca_id: &str) -> Result<String> {
1180 let mut cas = self.ca_certificates.write().await;
1181 let ca = cas
1182 .get_mut(ca_id)
1183 .ok_or_else(|| AuthError::InvalidRequest(format!("CA not found: {}", ca_id)))?;
1184
1185 let serial = ca.next_serial;
1186 ca.next_serial += 1;
1187
1188 Ok(serial.to_string())
1189 }
1190
1191 async fn increment_ca_issued_count(&self, ca_id: &str) -> Result<()> {
1193 let mut cas = self.ca_certificates.write().await;
1194 let ca = cas
1195 .get_mut(ca_id)
1196 .ok_or_else(|| AuthError::InvalidRequest(format!("CA not found: {}", ca_id)))?;
1197
1198 ca.issued_count += 1;
1199
1200 Ok(())
1201 }
1202
1203 async fn calculate_fingerprint(&self, certificate_pem: &str) -> Result<String> {
1205 use sha2::{Digest, Sha256};
1207
1208 let cert_data = certificate_pem
1210 .lines()
1211 .filter(|line| !line.starts_with("-----"))
1212 .collect::<Vec<&str>>()
1213 .join("");
1214
1215 let cert_bytes = BASE64_STANDARD
1217 .decode(&cert_data)
1218 .map_err(|e| AuthError::internal(format!("Invalid certificate PEM: {}", e)))?;
1219
1220 let mut hasher = Sha256::new();
1222 hasher.update(&cert_bytes);
1223 let result = hasher.finalize();
1224
1225 let fingerprint = result
1227 .iter()
1228 .map(|byte| format!("{:02X}", byte))
1229 .collect::<Vec<String>>()
1230 .join(":");
1231
1232 tracing::debug!("Calculated certificate fingerprint: {}", fingerprint);
1233 Ok(fingerprint)
1234 }
1235
1236 pub async fn revoke_certificate(
1238 &self,
1239 serial_number: &str,
1240 reason: RevocationReason,
1241 additional_info: Option<String>,
1242 ) -> Result<()> {
1243 let mut store = self.certificate_store.write().await;
1245 for cert in store.values_mut() {
1246 if cert.serial_number == serial_number {
1247 cert.status = CertificateStatus::Revoked;
1248 break;
1249 }
1250 }
1251
1252 let revocation_entry = RevocationEntry {
1254 serial_number: serial_number.to_string(),
1255 revocation_date: Utc::now(),
1256 reason,
1257 additional_info,
1258 };
1259
1260 let mut revocation_list = self.revocation_list.write().await;
1261 revocation_list.insert(serial_number.to_string(), revocation_entry);
1262
1263 Ok(())
1264 }
1265
1266 pub async fn check_certificate_status(&self, serial_number: &str) -> Result<CertificateStatus> {
1268 let revocation_list = self.revocation_list.read().await;
1270 if revocation_list.contains_key(serial_number) {
1271 return Ok(CertificateStatus::Revoked);
1272 }
1273
1274 let store = self.certificate_store.read().await;
1276 for cert in store.values() {
1277 if cert.serial_number == serial_number {
1278 if Utc::now() > cert.not_after {
1280 return Ok(CertificateStatus::Expired);
1281 }
1282 return Ok(cert.status.clone());
1283 }
1284 }
1285
1286 Err(AuthError::InvalidRequest(
1287 "Certificate not found".to_string(),
1288 ))
1289 }
1290
1291 pub async fn get_certificate(&self, cert_id: &str) -> Result<Option<StoredCertificate>> {
1293 let store = self.certificate_store.read().await;
1294 Ok(store.get(cert_id).cloned())
1295 }
1296
1297 pub async fn list_certificates(
1299 &self,
1300 filter: Option<CertificateFilter>,
1301 ) -> Result<Vec<StoredCertificate>> {
1302 let store = self.certificate_store.read().await;
1303 let mut certificates: Vec<StoredCertificate> = store.values().cloned().collect();
1304
1305 if let Some(f) = filter {
1307 certificates.retain(|cert| f.matches(cert));
1308 }
1309
1310 Ok(certificates)
1311 }
1312
1313 pub async fn generate_crl(&self, ca_id: &str) -> Result<String> {
1315 let revocation_list = self.revocation_list.read().await;
1316
1317 let cas = self.ca_certificates.read().await;
1319 let ca = cas
1320 .get(ca_id)
1321 .ok_or_else(|| AuthError::InvalidRequest(format!("CA not found: {}", ca_id)))?;
1322
1323 let crl_number = revocation_list.len() as u64;
1326 let this_update = Utc::now();
1327 let next_update = this_update + Duration::days(7); let mut crl_content = format!(
1331 "Certificate Revocation List (CRL):\n\
1332 \x20\x20\x20\x20Version 2 (0x1)\n\
1333 \x20\x20\x20\x20Signature Algorithm: sha256WithRSAEncryption\n\
1334 \x20\x20\x20\x20Issuer: {}\n\
1335 \x20\x20\x20\x20Last Update: {}\n\
1336 \x20\x20\x20\x20Next Update: {}\n\
1337 \x20\x20\x20\x20CRL Number: {}\n",
1338 ca.subject,
1339 this_update.format("%b %d %H:%M:%S %Y GMT"),
1340 next_update.format("%b %d %H:%M:%S %Y GMT"),
1341 crl_number
1342 );
1343
1344 if !revocation_list.is_empty() {
1346 crl_content.push_str("Revoked Certificates:\n");
1347 for entry in revocation_list.values() {
1348 crl_content.push_str(&format!(
1349 " Serial Number: {}\n\
1350 \x20\x20\x20\x20\x20\x20\x20\x20Revocation Date: {}\n\
1351 \x20\x20\x20\x20\x20\x20\x20\x20CRL Reason Code: {:?}\n",
1352 entry.serial_number,
1353 entry.revocation_date.format("%b %d %H:%M:%S %Y GMT"),
1354 entry.reason
1355 ));
1356 }
1357 } else {
1358 crl_content.push_str("No Revoked Certificates.\n");
1359 }
1360
1361 let crl_b64 = BASE64_STANDARD.encode(crl_content.as_bytes());
1363 let crl_pem = format!(
1364 "-----BEGIN X509 CRL-----\n{}\n-----END X509 CRL-----",
1365 crl_b64
1366 .chars()
1367 .collect::<Vec<char>>()
1368 .chunks(64)
1369 .map(|chunk| chunk.iter().collect::<String>())
1370 .collect::<Vec<String>>()
1371 .join("\n")
1372 );
1373
1374 tracing::info!(
1375 "Generated CRL for CA {} with {} revoked certificates",
1376 ca_id,
1377 revocation_list.len()
1378 );
1379 Ok(crl_pem)
1380 }
1381
1382 pub async fn validate_certificate_chain(&self, cert_pem: &str) -> Result<bool> {
1384 let cert_der = self.pem_to_der(cert_pem)?;
1386 let (_, cert) = parse_x509_certificate(&cert_der)
1387 .map_err(|e| AuthError::token(format!("Failed to parse certificate: {:?}", e)))?;
1388
1389 let now = SystemTime::now();
1394 let not_before = cert.validity().not_before.to_datetime();
1395 let not_after = cert.validity().not_after.to_datetime();
1396
1397 if now < not_before {
1398 tracing::warn!("Certificate not yet valid");
1399 return Ok(false);
1400 }
1401
1402 if now > not_after {
1403 tracing::warn!("Certificate has expired");
1404 return Ok(false);
1405 }
1406
1407 let issuer_dn = cert.issuer().to_string();
1409 let subject_dn = cert.subject().to_string();
1410
1411 let is_self_signed = issuer_dn == subject_dn;
1413
1414 if is_self_signed {
1415 let cas = self.ca_certificates.read().await;
1417 for ca in cas.values() {
1418 if ca.subject == subject_dn {
1419 tracing::info!("Certificate validated against trusted root CA");
1420 return Ok(true);
1421 }
1422 }
1423 tracing::warn!("Self-signed certificate not in trusted root store");
1424 return Ok(false);
1425 }
1426
1427 let serial_number = cert.serial.to_string();
1429 let revocation_list = self.revocation_list.read().await;
1430 if revocation_list.contains_key(&serial_number) {
1431 tracing::warn!("Certificate has been revoked: {}", serial_number);
1432 return Ok(false);
1433 }
1434
1435 tracing::info!("Certificate chain validation passed for: {}", subject_dn);
1438 Ok(true)
1439 }
1440
1441 fn pem_to_der(&self, pem: &str) -> Result<Vec<u8>> {
1443 let pem_lines: Vec<&str> = pem
1447 .lines()
1448 .filter(|line| !line.starts_with("-----"))
1449 .collect();
1450
1451 let pem_content = pem_lines.join("");
1452
1453 BASE64_STANDARD
1454 .decode(&pem_content)
1455 .map_err(|e| AuthError::internal(format!("Failed to decode PEM certificate: {}", e)))
1456 }
1457
1458 async fn generate_self_signed_root_ca(&self) -> Result<(String, String)> {
1462 use rcgen::{
1463 BasicConstraints, CertificateParams, DnType, IsCa, KeyUsagePurpose, SerialNumber,
1464 };
1465
1466 let mut params = CertificateParams::default();
1467
1468 params
1470 .distinguished_name
1471 .push(DnType::CommonName, "AuthFramework Dev Root CA");
1472 params
1473 .distinguished_name
1474 .push(DnType::OrganizationName, "Auth Framework");
1475 params.distinguished_name.push(DnType::CountryName, "US");
1476
1477 params.not_before = time::OffsetDateTime::now_utc();
1479 params.not_after = time::OffsetDateTime::now_utc() + time::Duration::days(365 * 10);
1480
1481 params.serial_number = Some(SerialNumber::from(1u64.to_be_bytes().to_vec()));
1483
1484 params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
1486 params.key_usages = vec![
1487 KeyUsagePurpose::KeyCertSign,
1488 KeyUsagePurpose::CrlSign,
1489 KeyUsagePurpose::DigitalSignature,
1490 ];
1491
1492 let key_pair = rcgen::KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)
1494 .map_err(|e| AuthError::internal(format!("CA key pair generation failed: {}", e)))?;
1495
1496 let cert = params
1497 .self_signed(&key_pair)
1498 .map_err(|e| AuthError::internal(format!("CA self-sign failed: {}", e)))?;
1499
1500 let cert_pem = cert.pem();
1501 let key_pem = key_pair.serialize_pem();
1502
1503 tracing::info!("Generated self-signed root CA certificate for development use");
1504
1505 Ok((cert_pem, key_pem))
1506 }
1507
1508 async fn store_ca_certificate_from_pem(&self, cert_pem: &str, source: &str) -> Result<()> {
1514 let fingerprint = self.calculate_certificate_fingerprint(cert_pem)?;
1515
1516 let (subject, issuer, serial_number) = match self.pem_to_der(cert_pem) {
1518 Ok(der) => match parse_x509_certificate(&der) {
1519 Ok((_, cert)) => (
1520 cert.subject().to_string(),
1521 cert.issuer().to_string(),
1522 cert.serial.to_string(),
1523 ),
1524 Err(_) => (
1525 format!("CN=Imported CA via {}", source),
1526 format!("CN=Imported CA via {}", source),
1527 "0".to_string(),
1528 ),
1529 },
1530 Err(_) => (
1531 format!("CN=Imported CA via {}", source),
1532 format!("CN=Imported CA via {}", source),
1533 "0".to_string(),
1534 ),
1535 };
1536
1537 let ca_cert = StoredCertificate {
1538 cert_id: "root_ca".to_string(),
1539 certificate_pem: cert_pem.to_string(),
1540 private_key_pem: None,
1541 subject: subject.clone(),
1542 issuer,
1543 serial_number,
1544 not_before: Utc::now() - Duration::days(365),
1545 not_after: Utc::now() + Duration::days(365 * 10),
1546 profile: "root_ca".to_string(),
1547 status: CertificateStatus::Valid,
1548 fingerprint,
1549 created_at: Utc::now(),
1550 metadata: {
1551 let mut m = HashMap::new();
1552 m.insert("source".to_string(), Value::String(source.to_string()));
1553 m
1554 },
1555 };
1556
1557 let ca = CACertificate {
1558 ca_id: "root_ca".to_string(),
1559 certificate: ca_cert,
1560 subject,
1561 private_key: vec![],
1562 ca_type: CAType::Root,
1563 issued_count: 0,
1564 next_serial: 1000,
1565 };
1566
1567 let mut cas = self.ca_certificates.write().await;
1568 cas.insert("root_ca".to_string(), ca);
1569 Ok(())
1570 }
1571
1572 fn calculate_certificate_fingerprint(&self, cert_pem: &str) -> Result<String> {
1573 use sha2::{Digest, Sha256};
1574
1575 let cert_lines: String = cert_pem
1577 .lines()
1578 .filter(|line| !line.starts_with("-----"))
1579 .collect();
1580
1581 let cert_der = BASE64_STANDARD.decode(&cert_lines).map_err(|e| {
1583 AuthError::internal(format!(
1584 "Failed to decode certificate for fingerprint: {}",
1585 e
1586 ))
1587 })?;
1588
1589 let mut hasher = Sha256::new();
1591 hasher.update(&cert_der);
1592 let hash_result = hasher.finalize();
1593
1594 let fingerprint = hash_result
1596 .iter()
1597 .map(|b| format!("{:02X}", b))
1598 .collect::<Vec<_>>()
1599 .join(":");
1600
1601 Ok(fingerprint)
1602 }
1603}
1604
1605#[derive(Debug, Clone)]
1607pub struct CertificateFilter {
1608 pub status: Option<CertificateStatus>,
1610
1611 pub profile: Option<String>,
1613
1614 pub expires_before: Option<DateTime<Utc>>,
1616
1617 pub expires_after: Option<DateTime<Utc>>,
1619
1620 pub subject_contains: Option<String>,
1622}
1623
1624impl CertificateFilter {
1625 pub fn matches(&self, cert: &StoredCertificate) -> bool {
1627 if let Some(ref status) = self.status
1628 && &cert.status != status
1629 {
1630 return false;
1631 }
1632
1633 if let Some(ref profile) = self.profile
1634 && &cert.profile != profile
1635 {
1636 return false;
1637 }
1638
1639 if let Some(expires_before) = self.expires_before
1640 && cert.not_after > expires_before
1641 {
1642 return false;
1643 }
1644
1645 if let Some(expires_after) = self.expires_after
1646 && cert.not_after < expires_after
1647 {
1648 return false;
1649 }
1650
1651 if let Some(ref subject_contains) = self.subject_contains
1652 && !cert.subject.contains(subject_contains)
1653 {
1654 return false;
1655 }
1656
1657 true
1658 }
1659}
1660
1661impl Default for X509Config {
1664 fn default() -> Self {
1665 let mut certificate_profiles = HashMap::new();
1666
1667 certificate_profiles.insert(
1669 "tls_server".to_string(),
1670 CertificateProfile {
1671 name: "TLS Server".to_string(),
1672 cert_type: CertificateType::TlsServer,
1673 key_usage: vec![KeyUsage::DigitalSignature, KeyUsage::KeyEncipherment],
1674 extended_key_usage: vec![ExtendedKeyUsage::ServerAuth],
1675 subject_alt_names: vec![],
1676 validity_days: 365,
1677 preferred_key_type: KeyType::Rsa(2048),
1678 extensions: HashMap::new(),
1679 },
1680 );
1681
1682 certificate_profiles.insert(
1683 "tls_client".to_string(),
1684 CertificateProfile {
1685 name: "TLS Client".to_string(),
1686 cert_type: CertificateType::TlsClient,
1687 key_usage: vec![KeyUsage::DigitalSignature, KeyUsage::KeyAgreement],
1688 extended_key_usage: vec![ExtendedKeyUsage::ClientAuth],
1689 subject_alt_names: vec![],
1690 validity_days: 365,
1691 preferred_key_type: KeyType::Rsa(2048),
1692 extensions: HashMap::new(),
1693 },
1694 );
1695
1696 Self {
1697 default_validity_days: 365,
1698 root_ca_cert_path: "ca/root-ca.crt".to_string(),
1699 root_ca_path: "ca/root-ca.crt".to_string(),
1700 root_ca_key_path: "ca/root-ca.key".to_string(),
1701 intermediate_ca_cert_path: None,
1702 intermediate_ca_path: None,
1703 intermediate_ca_key_path: None,
1704 default_rsa_key_size: 2048,
1705 default_ecdsa_curve: EcdsaCurve::P256,
1706 certificate_profiles,
1707 enable_ocsp: false,
1708 ocsp_responder_url: None,
1709 enable_crl: true,
1710 crl_distribution_url: Some("https://example.com/crl".to_string()),
1711 }
1712 }
1713}
1714
1715fn x509_extract_certificate_pem(pem: &str) -> String {
1721 let mut in_cert = false;
1722 let mut lines: Vec<&str> = Vec::new();
1723 let mut collected = false;
1724
1725 for line in pem.lines() {
1726 if line.starts_with("-----BEGIN CERTIFICATE-----") {
1727 if collected {
1728 break; }
1730 in_cert = true;
1731 collected = true;
1732 lines.push(line);
1733 } else if line.starts_with("-----END CERTIFICATE-----") {
1734 lines.push(line);
1735 in_cert = false;
1736 } else if in_cert {
1737 lines.push(line);
1738 }
1739 }
1740
1741 if collected {
1742 lines.join("\n") + "\n"
1743 } else {
1744 pem.to_string()
1745 }
1746}
1747
1748struct AwsSigV4Request<'a> {
1766 access_key: &'a str,
1767 secret_key: &'a str,
1768 session_token: Option<&'a str>,
1769 region: &'a str,
1770 service: &'a str,
1771 method: &'a str,
1772 host: &'a str,
1773 path: &'a str,
1774 query: &'a str,
1775 payload: &'a [u8],
1776 amz_date: &'a str,
1777 date_stamp: &'a str,
1778 amz_target: &'a str,
1779}
1780
1781impl<'a> AwsSigV4Request<'a> {
1782 fn new(access_key: &'a str, secret_key: &'a str) -> Self {
1784 Self {
1785 access_key,
1786 secret_key,
1787 session_token: None,
1788 region: "us-east-1",
1789 service: "",
1790 method: "POST",
1791 host: "",
1792 path: "/",
1793 query: "",
1794 payload: b"",
1795 amz_date: "",
1796 date_stamp: "",
1797 amz_target: "",
1798 }
1799 }
1800
1801 fn session_token(mut self, token: Option<&'a str>) -> Self {
1802 self.session_token = token;
1803 self
1804 }
1805
1806 fn region(mut self, region: &'a str) -> Self {
1807 self.region = region;
1808 self
1809 }
1810
1811 fn service(mut self, service: &'a str) -> Self {
1812 self.service = service;
1813 self
1814 }
1815
1816 fn method(mut self, method: &'a str) -> Self {
1817 self.method = method;
1818 self
1819 }
1820
1821 fn host(mut self, host: &'a str) -> Self {
1822 self.host = host;
1823 self
1824 }
1825
1826 fn payload(mut self, payload: &'a [u8]) -> Self {
1827 self.payload = payload;
1828 self
1829 }
1830
1831 fn amz_date(mut self, amz_date: &'a str) -> Self {
1832 self.amz_date = amz_date;
1833 self
1834 }
1835
1836 fn date_stamp(mut self, date_stamp: &'a str) -> Self {
1837 self.date_stamp = date_stamp;
1838 self
1839 }
1840
1841 fn amz_target(mut self, amz_target: &'a str) -> Self {
1842 self.amz_target = amz_target;
1843 self
1844 }
1845
1846 fn sign(&self) -> String {
1850 use hmac::{Mac, SimpleHmac};
1851 use sha2::{Digest, Sha256};
1852
1853 fn hmac_sha256(key: &[u8], data: &[u8]) -> Vec<u8> {
1854 let mut mac =
1855 <SimpleHmac<Sha256>>::new_from_slice(key).expect("HMAC accepts any key size");
1856 mac.update(data);
1857 mac.finalize().into_bytes().to_vec()
1858 }
1859
1860 fn sha256hex(data: &[u8]) -> String {
1861 let mut h = Sha256::new();
1862 h.update(data);
1863 hex::encode(h.finalize())
1864 }
1865
1866 let mut headers: Vec<(String, String)> = vec![
1867 ("content-type".into(), "application/x-amz-json-1.1".into()),
1868 ("host".into(), self.host.into()),
1869 ("x-amz-date".into(), self.amz_date.into()),
1870 ("x-amz-target".into(), self.amz_target.into()),
1871 ];
1872 if let Some(tok) = self.session_token {
1873 headers.push(("x-amz-security-token".into(), tok.into()));
1874 }
1875 headers.sort_by(|a, b| a.0.cmp(&b.0));
1876
1877 let canonical_headers: String = headers
1878 .iter()
1879 .map(|(k, v)| format!("{}:{}\n", k, v.trim()))
1880 .collect();
1881 let signed_headers: String = headers
1882 .iter()
1883 .map(|(k, _)| k.as_str())
1884 .collect::<Vec<_>>()
1885 .join(";");
1886
1887 let canonical_request = format!(
1888 "{method}\n{path}\n{query}\n{canonical_headers}\n{signed_headers}\n{payload_hash}",
1889 method = self.method,
1890 path = self.path,
1891 query = self.query,
1892 canonical_headers = canonical_headers,
1893 signed_headers = signed_headers,
1894 payload_hash = sha256hex(self.payload),
1895 );
1896
1897 let credential_scope =
1898 format!("{}/{}/{}/aws4_request", self.date_stamp, self.region, self.service);
1899 let string_to_sign = format!(
1900 "AWS4-HMAC-SHA256\n{amz_date}\n{credential_scope}\n{canonical_hash}",
1901 amz_date = self.amz_date,
1902 credential_scope = credential_scope,
1903 canonical_hash = sha256hex(canonical_request.as_bytes()),
1904 );
1905
1906 let signing_key = hmac_sha256(
1907 &hmac_sha256(
1908 &hmac_sha256(
1909 &hmac_sha256(
1910 format!("AWS4{}", self.secret_key).as_bytes(),
1911 self.date_stamp.as_bytes(),
1912 ),
1913 self.region.as_bytes(),
1914 ),
1915 self.service.as_bytes(),
1916 ),
1917 b"aws4_request",
1918 );
1919
1920 let signature = hex::encode(hmac_sha256(&signing_key, string_to_sign.as_bytes()));
1921
1922 format!(
1923 "AWS4-HMAC-SHA256 Credential={access_key}/{credential_scope}, SignedHeaders={signed_headers}, Signature={signature}",
1924 access_key = self.access_key,
1925 credential_scope = credential_scope,
1926 signed_headers = signed_headers,
1927 signature = signature,
1928 )
1929 }
1930}
1931
1932#[cfg(test)]
1933mod tests {
1934 use super::*;
1935
1936 #[tokio::test]
1937 async fn test_x509_manager_creation() {
1938 let config = X509Config::default();
1939 let manager = X509CertificateManager::new(config);
1940
1941 assert!(!manager.config.certificate_profiles.is_empty());
1943 assert_eq!(manager.config.default_validity_days, 365);
1944 }
1945
1946 #[tokio::test]
1947 async fn test_certificate_profile() {
1948 let config = X509Config::default();
1949
1950 assert!(config.certificate_profiles.contains_key("tls_server"));
1952 assert!(config.certificate_profiles.contains_key("tls_client"));
1953
1954 let tls_server_profile = &config.certificate_profiles["tls_server"];
1955 assert_eq!(tls_server_profile.cert_type, CertificateType::TlsServer);
1956 assert!(
1957 tls_server_profile
1958 .extended_key_usage
1959 .contains(&ExtendedKeyUsage::ServerAuth)
1960 );
1961 }
1962
1963 #[tokio::test]
1964 async fn test_certificate_filter() {
1965 let filter = CertificateFilter {
1966 status: Some(CertificateStatus::Valid),
1967 profile: None,
1968 expires_before: None,
1969 expires_after: None,
1970 subject_contains: Some("example.com".to_string()),
1971 };
1972
1973 let cert = StoredCertificate {
1974 cert_id: "test".to_string(),
1975 certificate_pem: "".to_string(),
1976 private_key_pem: None,
1977 subject: "CN=example.com".to_string(),
1978 issuer: "CN=Test CA".to_string(),
1979 serial_number: "123".to_string(),
1980 not_before: Utc::now(),
1981 not_after: Utc::now() + Duration::days(365),
1982 profile: "tls_server".to_string(),
1983 status: CertificateStatus::Valid,
1984 fingerprint: "test_fp".to_string(),
1985 created_at: Utc::now(),
1986 metadata: HashMap::new(),
1987 };
1988
1989 assert!(filter.matches(&cert));
1990 }
1991
1992 #[test]
1995 fn test_x509_extract_certificate_pem_single_cert() {
1996 let pem = "-----BEGIN CERTIFICATE-----\nMIIBxx==\n-----END CERTIFICATE-----\n";
1997 let extracted = x509_extract_certificate_pem(pem);
1998 assert!(extracted.contains("-----BEGIN CERTIFICATE-----"));
1999 assert!(extracted.contains("-----END CERTIFICATE-----"));
2000 assert!(extracted.contains("MIIBxx=="));
2001 }
2002
2003 #[test]
2004 fn test_x509_extract_certificate_pem_strips_key() {
2005 let bundle = concat!(
2007 "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAK==\n-----END RSA PRIVATE KEY-----\n",
2008 "-----BEGIN CERTIFICATE-----\nMIICert==\n-----END CERTIFICATE-----\n",
2009 );
2010 let extracted = x509_extract_certificate_pem(bundle);
2011 assert!(
2012 !extracted.contains("PRIVATE KEY"),
2013 "Private key must be stripped"
2014 );
2015 assert!(extracted.contains("-----BEGIN CERTIFICATE-----"));
2016 assert!(extracted.contains("MIICert=="));
2017 }
2018
2019 #[test]
2020 fn test_x509_extract_certificate_pem_keeps_first_only() {
2021 let bundle = concat!(
2022 "-----BEGIN CERTIFICATE-----\nMIIFirst==\n-----END CERTIFICATE-----\n",
2023 "-----BEGIN CERTIFICATE-----\nMIISecond==\n-----END CERTIFICATE-----\n",
2024 );
2025 let extracted = x509_extract_certificate_pem(bundle);
2026 assert!(
2027 extracted.contains("MIIFirst=="),
2028 "First cert should be kept"
2029 );
2030 assert!(
2031 !extracted.contains("MIISecond=="),
2032 "Second cert must be discarded"
2033 );
2034 }
2035
2036 #[test]
2037 fn test_aws_sigv4_authorization_format() {
2038 let auth = AwsSigV4Request::new(
2040 "AKIAIOSFODNN7EXAMPLE",
2041 "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
2042 )
2043 .region("us-east-1")
2044 .service("secretsmanager")
2045 .method("POST")
2046 .host("secretsmanager.us-east-1.amazonaws.com")
2047 .payload(b"{\"SecretId\":\"my-secret\"}")
2048 .amz_date("20230101T000000Z")
2049 .date_stamp("20230101")
2050 .amz_target("secretsmanager.GetSecretValue")
2051 .sign();
2052 assert!(auth.starts_with("AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20230101/"));
2053 assert!(auth.contains("SignedHeaders="));
2054 assert!(auth.contains("Signature="));
2055 let sig_part = auth.split("Signature=").nth(1).unwrap_or("");
2057 assert_eq!(sig_part.len(), 64, "SigV4 signature must be 64 hex chars");
2058 }
2059
2060 #[tokio::test]
2061 async fn test_azure_vault_missing_tenant_id() {
2062 if std::env::var("X509_AZURE_TENANT_ID").is_ok() {
2065 return;
2066 }
2067 let config = X509Config::default();
2068 let manager = X509CertificateManager::new(config);
2069 let result = manager
2070 .load_ca_from_azure_vault("https://test.vault.azure.net", "my-ca")
2071 .await;
2072 assert!(result.is_err(), "Should fail when tenant_id is not set");
2073 let msg = format!("{}", result.unwrap_err());
2074 assert!(
2075 msg.contains("X509_AZURE_TENANT_ID"),
2076 "Error should name the missing variable: {msg}"
2077 );
2078 }
2079
2080 #[tokio::test]
2081 async fn test_aws_secrets_missing_access_key() {
2082 if std::env::var("AWS_ACCESS_KEY_ID").is_ok() {
2084 return;
2085 }
2086 let config = X509Config::default();
2087 let manager = X509CertificateManager::new(config);
2088 let result = manager.load_ca_from_aws_secrets("my-ca-cert").await;
2089 assert!(
2090 result.is_err(),
2091 "Should fail when AWS_ACCESS_KEY_ID is not set"
2092 );
2093 let msg = format!("{}", result.unwrap_err());
2094 assert!(
2095 msg.contains("AWS_ACCESS_KEY_ID"),
2096 "Error should name the missing variable: {msg}"
2097 );
2098 }
2099
2100 #[tokio::test]
2101 #[cfg(feature = "hsm")]
2102 async fn test_hsm_invalid_json_config() {
2103 let config = X509Config::default();
2104 let manager = X509CertificateManager::new(config);
2105 let result = manager.load_ca_from_hsm("not-valid-json").await;
2107 assert!(result.is_err());
2108 let msg = format!("{}", result.unwrap_err());
2109 assert!(
2110 msg.contains("JSON") || msg.contains("json") || msg.contains("X509_HSM_CONFIG"),
2111 "Error should mention JSON parsing: {msg}"
2112 );
2113 }
2114
2115 #[tokio::test]
2116 #[cfg(feature = "hsm")]
2117 async fn test_hsm_missing_library_field() {
2118 let config = X509Config::default();
2119 let manager = X509CertificateManager::new(config);
2120 let result = manager
2122 .load_ca_from_hsm(r#"{"slot": 0, "pin": "1234", "label": "ca-cert"}"#)
2123 .await;
2124 assert!(result.is_err());
2125 let msg = format!("{}", result.unwrap_err());
2126 assert!(
2127 msg.contains("library"),
2128 "Error should mention the missing 'library' field: {msg}"
2129 );
2130 }
2131
2132 #[tokio::test]
2133 #[cfg(feature = "hsm")]
2134 async fn test_hsm_nonexistent_library_path() {
2135 let config = X509Config::default();
2136 let manager = X509CertificateManager::new(config);
2137 let result = manager
2138 .load_ca_from_hsm(
2139 r#"{"library": "/nonexistent/pkcs11/libpkcs11.so", "slot": 0, "pin": "", "label": "ca-cert"}"#,
2140 )
2141 .await;
2142 assert!(
2144 result.is_err(),
2145 "Expected error loading non-existent PKCS#11 library"
2146 );
2147 }
2148}