Files
quicproquo/crates/quicproquo-server/src/tls.rs

118 lines
3.8 KiB
Rust

use std::path::PathBuf;
use anyhow::Context;
use quinn::ServerConfig;
use quinn_proto::crypto::rustls::QuicServerConfig;
use rcgen::generate_simple_self_signed;
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
use rustls::version::TLS13;
/// Ensure a self-signed certificate exists on disk and return a QUIC server config.
/// When `production` is true, cert and key must already exist (no auto-generation).
pub fn build_server_config(
cert_path: &PathBuf,
key_path: &PathBuf,
production: bool,
) -> anyhow::Result<ServerConfig> {
if !cert_path.exists() || !key_path.exists() {
if production {
anyhow::bail!(
"TLS cert or key missing at {:?} / {:?}; production mode forbids auto-generation",
cert_path,
key_path
);
}
generate_self_signed_cert(cert_path, key_path)?;
}
let cert_bytes = std::fs::read(cert_path).context("read cert")?;
let key_bytes = std::fs::read(key_path).context("read key")?;
// Validate certificate expiry and warn about self-signed certs.
validate_certificate(&cert_bytes)?;
let cert_chain = vec![CertificateDer::from(cert_bytes)];
let key = PrivateKeyDer::try_from(key_bytes).map_err(|_| anyhow::anyhow!("invalid key"))?;
let mut tls = rustls::ServerConfig::builder_with_protocol_versions(&[&TLS13])
.with_no_client_auth()
.with_single_cert(cert_chain, key)?;
tls.alpn_protocols = vec![b"capnp".to_vec()];
let crypto = QuicServerConfig::try_from(tls)
.map_err(|e| anyhow::anyhow!("invalid server TLS config: {e}"))?;
Ok(ServerConfig::with_crypto(std::sync::Arc::new(crypto)))
}
fn generate_self_signed_cert(cert_path: &PathBuf, key_path: &PathBuf) -> anyhow::Result<()> {
if let Some(parent) = cert_path.parent() {
std::fs::create_dir_all(parent).context("create cert dir")?;
}
if let Some(parent) = key_path.parent() {
std::fs::create_dir_all(parent).context("create key dir")?;
}
let subject_alt_names = vec![
"localhost".to_string(),
"127.0.0.1".to_string(),
"::1".to_string(),
];
let issued = generate_simple_self_signed(subject_alt_names)?;
let key_der = issued.key_pair.serialize_der();
std::fs::write(cert_path, issued.cert.der()).context("write cert")?;
std::fs::write(key_path, &key_der).context("write key")?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(0o600);
std::fs::set_permissions(key_path, perms).context("set key permissions")?;
}
tracing::info!(
cert = %cert_path.display(),
key = %key_path.display(),
"generated self-signed TLS certificate"
);
Ok(())
}
/// Validate a DER-encoded X.509 certificate: bail if expired, warn if expiring
/// soon or self-signed.
fn validate_certificate(der_bytes: &[u8]) -> anyhow::Result<()> {
use x509_parser::prelude::*;
let (_, cert) = X509Certificate::from_der(der_bytes)
.map_err(|e| anyhow::anyhow!("failed to parse X.509 certificate: {e}"))?;
let validity = cert.validity();
let now = ASN1Time::now();
if !validity.is_valid_at(now) {
anyhow::bail!(
"TLS certificate expired (not_after: {})",
validity.not_after
);
}
// Warn if expiring within 30 days.
let thirty_days = std::time::Duration::from_secs(30 * 24 * 60 * 60);
let cutoff = now.timestamp() + thirty_days.as_secs() as i64;
if validity.not_after.timestamp() < cutoff {
tracing::warn!(
not_after = %validity.not_after,
"TLS certificate expires within 30 days"
);
}
// Warn if self-signed (issuer == subject).
if cert.issuer() == cert.subject() {
tracing::warn!("TLS certificate is self-signed (issuer == subject)");
}
Ok(())
}