use std::fmt;
use std::fs;
use std::io::{self, BufReader, Cursor};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use rustls::RootCertStore;
use rustls::client::{VerifierBuilderError, WebPkiServerVerifier, danger::ServerCertVerifier};
use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName, UnixTime};
use x509_parser::parse_x509_certificate;
use crate::quic::{QuicConfigError, make_server_quic_config};
pub(crate) const SERVER_CERT_FILENAME: &str = "server.crt";
pub(crate) const SERVER_KEY_FILENAME: &str = "server.key";
#[derive(Debug)]
pub(crate) enum TlsMaterialError {
ReadFile {
path: PathBuf,
source: io::Error,
},
MissingCertificate {
path: PathBuf,
},
MissingPrivateKey {
path: PathBuf,
},
ParsePem {
path: PathBuf,
source: io::Error,
},
ParseX509 {
path: PathBuf,
},
AddRootCertificate {
path: PathBuf,
source: rustls::Error,
},
BuildServerVerifier(VerifierBuilderError),
InvalidServerName {
server_name: String,
},
InvalidCertificateAuthority {
path: PathBuf,
},
InvalidServerCertificate {
server_name: String,
source: rustls::Error,
},
InvalidConfiguration(QuicConfigError),
}
impl fmt::Display for TlsMaterialError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ReadFile { path, .. } => {
write!(formatter, "failed to read {}", path.display())
}
Self::MissingCertificate { path } => {
write!(formatter, "no certificates found in {}", path.display())
}
Self::MissingPrivateKey { path } => {
write!(formatter, "no private key found in {}", path.display())
}
Self::ParsePem { path, .. } => {
write!(formatter, "failed to parse PEM in {}", path.display())
}
Self::ParseX509 { path } => {
write!(formatter, "failed to parse X.509 DER in {}", path.display())
}
Self::AddRootCertificate { path, .. } => {
write!(
formatter,
"failed to load root certificate from {}",
path.display()
)
}
Self::BuildServerVerifier(_) => {
formatter.write_str("failed to build the server certificate verifier")
}
Self::InvalidServerName { server_name } => {
write!(
formatter,
"server hostname is not a valid DNS name: {server_name}"
)
}
Self::InvalidCertificateAuthority { path } => write!(
formatter,
"{} must contain a CA certificate that issued the server certificate",
path.display()
),
Self::InvalidServerCertificate { server_name, .. } => write!(
formatter,
"server certificate is not valid for {server_name}"
),
Self::InvalidConfiguration(source) => write!(formatter, "{source}"),
}
}
}
impl std::error::Error for TlsMaterialError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::ReadFile { source, .. } => Some(source),
Self::ParsePem { source, .. } => Some(source),
Self::AddRootCertificate { source, .. } => Some(source),
Self::BuildServerVerifier(source) => Some(source),
Self::InvalidServerCertificate { source, .. } => Some(source),
Self::InvalidConfiguration(source) => Some(source),
Self::MissingCertificate { .. }
| Self::MissingPrivateKey { .. }
| Self::ParseX509 { .. }
| Self::InvalidServerName { .. }
| Self::InvalidCertificateAuthority { .. } => None,
}
}
}
pub(crate) fn load_certificate_chain(
path: &Path,
) -> Result<Vec<CertificateDer<'static>>, TlsMaterialError> {
let bytes = fs::read(path).map_err(|source| TlsMaterialError::ReadFile {
path: path.to_path_buf(),
source,
})?;
let mut reader = BufReader::new(Cursor::new(bytes));
let certs = rustls_pemfile::certs(&mut reader)
.collect::<Result<Vec<_>, _>>()
.map_err(|source| TlsMaterialError::ParsePem {
path: path.to_path_buf(),
source,
})?;
if certs.is_empty() {
return Err(TlsMaterialError::MissingCertificate {
path: path.to_path_buf(),
});
}
Ok(certs)
}
pub(crate) fn load_private_key(path: &Path) -> Result<PrivateKeyDer<'static>, TlsMaterialError> {
let bytes = fs::read(path).map_err(|source| TlsMaterialError::ReadFile {
path: path.to_path_buf(),
source,
})?;
let mut reader = BufReader::new(Cursor::new(bytes));
let private_key =
rustls_pemfile::private_key(&mut reader).map_err(|source| TlsMaterialError::ParsePem {
path: path.to_path_buf(),
source,
})?;
private_key.ok_or_else(|| TlsMaterialError::MissingPrivateKey {
path: path.to_path_buf(),
})
}
pub(crate) fn validate_server_tls_material(
cert_file: &Path,
key_file: &Path,
ca_file: &Path,
server_hostname: &str,
) -> Result<(), TlsMaterialError> {
let cert_chain = load_certificate_chain(cert_file)?;
let private_key = load_private_key(key_file)?;
make_server_quic_config(cert_chain.clone(), private_key)
.map_err(TlsMaterialError::InvalidConfiguration)?;
let ca_certificates = load_certificate_chain(ca_file)?;
let mut roots = RootCertStore::empty();
let mut has_certificate_authority = false;
for ca_certificate in &ca_certificates {
roots.add(ca_certificate.clone()).map_err(|source| {
TlsMaterialError::AddRootCertificate {
path: ca_file.to_path_buf(),
source,
}
})?;
let (_, parsed_ca) = parse_x509_certificate(ca_certificate.as_ref()).map_err(|_| {
TlsMaterialError::ParseX509 {
path: ca_file.to_path_buf(),
}
})?;
let is_certificate_authority = parsed_ca
.basic_constraints()
.ok()
.flatten()
.is_some_and(|constraints| constraints.value.ca);
if is_certificate_authority {
has_certificate_authority = true;
}
}
if !has_certificate_authority {
return Err(TlsMaterialError::InvalidCertificateAuthority {
path: ca_file.to_path_buf(),
});
}
if server_hostname.is_empty() {
return Ok(());
}
let verifier = WebPkiServerVerifier::builder(Arc::new(roots))
.build()
.map_err(TlsMaterialError::BuildServerVerifier)?;
let server_name = ServerName::try_from(server_hostname.to_owned()).map_err(|_| {
TlsMaterialError::InvalidServerName {
server_name: server_hostname.to_owned(),
}
})?;
verifier
.verify_server_cert(
&cert_chain[0],
&cert_chain[1..],
&server_name,
&[],
UnixTime::now(),
)
.map_err(|source| TlsMaterialError::InvalidServerCertificate {
server_name: server_hostname.to_owned(),
source,
})?;
Ok(())
}