It builds with one warning.

This commit is contained in:
Kalzu Rekku committed 2026-10-26 12:24:27 +02:00
1 parent 911745fda6
commit ab42f2dad5
9 files changed
+2537 -943

No files matched your search

+219
View File
@@ -0,0 +1,219 @@
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<Utc>,
pub used: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct JoinClusterRequest {
pub token: String,
pub node_id: Uuid,
#[serde(default)]
pub san_list: Vec<String>,
}
#[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<String>,
) -> 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<Vec<CertificateDer<'static>>, String> {
let mut reader = pem.as_bytes();
let certs: Result<Vec<_>, _> = rustls_pemfile::certs(&mut reader).collect();
certs.map_err(|e| format!("failed to parse certs: {e}"))
}
fn parse_key(pem: &str) -> Result<PrivateKeyDer<'static>, 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<rustls::ServerConfig, String> {
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<reqwest::Client, String> {
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(())
}