aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.bleep2
-rw-r--r--pingora-core/Cargo.toml3
-rw-r--r--pingora-core/src/connectors/tls/rustls/mod.rs106
-rw-r--r--pingora-core/src/listeners/tls/rustls/mod.rs38
-rw-r--r--pingora-core/src/protocols/tls/rustls/client.rs4
-rw-r--r--pingora-core/src/protocols/tls/rustls/mod.rs15
-rw-r--r--pingora-core/src/protocols/tls/rustls/server.rs5
-rw-r--r--pingora-core/src/protocols/tls/rustls/stream.rs86
-rw-r--r--pingora-core/src/utils/tls/rustls.rs137
-rw-r--r--pingora-proxy/tests/test_basic.rs4
-rw-r--r--pingora-proxy/tests/utils/cert.rs4
-rw-r--r--pingora-rustls/Cargo.toml5
-rw-r--r--pingora-rustls/src/lib.rs225
-rw-r--r--pingora/Cargo.toml7
14 files changed, 342 insertions, 299 deletions
diff --git a/.bleep b/.bleep
index 44a497b..957afcb 100644
--- a/.bleep
+++ b/.bleep
@@ -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",