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
43 changes: 41 additions & 2 deletions pingora-core/src/protocols/tls/rustls/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ mod stream;
pub use stream::*;

use crate::utils::tls::WrappedX509;
use pingora_rustls::CertificateDer;
use pingora_rustls::{CertificateDer, NamedGroup};

pub type CaType = [WrappedX509];

/// TLS connection state exposed to post-handshake callbacks.
///
/// Provides access to peer certificates and negotiated cipher suite
/// Provides access to peer certificates and negotiated TLS parameters
/// after a TLS handshake completes. This is the rustls equivalent of
/// the OpenSSL `SslRef` that is used as `TlsRef` in the boringssl/openssl path.
#[derive(Debug)]
Expand All @@ -34,6 +34,7 @@ pub struct TlsRef {
pub(super) cipher: Option<&'static str>,
pub(super) version: Option<&'static str>,
pub(super) server_name: Option<String>,
pub(super) key_exchange_group: Option<NamedGroup>,
}

impl TlsRef {
Expand All @@ -56,6 +57,44 @@ impl TlsRef {
self.cipher
}

/// Returns the negotiated key exchange group, if available.
///
/// Returns `None` when no key exchange occurred, including resumed TLS 1.2
/// handshakes. This reports the group for this handshake, not an earlier
/// handshake that established a resumed session.
///
/// A downstream [`TlsAccept`](crate::listeners::TlsAccept) callback can
/// retain this value in an application-defined digest extension:
///
/// ```
/// use async_trait::async_trait;
/// use pingora_core::listeners::TlsAccept;
/// use pingora_core::protocols::tls::{SslDigest, TlsRef};
/// use pingora_rustls::NamedGroup;
/// use std::{any::Any, sync::Arc};
///
/// struct Group(Option<NamedGroup>);
/// struct CaptureGroup;
///
/// #[async_trait]
/// impl TlsAccept for CaptureGroup {
/// async fn handshake_complete_callback(
/// &self,
/// tls: &TlsRef,
/// ) -> Option<Arc<dyn Any + Send + Sync>> {
/// Some(Arc::new(Group(tls.negotiated_key_exchange_group())))
/// }
/// }
///
/// fn group_id(digest: &SslDigest) -> Option<u16> {
/// // Numeric identifiers also preserve groups without a known name.
/// digest.extension.get::<Group>()?.0.map(u16::from)
/// }
/// ```
pub fn negotiated_key_exchange_group(&self) -> Option<NamedGroup> {
self.key_exchange_group
}

/// Returns the negotiated TLS protocol version (e.g. "TLSv1.3"), if available.
pub fn version(&self) -> Option<&'static str> {
self.version
Expand Down
136 changes: 136 additions & 0 deletions pingora-core/src/protocols/tls/rustls/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,4 +203,140 @@ mod tests {
let cipher = digest.extension.get::<CipherName>().unwrap();
assert!(!cipher.0.is_empty());
}

#[derive(Debug)]
struct NegotiatedTls {
group: Option<rustls::NamedGroup>,
cipher: String,
}

struct GroupCallback;

#[async_trait]
impl TlsAccept for GroupCallback {
async fn handshake_complete_callback(
&self,
tls: &TlsRef,
) -> Option<Arc<dyn std::any::Any + Send + Sync>> {
Some(Arc::new(NegotiatedTls {
group: tls.negotiated_key_exchange_group(),
cipher: tls.current_cipher_name()?.to_owned(),
}))
}
}

fn group_acceptor() -> crate::listeners::tls::Acceptor {
let cert = format!("{}/tests/keys/server.crt", env!("CARGO_MANIFEST_DIR"));
let key = format!("{}/tests/keys/key.pem", env!("CARGO_MANIFEST_DIR"));
let mut settings = TlsSettings::with_callbacks(Box::new(GroupCallback)).unwrap();
settings.set_certificate_chain_file(&cert).unwrap();
settings.set_private_key_file(&key).unwrap();
settings.build()
}

fn group_connector(
group: &'static dyn rustls::crypto::SupportedKxGroup,
version: &'static rustls::SupportedProtocolVersion,
) -> pingora_rustls::TlsConnector {
let mut provider = rustls::crypto::ring::default_provider();
provider.kx_groups = vec![group];
let config = ClientConfig::builder_with_provider(Arc::new(provider))
.with_protocol_versions(&[version])
.unwrap()
.dangerous()
.with_custom_certificate_verifier(Arc::new(NoVerify))
.with_no_client_auth();
pingora_rustls::TlsConnector::from(Arc::new(config))
}

async fn group_handshake(
acceptor: &crate::listeners::tls::Acceptor,
connector: &pingora_rustls::TlsConnector,
) -> (
super::TlsStream<DuplexStream>,
super::TlsStream<DuplexStream>,
) {
let (client, server) = tokio::io::duplex(4096);
tokio::time::timeout(std::time::Duration::from_secs(5), async {
let (client, server) = tokio::join!(
crate::protocols::tls::rustls::client::handshake(
connector,
"openrusty.org",
client
),
acceptor.tls_handshake(server),
);
(client.unwrap(), server.unwrap())
})
.await
.expect("TLS handshake timed out")
}

#[tokio::test]
async fn test_negotiated_group_digest_extension() {
use crate::protocols::Ssl;
use rustls::crypto::ring::kx_group::{SECP256R1, X25519};

let acceptor = group_acceptor();
let mut digests = Vec::new();
for version in [&rustls::version::TLS12, &rustls::version::TLS13] {
for group in [X25519, SECP256R1] {
let connector = group_connector(group, version);
let (client, server) = group_handshake(&acceptor, &connector).await;
assert_eq!(
client.get_ssl().unwrap().negotiated_key_exchange_group(),
Some(group.name())
);
let digest = server.ssl_digest().unwrap();
let metadata = digest.extension.get::<NegotiatedTls>().unwrap();
assert_eq!(metadata.group, Some(group.name()));
assert_eq!(metadata.cipher, digest.cipher);
assert_eq!(client.ssl_digest().unwrap().version, digest.version);
digests.push((digest, group.name()));
}
}
// Retained digests must keep their own connection's metadata.
for (digest, group) in digests {
assert_eq!(
digest.extension.get::<NegotiatedTls>().unwrap().group,
Some(group)
);
}
}

#[tokio::test]
async fn test_negotiated_group_tls12_resumption() {
use crate::protocols::Ssl;

let acceptor = group_acceptor();
let connector = group_connector(
rustls::crypto::ring::kx_group::X25519,
&rustls::version::TLS12,
);
let (client, server) = group_handshake(&acceptor, &connector).await;
let original = server.ssl_digest().unwrap();
assert_eq!(
original.extension.get::<NegotiatedTls>().unwrap().group,
Some(rustls::NamedGroup::X25519)
);
drop((client, server));

let (client, server) = group_handshake(&acceptor, &connector).await;
assert_eq!(
client.stream.as_ref().unwrap().get_ref().1.handshake_kind(),
Some(rustls::HandshakeKind::Resumed)
);
assert_eq!(
client.get_ssl().unwrap().negotiated_key_exchange_group(),
None
);
let digest = server.ssl_digest().unwrap();
let metadata = digest.extension.get::<NegotiatedTls>().unwrap();
assert_eq!(metadata.group, None);
assert_eq!(metadata.cipher, digest.cipher);
assert_eq!(
original.extension.get::<NegotiatedTls>().unwrap().group,
Some(rustls::NamedGroup::X25519)
);
}
}
4 changes: 4 additions & 0 deletions pingora-core/src/protocols/tls/rustls/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,9 @@ impl<T: AsyncRead + AsyncWrite + Unpin + Send> InnerStream<T> {
.negotiated_cipher_suite()
.and_then(|suite| suite.suite().as_str());
let version = session.protocol_version().and_then(|v| v.as_str());
let key_exchange_group = session
.negotiated_key_exchange_group()
.map(|group| group.name());
let server_name = match stream {
RusTlsStream::Server(s) => s.get_ref().1.server_name().map(|n| n.to_string()),
RusTlsStream::Client(_) => None,
Expand All @@ -342,6 +345,7 @@ impl<T: AsyncRead + AsyncWrite + Unpin + Send> InnerStream<T> {
cipher,
version,
server_name,
key_exchange_group,
})
}
}
Expand Down
4 changes: 2 additions & 2 deletions pingora-rustls/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ pub use rustls::server::{
pub use rustls::sign;
pub use rustls::{
client::WebPkiServerVerifier, crypto::CryptoProvider, version, CertificateError, ClientConfig,
DigitallySignedStruct, Error as RusTlsError, KeyLogFile, RootCertStore, ServerConfig,
SignatureScheme, Stream,
DigitallySignedStruct, Error as RusTlsError, KeyLogFile, NamedGroup, RootCertStore,
ServerConfig, SignatureScheme, Stream,
};

/// Install the default `ring` CryptoProvider for rustls.
Expand Down
Loading