use std::path::PathBuf;
use rustls::pki_types::CertificateDer;
use sha2::{Digest, Sha256};
#[derive(Debug, Clone)]
pub enum ServerTlsConfig {
Insecure,
SelfSigned { state_dir: PathBuf },
Provided { cert: PathBuf, key: PathBuf },
Mtls {
cert: PathBuf,
key: PathBuf,
ca: PathBuf,
},
}
#[derive(Debug, Clone)]
pub enum ClientTlsConfig {
Insecure,
Fingerprint {
sha256: [u8; 32],
server_name: Option<String>,
},
Ca {
ca: PathBuf,
server_name: Option<String>,
},
Mtls {
ca: PathBuf,
cert: PathBuf,
key: PathBuf,
server_name: Option<String>,
},
}
pub fn cert_sha256(cert: &CertificateDer<'_>) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(cert.as_ref());
hasher.finalize().into()
}
pub fn format_fingerprint(digest: &[u8; 32]) -> String {
let mut s = String::with_capacity(7 + 64);
s.push_str("sha256:");
for b in digest {
use std::fmt::Write as _;
let _ = write!(s, "{b:02x}");
}
s
}
pub fn parse_fingerprint(s: &str) -> Result<[u8; 32], String> {
let trimmed = s.trim();
let body = trimmed
.strip_prefix("sha256:")
.or_else(|| trimmed.strip_prefix("SHA256:"))
.unwrap_or(trimmed);
let cleaned: String = body.chars().filter(|c| *c != ':').collect();
if cleaned.len() != 64 {
return Err(format!(
"expected 64 hex chars (32 bytes) in fingerprint, got {}",
cleaned.len()
));
}
let mut out = [0u8; 32];
for (i, byte) in out.iter_mut().enumerate() {
let chunk = &cleaned[i * 2..i * 2 + 2];
*byte = u8::from_str_radix(chunk, 16)
.map_err(|e| format!("invalid hex `{chunk}` at byte {i}: {e}"))?;
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fingerprint_roundtrip() {
let digest = [0xabu8; 32];
let s = format_fingerprint(&digest);
assert_eq!(s, format!("sha256:{}", "ab".repeat(32)));
assert_eq!(parse_fingerprint(&s).unwrap(), digest);
}
#[test]
fn fingerprint_accepts_bare_hex() {
let digest = [0x12u8; 32];
let bare = "12".repeat(32);
assert_eq!(parse_fingerprint(&bare).unwrap(), digest);
}
#[test]
fn fingerprint_accepts_colon_separators() {
let digest = [0x42u8; 32];
let with_colons = std::iter::repeat_n("42", 32).collect::<Vec<_>>().join(":");
assert_eq!(parse_fingerprint(&with_colons).unwrap(), digest);
}
#[test]
fn fingerprint_rejects_wrong_length() {
assert!(parse_fingerprint("sha256:deadbeef").is_err());
}
#[test]
fn fingerprint_rejects_non_hex() {
let s = format!("sha256:{}", "zz".repeat(32));
assert!(parse_fingerprint(&s).is_err());
}
}