118 lines
3.8 KiB
Rust
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(())
|
|
}
|