diff --git a/h3-quinn/src/lib.rs b/h3-quinn/src/lib.rs index 3e6e03e3..cc7e2690 100644 --- a/h3-quinn/src/lib.rs +++ b/h3-quinn/src/lib.rs @@ -338,12 +338,22 @@ where } } +impl quic::Is0rtt for BidiStream +where + B: Buf, +{ + fn is_0rtt(&self) -> bool { + self.recv.is_0rtt() + } +} + /// Quinn-backed receive stream /// /// Implements a [`quic::RecvStream`] backed by a [`quinn::RecvStream`]. pub struct RecvStream { stream: Option, read_chunk_fut: ReadChunkFuture, + is_0rtt: bool, } type ReadChunkFuture = ReusableBoxFuture< @@ -356,12 +366,22 @@ type ReadChunkFuture = ReusableBoxFuture< impl RecvStream { fn new(stream: quinn::RecvStream) -> Self { + let is_0rtt = stream.is_0rtt(); Self { stream: Some(stream), // Should only allocate once the first time it's used read_chunk_fut: ReusableBoxFuture::new(async { unreachable!() }), + is_0rtt, } } + + /// Check if this stream has been opened during 0-RTT. + /// + /// In which case any non-idempotent request should be considered dangerous at the application + /// level. Because read data is subject to replay attacks. + pub fn is_0rtt(&self) -> bool { + self.is_0rtt + } } impl quic::RecvStream for RecvStream { @@ -403,6 +423,12 @@ impl quic::RecvStream for RecvStream { } } +impl quic::Is0rtt for RecvStream { + fn is_0rtt(&self) -> bool { + self.is_0rtt + } +} + fn convert_read_error_to_stream_error(error: ReadError) -> StreamErrorIncoming { match error { ReadError::Reset(var_int) => StreamErrorIncoming::StreamTerminated { diff --git a/h3/src/frame.rs b/h3/src/frame.rs index 2bb07dfa..420ac6b8 100644 --- a/h3/src/frame.rs +++ b/h3/src/frame.rs @@ -41,6 +41,13 @@ impl FrameStream { pub fn into_inner(self) -> BufRecvStream { self.stream } + + pub(crate) fn is_0rtt(&self) -> bool + where + S: crate::quic::Is0rtt, + { + self.stream.is_0rtt() + } } impl FrameStream diff --git a/h3/src/quic.rs b/h3/src/quic.rs index f8e57035..923b46f8 100644 --- a/h3/src/quic.rs +++ b/h3/src/quic.rs @@ -230,3 +230,19 @@ pub trait BidiStream: SendStream + RecvStream { /// Split this stream into two halves. fn split(self) -> (Self::SendStream, Self::RecvStream); } + +/// Trait for QUIC streams that support 0-RTT detection. +/// +/// This allows detection of streams opened during the 0-RTT phase of a QUIC connection. +/// 0-RTT data is vulnerable to replay attacks, so applications should be cautious when +/// processing non-idempotent requests on such streams. +/// +/// See [RFC 8470 Section 5.2](https://www.rfc-editor.org/rfc/rfc8470.html#section-5.2) +/// for guidance on handling 0-RTT data in HTTP/3. +pub trait Is0rtt { + /// Check if this stream was opened during 0-RTT. + /// + /// Returns `true` if the stream was opened during the 0-RTT phase, + /// `false` otherwise. + fn is_0rtt(&self) -> bool; +} diff --git a/h3/src/server/stream.rs b/h3/src/server/stream.rs index fa1aecf2..e6c94bed 100644 --- a/h3/src/server/stream.rs +++ b/h3/src/server/stream.rs @@ -105,6 +105,28 @@ where pub fn id(&self) -> StreamId { self.inner.stream.id() } + + /// Check if this stream was opened during 0-RTT. + /// + /// See [RFC 8470 Section 5.2](https://www.rfc-editor.org/rfc/rfc8470.html#section-5.2). + /// + /// # Example + /// + /// ```no_run + /// # use h3::server::RequestStream; + /// # async fn example(mut stream: RequestStream, bytes::Bytes>) { + /// if stream.is_0rtt() { + /// // Reject non-idempotent methods (e.g., POST, PUT, DELETE) + /// // to prevent replay attacks + /// } + /// # } + /// ``` + pub fn is_0rtt(&self) -> bool + where + S: quic::Is0rtt, + { + self.inner.stream.is_0rtt() + } } impl RequestStream diff --git a/h3/src/stream.rs b/h3/src/stream.rs index ff6fdd52..764eccb8 100644 --- a/h3/src/stream.rs +++ b/h3/src/stream.rs @@ -437,6 +437,13 @@ impl BufRecvStream { _marker: PhantomData, } } + + pub(crate) fn is_0rtt(&self) -> bool + where + S: crate::quic::Is0rtt, + { + self.stream.is_0rtt() + } } impl BufRecvStream {