use std::sync::Arc; use chrono::{DateTime, Duration as ChronoDuration, Utc}; use rcgen::{ BasicConstraints, CertificateParams, DnType, ExtendedKeyUsagePurpose, IsCa, Issuer, KeyPair, KeyUsagePurpose, }; use rusqlite::Connection; use rustls::server::WebPkiClientVerifier; use rustls::RootCertStore; use rustls_pki_types::{CertificateDer, PrivateKeyDer}; use serde::{Deserialize, Serialize}; use tracing::info; use uuid::Uuid; use crate::db::{meta_set}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct JoinToken { pub token: String, pub expires_at: DateTime, pub used: bool, } #[derive(Debug, Serialize, Deserialize)] pub struct JoinClusterRequest { pub token: String, pub node_id: Uuid, #[serde(default)] pub san_list: Vec, } #[derive(Debug, Serialize, Deserialize)] pub struct JoinClusterResponse { pub ca_pem: String, pub cert_pem: String, pub key_pem: String, } // --------------------------------------------------------------------------- // Certificate Generation // --------------------------------------------------------------------------- pub fn generate_ca() -> Result<(String, String), String> { let ca_key = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256) .map_err(|e| format!("CA key error: {e}"))?; let mut ca_params = CertificateParams::new(vec![]) .map_err(|e| format!("CA params error: {e}"))?; ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); ca_params.distinguished_name.push(DnType::CommonName, "Cluster Root CA"); ca_params.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign]; let ca_cert = ca_params .self_signed(&ca_key) .map_err(|e| format!("CA self-sign error: {e}"))?; Ok((ca_cert.pem(), ca_key.serialize_pem())) } pub fn issue_leaf_cert( ca_cert_pem: &str, ca_key_pem: &str, common_name: &str, san_list: Vec, ) -> Result<(String, String), String> { let ca_key = KeyPair::from_pem(ca_key_pem) .map_err(|e| format!("Failed to parse CA private key: {e}"))?; // In rcgen 0.14+, from_ca_cert_pem belongs to Issuer: let issuer = Issuer::from_ca_cert_pem(ca_cert_pem, &ca_key) .map_err(|e| format!("Failed to parse CA cert/create issuer: {e}"))?; let leaf_key = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256) .map_err(|e| format!("Leaf key error: {e}"))?; let mut leaf_params = CertificateParams::new(san_list) .map_err(|e| format!("Leaf params error: {e}"))?; leaf_params.distinguished_name.push(DnType::CommonName, common_name); leaf_params.use_authority_key_identifier_extension = true; leaf_params.key_usages = vec![KeyUsagePurpose::DigitalSignature]; leaf_params.extended_key_usages = vec![ ExtendedKeyUsagePurpose::ServerAuth, ExtendedKeyUsagePurpose::ClientAuth, ]; let leaf_cert = leaf_params .signed_by(&leaf_key, &issuer) .map_err(|e| format!("Leaf sign error: {e}"))?; Ok((leaf_cert.pem(), leaf_key.serialize_pem())) } // --------------------------------------------------------------------------- // Rustls Server & Reqwest Client Configuration // --------------------------------------------------------------------------- fn parse_certs(pem: &str) -> Result>, String> { let mut reader = pem.as_bytes(); let certs: Result, _> = rustls_pemfile::certs(&mut reader).collect(); certs.map_err(|e| format!("failed to parse certs: {e}")) } fn parse_key(pem: &str) -> Result, String> { let mut reader = pem.as_bytes(); rustls_pemfile::private_key(&mut reader) .map_err(|e| format!("failed to parse private key: {e}"))? .ok_or_else(|| "no private key found in pem".to_string()) } pub fn create_mtls_server_config( ca_pem: &str, server_cert_pem: &str, server_key_pem: &str, ) -> Result { let mut root_store = RootCertStore::empty(); for cert in parse_certs(ca_pem)? { root_store .add(cert) .map_err(|e| format!("failed to add root cert: {e}"))?; } // allow_unauthenticated permits anonymous TLS connections (e.g., initial join requests) // while enforcing valid CA signatures on any presented client certificates. let client_verifier = WebPkiClientVerifier::builder(Arc::new(root_store)) .allow_unauthenticated() .build() .map_err(|e| format!("failed to build client verifier: {e}"))?; let cert_chain = parse_certs(server_cert_pem)?; let key = parse_key(server_key_pem)?; let mut config = rustls::ServerConfig::builder() .with_client_cert_verifier(client_verifier) .with_single_cert(cert_chain, key) .map_err(|e| format!("failed to build ServerConfig: {e}"))?; config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; Ok(config) } pub fn create_mtls_client( ca_pem: &str, client_cert_pem: &str, client_key_pem: &str, ) -> Result { let root_cert = reqwest::Certificate::from_pem(ca_pem.as_bytes()) .map_err(|e| format!("failed to load CA pem for client: {e}"))?; let identity_pem = format!("{client_cert_pem}\n{client_key_pem}"); let identity = reqwest::Identity::from_pem(identity_pem.as_bytes()) .map_err(|e| format!("failed to create client identity: {e}"))?; reqwest::Client::builder() .use_rustls_tls() .add_root_certificate(root_cert) .identity(identity) .connect_timeout(std::time::Duration::from_secs(10)) .timeout(std::time::Duration::from_secs(60)) .build() .map_err(|e| format!("failed to build reqwest client: {e}")) } // --------------------------------------------------------------------------- // Dynamic Enrollment / Join Helpers // --------------------------------------------------------------------------- pub fn create_join_token(ttl_secs: u64) -> JoinToken { JoinToken { token: Uuid::new_v4().to_string().replace('-', ""), expires_at: Utc::now() + ChronoDuration::seconds(ttl_secs as i64), used: false, } } pub async fn bootstrap_replica_join( primary_url: &str, token: &str, node_id: Uuid, db: &Connection, ) -> Result<(), String> { let base = primary_url.trim_end_matches('/'); // 1. Initial bootstrap client without mTLS client cert let bootstrap_client = reqwest::Client::builder() .danger_accept_invalid_certs(true) .build() .map_err(|e| format!("bootstrap client build failed: {e}"))?; // 2. Request enrollment let join_req = JoinClusterRequest { token: token.to_string(), node_id, san_list: vec!["localhost".into(), "127.0.0.1".into()], }; let resp = bootstrap_client .post(format!("{base}/api/cluster/join")) .json(&join_req) .send() .await .map_err(|e| format!("join request failed: {e}"))?; if !resp.status().is_success() { return Err(format!("join rejected with HTTP {}", resp.status())); } let payload: JoinClusterResponse = resp .json() .await .map_err(|e| format!("failed to decode join response: {e}"))?; // 3. Persist credentials locally meta_set(db, "tls_ca_pem", &payload.ca_pem).map_err(|e| e.to_string())?; meta_set(db, "tls_cert_pem", &payload.cert_pem).map_err(|e| e.to_string())?; meta_set(db, "tls_key_pem", &payload.key_pem).map_err(|e| e.to_string())?; info!("Replica successfully joined cluster and persisted mTLS certificates"); Ok(()) }