Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion pingora-core/src/listeners/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,11 @@ impl TransportStackBuilder {

Ok(TransportStack {
l4,
tls: self.tls.take().map(|tls| Arc::new(tls.build())),
tls: self
.tls
.take()
.map(|tls| tls.try_build().map(Arc::new))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do the other TLS backends need try_build() too? This path can use their TlsSettings as well.

.transpose()?,
l4_buffer: self.l4_buffer,
})
}
Expand Down
4 changes: 4 additions & 0 deletions pingora-core/src/listeners/tls/boringssl_openssl/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ impl TlsSettings {
callbacks: self.callbacks,
}
}

pub(crate) fn try_build(self) -> Result<Acceptor> {
Ok(self.build())
}
}

impl Acceptor {
Expand Down
179 changes: 158 additions & 21 deletions pingora-core/src/listeners/tls/rustls/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,31 @@ use pingora_error::{Error, OrErr, Result};
use pingora_rustls::load_certs_and_key_files;
use pingora_rustls::ClientCertVerifier;
use pingora_rustls::ServerConfig;
use pingora_rustls::{version, TlsAcceptor as RusTlsAcceptor};
use pingora_rustls::{
version, CertificateHashProvider, CryptoProvider, ResolvesServerCert, SupportedCipherSuite,
SupportedKxGroup, SupportedProtocolVersion, TlsAcceptor as RusTlsAcceptor,
};

use crate::protocols::{ALPN, IO};

static TLS12_AND_13: [&SupportedProtocolVersion; 2] = [&version::TLS12, &version::TLS13];
static TLS13_ONLY: [&SupportedProtocolVersion; 1] = [&version::TLS13];

/// The TLS settings of a listening endpoint
pub struct TlsSettings {
alpn_protocols: Option<Vec<Vec<u8>>>,
protocol_versions: &'static [&'static SupportedProtocolVersion],
crypto_provider: Option<CryptoProvider>,
cert_path: String,
key_path: String,
client_cert_verifier: Option<Arc<dyn ClientCertVerifier>>,
cert_resolver: Option<Arc<dyn ResolvesServerCert>>,
require_fips: bool,
}

pub struct Acceptor {
pub acceptor: RusTlsAcceptor,
pub(crate) cert_digest_hash: Option<&'static CertificateHashProvider>,
callbacks: Option<TlsAcceptCallbacks>,
}

Expand All @@ -46,41 +57,80 @@ impl TlsSettings {
/// _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
/// Use [`Self::try_build`] to receive these failures as a [`Result`].
pub fn build(self) -> Acceptor {
self.try_build().unwrap()
}

/// Create a Rustls acceptor and return configuration failures as errors.
pub fn try_build(self) -> Result<Acceptor> {
let has_cert_resolver = self.cert_resolver.is_some();
// rustls 0.23+ requires an explicit CryptoProvider.
pingora_rustls::install_default_crypto_provider();

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
)
};
let builder = if let Some(crypto_provider) = self.crypto_provider {
ServerConfig::builder_with_provider(Arc::new(crypto_provider))
.with_protocol_versions(self.protocol_versions)
} else {
pingora_rustls::install_default_crypto_provider();
Ok(ServerConfig::builder_with_protocol_versions(
self.protocol_versions,
))
}
.explain_err(InternalError, |e| {
format!("Failed to create server listener config builder: {e}")
})?;

let builder =
ServerConfig::builder_with_protocol_versions(&[&version::TLS12, &version::TLS13]);
let builder = if let Some(verifier) = self.client_cert_verifier {
builder.with_client_cert_verifier(verifier)
} else {
builder.with_no_client_auth()
};
let mut config = builder
.with_single_cert(certs, key)
.explain_err(InternalError, |e| {
format!("Failed to create server listener config: {e}")
})
.unwrap();
let mut config = if let Some(cert_resolver) = self.cert_resolver {
builder.with_cert_resolver(cert_resolver)
} else {
let (certs, key) = load_certs_and_key_files(&self.cert_path, &self.key_path)?
.ok_or_else(|| {
Error::explain(
InternalError,
format!(
"Failed to load provided certificates \"{}\" or key \"{}\".",
self.cert_path, self.key_path
),
)
})?;

builder
.with_single_cert(certs, key)
.explain_err(InternalError, |e| {
format!("Failed to create server listener config: {e}")
})?
};

if let Some(alpn_protocols) = self.alpn_protocols {
config.alpn_protocols = alpn_protocols;
}
let cert_digest_hash =
pingora_rustls::sha256_hash_provider(config.crypto_provider().as_ref());
if self.require_fips && !config.fips() {
return Error::e_explain(
InternalError,
format!(
"Rustls listener configuration does not report FIPS mode; cert_path=\"{}\" key_path=\"{}\" cert_resolver={}",
self.cert_path, self.key_path, has_cert_resolver
),
);
}
if self.require_fips && cert_digest_hash.is_none() {
return Error::e_explain(
InternalError,
"Rustls listener configuration does not provide a SHA-256 hash provider for certificate digests.",
);
}

Acceptor {
Ok(Acceptor {
acceptor: RusTlsAcceptor::from(Arc::new(config)),
cert_digest_hash,
callbacks: None,
}
})
}

/// Enable HTTP/2 support for this endpoint, which is default off.
Expand All @@ -93,20 +143,79 @@ impl TlsSettings {
self.alpn_protocols = Some(alpn.to_wire_protocols());
}

/// Configure ALPN protocols directly.
pub fn set_alpn_protocols(&mut self, protocols: Vec<Vec<u8>>) {
self.alpn_protocols = Some(protocols);
}

/// Require at least TLS 1.2 for this endpoint.
pub fn set_min_protocol_tls12(&mut self) {
self.protocol_versions = &TLS12_AND_13;
}

/// Require at least TLS 1.3 for this endpoint.
pub fn set_min_protocol_tls13(&mut self) {
self.protocol_versions = &TLS13_ONLY;
}

/// Set the rustls crypto provider for this endpoint.
pub fn set_crypto_provider(&mut self, crypto_provider: CryptoProvider) {
self.crypto_provider = Some(crypto_provider);
}

/// Set the supported cipher suites for this endpoint.
pub fn set_cipher_suites(&mut self, cipher_suites: Vec<SupportedCipherSuite>) {
self.crypto_provider
.get_or_insert_with(pingora_rustls::ring_default_crypto_provider)
.cipher_suites = cipher_suites;
}

/// Set the supported key exchange groups for this endpoint.
pub fn set_kx_groups(&mut self, kx_groups: Vec<&'static dyn SupportedKxGroup>) {
self.crypto_provider
.get_or_insert_with(pingora_rustls::ring_default_crypto_provider)
.kx_groups = kx_groups;
}

/// Configure mTLS by providing a rustls client certificate verifier.
pub fn set_client_cert_verifier(&mut self, verifier: Arc<dyn ClientCertVerifier>) {
self.client_cert_verifier = Some(verifier);
}

/// Require rustls to report FIPS mode for the generated server config.
pub fn set_require_fips(&mut self, require_fips: bool) {
self.require_fips = require_fips;
}

pub fn intermediate(cert_path: &str, key_path: &str) -> Result<Self>
where
Self: Sized,
{
Ok(TlsSettings {
alpn_protocols: None,
protocol_versions: &TLS12_AND_13,
crypto_provider: None,
cert_path: cert_path.to_string(),
key_path: key_path.to_string(),
client_cert_verifier: None,
cert_resolver: None,
require_fips: false,
})
}

pub fn with_cert_resolver(cert_resolver: Arc<dyn ResolvesServerCert>) -> Result<Self>
where
Self: Sized,
{
Ok(TlsSettings {
alpn_protocols: None,
protocol_versions: &TLS12_AND_13,
crypto_provider: None,
cert_path: String::new(),
key_path: String::new(),
client_cert_verifier: None,
cert_resolver: Some(cert_resolver),
require_fips: false,
})
}

Expand All @@ -133,3 +242,31 @@ impl Acceptor {
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn try_build_returns_error_when_required_fips_is_not_reported() {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let cert_path = format!("{manifest_dir}/tests/keys/server.crt");
let key_path = format!("{manifest_dir}/tests/keys/key.pem");
let mut settings = TlsSettings::intermediate(&cert_path, &key_path).unwrap();
settings.set_crypto_provider(pingora_rustls::ring_default_crypto_provider());
settings.set_require_fips(true);

match settings.try_build() {
Ok(_) => panic!("expected non-FIPS rustls listener config to return an error"),
Err(error) => {
assert_eq!(error.etype(), &InternalError);
assert!(error
.context
.as_ref()
.map(|context| context.as_str())
.unwrap_or_default()
.contains("does not report FIPS mode"));
}
}
}
}
4 changes: 4 additions & 0 deletions pingora-core/src/listeners/tls/s2n/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ impl TlsSettings {
}
}

pub fn try_build(self) -> Result<Acceptor> {
Ok(self.build())
}

/// Enable HTTP/2 support for this endpoint, which is default off.
/// This effectively sets the ALPN to prefer HTTP/2 with HTTP/1.1 allowed
pub fn enable_h2(&mut self) {
Expand Down
4 changes: 4 additions & 0 deletions pingora-core/src/protocols/tls/noop_tls/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ pub mod listeners {
Acceptor
}

pub fn try_build(&self) -> Result<Acceptor> {
Ok(self.build())
}

pub fn intermediate(_: &str, _: &str) -> Result<Self> {
Ok(Self)
}
Expand Down
31 changes: 24 additions & 7 deletions pingora-core/src/protocols/tls/rustls/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ use crate::utils::tls::get_organization_serial_bytes;
use pingora_error::ErrorType::{AcceptError, ConnectError, InternalError, TLSHandshakeFailure};
use pingora_error::{OkOrErr, OrErr, Result};
use pingora_rustls::TlsStream as RusTlsStream;
use pingora_rustls::{hash_certificate, NoDebug};
use pingora_rustls::{
hash_certificate, hash_certificate_with_provider, CertificateHashProvider, NoDebug,
};
use pingora_rustls::{Accept, Connect, ServerName, TlsConnector};
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use x509_parser::nom::AsBytes;
Expand All @@ -46,6 +48,7 @@ pub struct InnerStream<T> {
pub struct TlsStream<T> {
tls: InnerStream<T>,
digest: Option<Arc<SslDigest>>,
cert_digest_hash: NoDebug<Option<&'static CertificateHashProvider>>,
timing: TimingDigest,
}

Expand All @@ -69,6 +72,7 @@ where
Ok(TlsStream {
tls,
digest: None,
cert_digest_hash: None.into(),
timing: Default::default(),
})
}
Expand All @@ -85,6 +89,7 @@ where
Ok(TlsStream {
tls,
digest: None,
cert_digest_hash: acceptor.cert_digest_hash.into(),
timing: Default::default(),
})
}
Expand Down Expand Up @@ -163,15 +168,15 @@ where
pub(crate) async fn connect(&mut self) -> Result<()> {
self.tls.connect().await?;
self.timing.established_ts = SystemTime::now();
self.digest = self.tls.digest();
self.digest = self.tls.digest(*self.cert_digest_hash);
Ok(())
}

/// Finish the TLS handshake from client as a server
pub(crate) async fn accept(&mut self) -> Result<()> {
self.tls.accept().await?;
self.timing.established_ts = SystemTime::now();
self.digest = self.tls.digest();
self.digest = self.tls.digest(*self.cert_digest_hash);
Ok(())
}
}
Expand Down Expand Up @@ -307,8 +312,14 @@ impl<T: AsyncRead + AsyncWrite + Unpin + Send> InnerStream<T> {
Ok(())
}

pub(crate) fn digest(&mut self) -> Option<Arc<SslDigest>> {
Some(Arc::new(SslDigest::from_stream(&self.stream)))
pub(crate) fn digest(
&mut self,
cert_digest_hash: Option<&'static CertificateHashProvider>,
) -> Option<Arc<SslDigest>> {
Some(Arc::new(SslDigest::from_stream(
&self.stream,
cert_digest_hash,
)))
}
}

Expand Down Expand Up @@ -361,7 +372,10 @@ where
}

impl SslDigest {
fn from_stream<T>(stream: &Option<RusTlsStream<T>>) -> Self {
fn from_stream<T>(
stream: &Option<RusTlsStream<T>>,
cert_digest_hash: Option<&'static CertificateHashProvider>,
) -> Self {
let stream = stream.as_ref().unwrap();
let (_io, session) = stream.get_ref();
let protocol = session.protocol_version();
Expand All @@ -378,7 +392,10 @@ impl SslDigest {

let cert_digest = peer_certificates
.and_then(|certs| certs.first())
.map(|cert| hash_certificate(cert))
.map(|cert| match cert_digest_hash {
Some(hash_provider) => hash_certificate_with_provider(cert, hash_provider),
None => hash_certificate(cert),
})
.unwrap_or_default();

let (organization, serial_number) = peer_certificates
Expand Down
Loading
Loading