use base64::engine::general_purpose::STANDARD as Base64Engine;
use base64::Engine;
use ring::digest::digest;
use ring::digest::SHA256;
use std::error::Error;
use std::fmt::Debug;
use std::fmt::Display;
use std::path::Path;
use std::path::PathBuf;
use x509_parser::certificate::X509Certificate;
use x509_parser::prelude::FromDer;
pub enum CertificateLoadError {
InvalidCertificate,
InvalidPrivateKey,
FileError {
file: PathBuf,
error: std::io::Error,
},
}
pub struct InvalidCertificate(usize);
#[derive(Clone)]
pub struct Certificate {
pub(crate) certificates: Vec<rustls::Certificate>,
pub(crate) key: rustls::PrivateKey,
}
impl Certificate {
pub fn new(
certificates: Vec<Vec<u8>>,
private_key: Vec<u8>,
) -> Result<Self, InvalidCertificate> {
for (index, cert) in certificates.iter().enumerate() {
if X509Certificate::from_der(cert).is_err() {
return Err(InvalidCertificate(index));
}
}
let certificates = certificates.into_iter().map(rustls::Certificate).collect();
let key = rustls::PrivateKey(private_key);
Ok(Self { certificates, key })
}
pub fn self_signed<I, S>(subject_alt_names: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
use rcgen::CertificateParams;
use rcgen::DistinguishedName;
use rcgen::DnType;
use rcgen::PKCS_ECDSA_P256_SHA256;
use time::Duration;
use time::OffsetDateTime;
let subject_alt_names = subject_alt_names
.into_iter()
.map(|s| s.as_ref().to_string())
.collect::<Vec<_>>();
let mut dname = DistinguishedName::new();
dname.push(DnType::CommonName, "wtransport self-signed");
let mut cert_params = CertificateParams::new(subject_alt_names);
cert_params.alg = &PKCS_ECDSA_P256_SHA256;
cert_params.distinguished_name = dname;
cert_params.not_before = OffsetDateTime::now_utc();
cert_params.not_after = OffsetDateTime::now_utc()
.checked_add(Duration::days(14))
.expect("addition does not overflow");
let cert = rcgen::Certificate::from_params(cert_params).expect("inner params are valid");
Self::new(
vec![cert.serialize_der().expect("valid certificate")],
cert.serialize_private_key_der(),
)
.expect("valid certificate")
}
pub async fn load(
cert_path: impl AsRef<Path>,
key_path: impl AsRef<Path>,
) -> Result<Self, CertificateLoadError> {
let certificates =
rustls_pemfile::certs(&mut &*tokio::fs::read(cert_path.as_ref()).await.map_err(
|io_error| CertificateLoadError::FileError {
file: cert_path.as_ref().to_path_buf(),
error: io_error,
},
)?)
.map_err(|io_error| CertificateLoadError::FileError {
file: cert_path.as_ref().to_path_buf(),
error: io_error,
})?;
if certificates.is_empty() {
return Err(CertificateLoadError::InvalidCertificate);
}
let private_key =
rustls_pemfile::read_one(&mut &*tokio::fs::read(key_path.as_ref()).await.map_err(
|io_error| CertificateLoadError::FileError {
file: key_path.as_ref().to_path_buf(),
error: io_error,
},
)?)
.map_err(|io_error| CertificateLoadError::FileError {
file: key_path.as_ref().to_path_buf(),
error: io_error,
})?
.and_then(|item| match item {
rustls_pemfile::Item::RSAKey(d) => Some(d),
rustls_pemfile::Item::PKCS8Key(d) => Some(d),
rustls_pemfile::Item::ECKey(d) => Some(d),
_ => None,
})
.ok_or(CertificateLoadError::InvalidPrivateKey)?;
let private_key = rustls::PrivateKey(private_key);
if rustls::sign::any_supported_type(&private_key).is_err() {
return Err(CertificateLoadError::InvalidPrivateKey);
}
Ok(Self::new(certificates, private_key.0).expect("validated certificate"))
}
pub fn fingerprints(&self) -> Vec<String> {
self.certificates
.iter()
.map(|cert| {
let x509_cert = X509Certificate::from_der(&cert.0)
.expect("valid certificate")
.1;
let digest = digest(&SHA256, x509_cert.public_key().raw);
Base64Engine.encode(digest)
})
.collect()
}
}
impl Error for CertificateLoadError {}
impl Debug for CertificateLoadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidCertificate => write!(f, "no valid certificate to load found"),
Self::InvalidPrivateKey => write!(f, "no valid private key to load found"),
Self::FileError { file, error } => {
write!(f, "file ('{}') error: {:?}", file.display(), error)
}
}
}
}
impl Display for CertificateLoadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CertificateLoadError::InvalidCertificate => Debug::fmt(&self, f),
CertificateLoadError::InvalidPrivateKey => Debug::fmt(&self, f),
CertificateLoadError::FileError { file, error } => {
write!(f, "file ('{}') error: {}", file.display(), error)
}
}
}
}
impl Error for InvalidCertificate {}
impl Debug for InvalidCertificate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!(
"invalid certificate (chain index: {})",
self.0
))
}
}
impl Display for InvalidCertificate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Debug::fmt(self, f)
}
}
pub use rustls;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn invalid() {
assert!(matches!(
Certificate::new(vec![b"hello".to_vec()], b"hello".to_vec()),
Err(InvalidCertificate(0))
));
}
#[test]
fn valid_self() {
let cert = Certificate::self_signed(["localhost"]);
let chain = cert.certificates.into_iter().map(|c| c.0).collect();
let private_key = cert.key.0;
Certificate::new(chain, private_key).unwrap();
}
}