use crate::{
credentials,
crypto::{self, awslc, open::Application as _},
event,
packet::stream::{self, decoder::Packet, encoder},
stream::{
recv::{self, state::State},
shared::AcceptState,
TransportFeatures,
},
};
use s2n_codec::{DecoderBufferMut, EncoderBuffer};
use s2n_quic_core::{
buffer::{reader::incremental::Incremental, Reassembler},
dc,
inet::ExplicitCongestionNotification,
time::clock::testing as clock,
varint::VarInt,
};
use s2n_quic_core::buffer::{reader::storage::Chunk, Reader as BufReader};
struct ArbitraryFinReader<'a> {
offset: VarInt,
payload: &'a [u8],
cursor: usize,
final_offset: Option<VarInt>,
}
impl<'a> ArbitraryFinReader<'a> {
fn new(stream_offset: u64, payload: &'a [u8], final_offset: u64) -> Self {
Self {
offset: VarInt::new(stream_offset).unwrap(),
payload,
cursor: 0,
final_offset: Some(VarInt::new(final_offset).unwrap()),
}
}
}
impl s2n_quic_core::buffer::reader::Storage for ArbitraryFinReader<'_> {
type Error = core::convert::Infallible;
fn buffered_len(&self) -> usize {
self.payload.len() - self.cursor
}
fn read_chunk(&mut self, watermark: usize) -> Result<Chunk<'_>, Self::Error> {
let remaining = &self.payload[self.cursor..];
let len = remaining.len().min(watermark);
self.cursor += len;
Ok((&remaining[..len]).into())
}
fn partial_copy_into<Dest>(&mut self, dest: &mut Dest) -> Result<Chunk<'_>, Self::Error>
where
Dest: s2n_quic_core::buffer::writer::Storage + ?Sized,
{
self.read_chunk(dest.remaining_capacity())
}
}
impl BufReader for ArbitraryFinReader<'_> {
fn current_offset(&self) -> VarInt {
self.offset + self.cursor
}
fn final_offset(&self) -> Option<VarInt> {
self.final_offset
}
}
const KEY: &[u8; 16] = b"test-key-128bit!";
const IV: [u8; 12] = [0x42; 12];
fn sealer() -> awslc::seal::Application {
awslc::seal::Application::new(KEY, IV, &awslc::AES_128_GCM)
}
fn opener() -> awslc::open::Application {
awslc::open::Application::new(KEY, IV, &awslc::AES_128_GCM)
}
struct Harness {
state: State,
credentials: credentials::Credentials,
stream_id: stream::Id,
clock: clock::Clock,
sealer: awslc::seal::Application,
opener: awslc::open::Application,
next_pn: u64,
incremental: Incremental,
out_buf: Reassembler,
}
impl Harness {
fn new() -> Self {
let clock = clock::Clock::default();
let stream_id = stream::Id::default().reliable();
let credentials = credentials::testing::new(0, 0);
let params = dc::testing::TEST_APPLICATION_PARAMS;
let state = State::new(stream_id, ¶ms, TransportFeatures::TCP, &clock);
Self {
state,
credentials,
stream_id,
clock,
sealer: sealer(),
opener: opener(),
next_pn: 0,
incremental: Incremental::new(VarInt::ZERO),
out_buf: Reassembler::default(),
}
}
fn encode_packet(&mut self, payload: &[u8], is_fin: bool) -> Vec<u8> {
self.encode_packet_with_pn(self.next_pn, payload, is_fin)
}
fn encode_packet_with_pn(&mut self, pn: u64, mut payload: &[u8], is_fin: bool) -> Vec<u8> {
let mut buf = vec![0u8; 1024];
let encoder_buf = EncoderBuffer::new(&mut buf);
let mut reader = self.incremental.with_storage(&mut payload, is_fin).unwrap();
let packet_len = encoder::encode(
encoder_buf,
None,
self.stream_id,
VarInt::new(pn).unwrap(),
VarInt::ZERO,
VarInt::ZERO,
&mut &[][..],
VarInt::ZERO,
&(),
&mut reader,
&self.sealer,
&self.credentials,
);
self.next_pn = pn + 1;
buf.truncate(packet_len);
buf
}
fn encode_packet_at_offset(
&mut self,
pn: u64,
stream_offset: u64,
mut payload: &[u8],
is_fin: bool,
) -> Vec<u8> {
let mut buf = vec![0u8; 1024];
let encoder_buf = EncoderBuffer::new(&mut buf);
let mut inc = Incremental::new(VarInt::new(stream_offset).unwrap());
let mut reader = inc.with_storage(&mut payload, is_fin).unwrap();
let packet_len = encoder::encode(
encoder_buf,
None,
self.stream_id,
VarInt::new(pn).unwrap(),
VarInt::ZERO,
VarInt::ZERO,
&mut &[][..],
VarInt::ZERO,
&(),
&mut reader,
&self.sealer,
&self.credentials,
);
self.next_pn = pn + 1;
buf.truncate(packet_len);
buf
}
fn encode_packet_with_final_offset(
&mut self,
pn: u64,
stream_offset: u64,
payload: &[u8],
final_offset: u64,
) -> Vec<u8> {
let mut buf = vec![0u8; 1024];
let encoder_buf = EncoderBuffer::new(&mut buf);
let mut reader = ArbitraryFinReader::new(stream_offset, payload, final_offset);
let packet_len = encoder::encode(
encoder_buf,
None,
self.stream_id,
VarInt::new(pn).unwrap(),
VarInt::ZERO,
VarInt::ZERO,
&mut &[][..],
VarInt::ZERO,
&(),
&mut reader,
&self.sealer,
&self.credentials,
);
self.next_pn = pn + 1;
buf.truncate(packet_len);
buf
}
fn feed(&mut self, raw: &mut [u8]) -> Result<(), recv::ErrorKind> {
let tag_len = self.opener.tag_len();
let decoder = DecoderBufferMut::new(raw);
let (mut packet, _) = Packet::decode(decoder, (), tag_len).unwrap();
let control = crypto::open::control::stream::Reliable::default();
let publisher = event::testing::Publisher::no_snapshot();
self.state
.on_stream_packet(
&self.opener,
&control,
&self.credentials,
&mut packet,
ExplicitCongestionNotification::default(),
AcceptState::Accepted,
&self.clock,
&mut self.out_buf,
&publisher,
)
.map_err(|e| e.kind)
}
}
#[test]
fn sequential_packets_accepted() {
let mut h = Harness::new();
for i in 0..5u8 {
let payload = vec![i; 100];
let mut pkt = h.encode_packet(&payload, false);
h.feed(&mut pkt).expect("sequential packet should succeed");
}
}
#[test]
fn sequential_packets_with_fin_accepted() {
let mut h = Harness::new();
let mut pkt = h.encode_packet(b"hello", false);
h.feed(&mut pkt).unwrap();
let mut pkt = h.encode_packet(b" world", true);
h.feed(&mut pkt).unwrap();
}
#[test]
fn out_of_order_packet_number_rejected() {
let mut h = Harness::new();
let mut pkt = h.encode_packet(b"first", false);
h.feed(&mut pkt).unwrap();
let mut pkt = h.encode_packet_with_pn(2, b"third", false);
let err = h.feed(&mut pkt).unwrap_err();
assert!(
matches!(
err,
recv::ErrorKind::OutOfOrder {
expected: 1,
actual: 2
}
),
"expected OutOfOrder, got {err:?}"
);
}
#[test]
fn duplicate_packet_number_rejected() {
let mut h = Harness::new();
let mut pkt = h.encode_packet(b"first", false);
h.feed(&mut pkt).unwrap();
let mut pkt = h.encode_packet_with_pn(0, b"first", false);
let err = h.feed(&mut pkt).unwrap_err();
assert!(
matches!(
err,
recv::ErrorKind::OutOfOrder {
expected: 1,
actual: 0
}
),
"expected OutOfOrder, got {err:?}"
);
}
#[test]
fn gap_in_stream_offset_with_sequential_pn() {
let mut h = Harness::new();
let mut pkt = h.encode_packet(b"0123456789", false);
h.feed(&mut pkt).unwrap();
let mut pkt = h.encode_packet_at_offset(1, 20, b"abcdefghij", false);
let err = h.feed(&mut pkt).unwrap_err();
assert!(
matches!(
err,
recv::ErrorKind::OutOfOrder {
expected: 10,
actual: 20
}
),
"expected OutOfOrder for non-contiguous stream offset, got {err:?}"
);
}
#[test]
fn conflicting_final_offset_rejected() {
let mut h = Harness::new();
let mut pkt = h.encode_packet(b"0123456789", true);
h.feed(&mut pkt).unwrap();
let mut pkt = h.encode_packet_at_offset(1, 10, b"extra", true);
let err = h.feed(&mut pkt).unwrap_err();
assert!(
matches!(err, recv::ErrorKind::InvalidFin),
"expected InvalidFin, got {err:?}"
);
}
#[test]
fn final_offset_regresses_rejected() {
let mut h = Harness::new();
let mut pkt = h.encode_packet(&[0xAB; 100], false);
h.feed(&mut pkt).unwrap();
let mut pkt = h.encode_packet(&[0xCD; 10], true);
h.feed(&mut pkt).unwrap();
let mut pkt = h.encode_packet_at_offset(2, 110, &[0xEF; 5], true);
let err = h.feed(&mut pkt).unwrap_err();
assert!(
matches!(err, recv::ErrorKind::InvalidFin),
"expected InvalidFin, got {err:?}"
);
}
#[test]
fn tampered_payload_rejected() {
let mut h = Harness::new();
let mut pkt = h.encode_packet(b"authentic", false);
let tag_start = pkt.len() - 16;
pkt[tag_start] ^= 0xFF;
let err = h.feed(&mut pkt).unwrap_err();
assert!(
matches!(err, recv::ErrorKind::Crypto(_)),
"tampered packet should fail AEAD: {err:?}"
);
}
#[test]
fn tampered_header_rejected() {
let mut h = Harness::new();
let mut pkt = h.encode_packet(b"authentic", false);
pkt[5] ^= 0xFF;
let err = h.feed(&mut pkt).unwrap_err();
assert!(
matches!(
err,
recv::ErrorKind::Crypto(_) | recv::ErrorKind::CredentialMismatch { .. }
),
"tampered header should be rejected: {err:?}"
);
}
#[test]
fn forged_final_offset_at_wrong_offset_rejected() {
let mut h = Harness::new();
let mut pkt = h.encode_packet(b"0123456789", false);
h.feed(&mut pkt).unwrap();
let mut forged = h.encode_packet_at_offset(1, 0, b"XXXXXXXXXX", true);
let tag_start = forged.len() - 16;
forged[tag_start..].fill(0x00);
let err = h.feed(&mut forged).unwrap_err();
assert!(
matches!(
err,
recv::ErrorKind::OutOfOrder {
expected: 10,
actual: 0
}
),
"expected OutOfOrder, got {err:?}"
);
assert_eq!(h.out_buf.final_size(), None);
}
#[test]
fn forged_final_offset_at_correct_offset_rejected_by_aead() {
let mut h = Harness::new();
let mut pkt = h.encode_packet(b"0123456789", false);
h.feed(&mut pkt).unwrap();
let mut forged = h.encode_packet_at_offset(1, 10, b"YYYYYYYYYY", true);
let tag_start = forged.len() - 16;
forged[tag_start..].fill(0x00);
let err = h.feed(&mut forged).unwrap_err();
assert!(
matches!(err, recv::ErrorKind::Crypto(_)),
"expected Crypto error, got {err:?}"
);
assert_eq!(
h.out_buf.final_size(),
None,
"final_offset must not be set for unauthenticated packet"
);
}
struct UdpHarness {
state: State,
credentials: credentials::Credentials,
stream_id: stream::Id,
clock: clock::Clock,
sealer: awslc::seal::Application,
opener: awslc::open::Application,
out_buf: Reassembler,
}
impl UdpHarness {
fn new() -> Self {
let clock = clock::Clock::default();
let stream_id = stream::Id::default();
let credentials = credentials::testing::new(0, 0);
let params = dc::testing::TEST_APPLICATION_PARAMS;
let state = State::new(stream_id, ¶ms, TransportFeatures::UDP, &clock);
Self {
state,
credentials,
stream_id,
clock,
sealer: sealer(),
opener: opener(),
out_buf: Reassembler::default(),
}
}
fn encode_packet_at_offset(
&self,
pn: u64,
stream_offset: u64,
payload: &[u8],
is_fin: bool,
) -> Vec<u8> {
let mut buf = vec![0u8; 1024];
let encoder_buf = EncoderBuffer::new(&mut buf);
let mut inc = Incremental::new(VarInt::new(stream_offset).unwrap());
let mut storage = payload;
let mut reader = inc.with_storage(&mut storage, is_fin).unwrap();
let packet_len = encoder::encode(
encoder_buf,
None,
self.stream_id,
VarInt::new(pn).unwrap(),
VarInt::ZERO,
VarInt::ZERO,
&mut &[][..],
VarInt::ZERO,
&(),
&mut reader,
&self.sealer,
&self.credentials,
);
buf.truncate(packet_len);
buf
}
fn encode_packet_with_final_offset(
&self,
pn: u64,
stream_offset: u64,
payload: &[u8],
final_offset: u64,
) -> Vec<u8> {
let mut buf = vec![0u8; 1024];
let encoder_buf = EncoderBuffer::new(&mut buf);
let mut reader = ArbitraryFinReader::new(stream_offset, payload, final_offset);
let packet_len = encoder::encode(
encoder_buf,
None,
self.stream_id,
VarInt::new(pn).unwrap(),
VarInt::ZERO,
VarInt::ZERO,
&mut &[][..],
VarInt::ZERO,
&(),
&mut reader,
&self.sealer,
&self.credentials,
);
buf.truncate(packet_len);
buf
}
fn feed(&mut self, raw: &mut [u8]) -> Result<(), recv::ErrorKind> {
let tag_len = self.opener.tag_len();
let decoder = DecoderBufferMut::new(raw);
let (mut packet, _) = Packet::decode(decoder, (), tag_len).unwrap();
let control = crypto::open::control::stream::Reliable::default();
let publisher = event::testing::Publisher::no_snapshot();
self.state
.on_stream_packet(
&self.opener,
&control,
&self.credentials,
&mut packet,
ExplicitCongestionNotification::default(),
AcceptState::Accepted,
&self.clock,
&mut self.out_buf,
&publisher,
)
.map_err(|e| e.kind)
}
}
#[test]
fn udp_forged_fin_on_overlapping_data_rejected_by_aead() {
use s2n_quic_core::buffer::reader::Storage as _;
let mut h = UdpHarness::new();
let mut pkt = h.encode_packet_at_offset(0, 0, b"0123456789", false);
h.feed(&mut pkt).unwrap();
let _ = h.out_buf.read_chunk(10).unwrap();
assert_eq!(h.out_buf.consumed_len(), 10);
let mut forged = h.encode_packet_at_offset(1, 0, b"0123456789", true);
let tag_start = forged.len() - 16;
forged[tag_start..].fill(0x00);
let err = h.feed(&mut forged).unwrap_err();
assert!(
matches!(err, recv::ErrorKind::Crypto(_)),
"expected Crypto error from AEAD in skip_until, got {err:?}"
);
assert_eq!(
h.out_buf.final_size(),
None,
"forged final_offset must not be accepted"
);
}
#[test]
fn udp_valid_fin_on_overlapping_data_accepted() {
use s2n_quic_core::buffer::reader::Storage as _;
let mut h = UdpHarness::new();
let mut pkt = h.encode_packet_at_offset(0, 0, b"0123456789", false);
h.feed(&mut pkt).unwrap();
let _ = h.out_buf.read_chunk(10).unwrap();
let mut pkt = h.encode_packet_at_offset(1, 0, b"0123456789", true);
h.feed(&mut pkt).unwrap();
assert_eq!(
h.out_buf.final_size(),
Some(10),
"valid FIN should set final_size"
);
}
#[test]
fn empty_payload_fin_accepted() {
let mut h = Harness::new();
let mut pkt = h.encode_packet(b"hello", false);
h.feed(&mut pkt).unwrap();
let mut pkt = h.encode_packet(b"", true);
h.feed(&mut pkt).unwrap();
assert_eq!(h.out_buf.final_size(), Some(5));
}
#[test]
fn duplicate_stream_offset_rejected() {
let mut h = Harness::new();
let mut pkt = h.encode_packet(b"first", false);
h.feed(&mut pkt).unwrap();
let mut pkt = h.encode_packet_at_offset(1, 0, b"first", false);
let err = h.feed(&mut pkt).unwrap_err();
assert!(
matches!(
err,
recv::ErrorKind::OutOfOrder {
expected: 5,
actual: 0
}
),
"expected OutOfOrder, got {err:?}"
);
}
#[test]
fn one_byte_gap_in_stream_offset_rejected() {
let mut h = Harness::new();
let mut pkt = h.encode_packet(b"A", false);
h.feed(&mut pkt).unwrap();
let mut pkt = h.encode_packet_at_offset(1, 2, b"C", false);
let err = h.feed(&mut pkt).unwrap_err();
assert!(
matches!(
err,
recv::ErrorKind::OutOfOrder {
expected: 1,
actual: 2
}
),
"expected OutOfOrder, got {err:?}"
);
}
#[test]
fn final_offset_ahead_of_payload_accepted_on_tcp() {
let mut h = Harness::new();
let mut pkt = h.encode_packet_with_final_offset(0, 0, b"0123456789", 50);
h.feed(&mut pkt).unwrap();
assert_eq!(h.out_buf.final_size(), Some(50));
}
#[test]
fn conflicting_final_offset_varint_rejected() {
let mut h = Harness::new();
let mut pkt = h.encode_packet_with_final_offset(0, 0, b"0123456789", 50);
h.feed(&mut pkt).unwrap();
let mut pkt = h.encode_packet_with_final_offset(1, 10, b"abcdefghij", 100);
let err = h.feed(&mut pkt).unwrap_err();
assert!(
matches!(err, recv::ErrorKind::InvalidFin),
"expected InvalidFin for conflicting final_offset, got {err:?}"
);
}
#[test]
fn final_offset_less_than_received_data_rejected() {
let mut h = Harness::new();
let mut pkt = h.encode_packet(b"01234567890123456789", false);
h.feed(&mut pkt).unwrap();
let mut pkt = h.encode_packet_with_final_offset(1, 20, b"abcde", 15);
let err = h.feed(&mut pkt).unwrap_err();
assert!(
matches!(err, recv::ErrorKind::InvalidFin),
"expected InvalidFin for final_offset < received data, got {err:?}"
);
}
#[test]
fn udp_forged_ahead_final_offset_on_overlap_rejected_by_aead() {
use s2n_quic_core::buffer::reader::Storage as _;
let mut h = UdpHarness::new();
let mut pkt = h.encode_packet_at_offset(0, 0, b"0123456789", false);
h.feed(&mut pkt).unwrap();
let _ = h.out_buf.read_chunk(10).unwrap();
let mut forged = h.encode_packet_with_final_offset(1, 0, b"0123456789", 100);
let tag_start = forged.len() - 16;
forged[tag_start..].fill(0x00);
let err = h.feed(&mut forged).unwrap_err();
assert!(
matches!(err, recv::ErrorKind::Crypto(_)),
"expected Crypto error from AEAD in skip_until, got {err:?}"
);
assert_eq!(
h.out_buf.final_size(),
None,
"forged ahead final_offset must not be accepted"
);
}
#[test]
fn udp_valid_ahead_final_offset_on_overlap_accepted() {
use s2n_quic_core::buffer::reader::Storage as _;
let mut h = UdpHarness::new();
let mut pkt = h.encode_packet_at_offset(0, 0, b"0123456789", false);
h.feed(&mut pkt).unwrap();
let _ = h.out_buf.read_chunk(10).unwrap();
let mut pkt = h.encode_packet_with_final_offset(1, 0, b"0123456789", 50);
h.feed(&mut pkt).unwrap();
assert_eq!(
h.out_buf.final_size(),
Some(50),
"valid ahead final_offset should be accepted"
);
}
#[test]
fn udp_conflicting_final_offset_on_overlap_rejected() {
use s2n_quic_core::buffer::reader::Storage as _;
let mut h = UdpHarness::new();
let mut pkt = h.encode_packet_with_final_offset(0, 0, b"0123456789", 50);
h.feed(&mut pkt).unwrap();
let _ = h.out_buf.read_chunk(10).unwrap();
let mut pkt = h.encode_packet_with_final_offset(1, 0, b"0123456789", 100);
let err = h.feed(&mut pkt).unwrap_err();
assert!(
matches!(err, recv::ErrorKind::InvalidFin),
"expected InvalidFin for conflicting final_offset on overlap, got {err:?}"
);
}
#[test]
fn ignores_forged_final_offset() {
use s2n_quic_core::buffer::reader::Storage as _;
let mut h = UdpHarness::new();
let mut pkt = h.encode_packet_at_offset(0, 0, b"0123456789", false);
h.feed(&mut pkt).unwrap();
let _ = h.out_buf.read_chunk(10).unwrap();
let mut forged = h.encode_packet_with_final_offset(1, 0, b"", 0);
let tag_start = forged.len() - 16;
forged[tag_start..].fill(0xAB);
let err = h.feed(&mut forged).unwrap_err();
assert!(
matches!(err, recv::ErrorKind::Crypto(_)),
"forged packet should be rejected by AEAD, not trusted pre-auth; got {err:?}"
);
}
#[test]
fn udp_forged_conflicting_fin_at_cursor_rejected_by_aead() {
use s2n_quic_core::buffer::reader::Storage as _;
let mut h = UdpHarness::new();
let mut pkt = h.encode_packet_with_final_offset(0, 0, b"0123456789", 50);
h.feed(&mut pkt).unwrap();
let _ = h.out_buf.read_chunk(10).unwrap();
assert_eq!(h.out_buf.consumed_len(), 10);
let mut forged = h.encode_packet_with_final_offset(1, 10, b"XXXXXXXXXX", 20);
let tag_start = forged.len() - 16;
forged[tag_start..].fill(0x00);
let err = h.feed(&mut forged).unwrap_err();
assert!(
matches!(err, recv::ErrorKind::Crypto(_)),
"forged packet at cursor should be rejected by AEAD, not trusted pre-auth; got {err:?}"
);
assert_eq!(
h.out_buf.final_size(),
Some(50),
"final_size must remain unchanged after forged packet"
);
}
#[test]
fn udp_forged_regressed_fin_at_cursor_rejected_by_aead() {
use s2n_quic_core::buffer::reader::Storage as _;
let mut h = UdpHarness::new();
let mut pkt = h.encode_packet_at_offset(0, 0, b"01234567890123456789", false);
h.feed(&mut pkt).unwrap();
let _ = h.out_buf.read_chunk(20).unwrap();
let mut forged = h.encode_packet_with_final_offset(1, 20, b"YYYYY", 5);
let tag_start = forged.len() - 16;
forged[tag_start..].fill(0x00);
let err = h.feed(&mut forged).unwrap_err();
assert!(
matches!(err, recv::ErrorKind::Crypto(_)),
"forged packet should be rejected by AEAD, not InvalidFin; got {err:?}"
);
assert_eq!(
h.out_buf.final_size(),
None,
"final_size must not be set by forged packet"
);
}
#[test]
fn udp_forged_empty_payload_conflicting_fin_at_cursor_rejected_by_aead() {
use s2n_quic_core::buffer::reader::Storage as _;
let mut h = UdpHarness::new();
let mut pkt = h.encode_packet_with_final_offset(0, 0, b"0123456789", 30);
h.feed(&mut pkt).unwrap();
let _ = h.out_buf.read_chunk(10).unwrap();
let mut forged = h.encode_packet_with_final_offset(1, 10, b"", 99);
let tag_start = forged.len() - 16;
forged[tag_start..].fill(0x00);
let err = h.feed(&mut forged).unwrap_err();
assert!(
matches!(err, recv::ErrorKind::Crypto(_)),
"forged empty packet should be rejected by AEAD; got {err:?}"
);
assert_eq!(h.out_buf.final_size(), Some(30));
}
#[test]
fn udp_forged_payload_exceeds_final_size_rejected_by_aead() {
use s2n_quic_core::buffer::reader::Storage as _;
let mut h = UdpHarness::new();
let mut pkt = h.encode_packet_at_offset(0, 0, b"0123456789", true);
h.feed(&mut pkt).unwrap();
let _ = h.out_buf.read_chunk(10).unwrap();
let mut forged = h.encode_packet_at_offset(1, 10, b"ZZZZZ", false);
let tag_start = forged.len() - 16;
forged[tag_start..].fill(0x00);
let err = h.feed(&mut forged).unwrap_err();
assert!(
matches!(err, recv::ErrorKind::Crypto(_)),
"forged packet exceeding final_size should be rejected by AEAD; got {err:?}"
);
}