From 7b36fbd551722b38e9f46376bebaf7f0a5b068ef Mon Sep 17 00:00:00 2001 From: Mark Rousskov Date: Wed, 4 Oct 2023 17:00:55 +0000 Subject: [PATCH] Support exporting from TLS sessions This also bumps s2n-tls dependency to 0.0.39 so that we can make use of the new TLS-Exporter functionality in s2n-tls, not just in rustls. --- examples/resumption/Cargo.toml | 2 +- netbench/netbench-driver/Cargo.toml | 4 +- quic/s2n-quic-core/src/crypto/tls.rs | 28 ++++ quic/s2n-quic-core/src/crypto/tls/testing.rs | 7 + quic/s2n-quic-core/src/event.rs | 34 +++++ quic/s2n-quic-core/src/event/generated.rs | 87 +++++++++++++ quic/s2n-quic-events/events/connection.rs | 5 + quic/s2n-quic-rustls/src/session.rs | 18 +++ quic/s2n-quic-tls/Cargo.toml | 2 +- quic/s2n-quic-tls/src/session.rs | 14 ++ .../src/space/session_context.rs | 11 ++ quic/s2n-quic/src/tests.rs | 1 + quic/s2n-quic/src/tests/exporter.rs | 120 ++++++++++++++++++ 13 files changed, 329 insertions(+), 4 deletions(-) create mode 100644 quic/s2n-quic/src/tests/exporter.rs diff --git a/examples/resumption/Cargo.toml b/examples/resumption/Cargo.toml index 0e43ad3779..338a442649 100644 --- a/examples/resumption/Cargo.toml +++ b/examples/resumption/Cargo.toml @@ -5,5 +5,5 @@ edition = "2021" [dependencies] s2n-quic = { version = "1", path = "../../quic/s2n-quic", features = ["provider-tls-s2n", "unstable_resumption"]} -s2n-tls = { version = "=0.0.36", features = ["quic"] } +s2n-tls = { version = "=0.0.39", features = ["quic"] } tokio = { version = "1", features = ["full"] } diff --git a/netbench/netbench-driver/Cargo.toml b/netbench/netbench-driver/Cargo.toml index e76a004430..57fa862646 100644 --- a/netbench/netbench-driver/Cargo.toml +++ b/netbench/netbench-driver/Cargo.toml @@ -16,8 +16,8 @@ netbench = { version = "0.1", path = "../netbench" } probe = "0.5" s2n-quic = { path = "../../quic/s2n-quic", features = ["provider-tls-s2n"] } s2n-quic-core = { path = "../../quic/s2n-quic-core", features = ["testing"] } -s2n-tls = { version = "0.0.36" } -s2n-tls-tokio = { version = "0.0.36" } +s2n-tls = { version = "0.0.39" } +s2n-tls-tokio = { version = "0.0.39" } structopt = "0.3" tokio = { version = "1", features = ["io-util", "net", "time", "rt-multi-thread"] } tokio-native-tls = "0.3" diff --git a/quic/s2n-quic-core/src/crypto/tls.rs b/quic/s2n-quic-core/src/crypto/tls.rs index 2e993128de..9a3720412a 100644 --- a/quic/s2n-quic-core/src/crypto/tls.rs +++ b/quic/s2n-quic-core/src/crypto/tls.rs @@ -19,6 +19,29 @@ pub struct ApplicationParameters<'a> { pub transport_parameters: &'a [u8], } +#[derive(Debug)] +#[non_exhaustive] +pub enum TlsExportError { + #[non_exhaustive] + Failure, +} + +impl TlsExportError { + pub fn failure() -> Self { + TlsExportError::Failure + } +} + +pub trait TlsSession: Send + Sync { + /// See and . + fn tls_exporter( + &self, + label: &[u8], + context: &[u8], + output: &mut [u8], + ) -> Result<(), TlsExportError>; +} + //= https://www.rfc-editor.org/rfc/rfc9000#section-4 //= type=TODO //= tracking-issue=332 @@ -64,6 +87,11 @@ pub trait Context { //# peer's Finished message. fn on_handshake_complete(&mut self) -> Result<(), crate::transport::Error>; + fn on_tls_exporter_ready( + &mut self, + session: &impl TlsSession, + ) -> Result<(), crate::transport::Error>; + /// Receives data from the initial packet space /// /// A `max_len` may be provided to indicate how many bytes the TLS implementation diff --git a/quic/s2n-quic-core/src/crypto/tls/testing.rs b/quic/s2n-quic-core/src/crypto/tls/testing.rs index 42afb8d994..fe7676184f 100644 --- a/quic/s2n-quic-core/src/crypto/tls/testing.rs +++ b/quic/s2n-quic-core/src/crypto/tls/testing.rs @@ -716,6 +716,13 @@ where Ok(()) } + fn on_tls_exporter_ready( + &mut self, + _: &impl super::TlsSession, + ) -> Result<(), crate::transport::Error> { + Ok(()) + } + fn receive_initial(&mut self, max_len: Option) -> Option { self.log("rx initial"); self.initial.rx(max_len) diff --git a/quic/s2n-quic-core/src/event.rs b/quic/s2n-quic-core/src/event.rs index e208fbaac9..9e084a5960 100644 --- a/quic/s2n-quic-core/src/event.rs +++ b/quic/s2n-quic-core/src/event.rs @@ -127,3 +127,37 @@ impl IntoEvent for crate::time::Timestamp { Timestamp(self) } } + +#[derive(Clone)] +pub struct TlsSession<'a> { + session: &'a dyn crate::crypto::tls::TlsSession, +} + +impl<'a> TlsSession<'a> { + #[doc(hidden)] + pub fn new(session: &'a dyn crate::crypto::tls::TlsSession) -> TlsSession<'a> { + TlsSession { session } + } + + pub fn tls_exporter( + &self, + label: &[u8], + context: &[u8], + output: &mut [u8], + ) -> Result<(), crate::crypto::tls::TlsExportError> { + self.session.tls_exporter(label, context, output) + } +} + +impl<'a> crate::event::IntoEvent> for TlsSession<'a> { + #[inline] + fn into_event(self) -> Self { + self + } +} + +impl core::fmt::Debug for TlsSession<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("TlsSession").finish_non_exhaustive() + } +} diff --git a/quic/s2n-quic-core/src/event/generated.rs b/quic/s2n-quic-core/src/event/generated.rs index a0087e196a..491739a6d3 100644 --- a/quic/s2n-quic-core/src/event/generated.rs +++ b/quic/s2n-quic-core/src/event/generated.rs @@ -905,6 +905,14 @@ pub mod api { } #[derive(Clone, Debug)] #[non_exhaustive] + pub struct TlsExporterReady<'a> { + pub session: crate::event::TlsSession<'a>, + } + impl<'a> Event for TlsExporterReady<'a> { + const NAME: &'static str = "connectivity:tls_exporter_ready"; + } + #[derive(Clone, Debug)] + #[non_exhaustive] #[doc = " Path challenge updated"] pub struct PathChallengeUpdated<'a> { pub path_challenge_status: PathChallengeStatus, @@ -2048,6 +2056,17 @@ pub mod tracing { tracing :: event ! (target : "handshake_status_updated" , parent : id , tracing :: Level :: DEBUG , status = tracing :: field :: debug (status)); } #[inline] + fn on_tls_exporter_ready( + &mut self, + context: &mut Self::ConnectionContext, + _meta: &api::ConnectionMeta, + event: &api::TlsExporterReady, + ) { + let id = context.id(); + let api::TlsExporterReady { session } = event; + tracing :: event ! (target : "tls_exporter_ready" , parent : id , tracing :: Level :: DEBUG , session = tracing :: field :: debug (session)); + } + #[inline] fn on_path_challenge_updated( &mut self, context: &mut Self::ConnectionContext, @@ -3985,6 +4004,19 @@ pub mod builder { } } #[derive(Clone, Debug)] + pub struct TlsExporterReady<'a> { + pub session: crate::event::TlsSession<'a>, + } + impl<'a> IntoEvent> for TlsExporterReady<'a> { + #[inline] + fn into_event(self) -> api::TlsExporterReady<'a> { + let TlsExporterReady { session } = self; + api::TlsExporterReady { + session: session.into_event(), + } + } + } + #[derive(Clone, Debug)] #[doc = " Path challenge updated"] pub struct PathChallengeUpdated<'a> { pub path_challenge_status: PathChallengeStatus, @@ -4985,6 +5017,18 @@ mod traits { let _ = meta; let _ = event; } + #[doc = "Called when the `TlsExporterReady` event is triggered"] + #[inline] + fn on_tls_exporter_ready( + &mut self, + context: &mut Self::ConnectionContext, + meta: &ConnectionMeta, + event: &TlsExporterReady, + ) { + let _ = context; + let _ = meta; + let _ = event; + } #[doc = "Called when the `PathChallengeUpdated` event is triggered"] #[inline] fn on_path_challenge_updated( @@ -5629,6 +5673,16 @@ mod traits { (self.1).on_handshake_status_updated(&mut context.1, meta, event); } #[inline] + fn on_tls_exporter_ready( + &mut self, + context: &mut Self::ConnectionContext, + meta: &ConnectionMeta, + event: &TlsExporterReady, + ) { + (self.0).on_tls_exporter_ready(&mut context.0, meta, event); + (self.1).on_tls_exporter_ready(&mut context.1, meta, event); + } + #[inline] fn on_path_challenge_updated( &mut self, context: &mut Self::ConnectionContext, @@ -6099,6 +6153,8 @@ mod traits { fn on_connection_migration_denied(&mut self, event: builder::ConnectionMigrationDenied); #[doc = "Publishes a `HandshakeStatusUpdated` event to the publisher's subscriber"] fn on_handshake_status_updated(&mut self, event: builder::HandshakeStatusUpdated); + #[doc = "Publishes a `TlsExporterReady` event to the publisher's subscriber"] + fn on_tls_exporter_ready(&mut self, event: builder::TlsExporterReady); #[doc = "Publishes a `PathChallengeUpdated` event to the publisher's subscriber"] fn on_path_challenge_updated(&mut self, event: builder::PathChallengeUpdated); #[doc = "Publishes a `TlsClientHello` event to the publisher's subscriber"] @@ -6435,6 +6491,15 @@ mod traits { self.subscriber.on_event(&self.meta, &event); } #[inline] + fn on_tls_exporter_ready(&mut self, event: builder::TlsExporterReady) { + let event = event.into_event(); + self.subscriber + .on_tls_exporter_ready(self.context, &self.meta, &event); + self.subscriber + .on_connection_event(self.context, &self.meta, &event); + self.subscriber.on_event(&self.meta, &event); + } + #[inline] fn on_path_challenge_updated(&mut self, event: builder::PathChallengeUpdated) { let event = event.into_event(); self.subscriber @@ -6580,6 +6645,7 @@ pub mod testing { pub ecn_state_changed: u32, pub connection_migration_denied: u32, pub handshake_status_updated: u32, + pub tls_exporter_ready: u32, pub path_challenge_updated: u32, pub tls_client_hello: u32, pub tls_server_hello: u32, @@ -6659,6 +6725,7 @@ pub mod testing { ecn_state_changed: 0, connection_migration_denied: 0, handshake_status_updated: 0, + tls_exporter_ready: 0, path_challenge_updated: 0, tls_client_hello: 0, tls_server_hello: 0, @@ -7026,6 +7093,17 @@ pub mod testing { self.output.push(format!("{meta:?} {event:?}")); } } + fn on_tls_exporter_ready( + &mut self, + _context: &mut Self::ConnectionContext, + meta: &api::ConnectionMeta, + event: &api::TlsExporterReady, + ) { + self.tls_exporter_ready += 1; + if self.location.is_some() { + self.output.push(format!("{meta:?} {event:?}")); + } + } fn on_path_challenge_updated( &mut self, _context: &mut Self::ConnectionContext, @@ -7278,6 +7356,7 @@ pub mod testing { pub ecn_state_changed: u32, pub connection_migration_denied: u32, pub handshake_status_updated: u32, + pub tls_exporter_ready: u32, pub path_challenge_updated: u32, pub tls_client_hello: u32, pub tls_server_hello: u32, @@ -7347,6 +7426,7 @@ pub mod testing { ecn_state_changed: 0, connection_migration_denied: 0, handshake_status_updated: 0, + tls_exporter_ready: 0, path_challenge_updated: 0, tls_client_hello: 0, tls_server_hello: 0, @@ -7671,6 +7751,13 @@ pub mod testing { self.output.push(format!("{event:?}")); } } + fn on_tls_exporter_ready(&mut self, event: builder::TlsExporterReady) { + self.tls_exporter_ready += 1; + let event = event.into_event(); + if self.location.is_some() { + self.output.push(format!("{event:?}")); + } + } fn on_path_challenge_updated(&mut self, event: builder::PathChallengeUpdated) { self.path_challenge_updated += 1; let event = event.into_event(); diff --git a/quic/s2n-quic-events/events/connection.rs b/quic/s2n-quic-events/events/connection.rs index 859b1b1380..ea99477027 100644 --- a/quic/s2n-quic-events/events/connection.rs +++ b/quic/s2n-quic-events/events/connection.rs @@ -258,6 +258,11 @@ struct HandshakeStatusUpdated { status: HandshakeStatus, } +#[event("connectivity:tls_exporter_ready")] +struct TlsExporterReady<'a> { + session: crate::event::TlsSession<'a>, +} + #[event("connectivity:path_challenge_updated")] /// Path challenge updated struct PathChallengeUpdated<'a> { diff --git a/quic/s2n-quic-rustls/src/session.rs b/quic/s2n-quic-rustls/src/session.rs index 1a54b51f0b..904afda7a9 100644 --- a/quic/s2n-quic-rustls/src/session.rs +++ b/quic/s2n-quic-rustls/src/session.rs @@ -26,6 +26,23 @@ pub struct Session { server_name: Option, } +impl tls::TlsSession for Session { + fn tls_exporter( + &self, + label: &[u8], + context: &[u8], + output: &mut [u8], + ) -> Result<(), tls::TlsExportError> { + match self + .connection + .export_keying_material(output, label, Some(context)) + { + Ok(_) => Ok(()), + Err(_) => Err(tls::TlsExportError::failure()), + } + } +} + impl fmt::Debug for Session { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_struct("Session") @@ -149,6 +166,7 @@ impl Session { if !self.emitted_handshake_complete { self.rx_phase.transition(); context.on_handshake_complete()?; + context.on_tls_exporter_ready(self)?; } self.emitted_handshake_complete = true; diff --git a/quic/s2n-quic-tls/Cargo.toml b/quic/s2n-quic-tls/Cargo.toml index 3051c6ca79..d44e47b533 100644 --- a/quic/s2n-quic-tls/Cargo.toml +++ b/quic/s2n-quic-tls/Cargo.toml @@ -21,7 +21,7 @@ libc = "0.2" s2n-codec = { version = "=0.7.0", path = "../../common/s2n-codec", default-features = false } s2n-quic-core = { version = "=0.28.0", path = "../s2n-quic-core", default-features = false, features = ["alloc"] } s2n-quic-crypto = { version = "=0.29.0", path = "../s2n-quic-crypto", default-features = false } -s2n-tls = { version = "=0.0.36", features = ["quic"] } +s2n-tls = { version = "=0.0.39", features = ["quic"] } [dev-dependencies] checkers = "0.6" diff --git a/quic/s2n-quic-tls/src/session.rs b/quic/s2n-quic-tls/src/session.rs index f16c1e8ce5..1a22ea40bf 100644 --- a/quic/s2n-quic-tls/src/session.rs +++ b/quic/s2n-quic-tls/src/session.rs @@ -77,6 +77,19 @@ impl CryptoSuite for Session { type RetryKey = ::RetryKey; } +impl tls::TlsSession for Session { + fn tls_exporter( + &self, + label: &[u8], + context: &[u8], + output: &mut [u8], + ) -> Result<(), tls::TlsExportError> { + self.connection + .tls_exporter(label, context, output) + .map_err(|_| tls::TlsExportError::failure()) + } +} + impl tls::Session for Session { fn poll(&mut self, context: &mut W) -> Poll> where @@ -109,6 +122,7 @@ impl tls::Session for Session { if !self.handshake_complete { self.state.on_handshake_complete(); context.on_handshake_complete()?; + context.on_tls_exporter_ready(self)?; self.handshake_complete = true; } Poll::Ready(Ok(())) diff --git a/quic/s2n-quic-transport/src/space/session_context.rs b/quic/s2n-quic-transport/src/space/session_context.rs index e9585481c0..59a9b4cd86 100644 --- a/quic/s2n-quic-transport/src/space/session_context.rs +++ b/quic/s2n-quic-transport/src/space/session_context.rs @@ -457,6 +457,17 @@ impl<'a, Config: endpoint::Config, Pub: event::ConnectionPublisher> Ok(()) } + fn on_tls_exporter_ready( + &mut self, + session: &impl tls::TlsSession, + ) -> Result<(), transport::Error> { + self.publisher + .on_tls_exporter_ready(event::builder::TlsExporterReady { + session: s2n_quic_core::event::TlsSession::new(session), + }); + Ok(()) + } + fn on_handshake_complete(&mut self) -> Result<(), transport::Error> { // After the handshake is complete, the handshake crypto stream should be completely // finished diff --git a/quic/s2n-quic/src/tests.rs b/quic/s2n-quic/src/tests.rs index 58015d3bc0..7b17aee88b 100644 --- a/quic/s2n-quic/src/tests.rs +++ b/quic/s2n-quic/src/tests.rs @@ -46,6 +46,7 @@ mod client_handshake_confirm; #[cfg(not(target_os = "windows"))] mod mtls; +mod exporter; mod issue_1361; mod issue_1427; mod issue_1464; diff --git a/quic/s2n-quic/src/tests/exporter.rs b/quic/s2n-quic/src/tests/exporter.rs new file mode 100644 index 0000000000..7cbe42568c --- /dev/null +++ b/quic/s2n-quic/src/tests/exporter.rs @@ -0,0 +1,120 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! This module shows an example of an event provider that exports a symmetric key from an s2n-quic +//! connection on both client and server. + +use super::*; +use crate::provider::event::events::{self, ConnectionInfo, ConnectionMeta, Subscriber}; + +struct Exporter; + +#[derive(Default)] +struct ExporterContext { + key: Option<[u8; 32]>, +} + +impl Subscriber for Exporter { + type ConnectionContext = ExporterContext; + + #[inline] + fn create_connection_context( + &mut self, + _: &ConnectionMeta, + _info: &ConnectionInfo, + ) -> Self::ConnectionContext { + ExporterContext::default() + } + + fn on_tls_exporter_ready( + &mut self, + context: &mut Self::ConnectionContext, + _meta: &ConnectionMeta, + event: &events::TlsExporterReady, + ) { + let mut key = [0; 32]; + event + .session + .tls_exporter(b"EXPERIMENTAL EXPORTER s2n-quic", b"some context", &mut key) + .unwrap(); + context.key = Some(key); + } +} + +fn start_server(mut server: Server) -> crate::provider::io::testing::Result { + let server_addr = server.local_addr()?; + + // accept connections and echo back + spawn(async move { + while let Some(mut connection) = server.accept().await { + let key = connection + .query_event_context(|ctx: &ExporterContext| ctx.key.unwrap()) + .unwrap(); + + tracing::debug!("accepted server connection: {}", connection.id()); + spawn(async move { + while let Ok(Some(mut stream)) = connection.accept_bidirectional_stream().await { + tracing::debug!("accepted server stream: {}", stream.id()); + spawn(async move { + stream.send(Bytes::from(key.to_vec())).await.unwrap(); + }); + } + }); + } + }); + + Ok(server_addr) +} + +fn tls_test(f: fn(crate::Connection) -> C) +where + C: 'static + core::future::Future + Send, +{ + let model = Model::default(); + model.set_delay(Duration::from_millis(50)); + + test(model, |handle| { + let server = Server::builder() + .with_io(handle.builder().build()?)? + .with_tls(SERVER_CERTS)? + .with_event((Exporter, tracing_events()))? + .start()?; + + let addr = start_server(server)?; + + let client = Client::builder() + .with_io(handle.builder().build().unwrap())? + .with_tls(certificates::CERT_PEM)? + .with_event((Exporter, tracing_events()))? + .start()?; + + // show it working for several connections + for _ in 0..10 { + let client = client.clone(); + primary::spawn(async move { + let connect = Connect::new(addr).with_server_name("localhost"); + let conn = client.connect(connect).await.unwrap(); + f(conn).await; + }); + } + + Ok(addr) + }) + .unwrap(); +} + +#[test] +fn happy_case() { + tls_test(|mut conn| async move { + let client_key = conn + .query_event_context(|ctx: &ExporterContext| ctx.key.unwrap()) + .unwrap(); + + let mut stream = conn.open_bidirectional_stream().await.unwrap(); + + let server_key = stream.receive().await.unwrap().unwrap(); + + // Both the server and the client are expected to derive the same key. + assert_eq!(client_key, &server_key[..]); + }); +}