It builds with one warning.
This commit is contained in:
1 parent
911745fda6
commit
ab42f2dad5
9 files changed
+2537
-943
No files matched your search
+219
@@ -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(())
|
||||
}
|
||||
Reference in new issue
Block a user