Skip to content

Commit 83c5402

Browse files
committed
Randomize the first packet number
This is mostly the work of @larseggert, I'm just repackaging it. This will target his branch with any changes. This randomizes the starting packet number the client uses for the Initial packet number space. We don't randomize this on the server, since otherwise we'd need even more changes to the tests to account for that. Fixes mozilla#2462. Closes mozilla#2499.
1 parent 5723e5c commit 83c5402

File tree

12 files changed

+168
-44
lines changed

12 files changed

+168
-44
lines changed

neqo-transport/src/connection/mod.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,7 @@ impl Connection {
392392
c.conn_params.get_versions().compatible(),
393393
Role::Client,
394394
&dcid,
395+
c.conn_params.randomize_first_pn_enabled(),
395396
)?;
396397
c.original_destination_cid = Some(dcid);
397398
let path = Path::temporary(
@@ -1336,6 +1337,7 @@ impl Connection {
13361337
self.conn_params.get_versions().compatible(),
13371338
self.role,
13381339
&retry_scid,
1340+
false, // don't randomize on Retry
13391341
)?;
13401342
self.address_validation = AddressValidationInfo::Retry {
13411343
token: packet.token().to_vec(),
@@ -1523,7 +1525,11 @@ impl Connection {
15231525
// Record the client's selected CID so that it can be accepted until
15241526
// the client starts using a real connection ID.
15251527
let dcid = ConnectionId::from(packet.dcid());
1526-
self.crypto.states_mut().init_server(version, &dcid)?;
1528+
self.crypto.states_mut().init_server(
1529+
version,
1530+
&dcid,
1531+
self.conn_params.randomize_first_pn_enabled(),
1532+
)?;
15271533
self.original_destination_cid = Some(dcid);
15281534
self.set_state(State::WaitInitial, now);
15291535

@@ -3072,7 +3078,8 @@ impl Connection {
30723078
.original_destination_cid
30733079
.as_ref()
30743080
.ok_or(Error::ProtocolViolation)?;
3075-
self.crypto.states_mut().init_server(version, dcid)?;
3081+
// No need to randomize the starting packet number; that's already taken care of.
3082+
self.crypto.states_mut().init_server(version, dcid, false)?;
30763083
version
30773084
};
30783085

neqo-transport/src/connection/params.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ pub struct ConnectionParameters {
9494
sni_slicing: bool,
9595
/// Whether to enable mlkem768nistp256-sha256.
9696
mlkem: bool,
97+
/// Whether to randomize the packet number of the first Initial packet.
98+
randomize_first_pn: bool,
9799
}
98100

99101
impl Default for ConnectionParameters {
@@ -125,6 +127,7 @@ impl Default for ConnectionParameters {
125127
pmtud_iface_mtu: true,
126128
sni_slicing: true,
127129
mlkem: true,
130+
randomize_first_pn: true,
128131
}
129132
}
130133
}
@@ -437,6 +440,17 @@ impl ConnectionParameters {
437440
self
438441
}
439442

443+
#[must_use]
444+
pub const fn randomize_first_pn_enabled(&self) -> bool {
445+
self.randomize_first_pn
446+
}
447+
448+
#[must_use]
449+
pub const fn randomize_first_pn(mut self, randomize_first_pn: bool) -> Self {
450+
self.randomize_first_pn = randomize_first_pn;
451+
self
452+
}
453+
440454
/// # Errors
441455
/// When a connection ID cannot be obtained.
442456
/// # Panics

neqo-transport/src/connection/tests/cc.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ enum CongestionSignal {
5858
}
5959

6060
fn cc_slow_start_to_cong_avoidance_recovery_period(congestion_signal: CongestionSignal) {
61-
let mut client = default_client();
61+
// This test needs to calculate largest_acked, which is difficult if packet number
62+
// randomization is enabled.
63+
let mut client = new_client(ConnectionParameters::default().randomize_first_pn(false));
6264
let mut server = default_server();
6365
let now = connect_rtt_idle(&mut client, &mut server, DEFAULT_RTT);
6466

neqo-transport/src/connection/tests/datagram.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,9 @@ use std::{cell::RefCell, rc::Rc};
88

99
use neqo_common::event::Provider as _;
1010
use static_assertions::const_assert;
11-
use test_fixture::now;
1211

1312
use super::{
14-
assert_error, connect_force_idle, default_client, default_server, new_client, new_server,
13+
assert_error, connect_force_idle, default_client, default_server, new_client, new_server, now,
1514
AT_LEAST_PTO,
1615
};
1716
use crate::{

neqo-transport/src/connection/tests/handshake.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1459,7 +1459,8 @@ fn client_initial_pto_matches_custom_initial_rtt() {
14591459
#[test]
14601460
fn server_initial_retransmits_identical() {
14611461
let mut now = now();
1462-
let mut client = default_client();
1462+
// We calculate largest_acked, which is difficult with packet number randomization.
1463+
let mut client = new_client(ConnectionParameters::default().randomize_first_pn(false));
14631464
let mut ci = client.process_output(now).dgram();
14641465
let mut ci2 = client.process_output(now).dgram();
14651466

neqo-transport/src/connection/tests/resumption.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,12 @@ fn ticket_rtt(rtt: Duration) -> Duration {
8686
// A simple ACK frame for a single packet with packet number 0.
8787
const ACK_FRAME_1: &[u8] = &[0x02, 0x00, 0x00, 0x00, 0x00];
8888

89-
// This test needs to decrypt the CI, so turn off MLKEM.
89+
// This test needs to predicts the exact shape of an ACK frame, so disable randomization.
9090
let mut client = new_client(
9191
ConnectionParameters::default()
9292
.versions(Version::Version1, vec![Version::Version1])
93-
.mlkem(false),
93+
.mlkem(false)
94+
.randomize_first_pn(false),
9495
);
9596
let mut server = default_server();
9697
let mut now = now();
@@ -109,7 +110,6 @@ fn ticket_rtt(rtt: Duration) -> Duration {
109110
// Now decrypt the packet.
110111
let (aead, hp) = initial_aead_and_hp(&client_dcid, Role::Server);
111112
let (header, pn) = header_protection::remove(&hp, protected_header, payload);
112-
assert_eq!(pn, 0);
113113
let pn_len = header.len() - protected_header.len();
114114
let mut buf = vec![0; payload.len()];
115115
let mut plaintext = aead

neqo-transport/src/crypto.rs

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ use enum_map::EnumMap;
1818
use neqo_common::{hex, hex_snip_middle, qdebug, qinfo, qtrace, Buffer, Encoder, Role};
1919
pub use neqo_crypto::Epoch;
2020
use neqo_crypto::{
21-
hkdf, hp, Aead, Agent, AntiReplay, Cipher, Error as CryptoError, HandshakeState, PrivateKey,
22-
PublicKey, Record, RecordList, ResumptionToken, SymKey, ZeroRttChecker, TLS_AES_128_GCM_SHA256,
23-
TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256, TLS_CT_HANDSHAKE, TLS_GRP_EC_SECP256R1,
24-
TLS_GRP_EC_SECP384R1, TLS_GRP_EC_SECP521R1, TLS_GRP_EC_X25519, TLS_GRP_KEM_MLKEM768X25519,
25-
TLS_VERSION_1_3,
21+
hkdf, hp, random, Aead, Agent, AntiReplay, Cipher, Error as CryptoError, HandshakeState,
22+
PrivateKey, PublicKey, Record, RecordList, ResumptionToken, SymKey, ZeroRttChecker,
23+
TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256, TLS_CT_HANDSHAKE,
24+
TLS_GRP_EC_SECP256R1, TLS_GRP_EC_SECP384R1, TLS_GRP_EC_SECP521R1, TLS_GRP_EC_X25519,
25+
TLS_GRP_KEM_MLKEM768X25519, TLS_VERSION_1_3,
2626
};
2727

2828
use crate::{
@@ -477,17 +477,18 @@ impl CryptoDxState {
477477
epoch: Epoch,
478478
secret: &SymKey,
479479
cipher: Cipher,
480+
min_pn: packet::Number,
480481
) -> Res<Self> {
481-
qdebug!("Making {direction:?} {epoch:?} CryptoDxState, v={version:?} cipher={cipher}",);
482+
qdebug!("Making {direction:?} {epoch:?} CryptoDxState, v={version:?} cipher={cipher} min_pn={min_pn}",);
482483
let hplabel = String::from(version.label_prefix()) + "hp";
483484
Ok(Self {
484485
version,
485486
direction,
486487
epoch: usize::from(epoch),
487488
aead: Aead::new(TLS_VERSION_1_3, cipher, secret, version.label_prefix())?,
488489
hpkey: hp::Key::extract(TLS_VERSION_1_3, cipher, secret, &hplabel)?,
489-
used_pn: 0..0,
490-
min_pn: 0,
490+
used_pn: min_pn..min_pn,
491+
min_pn,
491492
invocations: Self::limit(direction, cipher),
492493
largest_packet_len: INITIAL_LARGEST_PACKET_LEN,
493494
})
@@ -498,6 +499,7 @@ impl CryptoDxState {
498499
direction: CryptoDxDirection,
499500
label: &str,
500501
dcid: &[u8],
502+
min_pn: packet::Number,
501503
) -> Res<Self> {
502504
qtrace!("new_initial {version:?} {}", ConnectionIdRef::from(dcid));
503505
let salt = version.initial_salt();
@@ -511,7 +513,7 @@ impl CryptoDxState {
511513

512514
let secret = hkdf::expand_label(TLS_VERSION_1_3, cipher, &initial_secret, &[], label)?;
513515

514-
Self::new(version, direction, Epoch::Initial, &secret, cipher)
516+
Self::new(version, direction, Epoch::Initial, &secret, cipher, min_pn)
515517
}
516518

517519
/// Determine the confidentiality and integrity limits for the cipher.
@@ -732,6 +734,7 @@ impl CryptoDxState {
732734
CryptoDxDirection::Write,
733735
"server in",
734736
CLIENT_CID,
737+
0,
735738
)
736739
.unwrap()
737740
}
@@ -794,7 +797,7 @@ impl CryptoDxAppData {
794797
cipher: Cipher,
795798
) -> Res<Self> {
796799
Ok(Self {
797-
dx: CryptoDxState::new(version, dir, Epoch::ApplicationData, secret, cipher)?,
800+
dx: CryptoDxState::new(version, dir, Epoch::ApplicationData, secret, cipher, 0)?,
798801
cipher,
799802
next_secret: Self::update_secret(cipher, secret)?,
800803
})
@@ -1022,7 +1025,13 @@ impl CryptoStates {
10221025

10231026
/// Create the initial crypto state.
10241027
/// Note that the version here can change and that's OK.
1025-
pub fn init<'v, V>(&mut self, versions: V, role: Role, dcid: &[u8]) -> Res<()>
1028+
pub fn init<'v, V>(
1029+
&mut self,
1030+
versions: V,
1031+
role: Role,
1032+
dcid: &[u8],
1033+
randomize_first_pn: bool,
1034+
) -> Res<()>
10261035
where
10271036
V: IntoIterator<Item = &'v Version>,
10281037
{
@@ -1034,15 +1043,27 @@ impl CryptoStates {
10341043
Role::Server => (SERVER_INITIAL_LABEL, CLIENT_INITIAL_LABEL),
10351044
};
10361045

1046+
let min_pn = if randomize_first_pn {
1047+
let r = random::<4>();
1048+
let pn = packet::Number::from;
1049+
// A random starting packet number. 5 uniformly random bits.
1050+
// Then maybe add up to 256 to occasionally nudge into a second byte for
1051+
// both varint and packet number encodings, heavily weighted to small values.
1052+
// And a small offset to ensure that the value is always non-zero.
1053+
(pn(r[0]) & 0x1f) + (pn(r[1]) + pn(r[2]) + pn(r[3])).saturating_sub(512) + 1
1054+
} else {
1055+
0
1056+
};
1057+
10371058
for v in versions {
10381059
qdebug!(
10391060
"[{self}] Creating initial cipher state v={v:?}, role={role:?} dcid={}",
10401061
hex(dcid)
10411062
);
10421063

10431064
let mut initial = CryptoState {
1044-
tx: CryptoDxState::new_initial(*v, CryptoDxDirection::Write, write, dcid)?,
1045-
rx: CryptoDxState::new_initial(*v, CryptoDxDirection::Read, read, dcid)?,
1065+
tx: CryptoDxState::new_initial(*v, CryptoDxDirection::Write, write, dcid, min_pn)?,
1066+
rx: CryptoDxState::new_initial(*v, CryptoDxDirection::Read, read, dcid, 0)?,
10461067
};
10471068
if let Some(prev) = &self.initials[*v] {
10481069
qinfo!(
@@ -1063,9 +1084,14 @@ impl CryptoStates {
10631084
/// This is maybe slightly inefficient in the first case, because we might
10641085
/// not need the send keys if the packet is subsequently discarded, but
10651086
/// the overall effort is small enough to write off.
1066-
pub fn init_server(&mut self, version: Version, dcid: &[u8]) -> Res<()> {
1087+
pub fn init_server(
1088+
&mut self,
1089+
version: Version,
1090+
dcid: &[u8],
1091+
randomize_first_pn: bool,
1092+
) -> Res<()> {
10671093
if self.initials[version].is_none() {
1068-
self.init(&[version], Role::Server, dcid)?;
1094+
self.init(&[version], Role::Server, dcid, randomize_first_pn)?;
10691095
}
10701096
Ok(())
10711097
}
@@ -1102,6 +1128,7 @@ impl CryptoStates {
11021128
Epoch::ZeroRtt,
11031129
secret,
11041130
cipher,
1131+
0,
11051132
)?);
11061133
Ok(())
11071134
}
@@ -1143,13 +1170,15 @@ impl CryptoStates {
11431170
Epoch::Handshake,
11441171
write_secret,
11451172
cipher,
1173+
0,
11461174
)?,
11471175
rx: CryptoDxState::new(
11481176
version,
11491177
CryptoDxDirection::Read,
11501178
Epoch::Handshake,
11511179
read_secret,
11521180
cipher,
1181+
0,
11531182
)?,
11541183
});
11551184
Ok(())

neqo-transport/src/saved.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,10 @@ impl SavedDatagrams {
4747
let store = self.store(epoch);
4848

4949
if store.len() < MAX_SAVED_DATAGRAMS {
50-
qdebug!("saving datagram of {} bytes", d.len());
50+
qdebug!("saving {epoch:?} datagram of {} bytes", d.len());
5151
store.push(SavedDatagram { d, t });
5252
} else {
53-
qinfo!("not saving datagram of {} bytes", d.len());
53+
qinfo!("not saving {epoch:?} datagram of {} bytes", d.len());
5454
}
5555
}
5656

neqo-transport/tests/connection.rs

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,13 @@ fn reorder_server_initial() {
8585
// A simple ACK frame for a single packet with packet number 0.
8686
const ACK_FRAME: &[u8] = &[0x02, 0x00, 0x00, 0x00, 0x00];
8787

88-
// This test needs to decrypt the CI, so turn off MLKEM.
88+
// This test predicts the precise format of an ACK frame, so turn off MLKEM
89+
// and packet number randomization.
8990
let mut client = new_client(
9091
ConnectionParameters::default()
9192
.versions(Version::Version1, vec![Version::Version1])
92-
.mlkem(false),
93+
.mlkem(false)
94+
.randomize_first_pn(false),
9395
);
9496
let mut server = default_server();
9597

@@ -324,3 +326,65 @@ fn handshake_mlkem768x25519() {
324326
neqo_crypto::TLS_GRP_KEM_MLKEM768X25519
325327
);
326328
}
329+
330+
#[test]
331+
fn client_initial_packet_number() {
332+
// Check that the initial packet number is randomized (i.e, > 0) if the `randomize_first_pn`
333+
// connection parameter is set, and that it is zero when not.
334+
for randomize in [true, false] {
335+
// This test needs to decrypt the CI, so turn off MLKEM.
336+
let mut client = new_client(
337+
ConnectionParameters::default()
338+
.versions(Version::Version1, vec![Version::Version1])
339+
.mlkem(false)
340+
.randomize_first_pn(randomize),
341+
);
342+
343+
let client_initial = client.process_output(now());
344+
let (protected_header, client_dcid, _, payload) =
345+
decode_initial_header(client_initial.as_dgram_ref().unwrap(), Role::Client).unwrap();
346+
let (_, hp) = initial_aead_and_hp(client_dcid, Role::Client);
347+
let (_, pn) = header_protection::remove(&hp, protected_header, payload);
348+
assert!(
349+
randomize && pn > 0 || !randomize && pn == 0,
350+
"randomize {randomize} = {pn}"
351+
);
352+
}
353+
}
354+
355+
#[test]
356+
fn server_initial_packet_number() {
357+
// Check that the initial packet number is randomized (i.e, > 0) if the `randomize_first_pn`
358+
// connection parameter is set, and that it is zero when not.
359+
for randomize in [true, false] {
360+
// This test needs to decrypt the CI, so turn off MLKEM.
361+
let mut client = new_client(
362+
ConnectionParameters::default()
363+
.versions(Version::Version1, vec![Version::Version1])
364+
.mlkem(false),
365+
);
366+
let mut server = new_server(
367+
DEFAULT_ALPN,
368+
ConnectionParameters::default()
369+
.versions(Version::Version1, vec![Version::Version1])
370+
.randomize_first_pn(randomize),
371+
);
372+
373+
let client_initial = client.process_output(now()).dgram();
374+
let (_protected_header, client_dcid, _scid, _payload) =
375+
decode_initial_header(client_initial.as_ref().unwrap(), Role::Client).unwrap();
376+
377+
let (_, hp) = initial_aead_and_hp(client_dcid, Role::Server);
378+
379+
let server_initial = server.process(client_initial, now()).dgram();
380+
let (protected_header, _dcid, _scid, payload) =
381+
decode_initial_header(server_initial.as_ref().unwrap(), Role::Server).unwrap();
382+
383+
let (_, pn) = header_protection::remove(&hp, protected_header, payload);
384+
println!();
385+
assert!(
386+
randomize && pn > 0 || !randomize && pn == 0,
387+
"randomize {randomize} = {pn}"
388+
);
389+
}
390+
}

0 commit comments

Comments
 (0)