diff --git a/rpxy-certs/Cargo.toml b/rpxy-certs/Cargo.toml index e3660b8..cd5bdfb 100644 --- a/rpxy-certs/Cargo.toml +++ b/rpxy-certs/Cargo.toml @@ -13,3 +13,29 @@ publish.workspace = true [features] [dependencies] +rustc-hash = { version = "1.1.0" } +tracing = { version = "0.1.40" } +# anyhow = "1.0.86" +derive_builder = { version = "0.20.0" } +thiserror = { version = "1.0.61" } +# hot_reload = {version = "0.1.5"} +async-trait = { version = "0.1.80" } +# tokio-rustls = { version = "0.26.0", features = ["early-data"] } +rustls = { version = "0.23.8", default-features = false, features = [ + "aws_lc_rs", +] } +rustls-pemfile = { version = "2.1.2" } +rustls-webpki = { version = "0.102.4", default-features = false, features = [ + "std", + "aws_lc_rs", +] } +x509-parser = { version = "0.16.0" } + +[dev-dependencies] +tokio = { version = "1.37.0", default-features = false, features = [ + # "net", + "rt-multi-thread", + # "time", + # "sync", + "macros", +] } diff --git a/rpxy-certs/src/certs.rs b/rpxy-certs/src/certs.rs new file mode 100644 index 0000000..7df964e --- /dev/null +++ b/rpxy-certs/src/certs.rs @@ -0,0 +1,102 @@ +use crate::error::*; +use rustc_hash::FxHashMap as HashMap; +use rustls::{crypto::aws_lc_rs::sign::any_supported_type, pki_types, sign::CertifiedKey}; +use std::sync::Arc; +use x509_parser::prelude::*; + +/* ------------------------------------------------ */ +/// Raw certificates in rustls format +type Certificate = rustls::pki_types::CertificateDer<'static>; +/// Raw private key in rustls format +type PrivateKey = pki_types::PrivateKeyDer<'static>; +/// Client CA trust anchors subject to the subject key identifier +type TrustAnchors = HashMap, pki_types::TrustAnchor<'static>>; + +/* ------------------------------------------------ */ +/// Raw certificates and private keys loaded from files for a single server name +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct SingleServerCrypto { + certs: Vec, + cert_keys: Arc>, + client_ca_certs: Option>, +} + +impl SingleServerCrypto { + /// Create a new instance of SingleServerCrypto + pub fn new(certs: &[Certificate], cert_keys: &Arc>, client_ca_certs: &Option>) -> Self { + Self { + certs: certs.to_owned(), + cert_keys: cert_keys.clone(), + client_ca_certs: client_ca_certs.clone(), + } + } + /// Check if mutual tls is enabled + pub fn is_mutual_tls(&self) -> bool { + self.client_ca_certs.is_some() + } + /* ------------------------------------------------ */ + /// Convert the certificates to bytes in der + pub fn certs_bytes(&self) -> Vec> { + self.certs.iter().map(|c| c.to_vec()).collect() + } + /// Convert the private keys to bytes in der + pub fn cert_keys_bytes(&self) -> Vec> { + self + .cert_keys + .iter() + .map(|k| match k { + pki_types::PrivateKeyDer::Pkcs1(pkcs1) => pkcs1.secret_pkcs1_der().to_owned(), + pki_types::PrivateKeyDer::Sec1(sec1) => sec1.secret_sec1_der().to_owned(), + pki_types::PrivateKeyDer::Pkcs8(pkcs8) => pkcs8.secret_pkcs8_der().to_owned(), + _ => unreachable!(), + }) + .collect() + } + /// Convert the client CA certificates to bytes in der + pub fn client_ca_certs_bytes(&self) -> Option>> { + self.client_ca_certs.as_ref().map(|v| v.iter().map(|c| c.to_vec()).collect()) + } + /* ------------------------------------------------ */ + /// Parse the certificates and private keys for a single server and return a rustls CertifiedKey + pub fn rustls_certified_key(&self) -> Result { + let signing_key = self + .cert_keys + .clone() + .iter() + .find_map(|k| if let Ok(sk) = any_supported_type(k) { Some(sk) } else { None }) + .ok_or_else(|| RpxyCertError::InvalidCertificateAndKey)?; + + let cert = self.certs.iter().map(|c| Certificate::from(c.to_vec())).collect::>(); + Ok(CertifiedKey::new(cert, signing_key)) + } + + /* ------------------------------------------------ */ + /// Parse the client CA certificates and return a hashmap of pairs of a subject key identifier (key) and a trust anchor (value) + pub fn rustls_trust_anchors(&self) -> Result { + //-> Result<(Vec, HashSet>), anyhow::Error> { + let Some(certs) = self.client_ca_certs.as_ref() else { + return Err(RpxyCertError::NoClientCert); + }; + let certs = certs.iter().map(|c| Certificate::from(c.to_vec())).collect::>(); + + let trust_anchors = certs + .iter() + .filter_map(|v| { + // retrieve trust anchor + let trust_anchor = webpki::anchor_from_trusted_cert(v).ok()?; + + // retrieve ca key id (subject key id) + let x509_cert = parse_x509_certificate(v).map(|v| v.1).ok()?; + let mut subject_key_ids = x509_cert.iter_extensions().filter_map(|ext| match ext.parsed_extension() { + ParsedExtension::SubjectKeyIdentifier(skid) => Some(skid), + _ => None, + }); + let skid = subject_key_ids.next()?; + + Some((skid.0.to_owned(), trust_anchor.to_owned())) + }) + .collect::>(); + + Ok(trust_anchors) + } +} diff --git a/rpxy-certs/src/error.rs b/rpxy-certs/src/error.rs new file mode 100644 index 0000000..b5135a8 --- /dev/null +++ b/rpxy-certs/src/error.rs @@ -0,0 +1,15 @@ +use thiserror::Error; + +/// Describes things that can go wrong in the Rpxy certificate +#[derive(Debug, Error)] +pub enum RpxyCertError { + /// Error when reading certificates and keys + #[error("Failed to read certificates from file: {0}")] + IoError(#[from] std::io::Error), + /// Error when parsing certificates and keys to generate a rustls CertifiedKey + #[error("Unable to find a valid certificate and key")] + InvalidCertificateAndKey, + /// Error when parsing client CA certificates: No client certificate found + #[error("No client certificate found")] + NoClientCert, +} diff --git a/rpxy-certs/src/lib.rs b/rpxy-certs/src/lib.rs index e69de29..18b7da5 100644 --- a/rpxy-certs/src/lib.rs +++ b/rpxy-certs/src/lib.rs @@ -0,0 +1,68 @@ +mod certs; +mod error; +mod service; +mod source; + +#[allow(unused_imports)] +pub(crate) mod log { + pub(crate) use tracing::{debug, error, info, warn}; +} + +pub use crate::{ + certs::SingleServerCrypto, + source::{CryptoFileSource, CryptoFileSourceBuilder, CryptoFileSourceBuilderError, CryptoSource}, +}; + +/* ------------------------------------------------ */ +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn read_server_crt_key_files() { + let tls_cert_path = "../example-certs/server.crt"; + let tls_cert_key_path = "../example-certs/server.key"; + let crypto_file_source = CryptoFileSourceBuilder::default() + .tls_cert_key_path(tls_cert_key_path) + .tls_cert_path(tls_cert_path) + .build(); + assert!(crypto_file_source.is_ok()); + + let crypto_file_source = crypto_file_source.unwrap(); + let crypto_elem = crypto_file_source.read().await; + assert!(crypto_elem.is_ok()); + + let crypto_elem = crypto_elem.unwrap(); + let certificed_key = crypto_elem.rustls_certified_key(); + assert!(certificed_key.is_ok()); + } + + #[tokio::test] + async fn read_server_crt_key_files_with_client_ca_crt() { + let tls_cert_path = "../example-certs/server.crt"; + let tls_cert_key_path = "../example-certs/server.key"; + let client_ca_cert_path = Some("../example-certs/client.ca.crt"); + let crypto_file_source = CryptoFileSourceBuilder::default() + .tls_cert_key_path(tls_cert_key_path) + .tls_cert_path(tls_cert_path) + .client_ca_cert_path(client_ca_cert_path) + .build(); + assert!(crypto_file_source.is_ok()); + + let crypto_file_source = crypto_file_source.unwrap(); + let crypto_elem = crypto_file_source.read().await; + assert!(crypto_elem.is_ok()); + + let crypto_elem = crypto_elem.unwrap(); + assert!(crypto_elem.is_mutual_tls()); + + let certificed_key = crypto_elem.rustls_certified_key(); + assert!(certificed_key.is_ok()); + + let trust_anchors = crypto_elem.rustls_trust_anchors(); + assert!(trust_anchors.is_ok()); + + let trust_anchors = trust_anchors.unwrap(); + assert_eq!(trust_anchors.len(), 1); + } +} diff --git a/rpxy-certs/src/service.rs b/rpxy-certs/src/service.rs new file mode 100644 index 0000000..e69de29 diff --git a/rpxy-certs/src/source.rs b/rpxy-certs/src/source.rs new file mode 100644 index 0000000..e4dba81 --- /dev/null +++ b/rpxy-certs/src/source.rs @@ -0,0 +1,160 @@ +use crate::{certs::SingleServerCrypto, error::*, log::*}; +use async_trait::async_trait; +use derive_builder::Builder; +use std::{ + fs::File, + io::{self, BufReader, Cursor, Read}, + path::{Path, PathBuf}, + sync::Arc, +}; + +/* ------------------------------------------------ */ +#[async_trait] +// Trait to read certs and keys anywhere from KVS, file, sqlite, etc. +pub trait CryptoSource { + type Error; + + /// read crypto materials from source + async fn read(&self) -> Result; + + /// Returns true when mutual tls is enabled + fn is_mutual_tls(&self) -> bool; +} + +/* ------------------------------------------------ */ +#[derive(Builder, Debug, Clone)] +/// Crypto-related file reader implementing `CryptoSource`` trait +pub struct CryptoFileSource { + #[builder(setter(custom))] + /// Always exist + pub tls_cert_path: PathBuf, + + #[builder(setter(custom))] + /// Always exist + pub tls_cert_key_path: PathBuf, + + #[builder(setter(custom), default)] + /// This may not exist + pub client_ca_cert_path: Option, +} + +impl CryptoFileSourceBuilder { + pub fn tls_cert_path>(&mut self, v: T) -> &mut Self { + self.tls_cert_path = Some(v.as_ref().to_path_buf()); + self + } + pub fn tls_cert_key_path>(&mut self, v: T) -> &mut Self { + self.tls_cert_key_path = Some(v.as_ref().to_path_buf()); + self + } + pub fn client_ca_cert_path>(&mut self, v: Option) -> &mut Self { + self.client_ca_cert_path = Some(v.map(|p| p.as_ref().to_path_buf())); + self + } +} + +/* ------------------------------------------------ */ +#[async_trait] +impl CryptoSource for CryptoFileSource { + type Error = RpxyCertError; + /// read crypto materials from source + async fn read(&self) -> Result { + read_certs_and_keys( + &self.tls_cert_path, + &self.tls_cert_key_path, + self.client_ca_cert_path.as_ref(), + ) + } + /// Returns true when mutual tls is enabled + fn is_mutual_tls(&self) -> bool { + self.client_ca_cert_path.is_some() + } +} + +/* ------------------------------------------------ */ +/// Read certificates and private keys from file +fn read_certs_and_keys( + cert_path: &PathBuf, + cert_key_path: &PathBuf, + client_ca_cert_path: Option<&PathBuf>, +) -> Result { + debug!("Read TLS server certificates and private key"); + + // certificates + let raw_certs = { + let mut reader = BufReader::new(File::open(cert_path).map_err(|e| { + io::Error::new( + e.kind(), + format!("Unable to load the certificates [{}]: {e}", cert_path.display()), + ) + })?); + rustls_pemfile::certs(&mut reader) + .collect::, _>>() + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "Unable to parse the certificates"))? + }; + + // private keys + let raw_cert_keys = { + let encoded_keys = { + let mut encoded_keys = vec![]; + File::open(cert_key_path) + .map_err(|e| { + io::Error::new( + e.kind(), + format!("Unable to load the certificate keys [{}]: {e}", cert_key_path.display()), + ) + })? + .read_to_end(&mut encoded_keys)?; + encoded_keys + }; + let mut reader = Cursor::new(encoded_keys); + let pkcs8_keys = rustls_pemfile::pkcs8_private_keys(&mut reader) + .map(|v| v.map(rustls::pki_types::PrivateKeyDer::Pkcs8)) + .collect::, _>>() + .map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidInput, + "Unable to parse the certificates private keys (PKCS8)", + ) + })?; + reader.set_position(0); + let mut rsa_keys = rustls_pemfile::rsa_private_keys(&mut reader) + .map(|v| v.map(rustls::pki_types::PrivateKeyDer::Pkcs1)) + .collect::, _>>()?; + let mut keys = pkcs8_keys; + keys.append(&mut rsa_keys); + if keys.is_empty() { + return Err(RpxyCertError::IoError(io::Error::new( + io::ErrorKind::InvalidInput, + "No private keys found - Make sure that they are in PKCS#8/PEM format", + ))); + } + keys + }; + + // client ca certificates + let client_ca_certs = if let Some(path) = client_ca_cert_path { + debug!("Read CA certificates for client authentication"); + // Reads client certificate and returns client + let certs = { + let mut reader = BufReader::new(File::open(path).map_err(|e| { + io::Error::new( + e.kind(), + format!("Unable to load the client certificates [{}]: {e}", path.display()), + ) + })?); + rustls_pemfile::certs(&mut reader) + .collect::, _>>() + .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "Unable to parse the client certificates"))? + }; + Some(certs) + } else { + None + }; + + Ok(SingleServerCrypto::new( + &raw_certs, + &Arc::new(raw_cert_keys), + &client_ca_certs, + )) +}