diff options
-rw-r--r-- | .bleep | 2 | ||||
-rw-r--r-- | pingora-core/Cargo.toml | 3 | ||||
-rw-r--r-- | pingora-core/src/connectors/tls/rustls/mod.rs | 106 | ||||
-rw-r--r-- | pingora-core/src/listeners/tls/rustls/mod.rs | 38 | ||||
-rw-r--r-- | pingora-core/src/protocols/tls/rustls/client.rs | 4 | ||||
-rw-r--r-- | pingora-core/src/protocols/tls/rustls/mod.rs | 15 | ||||
-rw-r--r-- | pingora-core/src/protocols/tls/rustls/server.rs | 5 | ||||
-rw-r--r-- | pingora-core/src/protocols/tls/rustls/stream.rs | 86 | ||||
-rw-r--r-- | pingora-core/src/utils/tls/rustls.rs | 137 | ||||
-rw-r--r-- | pingora-proxy/tests/test_basic.rs | 4 | ||||
-rw-r--r-- | pingora-proxy/tests/utils/cert.rs | 4 | ||||
-rw-r--r-- | pingora-rustls/Cargo.toml | 5 | ||||
-rw-r--r-- | pingora-rustls/src/lib.rs | 225 | ||||
-rw-r--r-- | pingora/Cargo.toml | 7 |
14 files changed, 342 insertions, 299 deletions
@@ -1 +1 @@ -3919de32fe7a184847dd6ce7da98247ec9e1eb86
\ No newline at end of file +7c7f575484b512ad279084c427adf66069ec2619
\ No newline at end of file diff --git a/pingora-core/Cargo.toml b/pingora-core/Cargo.toml index cb9f741..d75e059 100644 --- a/pingora-core/Cargo.toml +++ b/pingora-core/Cargo.toml @@ -68,6 +68,7 @@ tokio-test = "0.4" zstd = "0" httpdate = "1" x509-parser = { version = "0.16.0", optional = true } +ouroboros = { version = "0.18.4", optional = true } [target.'cfg(unix)'.dependencies] daemonize = "0.5.0" @@ -92,7 +93,7 @@ jemallocator = "0.5" default = [] openssl = ["pingora-openssl", "openssl_derived",] boringssl = ["pingora-boringssl", "openssl_derived",] -rustls = ["pingora-rustls", "any_tls", "dep:x509-parser"] +rustls = ["pingora-rustls", "any_tls", "dep:x509-parser", "ouroboros"] patched_http1 = ["pingora-http/patched_http1"] openssl_derived = ["any_tls"] any_tls = [] diff --git a/pingora-core/src/connectors/tls/rustls/mod.rs b/pingora-core/src/connectors/tls/rustls/mod.rs index 7d10c0e..4a44f1d 100644 --- a/pingora-core/src/connectors/tls/rustls/mod.rs +++ b/pingora-core/src/connectors/tls/rustls/mod.rs @@ -21,8 +21,8 @@ use pingora_error::{ OrErr, Result, }; use pingora_rustls::{ - load_ca_file_into_store, load_certs_key_file, load_platform_certs_incl_env_into_store, version, - CertificateDer, ClientConfig as RusTlsClientConfig, PrivateKeyDer, RootCertStore, + load_ca_file_into_store, load_certs_and_key_files, load_platform_certs_incl_env_into_store, + version, CertificateDer, ClientConfig as RusTlsClientConfig, PrivateKeyDer, RootCertStore, TlsConnector as RusTlsConnector, }; @@ -37,18 +37,21 @@ pub struct Connector { } impl Connector { + /// Create a new connector based on the optional configurations. If no + /// configurations are provided, no customized certificates or keys will be + /// used pub fn new(config_opt: Option<ConnectorOptions>) -> Self { - TlsConnector::build_connector(config_opt) + TlsConnector::build_connector(config_opt).unwrap() } } pub(crate) struct TlsConnector { - config: RusTlsClientConfig, - ca_certs: RootCertStore, + config: Arc<RusTlsClientConfig>, + ca_certs: Arc<RootCertStore>, } impl TlsConnector { - pub(crate) fn build_connector(options: Option<ConnectorOptions>) -> Connector + pub(crate) fn build_connector(options: Option<ConnectorOptions>) -> Result<Connector> where Self: Sized, { @@ -65,16 +68,16 @@ impl TlsConnector { if let Some(conf) = options.as_ref() { if let Some(ca_file_path) = conf.ca_file.as_ref() { - load_ca_file_into_store(ca_file_path, &mut ca_certs); + load_ca_file_into_store(ca_file_path, &mut ca_certs)?; } else { - load_platform_certs_incl_env_into_store(&mut ca_certs); + load_platform_certs_incl_env_into_store(&mut ca_certs)?; } if let Some((cert, key)) = conf.cert_key_file.as_ref() { - certs_key = load_certs_key_file(cert, key); + certs_key = load_certs_and_key_files(cert, key)?; } // TODO: support SSLKEYLOGFILE } else { - load_platform_certs_incl_env_into_store(&mut ca_certs); + load_platform_certs_incl_env_into_store(&mut ca_certs)?; } (ca_certs, certs_key) @@ -92,19 +95,19 @@ impl TlsConnector { Err(err) => { // TODO: is there a viable alternative to the panic? // falling back to no client auth... does not seem to be reasonable. - panic!( - "{}", - format!("Failed to configure client auth cert/key. Error: {}", err) - ); + panic!("Failed to configure client auth cert/key. Error: {}", err); } } } None => builder.with_no_client_auth(), }; - Connector { - ctx: Arc::new(TlsConnector { config, ca_certs }), - } + Ok(Connector { + ctx: Arc::new(TlsConnector { + config: Arc::new(config), + ca_certs: Arc::new(ca_certs), + }), + }) } } @@ -118,34 +121,30 @@ where T: IO, P: Peer + Send + Sync, { - let mut config = tls_ctx.config.clone(); + let config = &tls_ctx.config; // TODO: setup CA/verify cert store from peer - // looks like the fields are always None - // peer.get_ca() - + // peer.get_ca() returns None by default. It must be replaced by the + // implementation of `peer` let key_pair = peer.get_client_cert_key(); - let updated_config: Option<RusTlsClientConfig> = match key_pair { + let mut updated_config_opt: Option<RusTlsClientConfig> = match key_pair { None => None, Some(key_arc) => { debug!("setting client cert and key"); let mut cert_chain = vec![]; debug!("adding leaf certificate to mTLS cert chain"); - cert_chain.push(key_arc.leaf().to_owned()); + cert_chain.push(key_arc.leaf()); debug!("adding intermediate certificates to mTLS cert chain"); key_arc .intermediates() .to_owned() .iter() - .map(|i| i.to_vec()) + .copied() .for_each(|i| cert_chain.push(i)); - let certs: Vec<CertificateDer> = cert_chain - .into_iter() - .map(|c| c.as_slice().to_owned().into()) - .collect(); + let certs: Vec<CertificateDer> = cert_chain.into_iter().map(|c| c.into()).collect(); let private_key: PrivateKeyDer = key_arc.key().as_slice().to_owned().try_into().unwrap(); @@ -153,61 +152,50 @@ where &version::TLS12, &version::TLS13, ]) - .with_root_certificates(tls_ctx.ca_certs.clone()); - - let updated_config = builder - .with_client_auth_cert(certs, private_key) - .explain_err(InvalidCert, |e| { - format!( - "Failed to use peer cert/key to update Rustls config: {:?}", - e - ) - })?; + .with_root_certificates(Arc::clone(&tls_ctx.ca_certs)); + debug!("added root ca certificates"); + + let updated_config = builder.with_client_auth_cert(certs, private_key).or_err( + InvalidCert, + "Failed to use peer cert/key to update Rustls config", + )?; Some(updated_config) } }; if let Some(alpn) = alpn_override.as_ref().or(peer.get_alpn()) { - config.alpn_protocols = alpn.to_wire_protocols(); + let alpn_protocols = alpn.to_wire_protocols(); + if let Some(updated_config) = updated_config_opt.as_mut() { + updated_config.alpn_protocols = alpn_protocols; + } else { + let mut updated_config = RusTlsClientConfig::clone(config); + updated_config.alpn_protocols = alpn_protocols; + updated_config_opt = Some(updated_config); + } } // TODO: curve setup from peer // - second key share from peer, currently only used in boringssl with PQ features - let tls_conn = if let Some(cfg) = updated_config { + let tls_conn = if let Some(cfg) = updated_config_opt { RusTlsConnector::from(Arc::new(cfg)) } else { - RusTlsConnector::from(Arc::new(config)) + RusTlsConnector::from(Arc::clone(config)) }; - // TODO: for consistent behaviour between TLS providers some additions are required + // TODO: for consistent behavior between TLS providers some additions are required // - allowing to disable verification - // - the validation/replace logic would need adjustments to match the boringssl/openssl behaviour - // implementing a custom certificate_verifier could be used to achieve matching behaviour + // - the validation/replace logic would need adjustments to match the boringssl/openssl behavior + // implementing a custom certificate_verifier could be used to achieve matching behavior //let d_conf = config.dangerous(); //d_conf.set_certificate_verifier(...); let mut domain = peer.sni().to_string(); - if peer.sni().is_empty() { - // use ip in case SNI is not present - // TODO: disable validation - domain = peer.address().as_inet().unwrap().ip().to_string() - } - if peer.verify_cert() && peer.verify_hostname() { // TODO: streamline logic with replacing first underscore within TLS implementations if let Some(sni_s) = replace_leftmost_underscore(peer.sni()) { domain = sni_s; } - if let Some(alt_cn) = peer.alternative_cn() { - if !alt_cn.is_empty() { - domain = alt_cn.to_string(); - // TODO: streamline logic with replacing first underscore within TLS implementations - if let Some(alt_cn_s) = replace_leftmost_underscore(alt_cn) { - domain = alt_cn_s; - } - } - } } let connect_future = handshake(&tls_conn, &domain, stream); diff --git a/pingora-core/src/listeners/tls/rustls/mod.rs b/pingora-core/src/listeners/tls/rustls/mod.rs index cfb2811..c8380e8 100644 --- a/pingora-core/src/listeners/tls/rustls/mod.rs +++ b/pingora-core/src/listeners/tls/rustls/mod.rs @@ -18,8 +18,8 @@ use crate::listeners::TlsAcceptCallbacks; use crate::protocols::tls::{server::handshake, server::handshake_with_callback, TlsStream}; use log::debug; use pingora_error::ErrorType::InternalError; -use pingora_error::{Error, ErrorSource, ImmutStr, OrErr, Result}; -use pingora_rustls::load_certs_key_file; +use pingora_error::{Error, OrErr, Result}; +use pingora_rustls::load_certs_and_key_files; use pingora_rustls::ServerConfig; use pingora_rustls::{version, TlsAcceptor as RusTlsAcceptor}; @@ -38,21 +38,29 @@ pub struct Acceptor { } impl TlsSettings { + /// Create a Rustls acceptor based on the current setting for certificates, + /// keys, and protocols. + /// + /// _NOTE_ This function will panic if there is an error in loading + /// certificate files or constructing the builder + /// + /// Todo: Return a result instead of panicking XD pub fn build(self) -> Acceptor { - let (certs, key) = - load_certs_key_file(&self.cert_path, &self.key_path).unwrap_or_else(|| { - panic!( - "Failed to load provided certificates \"{}\" or key \"{}\".", - self.cert_path, self.key_path - ) - }); + let Ok(Some((certs, key))) = load_certs_and_key_files(&self.cert_path, &self.key_path) + else { + panic!( + "Failed to load provided certificates \"{}\" or key \"{}\".", + self.cert_path, self.key_path + ) + }; + // TODO - Add support for client auth & custom CA support let mut config = ServerConfig::builder_with_protocol_versions(&[&version::TLS12, &version::TLS13]) .with_no_client_auth() .with_single_cert(certs, key) .explain_err(InternalError, |e| { - format!("Failed to create server listener config: {}", e) + format!("Failed to create server listener config: {e}") }) .unwrap(); @@ -92,14 +100,10 @@ impl TlsSettings { Self: Sized, { // TODO: verify if/how callback in handshake can be done using Rustls - Err(Error::create( + Error::e_explain( InternalError, - ErrorSource::Internal, - Some(ImmutStr::from( - "Certificate callbacks are not supported with feature \"rustls\".", - )), - None, - )) + "Certificate callbacks are not supported with feature \"rustls\".", + ) } } diff --git a/pingora-core/src/protocols/tls/rustls/client.rs b/pingora-core/src/protocols/tls/rustls/client.rs index bb9edc2..5d84e01 100644 --- a/pingora-core/src/protocols/tls/rustls/client.rs +++ b/pingora-core/src/protocols/tls/rustls/client.rs @@ -28,9 +28,7 @@ pub async fn handshake<S: IO>( ) -> Result<TlsStream<S>> { let mut stream = TlsStream::from_connector(connector, domain, io) .await - .explain_err(TLSHandshakeFailure, |e| { - format!("tip: tls stream error: {e}") - })?; + .or_err(TLSHandshakeFailure, "tls stream error")?; let handshake_result = stream.connect().await; match handshake_result { diff --git a/pingora-core/src/protocols/tls/rustls/mod.rs b/pingora-core/src/protocols/tls/rustls/mod.rs index 9622fac..f144363 100644 --- a/pingora-core/src/protocols/tls/rustls/mod.rs +++ b/pingora-core/src/protocols/tls/rustls/mod.rs @@ -17,20 +17,9 @@ pub mod server; mod stream; pub use stream::*; -use x509_parser::prelude::FromDer; -pub type CaType = [Box<CertWrapper>]; +use crate::utils::tls::WrappedX509; -#[derive(Debug)] -#[repr(transparent)] -pub struct CertWrapper(pub [u8]); - -impl CertWrapper { - pub fn not_after(&self) -> String { - let (_, x509cert) = x509_parser::certificate::X509Certificate::from_der(&self.0) - .expect("Failed to parse certificate from DER format."); - x509cert.validity.not_after.to_string() - } -} +pub type CaType = [WrappedX509]; pub struct TlsRef; diff --git a/pingora-core/src/protocols/tls/rustls/server.rs b/pingora-core/src/protocols/tls/rustls/server.rs index 49f303f..44bbe59 100644 --- a/pingora-core/src/protocols/tls/rustls/server.rs +++ b/pingora-core/src/protocols/tls/rustls/server.rs @@ -80,10 +80,9 @@ pub async fn handshake_with_callback<S: IO>( .resume_accept() .await .explain_err(TLSHandshakeFailure, |e| format!("TLS accept() failed: {e}"))?; - Ok(tls_stream) - } else { - Ok(tls_stream) } + + Ok(tls_stream) } #[async_trait] diff --git a/pingora-core/src/protocols/tls/rustls/stream.rs b/pingora-core/src/protocols/tls/rustls/stream.rs index 385a51e..9cdd2a7 100644 --- a/pingora-core/src/protocols/tls/rustls/stream.rs +++ b/pingora-core/src/protocols/tls/rustls/stream.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::io::{Error as IoError, ErrorKind as IoErrorKind, Result as IoResult}; +use std::io::Result as IoResult; use std::ops::{Deref, DerefMut}; use std::pin::Pin; use std::sync::Arc; @@ -21,11 +21,11 @@ use std::time::{Duration, SystemTime}; use crate::listeners::tls::Acceptor; use crate::protocols::raw_connect::ProxyDigest; -use crate::protocols::{tls::SslDigest, TimingDigest}; +use crate::protocols::{tls::SslDigest, Peek, TimingDigest}; use crate::protocols::{ GetProxyDigest, GetSocketDigest, GetTimingDigest, SocketDigest, Ssl, UniqueID, ALPN, }; -use crate::utils::tls::get_organization_serial; +use crate::utils::tls::get_organization_serial_bytes; use pingora_error::ErrorType::{AcceptError, ConnectError, InternalError, TLSHandshakeFailure}; use pingora_error::{Error, ImmutStr, OrErr, Result}; use pingora_rustls::TlsStream as RusTlsStream; @@ -58,12 +58,9 @@ where /// Using RustTLS the stream is only returned after the handshake. /// The caller does therefor not need to perform [`Self::connect()`]. pub async fn from_connector(connector: &TlsConnector, domain: &str, stream: T) -> Result<Self> { - let server = ServerName::try_from(domain) - .map_err(|e| IoError::new(IoErrorKind::InvalidInput, e)) - .explain_err(InternalError, |e| { - format!("failed to parse domain: {}, error: {}", domain, e) - })? - .to_owned(); + let server = ServerName::try_from(domain).or_err_with(InternalError, || { + format!("Invalid Input: Failed to parse domain: {domain}") + })?; let tls = InnerStream::from_connector(connector, server, stream) .await @@ -243,11 +240,11 @@ impl<T> Ssl for TlsStream<T> { } } +/// Create a new TLS connection from the given `stream` +/// +/// The caller needs to perform [`Self::connect()`] or [`Self::accept()`] to perform TLS +/// handshake after. impl<T: AsyncRead + AsyncWrite + Unpin> InnerStream<T> { - /// Create a new TLS connection from the given `stream` - /// - /// The caller needs to perform [`Self::connect()`] or [`Self::accept()`] to perform TLS - /// handshake after. pub(crate) async fn from_connector( connector: &TlsConnector, server: ServerName<'_>, @@ -277,19 +274,18 @@ impl<T: AsyncRead + AsyncWrite + Unpin + Send> InnerStream<T> { pub(crate) async fn connect(&mut self) -> Result<()> { let connect = &mut (*self.connect); - if let Some(ref mut connect) = connect { + if let Some(connect) = connect.take() { let stream = connect .await - .explain_err(TLSHandshakeFailure, |e| format!("tls connect error: {e}"))?; + .or_err(TLSHandshakeFailure, "tls connect error")?; self.stream = Some(RusTlsStream::Client(stream)); - self.connect = None.into(); Ok(()) } else { - Err(Error::explain( + Error::e_explain( ConnectError, ImmutStr::from("TLS connect not available to perform handshake."), - )) + ) } } @@ -298,12 +294,11 @@ impl<T: AsyncRead + AsyncWrite + Unpin + Send> InnerStream<T> { pub(crate) async fn accept(&mut self) -> Result<()> { let accept = &mut (*self.accept); - if let Some(ref mut accept) = accept { + if let Some(ref mut accept) = accept.take() { let stream = accept .await .explain_err(TLSHandshakeFailure, |e| format!("tls connect error: {e}"))?; self.stream = Some(RusTlsStream::Server(stream)); - self.connect = None.into(); Ok(()) } else { @@ -375,34 +370,27 @@ impl SslDigest { let cipher_suite = session.negotiated_cipher_suite(); let peer_certificates = session.peer_certificates(); - let cipher = match cipher_suite { - Some(suite) => suite.suite().as_str().unwrap_or_default(), - None => "", - }; - - let version = match protocol { - Some(proto) => proto.as_str().unwrap_or_default(), - None => "", - }; - - let cert_digest = match peer_certificates { - Some(certs) => match certs.first() { - Some(cert) => hash_certificate(cert.clone()), - None => vec![], - }, - None => vec![], - }; - - let (organization, serial_number) = match peer_certificates { - Some(certs) => match certs.first() { - Some(cert) => { - let (organization, serial) = get_organization_serial(cert.as_bytes()); - (organization, Some(serial)) - } - None => (None, None), - }, - None => (None, None), - }; + let cipher = cipher_suite + .and_then(|suite| suite.suite().as_str()) + .unwrap_or_default(); + + let version = protocol + .and_then(|proto| proto.as_str()) + .unwrap_or_default(); + + let cert_digest = peer_certificates + .and_then(|certs| certs.first()) + .map(|cert| hash_certificate(cert)) + .unwrap_or_default(); + + let (organization, serial_number) = peer_certificates + .and_then(|certs| certs.first()) + .map(|cert| get_organization_serial_bytes(cert.as_bytes())) + .transpose() + .ok() + .flatten() + .map(|(organization, serial)| (organization, Some(serial))) + .unwrap_or_default(); SslDigest { cipher, @@ -413,3 +401,5 @@ impl SslDigest { } } } + +impl<S> Peek for TlsStream<S> {} diff --git a/pingora-core/src/utils/tls/rustls.rs b/pingora-core/src/utils/tls/rustls.rs index 44c3999..5833495 100644 --- a/pingora-core/src/utils/tls/rustls.rs +++ b/pingora-core/src/utils/tls/rustls.rs @@ -12,26 +12,41 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::protocols::tls::CertWrapper; +use ouroboros::self_referencing; use pingora_error::Result; +use pingora_rustls::CertificateDer; use std::hash::{Hash, Hasher}; -use x509_parser::prelude::FromDer; +use x509_parser::prelude::{FromDer, X509Certificate}; -pub fn get_organization_serial(cert: &[u8]) -> (Option<String>, String) { - let serial = get_serial(cert).expect("Failed to get serial for certificate."); - (get_organization(cert), serial) +/// Get the organization and serial number associated with the given certificate +/// see https://en.wikipedia.org/wiki/X.509#Structure_of_a_certificate +pub fn get_organization_serial(x509cert: &WrappedX509) -> Result<(Option<String>, String)> { + let serial = get_serial(x509cert)?; + Ok((get_organization(x509cert), serial)) } -pub fn get_serial(cert: &[u8]) -> Result<String> { - let (_, x509cert) = x509_parser::certificate::X509Certificate::from_der(cert) - .expect("Failed to parse certificate from DER format."); - Ok(x509cert.raw_serial_as_string()) +fn get_organization_serial_x509( + x509cert: &X509Certificate<'_>, +) -> Result<(Option<String>, String)> { + let serial = x509cert.raw_serial_as_string(); + Ok((get_organization_x509(x509cert), serial)) +} + +/// Get the serial number associated with the given certificate +/// see https://en.wikipedia.org/wiki/X.509#Structure_of_a_certificate +pub fn get_serial(x509cert: &WrappedX509) -> Result<String> { + Ok(x509cert.borrow_cert().raw_serial_as_string()) } /// Return the organization associated with the X509 certificate. -pub fn get_organization(cert: &[u8]) -> Option<String> { - let (_, x509cert) = x509_parser::certificate::X509Certificate::from_der(cert) - .expect("Failed to parse certificate from DER format."); +/// see https://en.wikipedia.org/wiki/X.509#Structure_of_a_certificate +pub fn get_organization(x509cert: &WrappedX509) -> Option<String> { + get_organization_x509(x509cert.borrow_cert()) +} + +/// Return the organization associated with the X509 certificate. +/// see https://en.wikipedia.org/wiki/X.509#Structure_of_a_certificate +pub fn get_organization_x509(x509cert: &X509Certificate<'_>) -> Option<String> { x509cert .subject .iter_organization() @@ -40,16 +55,20 @@ pub fn get_organization(cert: &[u8]) -> Option<String> { .reduce(|cur, next| cur + &next) } -/// Return the organization unit associated with the X509 certificate. -pub fn get_organization_unit(cert: &CertWrapper) -> Option<String> { - get_organization_unit_bytes(&cert.0) +/// Return the organization associated with the X509 certificate (as bytes). +/// see https://en.wikipedia.org/wiki/X.509#Structure_of_a_certificate +pub fn get_organization_serial_bytes(cert: &[u8]) -> Result<(Option<String>, String)> { + let (_, x509cert) = x509_parser::certificate::X509Certificate::from_der(cert) + .expect("Failed to parse certificate from DER format."); + + get_organization_serial_x509(&x509cert) } /// Return the organization unit associated with the X509 certificate. -pub fn get_organization_unit_bytes(cert: &[u8]) -> Option<String> { - let (_, x509cert) = x509_parser::certificate::X509Certificate::from_der(cert) - .expect("Failed to parse certificate from DER format."); +/// see https://en.wikipedia.org/wiki/X.509#Structure_of_a_certificate +pub fn get_organization_unit(x509cert: &WrappedX509) -> Option<String> { x509cert + .borrow_cert() .subject .iter_organizational_unit() .filter_map(|a| a.as_str().ok()) @@ -57,10 +76,11 @@ pub fn get_organization_unit_bytes(cert: &[u8]) -> Option<String> { .reduce(|cur, next| cur + &next) } -pub fn get_common_name(cert: &[u8]) -> Option<String> { - let (_, x509cert) = x509_parser::certificate::X509Certificate::from_der(cert) - .expect("Failed to parse certificate from DER format."); +/// Get a combination of the common names for the given certificate +/// see https://en.wikipedia.org/wiki/X.509#Structure_of_a_certificate +pub fn get_common_name(x509cert: &WrappedX509) -> Option<String> { x509cert + .borrow_cert() .subject .iter_common_name() .filter_map(|a| a.as_str().ok()) @@ -68,20 +88,49 @@ pub fn get_common_name(cert: &[u8]) -> Option<String> { .reduce(|cur, next| cur + &next) } -/// Return the organization unit associated with the X509 certificate. -pub fn get_not_after(cert: &[u8]) -> String { - let (_, x509cert) = x509_parser::certificate::X509Certificate::from_der(cert) - .expect("Failed to parse certificate from DER format."); - x509cert.validity.not_after.to_string() +/// Get the `not_after` field for the valid time period for the given cert +/// see https://en.wikipedia.org/wiki/X.509#Structure_of_a_certificate +pub fn get_not_after(x509cert: &WrappedX509) -> String { + x509cert.borrow_cert().validity.not_after.to_string() } /// This type contains a list of one or more certificates and an associated private key. The leaf -/// certificate should always be first. The certificates and keys are stored in Vec<u8> DER encoded -/// form for usage within OpenSSL/BoringSSL & RusTLS. -#[derive(Clone)] +/// certificate should always be first. pub struct CertKey { - certificates: Vec<Vec<u8>>, key: Vec<u8>, + certificates: Vec<WrappedX509>, +} + +#[self_referencing] +#[derive(Debug)] +pub struct WrappedX509 { + raw_cert: Vec<u8>, + + #[borrows(raw_cert)] + #[covariant] + cert: X509Certificate<'this>, +} + +fn parse_x509<C>(raw_cert: &C) -> X509Certificate<'_> +where + C: AsRef<[u8]>, +{ + X509Certificate::from_der(raw_cert.as_ref()) + .expect("Failed to parse certificate from DER format.") + .1 +} + +impl Clone for CertKey { + fn clone(&self) -> Self { + CertKey { + key: self.key.clone(), + certificates: self + .certificates + .iter() + .map(|wrapper| WrappedX509::new(wrapper.borrow_raw_cert().clone(), parse_x509)) + .collect::<Vec<_>>(), + } + } } impl CertKey { @@ -92,12 +141,18 @@ impl CertKey { "expected a non-empty vector of certificates in CertKey::new" ); - CertKey { certificates, key } + CertKey { + key, + certificates: certificates + .into_iter() + .map(|raw_cert| WrappedX509::new(raw_cert, parse_x509)) + .collect::<Vec<_>>(), + } } /// Peek at the leaf certificate. - pub fn leaf(&self) -> &Vec<u8> { - // This is safe due to the assertion above. + pub fn leaf(&self) -> &WrappedX509 { + // This is safe due to the assertion in creation of a `CertKey` &self.certificates[0] } @@ -107,7 +162,7 @@ impl CertKey { } /// Return a slice of intermediate certificates. An empty slice means there are none. - pub fn intermediates(&self) -> Vec<&Vec<u8>> { + pub fn intermediates(&self) -> Vec<&WrappedX509> { self.certificates.iter().skip(1).collect() } @@ -122,6 +177,12 @@ impl CertKey { } } +impl WrappedX509 { + pub fn not_after(&self) -> String { + self.borrow_cert().validity.not_after.to_string() + } +} + // hide private key impl std::fmt::Debug for CertKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -137,7 +198,7 @@ impl std::fmt::Display for CertKey { if let Some(cn) = get_common_name(leaf) { // Write CN if it exists write!(f, "CN: {cn},")?; - } else if let Some(org_unit) = get_organization_unit_bytes(leaf) { + } else if let Some(org_unit) = get_organization_unit(leaf) { // CA cert might not have CN, so print its unit name instead write!(f, "Org Unit: {org_unit},")?; } @@ -155,3 +216,9 @@ impl Hash for CertKey { } } } + +impl<'a> From<&'a WrappedX509> for CertificateDer<'static> { + fn from(value: &'a WrappedX509) -> Self { + CertificateDer::from(value.borrow_raw_cert().as_slice().to_owned()) + } +} diff --git a/pingora-proxy/tests/test_basic.rs b/pingora-proxy/tests/test_basic.rs index f33a247..e305b5e 100644 --- a/pingora-proxy/tests/test_basic.rs +++ b/pingora-proxy/tests/test_basic.rs @@ -566,7 +566,7 @@ async fn test_tls_underscore_non_sub_sni_verify_host() { assert_eq!(headers[header::CONNECTION], "close"); } -#[cfg(feature = "any_tls")] +#[cfg(feature = "openssl_derived")] #[tokio::test] async fn test_tls_alt_verify_host() { init(); @@ -585,7 +585,7 @@ async fn test_tls_alt_verify_host() { assert_eq!(res.status(), StatusCode::OK); } -#[cfg(feature = "any_tls")] +#[cfg(feature = "openssl_derived")] #[tokio::test] async fn test_tls_underscore_sub_alt_verify_host() { init(); diff --git a/pingora-proxy/tests/utils/cert.rs b/pingora-proxy/tests/utils/cert.rs index fb6f54c..5cdcec4 100644 --- a/pingora-proxy/tests/utils/cert.rs +++ b/pingora-proxy/tests/utils/cert.rs @@ -66,11 +66,11 @@ fn load_key(path: &str) -> PKey<Private> { #[cfg(feature = "rustls")] fn load_cert(path: &str) -> Vec<u8> { let path = format!("{}/{path}", super::conf_dir()); - load_pem_file_ca(&path) + load_pem_file_ca(&path).unwrap() } #[cfg(feature = "rustls")] fn load_key(path: &str) -> Vec<u8> { let path = format!("{}/{path}", super::conf_dir()); - load_pem_file_private_key(&path) + load_pem_file_private_key(&path).unwrap() } diff --git a/pingora-rustls/Cargo.toml b/pingora-rustls/Cargo.toml index 1c84d30..7b8df33 100644 --- a/pingora-rustls/Cargo.toml +++ b/pingora-rustls/Cargo.toml @@ -16,6 +16,7 @@ path = "src/lib.rs" [dependencies] log = "0.4.21" +pingora-error = {version = "0.3.0", path = "../pingora-error"} ring = "0.17.8" rustls = "0.23.12" rustls-native-certs = "0.7.1" @@ -23,7 +24,3 @@ rustls-pemfile = "2.1.2" rustls-pki-types = "1.7.0" tokio-rustls = "0.26.0" no_debug = "3.1.0" - -[dev-dependencies] -tokio-test = "0.4.3" -tokio = { workspace = true, features = ["full"] } diff --git a/pingora-rustls/src/lib.rs b/pingora-rustls/src/lib.rs index e4d5de0..f56cd53 100644 --- a/pingora-rustls/src/lib.rs +++ b/pingora-rustls/src/lib.rs @@ -12,13 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! This module contains all the rustls specific pingora integration for things +//! like loading certificates and private keys + #![warn(clippy::all)] use std::fs::File; use std::io::BufReader; +use std::path::Path; -use log::{error, warn}; +use log::warn; pub use no_debug::{Ellipses, NoDebug, WithTypeInfo}; +use pingora_error::{Error, ErrorType, OrErr, Result}; pub use rustls::{version, ClientConfig, RootCertStore, ServerConfig, Stream}; pub use rustls_native_certs::load_native_certs; use rustls_pemfile::Item; @@ -27,132 +32,138 @@ pub use tokio_rustls::client::TlsStream as ClientTlsStream; pub use tokio_rustls::server::TlsStream as ServerTlsStream; pub use tokio_rustls::{Accept, Connect, TlsAcceptor, TlsConnector, TlsStream}; -fn load_file(path: &String) -> BufReader<File> { - let file = File::open(path).expect("io error"); - BufReader::new(file) +/// Load the given file from disk as a buffered reader and use the pingora Error +/// type instead of the std::io version +fn load_file<P>(path: P) -> Result<BufReader<File>> +where + P: AsRef<Path>, +{ + File::open(path) + .or_err(ErrorType::FileReadError, "Failed to load file") + .map(BufReader::new) } -fn load_pem_file(path: &String) -> Result<Vec<Item>, std::io::Error> { - let iter: Vec<Item> = rustls_pemfile::read_all(&mut load_file(path)) - .filter_map(|f| { - if let Ok(f) = f { - Some(f) - } else { - let err = f.err().unwrap(); - warn!( - "Skipping PEM element in file \"{}\" due to error \"{}\"", - path, err - ); - None - } + +/// Read the pem file at the given path from disk +fn load_pem_file<P>(path: P) -> Result<Vec<Item>> +where + P: AsRef<Path>, +{ + rustls_pemfile::read_all(&mut load_file(path)?) + .map(|item_res| { + item_res.or_err( + ErrorType::InvalidCert, + "Certificate in pem file could not be read", + ) }) - .collect(); - Ok(iter) + .collect() } -pub fn load_ca_file_into_store(path: &String, cert_store: &mut RootCertStore) { - let ca_file = load_pem_file(path); - match ca_file { - Ok(cas) => { - cas.into_iter().for_each(|pem_item| { - // only loading certificates, handling a CA file - match pem_item { - Item::X509Certificate(content) => match cert_store.add(content) { - Ok(_) => {} - Err(err) => { - error!("{}", err) - } - }, - Item::Pkcs1Key(_) => {} - Item::Pkcs8Key(_) => {} - Item::Sec1Key(_) => {} - Item::Crl(_) => {} - Item::Csr(_) => {} - _ => {} - } - }); - } - Err(err) => { - error!( - "Failed to load configured ca file located at \"{}\", error: \"{}\"", - path, err +/// Load the certificates from the given pem file path into the given +/// certificate store +pub fn load_ca_file_into_store<P>(path: P, cert_store: &mut RootCertStore) -> Result<()> +where + P: AsRef<Path>, +{ + for pem_item in load_pem_file(path)? { + // only loading certificates, handling a CA file + let Item::X509Certificate(content) = pem_item else { + return Error::e_explain( + ErrorType::InvalidCert, + "Pem file contains un-loadable certificate type", ); - } + }; + cert_store.add(content).or_err( + ErrorType::InvalidCert, + "Failed to load X509 certificate into root store", + )?; } + + Ok(()) } -pub fn load_platform_certs_incl_env_into_store(ca_certs: &mut RootCertStore) { +/// Attempt to load the native cas into the given root-certificate store +pub fn load_platform_certs_incl_env_into_store(ca_certs: &mut RootCertStore) -> Result<()> { // this includes handling of ENV vars SSL_CERT_FILE & SSL_CERT_DIR - let native_platform_certs = load_native_certs(); - match native_platform_certs { - Ok(certs) => { - for cert in certs { - ca_certs.add(cert).unwrap(); - } - } - Err(err) => { - error!( - "Failed to load native platform ca-certificates: \"{:?}\". Continuing without ...", - err - ); - } + for cert in load_native_certs() + .or_err(ErrorType::InvalidCert, "Failed to load native certificates")? + .into_iter() + { + ca_certs.add(cert).or_err( + ErrorType::InvalidCert, + "Failed to load native certificate into root store", + )?; } + + Ok(()) } -pub fn load_certs_key_file<'a>( - cert: &String, - key: &String, -) -> Option<(Vec<CertificateDer<'a>>, PrivateKeyDer<'a>)> { - let certs_file = load_pem_file(cert) - .unwrap_or_else(|_| panic!("Failed to load configured cert file located at {}.", cert)); - let key_file = load_pem_file(key) - .unwrap_or_else(|_| panic!("Failed to load configured key file located at {}.", cert)); - - let mut certs: Vec<CertificateDer<'a>> = vec![]; - certs_file.into_iter().for_each(|i| { - if let Item::X509Certificate(cert) = i { - certs.push(cert) - } - }); - - let private_key = match key_file.into_iter().next()? { - Item::Pkcs1Key(key) => Some(PrivateKeyDer::from(key)), - Item::Pkcs8Key(key) => Some(PrivateKeyDer::from(key)), - Item::Sec1Key(key) => Some(PrivateKeyDer::from(key)), - _ => None, - }; - - if certs.is_empty() || private_key.is_none() { - None +/// Load the certificates and private key files +pub fn load_certs_and_key_files<'a>( + cert: &str, + key: &str, +) -> Result<Option<(Vec<CertificateDer<'a>>, PrivateKeyDer<'a>)>> { + let certs_file = load_pem_file(cert)?; + let key_file = load_pem_file(key)?; + + let certs = certs_file + .into_iter() + .filter_map(|item| { + if let Item::X509Certificate(cert) = item { + Some(cert) + } else { + None + } + }) + .collect::<Vec<_>>(); + + // These are the currently supported pk types - + // [https://doc.servo.org/rustls/key/struct.PrivateKey.html] + let private_key_opt = key_file + .into_iter() + .filter_map(|key_item| match key_item { + Item::Pkcs1Key(key) => Some(PrivateKeyDer::from(key)), + Item::Pkcs8Key(key) => Some(PrivateKeyDer::from(key)), + Item::Sec1Key(key) => Some(PrivateKeyDer::from(key)), + _ => None, + }) + .next(); + + if let (Some(private_key), false) = (private_key_opt, certs.is_empty()) { + Ok(Some((certs, private_key))) } else { - Some((certs, private_key?)) + Ok(None) } } -pub fn load_pem_file_ca(path: &String) -> Vec<u8> { - let mut reader = load_file(path); - let cas_file = rustls_pemfile::certs(&mut reader); - let ca = cas_file.into_iter().find_map(|pem_item| { - if let Ok(item) = pem_item { - Some(item) - } else { - None - } - }); - match ca { - None => Vec::new(), - Some(ca) => ca.to_vec(), - } +/// Load the certificate +pub fn load_pem_file_ca(path: &String) -> Result<Vec<u8>> { + let mut reader = load_file(path)?; + let cas_file_items = rustls_pemfile::certs(&mut reader) + .map(|item_res| { + item_res.or_err( + ErrorType::InvalidCert, + "Failed to load certificate from file", + ) + }) + .collect::<Result<Vec<_>>>()?; + + Ok(cas_file_items + .first() + .map(|ca| ca.to_vec()) + .unwrap_or_default()) } -pub fn load_pem_file_private_key(path: &String) -> Vec<u8> { - let key = rustls_pemfile::private_key(&mut load_file(path)); - if let Ok(Some(key)) = key { - return key.secret_der().to_vec(); - } - Vec::new() +pub fn load_pem_file_private_key(path: &String) -> Result<Vec<u8>> { + Ok(rustls_pemfile::private_key(&mut load_file(path)?) + .or_err( + ErrorType::InvalidCert, + "Failed to load private key from file", + )? + .map(|key| key.secret_der().to_vec()) + .unwrap_or_default()) } -pub fn hash_certificate(cert: CertificateDer) -> Vec<u8> { +pub fn hash_certificate(cert: &CertificateDer) -> Vec<u8> { let hash = ring::digest::digest(&ring::digest::SHA256, cert.as_ref()); hash.as_ref().to_vec() } diff --git a/pingora/Cargo.toml b/pingora/Cargo.toml index 4213c65..7f14864 100644 --- a/pingora/Cargo.toml +++ b/pingora/Cargo.toml @@ -82,10 +82,9 @@ boringssl = [ "openssl_derived", ] -# Coming soon -# ## Use [rustls](https://crates.io/crates/rustls) for tls -# ## -# ## ⚠️ _Highly Experimental_! ⚠️ Try it, but don't rely on it (yet) +## Use [rustls](https://crates.io/crates/rustls) for tls +## +## ⚠️ _Highly Experimental_! ⚠️ Try it, but don't rely on it (yet) rustls = [ "pingora-core/rustls", "pingora-proxy?/rustls", |