use std::fs;
use std::io;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use rcgen::{
BasicConstraints, CertificateParams, DistinguishedName, DnType, ExtendedKeyUsagePurpose, IsCa,
Issuer, KeyPair, KeyUsagePurpose, SanType,
};
use time::{Duration as TimeDuration, OffsetDateTime};
pub const CA_CERT_FILE: &str = "ca-cert.pem";
const CA_KEY_FILE: &str = "ca-key.pem";
const LEAF_CERT_FILE: &str = "leaf-cert.pem";
const LEAF_KEY_FILE: &str = "leaf-key.pem";
const LEAF_CERT_LIFETIME_DAYS: i64 = 30;
const CA_CERT_LIFETIME_DAYS: i64 = 3650;
const NOT_BEFORE_SLACK_HOURS: i64 = 1;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive] pub enum TlsError {
#[error("failed to create TLS material directory {path}: {source}")]
CreateDir { path: PathBuf, source: io::Error },
#[error("failed to read {path}: {source}")]
Read { path: PathBuf, source: io::Error },
#[error("failed to write {path}: {source}")]
Write { path: PathBuf, source: io::Error },
#[error("failed to set permissions on {path}: {source}")]
Permissions { path: PathBuf, source: io::Error },
#[error("certificate generation failed: {0}")]
Rcgen(#[from] rcgen::Error),
#[error("invalid generated TLS certificate/key material: {0}")]
Rustls(#[from] rustls::Error),
#[error("failed to parse generated leaf certificate PEM: {0}")]
PemCert(io::Error),
#[error("failed to parse generated leaf private key PEM: {0}")]
PemKey(io::Error),
#[error("generated leaf material contained no private key")]
NoPrivateKey,
}
pub struct TlsMaterial {
pub cert_pem: Vec<u8>,
pub key_pem: Vec<u8>,
pub ca_cert_path: PathBuf,
}
#[must_use]
pub fn default_tls_dir() -> PathBuf {
let home = std::env::var_os("HOME")
.or_else(|| std::env::var_os("USERPROFILE"))
.filter(|h| !h.is_empty());
match home {
Some(home) => Path::new(&home).join(".velesdb-memory-tls"),
None => PathBuf::from("./velesdb-memory-tls"),
}
}
#[must_use]
pub fn tls_dir_from_env() -> PathBuf {
std::env::var_os("VELESDB_MEMORY_TLS_DIR").map_or_else(default_tls_dir, PathBuf::from)
}
pub fn ensure_tls_material(dir: &Path) -> Result<TlsMaterial, TlsError> {
create_private_dir(dir)?;
let issuer = ensure_ca(dir)?;
let (cert_pem, key_pem) = issue_leaf_cert(dir, &issuer)?;
Ok(TlsMaterial {
cert_pem,
key_pem,
ca_cert_path: dir.join(CA_CERT_FILE),
})
}
pub fn tls_acceptor_from_material(
material: &TlsMaterial,
) -> Result<tokio_rustls::TlsAcceptor, TlsError> {
let certs: Vec<_> = rustls_pemfile::certs(&mut &material.cert_pem[..])
.collect::<Result<Vec<_>, _>>()
.map_err(TlsError::PemCert)?;
let key = rustls_pemfile::private_key(&mut &material.key_pem[..])
.map_err(TlsError::PemKey)?
.ok_or(TlsError::NoPrivateKey)?;
let provider = Arc::new(rustls::crypto::ring::default_provider());
let config = rustls::ServerConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()?
.with_no_client_auth()
.with_single_cert(certs, key)?;
Ok(tokio_rustls::TlsAcceptor::from(Arc::new(config)))
}
fn create_private_dir(dir: &Path) -> Result<(), TlsError> {
fs::create_dir_all(dir).map_err(|source| TlsError::CreateDir {
path: dir.to_owned(),
source,
})?;
set_permissions(dir, 0o700)
}
fn load_existing_ca(dir: &Path) -> Result<Option<Issuer<'static, KeyPair>>, TlsError> {
let ca_cert_path = dir.join(CA_CERT_FILE);
let ca_key_path = dir.join(CA_KEY_FILE);
if !ca_cert_path.exists() || !ca_key_path.exists() {
return Ok(None);
}
let ca_cert_pem = read_to_string(&ca_cert_path)?;
let ca_key_pem = read_to_string(&ca_key_path)?;
let key_pair = KeyPair::from_pem(&ca_key_pem)?;
Ok(Some(Issuer::from_ca_cert_pem(&ca_cert_pem, key_pair)?))
}
fn generate_new_ca(dir: &Path) -> Result<Issuer<'static, KeyPair>, TlsError> {
let key_pair = KeyPair::generate()?;
let mut params = CertificateParams::new(Vec::<String>::new())?;
params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
params.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign];
params.not_before = OffsetDateTime::now_utc() - TimeDuration::hours(NOT_BEFORE_SLACK_HOURS);
params.not_after = OffsetDateTime::now_utc() + TimeDuration::days(CA_CERT_LIFETIME_DAYS);
let mut dn = DistinguishedName::new();
dn.push(DnType::CommonName, "VelesDB Memory Local CA");
dn.push(DnType::OrganizationName, "VelesDB Memory (local)");
params.distinguished_name = dn;
let ca_cert = params.self_signed(&key_pair)?;
write_file(&dir.join(CA_CERT_FILE), ca_cert.pem().as_bytes(), 0o644)?;
write_file(
&dir.join(CA_KEY_FILE),
key_pair.serialize_pem().as_bytes(),
0o600,
)?;
Ok(Issuer::new(params, key_pair))
}
fn ensure_ca(dir: &Path) -> Result<Issuer<'static, KeyPair>, TlsError> {
match load_existing_ca(dir)? {
Some(issuer) => Ok(issuer),
None => generate_new_ca(dir),
}
}
fn issue_leaf_cert(
dir: &Path,
issuer: &Issuer<'_, KeyPair>,
) -> Result<(Vec<u8>, Vec<u8>), TlsError> {
let leaf_key = KeyPair::generate()?;
let mut params = CertificateParams::new(vec!["localhost".to_owned()])?;
params.subject_alt_names = vec![
SanType::DnsName("localhost".try_into()?),
SanType::IpAddress(IpAddr::V4(Ipv4Addr::LOCALHOST)),
SanType::IpAddress(IpAddr::V6(Ipv6Addr::LOCALHOST)),
];
let mut dn = DistinguishedName::new();
dn.push(DnType::CommonName, "localhost");
params.distinguished_name = dn;
params.is_ca = IsCa::NoCa;
params.key_usages = vec![
KeyUsagePurpose::DigitalSignature,
KeyUsagePurpose::KeyEncipherment,
];
params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
params.not_before = OffsetDateTime::now_utc() - TimeDuration::hours(NOT_BEFORE_SLACK_HOURS);
params.not_after = OffsetDateTime::now_utc() + TimeDuration::days(LEAF_CERT_LIFETIME_DAYS);
let cert = params.signed_by(&leaf_key, issuer)?;
let cert_pem = cert.pem().into_bytes();
let key_pem = leaf_key.serialize_pem().into_bytes();
write_file(&dir.join(LEAF_CERT_FILE), &cert_pem, 0o644)?;
write_file(&dir.join(LEAF_KEY_FILE), &key_pem, 0o600)?;
Ok((cert_pem, key_pem))
}
fn read_to_string(path: &Path) -> Result<String, TlsError> {
fs::read_to_string(path).map_err(|source| TlsError::Read {
path: path.to_owned(),
source,
})
}
fn write_file(path: &Path, bytes: &[u8], mode: u32) -> Result<(), TlsError> {
fs::write(path, bytes).map_err(|source| TlsError::Write {
path: path.to_owned(),
source,
})?;
set_permissions(path, mode)
}
#[cfg(unix)]
fn set_permissions(path: &Path, mode: u32) -> Result<(), TlsError> {
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(path, fs::Permissions::from_mode(mode)).map_err(|source| {
TlsError::Permissions {
path: path.to_owned(),
source,
}
})
}
#[cfg(not(unix))]
#[allow(clippy::unnecessary_wraps)]
fn set_permissions(_path: &Path, _mode: u32) -> Result<(), TlsError> {
Ok(())
}
#[cfg(test)]
#[path = "tls_tests.rs"]
mod tests;