use bytes::Bytes;
use phantom_protocol::crypto::adaptive_crypto::{CipherSuite, CryptoSession, AEAD_OVERHEAD};
use phantom_protocol::crypto::hybrid_sign::{HybridSigningKey, HybridVerifyingKey};
use phantom_protocol::transport::handshake::{
ClientHello, HandshakeClient, HandshakeError, HandshakeResponse, HandshakeServer, ServerHello,
};
use phantom_protocol::transport::path::PathStateKind;
use phantom_protocol::transport::session::{
CryptoState, Session, MAX_REKEY_CATCHUP, REBIND_VALIDATION_PATH_ID,
};
use phantom_protocol::transport::shaping::{self, PaddingPolicy, MAX_SHAPED_WIRE};
use phantom_protocol::transport::stream::{Stream, INITIAL_STREAM_WINDOW};
use phantom_protocol::transport::types::{
PacketFlags, PacketHeader, PhantomPacket, SchedulerMode, SessionId, WIRE_VERSION,
};
use std::time::Duration;
fn make_session_pair(shared: [u8; 32]) -> (Session, Session) {
let id = SessionId::from_bytes([1u8; 32]);
let crypto_a = CryptoState::new(&shared, false).expect("client crypto");
let crypto_b = CryptoState::new(&shared, true).expect("server crypto");
(
Session::from_derived(id, crypto_a, SchedulerMode::LowLatency, shared, false),
Session::from_derived(id, crypto_b, SchedulerMode::LowLatency, shared, true),
)
}
#[test]
fn tampered_header_is_rejected_via_aad() {
let (client, server) = make_session_pair([0xB2u8; 32]);
let real_header = PacketHeader::new(
*server.id(),
7,
1,
PacketFlags::new(PacketFlags::ENCRYPTED | PacketFlags::RELIABLE),
);
let ct = client
.encrypt_packet(&real_header, b"AAD-bound payload", &[])
.expect("encrypt");
let tampered_header = PacketHeader {
stream_id: 8, ..real_header
};
let result = server.decrypt_packet(&tampered_header, &ct, &[]);
assert!(
result.is_err(),
"AEAD must reject a packet whose header (AAD) was mutated"
);
}
#[test]
fn tampered_extensions_is_rejected_via_aad() {
let (client, server) = make_session_pair([0x4Au8; 32]);
let header = PacketHeader::new(*server.id(), 3, 1, PacketFlags::new(PacketFlags::ENCRYPTED));
let ext = vec![0xFFu8, 0x01, 0x00, 0x04, b't', b'e', b's', b't'];
let ct = client
.encrypt_packet(&header, b"ext-bound payload", &ext)
.expect("encrypt");
let mut tampered = ext.clone();
tampered[0] ^= 0x80;
assert!(
server.decrypt_packet(&header, &ct, &tampered).is_err(),
"AEAD must reject a packet whose extensions (AAD) were mutated"
);
let pt = server
.decrypt_packet(&header, &ct, &ext)
.expect("decrypt with intact extensions");
assert_eq!(pt, b"ext-bound payload");
}
#[test]
fn hp_masks_header_fields_on_the_wire() {
let (client, server) = make_session_pair([0x71u8; 32]);
let header = PacketHeader::new(
*server.id(),
9,
0xA1B2C3D4E5F60718,
PacketFlags::new(PacketFlags::ENCRYPTED | PacketFlags::PRIORITY),
)
.with_epoch(2)
.with_path_id(3);
let ct = client
.encrypt_packet(&header, b"voice frame", &[])
.expect("encrypt");
let packet = PhantomPacket::new(header, ct);
let wire = client.protect_packet(&packet).expect("protect");
let cleartext = packet.to_wire();
assert_ne!(
&wire[0..15],
&cleartext[0..15],
"version/pn/flags/stream_id/epoch/path_id must all be masked on the wire"
);
assert_ne!(
wire[0], cleartext[0],
"the version byte must be masked on the v6 wire"
);
assert_ne!(
&wire[9..11],
&cleartext[9..11],
"the PRIORITY/voice flag must not be readable on the wire without the hp key"
);
let parsed = server.parse_protected(&wire).expect("parse");
assert_eq!(parsed.header, header);
assert_eq!(parsed.payload, packet.payload);
}
#[test]
fn hp_masked_region_tamper_fails_aead() {
let (client, server) = make_session_pair([0x72u8; 32]);
let header = PacketHeader::new(*server.id(), 1, 5, PacketFlags::new(PacketFlags::ENCRYPTED));
let ct = client
.encrypt_packet(&header, b"payload", &[])
.expect("encrypt");
let packet = PhantomPacket::new(header, ct);
let mut wire = client.protect_packet(&packet).expect("protect");
wire[5] ^= 0x40;
let tampered = server.parse_protected(&wire).expect("parse");
assert_ne!(
tampered.header, header,
"a flipped masked byte must change the recovered header"
);
assert!(
server
.decrypt_packet(&tampered.header, &tampered.payload, &tampered.extensions)
.is_err(),
"a tampered masked-header byte must fail the AEAD via the AAD"
);
}
#[test]
fn v5_session_id_is_off_wire_but_reconstructed() {
let shared = [0x33u8; 32];
let id = SessionId::from_bytes([0xC7u8; 32]);
let crypto_a = CryptoState::new(&shared, false).expect("client crypto");
let crypto_b = CryptoState::new(&shared, true).expect("server crypto");
let client = Session::from_derived(id, crypto_a, SchedulerMode::LowLatency, shared, false);
let server = Session::from_derived(id, crypto_b, SchedulerMode::LowLatency, shared, true);
let header = PacketHeader::new(*client.id(), 2, 9, PacketFlags::new(PacketFlags::ENCRYPTED));
let ct = client
.encrypt_packet(&header, b"hidden id", &[])
.expect("encrypt");
let packet = PhantomPacket::new(header, ct);
let wire = client.protect_packet(&packet).expect("protect");
assert!(
!wire.windows(8).any(|w| w == [0xC7u8; 8]),
"session_id must not be serialised onto the v5 wire"
);
let parsed = server.parse_protected(&wire).expect("parse");
assert_eq!(
parsed.header.session_id,
*server.id(),
"parse_protected reconstructs session_id from the routed session"
);
let pt = server
.decrypt_packet(&parsed.header, &parsed.payload, &parsed.extensions)
.expect("decrypt");
assert_eq!(pt, b"hidden id");
}
#[test]
fn v5_session_id_bound_via_aad_off_wire() {
let shared = [0x5Eu8; 32];
let id_a = SessionId::from_bytes([0xAAu8; 32]);
let id_b = SessionId::from_bytes([0xBBu8; 32]);
let sender = Session::from_derived(
id_a,
CryptoState::new(&shared, false).expect("sender crypto"),
SchedulerMode::LowLatency,
shared,
false,
);
let wrong = Session::from_derived(
id_b,
CryptoState::new(&shared, true).expect("wrong crypto"),
SchedulerMode::LowLatency,
shared,
true,
);
let right = Session::from_derived(
id_a,
CryptoState::new(&shared, true).expect("right crypto"),
SchedulerMode::LowLatency,
shared,
true,
);
assert_ne!(sender.id(), wrong.id(), "distinct session ids");
let header = PacketHeader::new(*sender.id(), 1, 1, PacketFlags::new(PacketFlags::ENCRYPTED));
let ct = sender
.encrypt_packet(&header, b"bound to A", &[])
.expect("encrypt");
let wrong_header =
PacketHeader::new(*wrong.id(), 1, 1, PacketFlags::new(PacketFlags::ENCRYPTED));
assert!(
wrong.decrypt_packet(&wrong_header, &ct, &[]).is_err(),
"a different session_id (off-wire, in the AAD) must not open the packet"
);
let right_header =
PacketHeader::new(*right.id(), 1, 1, PacketFlags::new(PacketFlags::ENCRYPTED));
let pt = right
.decrypt_packet(&right_header, &ct, &[])
.expect("matching session_id must open");
assert_eq!(pt, b"bound to A");
}
#[test]
fn v5_session_cid_chain_outbound_matches_peer_inbound_window() {
let (client, server) = make_session_pair([0x5Eu8; 32]);
let client_cid0 = client.current_outbound_cid();
let server_window = server.inbound_window_cids();
assert_eq!(
server_window[0], client_cid0,
"server inbound window[0] must equal the client's CID_0"
);
assert!(server_window.contains(&client_cid0));
let server_cid0 = server.current_outbound_cid();
let client_window = client.inbound_window_cids();
assert_eq!(client_window[0], server_cid0);
assert_eq!(
server_window.len(),
17,
"leading window is K+1 = 17 CIDs at start (K = 16)"
);
}
#[test]
fn v5_advance_outbound_cid_rotates_within_peer_window() {
let (client, server) = make_session_pair([0x6Au8; 32]);
let cid0 = client.current_outbound_cid();
let cid1 = client.advance_outbound_cid();
assert_ne!(cid0, cid1, "the CID must rotate on migrate");
assert_eq!(
cid1,
client.current_outbound_cid(),
"the outbound index advanced to 1"
);
let window = server.inbound_window_cids();
assert!(
window.contains(&cid1),
"CID_1 must be routable via the pre-registered window"
);
let cid2 = client.advance_outbound_cid();
assert_ne!(cid1, cid2);
assert!(window.contains(&cid2));
}
#[test]
fn v5_note_migration_path_slides_inbound_window() {
use std::collections::HashSet;
let (_client, server) = make_session_pair([0x7Bu8; 32]);
let original: HashSet<[u8; 8]> = server.inbound_window_cids().into_iter().collect();
assert!(
server.note_migration_path(0).is_none(),
"the initial path does not slide"
);
let s1 = server
.note_migration_path(1)
.expect("a forward path_id must slide the window");
assert_eq!(s1.add.len(), 1, "one CID added at the new leading edge");
assert!(
s1.remove.is_empty(),
"nothing removed yet (highest=1 <= trailing T)"
);
assert!(
!original.contains(&s1.add[0]),
"the added CID is a fresh leading-edge CID, not one already registered"
);
assert!(server.inbound_window_cids().contains(&s1.add[0]));
assert!(
server.note_migration_path(1).is_none(),
"a duplicate path_id slides nothing"
);
assert!(
server.note_migration_path(0).is_none(),
"a reordered-old path_id slides nothing"
);
let s2 = server
.note_migration_path(2)
.expect("the next forward path_id slides");
assert_ne!(
s1.add[0], s2.add[0],
"each slide adds a distinct leading-edge CID"
);
}
#[test]
fn m3_reserved_rebind_validation_path_is_disjoint_and_challengeable() {
let (_client, server) = make_session_pair([0x3Cu8; 32]);
for _ in 0..600 {
let id = server.next_migration_path_id();
assert_ne!(
id, 0,
"the migration counter must never reuse the handshake path"
);
assert_ne!(
id, REBIND_VALIDATION_PATH_ID,
"the migration counter must never reuse the reserved rebind validation path"
);
}
assert_eq!(server.path_state(0), Some(PathStateKind::Validated));
assert!(
server.begin_path_validation(0).is_none(),
"the always-Validated path 0 must refuse a new challenge"
);
assert_eq!(server.path_state(REBIND_VALIDATION_PATH_ID), None);
let challenge = server
.begin_path_validation(REBIND_VALIDATION_PATH_ID)
.expect("the reserved rebind id must be challengeable from scratch");
assert_eq!(
server.path_state(REBIND_VALIDATION_PATH_ID),
Some(PathStateKind::Validating)
);
let mut wrong = challenge;
wrong[0] ^= 0xFF;
assert!(
!server.complete_path_validation(REBIND_VALIDATION_PATH_ID, &wrong),
"a wrong echo must NOT validate the rebind path"
);
assert_eq!(
server.path_state(REBIND_VALIDATION_PATH_ID),
Some(PathStateKind::Failed),
"a wrong echo fails the path closed"
);
}
#[test]
fn malformed_versioned_packet_fails_to_parse_not_panic() {
let garbage: Vec<u8> = (0u8..10).collect();
let result = PhantomPacket::from_wire(&garbage);
assert!(
result.is_err(),
"Parser must reject random bytes with Err, not panic or accept"
);
let empty: Vec<u8> = Vec::new();
let result = PhantomPacket::from_wire(&empty);
assert!(result.is_err(), "Parser must reject empty input");
}
#[test]
fn cookie_equality_smoke_via_subtle() {
use subtle::ConstantTimeEq;
let a = [0x42u8; 32];
let b = [0x42u8; 32];
let mut c = [0x42u8; 32];
c[31] ^= 1;
assert!(bool::from(a.ct_eq(&b)), "equal cookies must compare equal");
assert!(
!bool::from(a.ct_eq(&c)),
"different cookies must compare unequal"
);
}
#[test]
fn server_identity_mismatch_aborts_handshake() {
let real_server = HandshakeServer::new().expect("server new");
let attacker_server = HandshakeServer::new().expect("attacker new");
let attacker_pk = attacker_server.verifying_key().clone();
let client = HandshakeClient::new().expect("client new");
let client_hello = client.create_client_hello();
let client_ip = "127.0.0.1".parse().expect("ip");
let server_hello = match real_server.process_client_hello(&client_hello, 0, client_ip) {
HandshakeResponse::Retry(retry) => {
let mut hello_retry = client_hello.clone();
hello_retry.cookie = retry.cookie;
match real_server.process_client_hello(&hello_retry, 0, client_ip) {
HandshakeResponse::Success(sh, _, _) => sh,
other => panic!("unexpected after retry: {:?}", other),
}
}
HandshakeResponse::Success(sh, _, _) => sh,
other => panic!("unexpected first response: {:?}", other),
};
let result = client.process_server_hello(&client_hello, &server_hello, Some(&attacker_pk));
match result {
Err(HandshakeError::ServerIdentityMismatch) => { }
other => panic!(
"expected ServerIdentityMismatch, got {:?}",
other.as_ref().map(|_| "Ok").unwrap_or("Err(<other>)")
),
}
}
#[test]
fn aead_invocations_counter_increments_per_op() {
let secret = [0xC3u8; 32];
let session = CryptoSession::with_suite(&secret, CipherSuite::Aes256Gcm).expect("session");
assert_eq!(
session.send_invocations(),
0,
"fresh session has zero count"
);
let _ = session.encrypt(&[], b"first").expect("encrypt 1");
assert_eq!(session.send_invocations(), 1);
let _ = session.encrypt(&[], b"second").expect("encrypt 2");
assert_eq!(session.send_invocations(), 2);
}
#[test]
fn cookie_tampering_yields_retry_not_success() {
let server = HandshakeServer::new().expect("server new");
let client_ip = "10.20.30.40".parse().expect("ip");
let client = HandshakeClient::new().expect("client new");
let mut hello = client.create_client_hello();
hello.cookie = Some([0xDEu8; 32]);
match server.process_client_hello(&hello, 0, client_ip) {
HandshakeResponse::Retry(retry) => {
assert!(retry.cookie.is_some(), "server must provide a fresh cookie");
}
other => panic!(
"expected Retry on bogus cookie, got {:?}",
std::mem::discriminant(&other)
),
}
}
#[test]
fn signing_keypair_generation_is_non_deterministic() {
let (_sk1, vk1) = HybridSigningKey::generate();
let (_sk2, vk2) = HybridSigningKey::generate();
assert_ne!(
vk1.to_bytes(),
vk2.to_bytes(),
"two consecutive HybridSigningKey::generate() returned identical public keys"
);
}
#[test]
fn encrypted_packet_round_trip_preserves_payload() {
let (client, server) = make_session_pair([0xD4u8; 32]);
let payload = b"production-ready transport payload";
let header = PacketHeader::new(
*server.id(),
2,
42,
PacketFlags::new(PacketFlags::ENCRYPTED | PacketFlags::RELIABLE),
);
let ct = client
.encrypt_packet(&header, payload, &[])
.expect("encrypt");
assert_ne!(
&ct[..payload.len()],
payload,
"ciphertext must not contain plaintext"
);
let pt = server.decrypt_packet(&header, &ct, &[]).expect("decrypt");
assert_eq!(&pt, payload);
}
#[test]
fn tampered_ciphertext_is_rejected() {
let (client, server) = make_session_pair([0xF1u8; 32]);
let header = PacketHeader::new(
*server.id(),
7,
1,
PacketFlags::new(PacketFlags::ENCRYPTED | PacketFlags::RELIABLE),
)
.with_epoch(2)
.with_path_id(3);
let mut ct = client
.encrypt_packet(&header, b"v2 payload", &[])
.expect("encrypt v2");
ct[0] ^= 0x01;
let result = server.decrypt_packet(&header, &ct, &[]);
assert!(
result.is_err(),
"V2 AEAD must reject bit-flipped ciphertext; got {:?}",
result.as_ref().ok().map(|v| v.len())
);
}
#[test]
fn tampered_epoch_or_path_id_is_rejected() {
let (client, server) = make_session_pair([0xF2u8; 32]);
let real_header = PacketHeader::new(
*server.id(),
7,
1,
PacketFlags::new(PacketFlags::ENCRYPTED | PacketFlags::RELIABLE),
)
.with_epoch(5)
.with_path_id(0);
let ct = client
.encrypt_packet(&real_header, b"epoch-bound payload", &[])
.expect("encrypt");
let tampered_epoch = PacketHeader {
epoch: 6,
..real_header
};
assert!(server.decrypt_packet(&tampered_epoch, &ct, &[]).is_err());
let ct2 = client
.encrypt_packet(&real_header, b"path-bound payload", &[])
.expect("re-encrypt");
let tampered_path = PacketHeader {
path_id: 7,
..real_header
};
assert!(server.decrypt_packet(&tampered_path, &ct2, &[]).is_err());
}
#[test]
fn replay_window_rejects_duplicate_sequence() {
use phantom_protocol::CoreError;
let (client, server) = make_session_pair([0xF4u8; 32]);
let header = PacketHeader::new(
*server.id(),
3,
17,
PacketFlags::new(PacketFlags::ENCRYPTED | PacketFlags::RELIABLE),
);
let ct1 = client.encrypt_packet(&header, b"payload", &[]).expect("e1");
server
.decrypt_packet(&header, &ct1, &[])
.expect("first decrypt");
assert_eq!(server.replay_rejected_total(), 0);
let ct2 = client.encrypt_packet(&header, b"payload", &[]).expect("e2");
match server.decrypt_packet(&header, &ct2, &[]) {
Err(CoreError::ReplayDetected(_)) => { }
other => panic!(
"expected ReplayDetected on V2 duplicate, got {:?}",
other.as_ref().map(|_| "Ok").unwrap_or("Err(<other>)")
),
}
assert_eq!(server.replay_rejected_total(), 1);
}
#[test]
fn eps_replay_rejected_across_cid_rotation() {
use phantom_protocol::CoreError;
let (client, server) = make_session_pair([0x77u8; 32]);
let header = PacketHeader::new(
*server.id(),
5,
9,
PacketFlags::new(PacketFlags::ENCRYPTED | PacketFlags::RELIABLE),
);
let ct1 = client
.encrypt_packet(&header, b"v2-payload", &[])
.expect("e1");
server
.decrypt_packet(&header, &ct1, &[])
.expect("first decrypt accepts the sequence");
assert_eq!(server.replay_rejected_total(), 0);
let cid_before = client.current_outbound_cid();
let _rotated = client.advance_outbound_cid();
let window_before = server.inbound_window_cids();
let _slide = server.note_migration_path(1);
assert_ne!(
client.current_outbound_cid(),
cid_before,
"outbound CID must actually rotate"
);
assert_ne!(
server.inbound_window_cids(),
window_before,
"inbound window must actually slide"
);
let ct2 = client
.encrypt_packet(&header, b"v2-payload", &[])
.expect("e2");
match server.decrypt_packet(&header, &ct2, &[]) {
Err(CoreError::ReplayDetected(_)) => { }
other => panic!(
"expected ReplayDetected after a CID rotation, got {:?}",
other.as_ref().map(|_| "Ok").unwrap_or("Err(<other>)")
),
}
assert_eq!(server.replay_rejected_total(), 1);
}
#[test]
fn eps01_multistep_path_jump_slides_window_by_the_full_delta() {
let (_client, server) = make_session_pair([0x3Cu8; 32]);
let slide = server
.note_migration_path(5)
.expect("a forward path_id jump must slide");
assert_eq!(
slide.add.len(),
5,
"a 5-step path_id jump must add 5 leading CIDs (multi-step slide), not 1 (single-step lag)"
);
let next = server
.note_migration_path(6)
.expect("the next migration slides");
assert_eq!(
next.add.len(),
1,
"a subsequent +1 migration adds exactly 1 leading CID"
);
}
#[test]
fn failed_decrypt_does_not_desync_session() {
let (client, server) = make_session_pair([0x20u8; 32]);
let h1 = PacketHeader::new(
*server.id(),
1,
1,
PacketFlags::new(PacketFlags::ENCRYPTED | PacketFlags::RELIABLE),
);
let ct1 = client
.encrypt_packet(&h1, b"first", &[])
.expect("encrypt 1");
let mut tampered = ct1.clone();
let n = tampered.len();
tampered[n - 1] ^= 0x80;
assert!(server.decrypt_packet(&h1, &tampered, &[]).is_err());
let pt1 = server.decrypt_packet(&h1, &ct1, &[]).expect("decrypt 1");
assert_eq!(pt1, b"first");
let h2 = PacketHeader {
packet_number: 2,
..h1
};
let ct2 = client
.encrypt_packet(&h2, b"second", &[])
.expect("encrypt 2");
let pt2 = server.decrypt_packet(&h2, &ct2, &[]).expect("decrypt 2");
assert_eq!(pt2, b"second");
}
#[test]
fn rekey_changes_keys_and_breaks_old_ciphertexts() {
let (client, server) = make_session_pair([0x10u8; 32]);
assert_eq!(client.current_epoch(), 0);
assert_eq!(server.current_epoch(), 0);
let header = PacketHeader::new(
*server.id(),
1,
100,
PacketFlags::new(PacketFlags::ENCRYPTED | PacketFlags::RELIABLE),
);
let ct_epoch0 = client
.encrypt_packet(&header, b"pre-rekey payload", &[])
.expect("encrypt e0");
let client_new = client.rekey().expect("client rekey");
let server_new = server.rekey().expect("server rekey");
assert_eq!(client_new, 1);
assert_eq!(server_new, 1);
assert_eq!(client.current_epoch(), 1);
assert_eq!(server.current_epoch(), 1);
let header_epoch1 = PacketHeader { epoch: 1, ..header };
assert!(
server
.decrypt_packet(&header_epoch1, &ct_epoch0, &[])
.is_err(),
"post-rekey CryptoState must reject pre-rekey ciphertext"
);
let header_v1_e1 = PacketHeader::new(
*server.id(),
1,
101,
PacketFlags::new(PacketFlags::ENCRYPTED | PacketFlags::RELIABLE),
)
.with_epoch(1);
let ct_epoch1 = client
.encrypt_packet(&header_v1_e1, b"post-rekey payload", &[])
.expect("encrypt e1");
let pt = server
.decrypt_packet(&header_v1_e1, &ct_epoch1, &[])
.expect("decrypt e1");
assert_eq!(pt, b"post-rekey payload");
}
#[test]
fn ratchet_to_epoch_walks_forward_n_steps() {
let (_client, server) = make_session_pair([0x11u8; 32]);
assert_eq!(server.current_epoch(), 0);
server.ratchet_to_epoch(5).expect("ratchet to 5");
assert_eq!(server.current_epoch(), 5);
server.ratchet_to_epoch(3).expect("ratchet to 3 (no-op)");
assert_eq!(server.current_epoch(), 5);
}
#[test]
fn rekey_saturates_at_u8_max() {
let (_, server) = make_session_pair([0x12u8; 32]);
server
.ratchet_to_epoch(u8::MAX)
.expect("walk up to u8::MAX");
assert_eq!(server.current_epoch(), u8::MAX);
assert!(server.rekey().is_err());
assert_eq!(server.current_epoch(), u8::MAX, "epoch must not wrap");
}
#[test]
fn accepting_decrypt_follows_one_authentic_rekey_step() {
let (client, server) = make_session_pair([0x20u8; 32]);
assert_eq!(server.current_epoch(), 0);
assert_eq!(client.rekey().expect("client rekey"), 1);
let header = PacketHeader::new(
*server.id(),
1,
7,
PacketFlags::new(PacketFlags::ENCRYPTED | PacketFlags::REKEY),
)
.with_epoch(1);
let ct = client
.encrypt_packet(&header, b"first post-rekey", &[])
.expect("encrypt e1");
let pt = server
.decrypt_packet_accepting_rekey(&header, &ct, &[])
.expect("accepting decrypt follows the bump");
assert_eq!(pt, b"first post-rekey");
assert_eq!(server.current_epoch(), 1, "receiver committed the ratchet");
}
#[test]
fn accepting_decrypt_rejects_forged_bump_without_desync() {
let (client, server) = make_session_pair([0x21u8; 32]);
let forged = PacketHeader::new(
*server.id(),
1,
1,
PacketFlags::new(PacketFlags::ENCRYPTED | PacketFlags::REKEY),
)
.with_epoch(1);
let garbage = vec![0xABu8; 64];
assert!(
server
.decrypt_packet_accepting_rekey(&forged, &garbage, &[])
.is_err(),
"a forged epoch bump must fail the AEAD trial"
);
assert_eq!(
server.current_epoch(),
0,
"a failed trial decrypt must NOT advance the epoch (no desync)"
);
let header = PacketHeader::new(*server.id(), 1, 2, PacketFlags::new(PacketFlags::ENCRYPTED));
let ct = client
.encrypt_packet(&header, b"still in sync", &[])
.expect("encrypt e0");
let pt = server
.decrypt_packet_accepting_rekey(&header, &ct, &[])
.expect("same-epoch decrypt still works");
assert_eq!(pt, b"still in sync");
}
#[test]
fn accepting_decrypt_follows_bounded_multi_step_catchup() {
let (client, server) = make_session_pair([0x22u8; 32]);
client.ratchet_to_epoch(3).expect("client to 3");
let header = PacketHeader::new(
*server.id(),
1,
1,
PacketFlags::new(PacketFlags::ENCRYPTED | PacketFlags::REKEY),
)
.with_epoch(3);
let ct = client
.encrypt_packet(&header, b"three ahead", &[])
.expect("encrypt e3");
let pt = server
.decrypt_packet_accepting_rekey(&header, &ct, &[])
.expect("bounded multi-step catch-up follows a valid jump");
assert_eq!(pt, b"three ahead");
assert_eq!(server.current_epoch(), 3, "receiver caught up to epoch 3");
}
#[test]
fn accepting_decrypt_rejects_jump_beyond_catchup_bound() {
let (client, server) = make_session_pair([0x24u8; 32]);
let target = MAX_REKEY_CATCHUP + 1;
client.ratchet_to_epoch(target).expect("client far ahead");
let header = PacketHeader::new(
*server.id(),
1,
1,
PacketFlags::new(PacketFlags::ENCRYPTED | PacketFlags::REKEY),
)
.with_epoch(target);
let ct = client
.encrypt_packet(&header, b"too far", &[])
.expect("encrypt far");
assert!(
server
.decrypt_packet_accepting_rekey(&header, &ct, &[])
.is_err(),
"a jump beyond MAX_REKEY_CATCHUP must be rejected"
);
assert_eq!(
server.current_epoch(),
0,
"no ratchet on an over-bound jump"
);
}
#[test]
fn send_needs_rekey_fires_at_threshold_and_clears_on_rekey() {
let (client, _server) = make_session_pair([0x23u8; 32]);
client.set_rekey_threshold(4);
assert!(
!client.send_needs_rekey(),
"fresh session is below threshold"
);
let header = PacketHeader::new(*client.id(), 1, 0, PacketFlags::new(PacketFlags::ENCRYPTED));
for i in 0..4u32 {
let h = PacketHeader {
packet_number: i as u64,
..header
};
client.encrypt_packet(&h, b"x", &[]).expect("encrypt");
}
assert!(
client.send_needs_rekey(),
"after {} sends the trigger must fire",
client.send_invocations()
);
assert_eq!(client.rekey().expect("rekey"), 1);
assert!(
!client.send_needs_rekey(),
"rekey resets the send counter under the new key, clearing the trigger"
);
}
#[test]
fn forward_epoch_without_rekey_flag_is_rejected_before_catchup() {
let (client, server) = make_session_pair([0x40u8; 32]);
assert_eq!(client.rekey().expect("client rekey"), 1);
let no_rekey = PacketHeader::new(
*server.id(),
1,
9,
PacketFlags::new(PacketFlags::ENCRYPTED), )
.with_epoch(1);
let ct = client
.encrypt_packet(&no_rekey, b"forward but unflagged", &[])
.expect("encrypt e1");
assert!(
server
.decrypt_packet_accepting_rekey(&no_rekey, &ct, &[])
.is_err(),
"a forward-epoch packet without REKEY must be rejected"
);
assert_eq!(
server.current_epoch(),
0,
"the gate rejects before catch-up: no HKDF step, no epoch advance"
);
let with_rekey = PacketHeader::new(
*server.id(),
1,
9,
PacketFlags::new(PacketFlags::ENCRYPTED | PacketFlags::REKEY),
)
.with_epoch(1);
let ct2 = client
.encrypt_packet(&with_rekey, b"forward and flagged", &[])
.expect("encrypt e1 flagged");
let pt = server
.decrypt_packet_accepting_rekey(&with_rekey, &ct2, &[])
.expect("a REKEY-flagged forward packet is followed");
assert_eq!(pt, b"forward and flagged");
assert_eq!(server.current_epoch(), 1);
}
#[test]
fn rekey_unconfirmed_set_on_rekey_cleared_only_by_peer_at_current_epoch() {
let (client, server) = make_session_pair([0x41u8; 32]);
assert!(
!client.rekey_unconfirmed(),
"fresh session: nothing to confirm"
);
assert_eq!(client.rekey().expect("client rekey"), 1);
assert!(
client.rekey_unconfirmed(),
"a locally-initiated rekey is unconfirmed until the peer catches up"
);
let behind = PacketHeader::new(*client.id(), 1, 1, PacketFlags::new(PacketFlags::ENCRYPTED));
let ct_behind = server
.encrypt_packet(&behind, b"still at e0", &[])
.expect("server encrypt e0");
assert!(
client
.decrypt_packet_accepting_rekey(&behind, &ct_behind, &[])
.is_err(),
"a peer packet behind our epoch is rejected"
);
assert!(
client.rekey_unconfirmed(),
"a behind-epoch peer packet does not confirm catch-up"
);
assert_eq!(server.rekey().expect("server rekey"), 1);
let at_current =
PacketHeader::new(*client.id(), 1, 2, PacketFlags::new(PacketFlags::ENCRYPTED))
.with_epoch(1);
let ct_current = server
.encrypt_packet(&at_current, b"caught up to e1", &[])
.expect("server encrypt e1");
let pt = client
.decrypt_packet_accepting_rekey(&at_current, &ct_current, &[])
.expect("peer-at-current decrypts");
assert_eq!(pt, b"caught up to e1");
assert!(
!client.rekey_unconfirmed(),
"an authenticated peer packet at our epoch confirms the rekey"
);
}
#[test]
fn rekey_survives_loss_of_the_first_rekey_packet() {
let (client, server) = make_session_pair([0x42u8; 32]);
assert_eq!(client.rekey().expect("client rekey"), 1);
assert!(client.rekey_unconfirmed());
let p1 = PacketHeader::new(
*server.id(),
1,
10,
PacketFlags::new(PacketFlags::ENCRYPTED | PacketFlags::REKEY),
)
.with_epoch(1);
let _dropped = client
.encrypt_packet(&p1, b"lost trigger", &[])
.expect("encrypt p1");
assert!(
client.rekey_unconfirmed(),
"no peer confirmation yet → keep re-advertising REKEY"
);
let p2 = PacketHeader::new(
*server.id(),
1,
11,
PacketFlags::new(PacketFlags::ENCRYPTED | PacketFlags::REKEY),
)
.with_epoch(1);
let ct2 = client
.encrypt_packet(&p2, b"retransmit catches up", &[])
.expect("encrypt p2");
let pt = server
.decrypt_packet_accepting_rekey(&p2, &ct2, &[])
.expect("re-advertised REKEY lets the receiver catch up after losing p1");
assert_eq!(pt, b"retransmit catches up");
assert_eq!(
server.current_epoch(),
1,
"receiver caught up despite the lost trigger packet"
);
}
#[test]
fn size_padding_is_inside_the_aead_and_strips_to_inner() {
let (client, server) = make_session_pair([0x90u8; 32]);
let inner = b"application data of some particular, fingerprintable length".to_vec();
let trailer = shaping::padding_trailer_len(inner.len(), PaddingPolicy::Padme);
assert!(trailer >= 2, "a small packet must be padded to a bucket");
let mut pt = inner.clone();
shaping::append_padding(&mut pt, trailer);
let header = PacketHeader::new(
*server.id(),
1,
1,
PacketFlags::new(PacketFlags::ENCRYPTED | PacketFlags::PADDED),
);
let ct = client
.encrypt_packet(&header, &pt, &[])
.expect("encrypt padded");
let wire = PhantomPacket::new(header, ct.clone()).to_wire();
let expected =
shaping::padme(PacketHeader::SIZE + inner.len() + AEAD_OVERHEAD + 2).min(MAX_SHAPED_WIRE);
assert_eq!(
wire.len(),
expected,
"padded wire size lands on a PADÉ bucket"
);
assert!(
!wire.windows(inner.len()).any(|w| w == inner.as_slice()),
"inner plaintext must not appear on the wire"
);
let dec = server
.decrypt_packet(&header, &ct, &[])
.expect("decrypt padded");
let stripped = shaping::strip_padding(&dec).expect("strip padding");
assert_eq!(
stripped,
&inner[..],
"strip recovers the exact inner plaintext"
);
}
#[test]
fn padded_flag_is_aead_bound() {
let (client, server) = make_session_pair([0x91u8; 32]);
let inner = b"padded payload".to_vec();
let trailer = shaping::padding_trailer_len(inner.len(), PaddingPolicy::Padme);
let mut pt = inner.clone();
shaping::append_padding(&mut pt, trailer);
let padded_header = PacketHeader::new(
*server.id(),
1,
1,
PacketFlags::new(PacketFlags::ENCRYPTED | PacketFlags::PADDED),
);
let ct = client
.encrypt_packet(&padded_header, &pt, &[])
.expect("encrypt");
let stripped_flag = PacketHeader {
flags: PacketFlags::new(PacketFlags::ENCRYPTED),
..padded_header
};
assert!(
server.decrypt_packet(&stripped_flag, &ct, &[]).is_err(),
"clearing the AEAD-bound PADDED flag must fail the open"
);
assert!(server.decrypt_packet(&padded_header, &ct, &[]).is_ok());
}
#[test]
fn cover_packet_is_authenticated_padded_and_carries_no_data() {
let (client, server) = make_session_pair([0x92u8; 32]);
let trailer = shaping::padding_trailer_len(0, PaddingPolicy::Padme);
let mut pt = Vec::new();
shaping::append_padding(&mut pt, trailer);
let cover_header = PacketHeader::new(
*server.id(),
1,
1,
PacketFlags::new(PacketFlags::ENCRYPTED | PacketFlags::COVER | PacketFlags::PADDED),
);
let ct = client
.encrypt_packet(&cover_header, &pt, &[])
.expect("encrypt cover");
let wire = PhantomPacket::new(cover_header, ct.clone()).to_wire();
assert!(
wire.len() > PacketHeader::SIZE,
"cover packet is padded, not a tiny tell"
);
let dec = server
.decrypt_packet(&cover_header, &ct, &[])
.expect("decrypt cover");
let inner = shaping::strip_padding(&dec).expect("strip cover");
assert!(
inner.is_empty(),
"a cover packet carries no application data"
);
let no_cover = PacketHeader {
flags: PacketFlags::new(PacketFlags::ENCRYPTED | PacketFlags::PADDED),
..cover_header
};
assert!(
server.decrypt_packet(&no_cover, &ct, &[]).is_err(),
"clearing the AEAD-bound COVER flag must fail the open"
);
}
#[test]
fn concurrent_rekeys_keep_epoch_and_key_in_lockstep() {
use std::sync::Arc;
const THREADS: usize = 8;
const PER_THREAD: usize = 20;
let (client, server) = make_session_pair([0x30u8; 32]);
let client = Arc::new(client);
let mut handles = Vec::new();
for _ in 0..THREADS {
let c = Arc::clone(&client);
handles.push(std::thread::spawn(move || {
for _ in 0..PER_THREAD {
c.rekey().expect("concurrent rekey");
}
}));
}
for h in handles {
h.join().expect("rekey thread");
}
let epoch = client.current_epoch();
assert_eq!(
epoch as usize,
THREADS * PER_THREAD,
"every concurrent rekey must advance the epoch exactly once (no lost/double bumps)"
);
server.ratchet_to_epoch(epoch).expect("server catch up");
let header = PacketHeader::new(*client.id(), 1, 1, PacketFlags::new(PacketFlags::ENCRYPTED))
.with_epoch(epoch);
let ct = client
.encrypt_packet(&header, b"post-race payload", &[])
.expect("encrypt at final epoch");
let pt = server
.decrypt_packet(&header, &ct, &[])
.expect("installed key depth must equal the epoch counter");
assert_eq!(pt, b"post-race payload");
}
#[test]
fn new_paths_default_to_unvalidated() {
let (_client, server) = make_session_pair([0x40u8; 32]);
assert_eq!(server.path_state(0), Some(PathStateKind::Validated));
assert_eq!(server.path_state(7), None);
let challenge = server.begin_path_validation(7).expect("challenge");
assert_eq!(challenge.len(), 32);
assert_eq!(server.path_state(7), Some(PathStateKind::Validating));
}
#[test]
fn correct_response_validates_path() {
let (_client, server) = make_session_pair([0x41u8; 32]);
let challenge = server.begin_path_validation(3).expect("challenge");
assert!(server.complete_path_validation(3, &challenge));
assert_eq!(server.path_state(3), Some(PathStateKind::Validated));
let mut validated = server.validated_paths();
validated.sort();
assert_eq!(validated, vec![0, 3]);
}
#[test]
fn wrong_response_marks_path_failed() {
let (_client, server) = make_session_pair([0x42u8; 32]);
let mut challenge = server.begin_path_validation(5).expect("challenge");
challenge[0] ^= 0xFF;
assert!(!server.complete_path_validation(5, &challenge));
assert_eq!(server.path_state(5), Some(PathStateKind::Failed));
assert!(!server.validated_paths().contains(&5));
}
#[test]
fn unchallenged_path_cannot_be_completed() {
let (_client, server) = make_session_pair([0x43u8; 32]);
assert!(!server.complete_path_validation(9, &[0u8; 32]));
assert_eq!(server.path_state(9), None);
}
#[test]
fn packet_roundtrip_preserves_fields() {
let header = PacketHeader::new(
SessionId::from_bytes([9u8; 32]),
99,
2025,
PacketFlags::new(PacketFlags::RELIABLE | PacketFlags::ENCRYPTED | PacketFlags::REKEY),
)
.with_epoch(11)
.with_path_id(2);
let packet = PhantomPacket::new(header, vec![0xDE, 0xAD]);
let buf = packet.to_wire();
let decoded = PhantomPacket::from_wire(&buf).expect("roundtrip");
assert_eq!(decoded.header.version, WIRE_VERSION);
assert_eq!(decoded.header.epoch, 11);
assert_eq!(decoded.header.path_id, 2);
assert!(decoded.header.flags.contains(PacketFlags::REKEY));
assert_eq!(decoded.payload, vec![0xDE, 0xAD]);
}
#[tokio::test]
async fn flow_control_bounds_new_data_to_the_advertised_window() {
let s = Stream::new(1);
assert!(s.try_consume_send_window(INITIAL_STREAM_WINDOW - 100));
assert_eq!(s.peer_send_window(), 100);
s.send_reliable(Bytes::from(vec![0u8; 60])).await.unwrap(); s.send_reliable(Bytes::from(vec![0u8; 60])).await.unwrap();
let first = s
.poll_send(u64::MAX)
.await
.expect("first segment fits the window");
assert!(!first.retransmit);
assert_eq!(first.data.len(), 60);
assert_eq!(s.peer_send_window(), 40, "window debited by the sent bytes");
assert!(
s.poll_send(u64::MAX).await.is_none(),
"new data exceeding the flow-control window must be withheld"
);
assert_eq!(
s.peer_send_window(),
40,
"a withheld segment must NOT debit the window (no credit leak)"
);
let s2 = Stream::new(2);
s2.send_reliable(Bytes::from(vec![0u8; 100])).await.unwrap();
assert!(
s2.poll_send(50).await.is_none(),
"new data exceeding the congestion window must be withheld"
);
assert_eq!(
s2.peer_send_window(),
INITIAL_STREAM_WINDOW,
"a cwnd-blocked segment must not debit the flow-control window"
);
}
#[tokio::test]
async fn retransmissions_bypass_congestion_and_flow_control_windows() {
tokio::time::pause();
let s = Stream::new(1);
s.send_reliable(Bytes::from(vec![0u8; 200])).await.unwrap();
let first = s.poll_send(u64::MAX).await.expect("first transmission");
assert!(!first.retransmit);
assert_eq!(first.data.len(), 200);
assert_eq!(s.peer_send_window(), INITIAL_STREAM_WINDOW - 200);
assert!(s.try_consume_send_window(s.peer_send_window()));
assert_eq!(s.peer_send_window(), 0);
assert!(s.poll_send(0).await.is_none());
tokio::time::advance(Duration::from_millis(1100)).await;
let rtx = s
.poll_send(0)
.await
.expect("retransmission must bypass both the congestion and flow-control windows");
assert!(rtx.retransmit, "must be flagged as a retransmission");
assert_eq!(rtx.stream_offset, first.stream_offset);
assert_eq!(rtx.data.len(), 200);
assert_eq!(
s.peer_send_window(),
0,
"a retransmission must not debit the flow-control window again"
);
}
fn drive_handshake_to_success(
server: &HandshakeServer,
client_hello: &ClientHello,
client_ip: std::net::IpAddr,
) -> (ClientHello, ServerHello) {
match server.process_client_hello(client_hello, 0, client_ip) {
HandshakeResponse::Success(sh, _, _) => (client_hello.clone(), sh),
HandshakeResponse::Retry(retry) => {
let mut retried = client_hello.clone();
retried.cookie = retry.cookie;
match server.process_client_hello(&retried, 0, client_ip) {
HandshakeResponse::Success(sh, _, _) => (retried, sh),
other => panic!("unexpected response after cookie retry: {:?}", other),
}
}
other => panic!("unexpected first handshake response: {:?}", other),
}
}
#[test]
fn flipped_early_data_accepted_bit_fails_signature() {
let server = HandshakeServer::new().expect("server");
let server_pk = server.verifying_key().clone();
let client = HandshakeClient::new().expect("client");
let hello = client.create_client_hello();
let ip = "127.0.0.1".parse().expect("ip");
let (effective_hello, sh) = drive_handshake_to_success(&server, &hello, ip);
assert!(
client
.process_server_hello(&effective_hello, &sh, Some(&server_pk))
.is_ok(),
"an untampered ServerHello must verify"
);
let mut tampered = sh.clone();
tampered.early_data_accepted = !tampered.early_data_accepted;
assert!(
matches!(
client.process_server_hello(&effective_hello, &tampered, Some(&server_pk)),
Err(HandshakeError::KemFailed(_))
),
"flipping early_data_accepted must fail the transcript signature check"
);
}
fn first_handshake_mint_ticket(
server: &HandshakeServer,
client: &HandshakeClient,
server_pk: &HybridVerifyingKey,
ip: std::net::IpAddr,
) -> ([u8; 32], [u8; 32]) {
let hello = client.create_client_hello();
let (effective, sh) = drive_handshake_to_success(server, &hello, ip);
let (session, _) = client
.process_server_hello(&effective, &sh, Some(server_pk))
.expect("client establishes session");
let secret = session
.resumption_secret()
.expect("resumption secret installed");
(sh.session_id, secret)
}
#[test]
fn binderless_resume_does_not_burn_ticket() {
let server = HandshakeServer::new().expect("server");
let server_pk = server.verifying_key().clone();
let ip = "127.0.0.1".parse().expect("ip");
let client1 = HandshakeClient::new().expect("client1");
let (rid, secret) = first_handshake_mint_ticket(&server, &client1, &server_pk, ip);
assert_eq!(
server.session_cache_len(),
1,
"first handshake mints a ticket"
);
let client2 = HandshakeClient::new().expect("client2");
let mut forged = client2.create_client_hello_with_resume(rid, &secret, None);
forged.resumption_binder = None;
match server.process_client_hello(&forged, 0, ip) {
HandshakeResponse::Retry(_) => {} other => panic!("a binderless resume must not bypass the gate: {:?}", other),
}
assert_eq!(
server.session_cache_len(),
1,
"a binderless resume must NOT consume the ticket"
);
let mut wrong = client2.create_client_hello_with_resume(rid, &secret, None);
wrong.resumption_binder = Some([0xAB; 32]);
let _ = server.process_client_hello(&wrong, 0, ip);
assert_eq!(
server.session_cache_len(),
1,
"a wrong-binder resume must NOT consume the ticket"
);
let client3 = HandshakeClient::new().expect("client3");
let valid = client3.create_client_hello_with_resume(rid, &secret, None);
match server.process_client_hello(&valid, 0, ip) {
HandshakeResponse::Success(..) => {} other => panic!("a valid resume should succeed: {:?}", other),
}
match server.process_client_hello(&valid, 0, ip) {
HandshakeResponse::Retry(_) => {}
other => panic!(
"a replayed resume must not resume again (one-shot): {:?}",
other
),
}
}
#[test]
fn failed_resume_handshake_leaves_ticket_usable() {
let server = HandshakeServer::new().expect("server");
let server_pk = server.verifying_key().clone();
let ip = "127.0.0.1".parse().expect("ip");
let client1 = HandshakeClient::new().expect("client1");
let (rid, secret) = first_handshake_mint_ticket(&server, &client1, &server_pk, ip);
assert_eq!(server.session_cache_len(), 1);
let client2 = HandshakeClient::new().expect("client2");
let mut hello = client2.create_client_hello_with_resume(rid, &secret, None);
hello.client_key_package.ml_kem_pk.truncate(1); match server.process_client_hello(&hello, 0, ip) {
HandshakeResponse::Fail(HandshakeError::KemFailed(_)) => {}
other => panic!("a corrupted-KEM resume should fail: {:?}", other),
}
assert_eq!(
server.session_cache_len(),
1,
"a resume that fails after consume must re-insert the ticket (not burn it)"
);
let client3 = HandshakeClient::new().expect("client3");
let valid = client3.create_client_hello_with_resume(rid, &secret, None);
assert!(
matches!(
server.process_client_hello(&valid, 0, ip),
HandshakeResponse::Success(..)
),
"the re-inserted ticket must be usable by a clean resume"
);
}
#[test]
fn zero_rtt_early_data_can_be_disabled_by_config() {
let server = HandshakeServer::new().expect("server");
let server_pk = server.verifying_key().clone();
let ip: std::net::IpAddr = "127.0.0.1".parse().expect("ip");
let c1 = HandshakeClient::new().expect("c1");
let (rid1, sec1) = first_handshake_mint_ticket(&server, &c1, &server_pk, ip);
let c2 = HandshakeClient::new().expect("c2");
let (rid2, sec2) = first_handshake_mint_ticket(&server, &c2, &server_pk, ip);
let r1 = c1.create_client_hello_with_resume(rid1, &sec1, Some(b"0rtt-payload"));
let (_e1, sh1) = drive_handshake_to_success(&server, &r1, ip);
assert!(
sh1.early_data_accepted,
"0-RTT early-data is accepted by default"
);
server.set_early_data_enabled(false);
assert!(!server.early_data_enabled());
let r2 = c2.create_client_hello_with_resume(rid2, &sec2, Some(b"0rtt-payload"));
let (_e2, sh2) = drive_handshake_to_success(&server, &r2, ip);
assert!(
!sh2.early_data_accepted,
"disabling 0-RTT must reject early-data even on a valid resume"
);
}
#[test]
fn distributed_anti_replay_store_blocks_a_cross_node_0rtt_replay() {
use phantom_protocol::transport::handshake::ZeroRttAntiReplay;
use std::collections::HashSet;
use std::sync::{Arc, Mutex};
struct SharedStore {
consumed: Mutex<HashSet<[u8; 32]>>,
}
impl SharedStore {
fn mark_consumed_elsewhere(&self, id: [u8; 32]) {
self.consumed.lock().unwrap().insert(id);
}
}
impl ZeroRttAntiReplay for SharedStore {
fn check_and_set(&self, ticket_id: &[u8; 32]) -> bool {
self.consumed.lock().unwrap().insert(*ticket_id)
}
}
let server = HandshakeServer::new().expect("server");
let server_pk = server.verifying_key().clone();
let ip: std::net::IpAddr = "127.0.0.1".parse().expect("ip");
let store = Arc::new(SharedStore {
consumed: Mutex::new(HashSet::new()),
});
server.set_zero_rtt_anti_replay(store.clone());
let c1 = HandshakeClient::new().expect("c1");
let (rid1, sec1) = first_handshake_mint_ticket(&server, &c1, &server_pk, ip);
let r1 = c1.create_client_hello_with_resume(rid1, &sec1, Some(b"first-use"));
let (_e1, sh1) = drive_handshake_to_success(&server, &r1, ip);
assert!(
sh1.early_data_accepted,
"the store must allow a genuine first-use 0-RTT"
);
let c2 = HandshakeClient::new().expect("c2");
let (rid2, sec2) = first_handshake_mint_ticket(&server, &c2, &server_pk, ip);
store.mark_consumed_elsewhere(rid2);
let r2 = c2.create_client_hello_with_resume(rid2, &sec2, Some(b"replayed-0rtt"));
let (_e2, sh2) = drive_handshake_to_success(&server, &r2, ip);
assert!(
!sh2.early_data_accepted,
"the distributed store must block a 0-RTT replay across nodes"
);
}
#[test]
fn packet_number_is_strictly_monotonic_and_unique() {
let (client, _server) = make_session_pair([0x91u8; 32]);
let mut seen = std::collections::HashSet::new();
let mut last: Option<u64> = None;
for _ in 0..10_000 {
let pn = client.next_send_pn();
assert!(seen.insert(pn), "packet number {pn} reused");
if let Some(prev) = last {
assert!(
pn > prev,
"packet number not strictly increasing: {prev} -> {pn}"
);
}
last = Some(pn);
}
}
#[test]
fn path_id_is_in_aad_not_nonce() {
let (client, _server) = make_session_pair([0x92u8; 32]);
let sid = *client.id();
let pt = b"phantom-path-id-nonce-probe";
let h0 =
PacketHeader::new(sid, 1, 42, PacketFlags::new(PacketFlags::ENCRYPTED)).with_path_id(0);
let h5 =
PacketHeader::new(sid, 1, 42, PacketFlags::new(PacketFlags::ENCRYPTED)).with_path_id(5);
let c0 = client.encrypt_packet(&h0, pt, &[]).expect("encrypt h0");
let c5 = client.encrypt_packet(&h5, pt, &[]).expect("encrypt h5");
const TAG: usize = 16;
assert_eq!(c0.len(), c5.len());
assert!(c0.len() > TAG);
let (body0, tag0) = c0.split_at(c0.len() - TAG);
let (body5, tag5) = c5.split_at(c5.len() - TAG);
assert_eq!(
body0, body5,
"ciphertext body differs => path_id leaked into the nonce"
);
assert_ne!(
tag0, tag5,
"tag identical => path_id not bound into the AAD"
);
}
#[test]
fn per_direction_window_accepts_interleaved_streams() {
let (client, server) = make_session_pair([0x93u8; 32]);
let sid = *client.id();
let mut pn = 0u64;
for _round in 0..500 {
for stream_id in [1u16, 7u16] {
let h = PacketHeader::new(sid, stream_id, pn, PacketFlags::new(PacketFlags::ENCRYPTED));
let ct = client.encrypt_packet(&h, b"x", &[]).expect("encrypt");
assert!(
server.decrypt_packet(&h, &ct, &[]).is_ok(),
"stream {stream_id} pn {pn} must be accepted by the single window"
);
pn += 1;
}
}
let h_dup = PacketHeader::new(sid, 1, 0, PacketFlags::new(PacketFlags::ENCRYPTED));
let ct_dup = client.encrypt_packet(&h_dup, b"x", &[]).expect("encrypt");
assert!(
matches!(
server.decrypt_packet(&h_dup, &ct_dup, &[]),
Err(phantom_protocol::CoreError::ReplayDetected(_))
),
"a replayed packet_number must be rejected after AEAD verify (Inv-4)"
);
}
#[test]
fn reset_congestion_returns_controller_to_initial() {
let (s, _server) = make_session_pair([0x94u8; 32]);
let (fresh, _f) = make_session_pair([0x95u8; 32]);
let initial_cwnd = fresh.bandwidth_snapshot().cwnd_bytes;
s.on_packet_sent(200_000);
s.on_packet_lost(100_000);
assert!(
s.bandwidth_snapshot().inflight_bytes > 0,
"precondition: on_packet_sent should register inflight bytes"
);
s.reset_congestion();
let snap = s.bandwidth_snapshot();
assert_eq!(snap.inflight_bytes, 0, "reset must clear inflight");
assert_eq!(
snap.cwnd_bytes, initial_cwnd,
"reset must restore the initial cwnd"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn udp_demux_routes_map_is_bounded_under_fresh_cid_spray() {
use phantom_protocol::api::udp_listener::PhantomUdpListener;
use tokio::net::UdpSocket;
const SPRAY: usize = 3000;
const BOUND: usize = 512;
let listener = PhantomUdpListener::bind_udp("127.0.0.1:0".to_string())
.await
.expect("bind_udp");
let server_addr: std::net::SocketAddr = listener.local_addr().parse().unwrap();
let l = listener.clone();
let _demux_pump = tokio::spawn(async move { l.accept().await });
let attacker = UdpSocket::bind("127.0.0.1:0").await.unwrap();
attacker.connect(server_addr).await.unwrap();
for i in 0..SPRAY {
let mut dg = Vec::with_capacity(20);
dg.push(0x00u8); dg.extend_from_slice(&(i as u64).to_be_bytes()); dg.extend_from_slice(b"not-a-clienthello"); let _ = attacker.send(&dg).await;
if i % 64 == 0 {
tokio::task::yield_now().await;
}
}
let mut observed = listener.active_route_count();
for _ in 0..250 {
observed = listener.active_route_count();
if observed <= BOUND {
break;
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
assert!(
observed <= BOUND,
"demux routes leaked under fresh-CID spray: active_route_count()={observed} after \
spraying {SPRAY} garbage Initials (expected <= {BOUND} once dead routes are reaped)"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn udp_cookieless_initials_get_no_slot_until_address_validated() {
use phantom_protocol::api::udp_listener::PhantomUdpListener;
use phantom_protocol::transport::handshake::HandshakeClient;
use phantom_protocol::transport::phantom_udp::datagram::encode_datagrams;
use phantom_protocol::transport::phantom_udp::envelope::PacketType;
use tokio::net::UdpSocket;
const SPRAY: usize = 400;
let listener = PhantomUdpListener::bind_udp("127.0.0.1:0".to_string())
.await
.expect("bind_udp");
let server_addr: std::net::SocketAddr = listener.local_addr().parse().unwrap();
let l = listener.clone();
let _demux_pump = tokio::spawn(async move { l.accept().await });
let hello = HandshakeClient::new()
.expect("client")
.create_client_hello();
assert!(
hello.cookie.is_none(),
"a first-flight hello must carry no cookie"
);
let hello_bytes = borsh::to_vec(&hello).expect("serialize hello");
let attacker = UdpSocket::bind("127.0.0.1:0").await.unwrap();
attacker.connect(server_addr).await.unwrap();
for i in 0..SPRAY {
let cid: [u8; 8] = (i as u64).to_be_bytes(); let dgrams = encode_datagrams(PacketType::Initial, &cid, 0, &hello_bytes).expect("encode");
for d in &dgrams {
let _ = attacker.send(d).await;
}
if i % 16 == 0 {
tokio::task::yield_now().await;
}
}
let mut peak = 0usize;
for _ in 0..100 {
peak = peak.max(listener.active_route_count());
tokio::time::sleep(Duration::from_millis(20)).await;
}
assert_eq!(
peak, 0,
"cookie-less Initials must allocate no demux slots (saw {peak} routes); the demux must \
answer the cookie round statelessly before committing any per-connection slot"
);
}
#[tokio::test]
async fn reorder_buffer_is_byte_bounded_when_the_head_is_missing() {
let stream = Stream::new(0);
let chunk = Bytes::from(vec![0u8; 4096]); for off in 1u32..2000 {
let _ = stream.accept_in_order(off, vec![chunk.clone()]).await;
}
let buffered = stream.recv_reorder_bytes();
assert!(
buffered <= 256 * 1024,
"reorder buffer must be byte-bounded under a missing head: held {buffered} B (≈{} KiB) \
— a per-stream byte budget must refuse future holes past the window",
buffered / 1024
);
}
#[test]
fn idle_keepalive_is_encrypted_authenticated_and_replay_protected() {
use phantom_protocol::CoreError;
for other in [
PacketFlags::RELIABLE,
PacketFlags::ACK,
PacketFlags::FIN,
PacketFlags::UNRELIABLE,
PacketFlags::PRIORITY,
PacketFlags::ENCRYPTED,
PacketFlags::COMPRESSED,
PacketFlags::CONTROL,
PacketFlags::REKEY,
PacketFlags::PATH_VALIDATION,
PacketFlags::COALESCED,
PacketFlags::WINDOW_UPDATE,
] {
assert_eq!(
PacketFlags::KEEPALIVE & other,
0,
"KEEPALIVE (0x{:04x}) must not overlap an existing flag (0x{other:04x})",
PacketFlags::KEEPALIVE
);
}
let (client, server) = make_session_pair([0x5Au8; 32]);
let pn = client.next_send_pn();
let header = PacketHeader::new(
*server.id(),
1,
pn,
PacketFlags::new(PacketFlags::ENCRYPTED | PacketFlags::KEEPALIVE),
)
.with_epoch(client.current_epoch());
assert!(
header.flags.contains(PacketFlags::ENCRYPTED),
"a keep-alive must be ENCRYPTED (Inv-2 downgrade defense)"
);
let ct = client
.encrypt_packet(&header, &[], &[])
.expect("seal keep-alive");
let pt = server
.decrypt_packet(&header, &ct, &[])
.expect("authenticated keep-alive opens");
assert!(pt.is_empty(), "a keep-alive carries no application bytes");
let replay = server.decrypt_packet(&header, &ct, &[]);
assert!(
matches!(replay, Err(CoreError::ReplayDetected(_))),
"a replayed keep-alive must be rejected by the replay window (Inv-4); got {replay:?}"
);
}
#[test]
fn failed_decrypt_does_not_advance_recv_invocation_counter() {
let secret = [0x77u8; 32];
let session = CryptoSession::with_suite(&secret, CipherSuite::Aes256Gcm).expect("session");
let before = session.recv_invocations();
let forged = vec![0u8; 48];
assert!(
session
.decrypt_with_nonce([0u8; 12], b"aad", &forged)
.is_err(),
"a forged ciphertext must fail to decrypt"
);
assert_eq!(
session.recv_invocations(),
before,
"a failed AEAD open must not advance the recv invocation counter (T5.5)"
);
}
#[tokio::test]
async fn client_server_migration_candidate_is_anti_amp_capped_and_never_the_send_target() {
use phantom_protocol::api::session::{FramePhase, SessionTransport};
use phantom_protocol::api::udp_transport::UdpClientTransport;
use phantom_protocol::transport::phantom_udp::datagram::encode_datagrams;
use phantom_protocol::transport::phantom_udp::envelope::PacketType;
use std::time::Duration;
use tokio::net::UdpSocket;
let server = UdpSocket::bind("127.0.0.1:0").await.unwrap();
let server_addr = server.local_addr().unwrap();
let client = UdpClientTransport::connect(server_addr).await.unwrap();
client.set_frame_phase(FramePhase::Established);
client.send_bytes(b"hi").await.unwrap();
let mut buf = vec![0u8; 2048];
let (_n, client_addr) = server.recv_from(&mut buf).await.unwrap();
let candidate = UdpSocket::bind("127.0.0.1:0").await.unwrap();
for d in encode_datagrams(PacketType::OneRtt, &[0u8; 8], 1, b"0123456789").unwrap() {
candidate.send_to(&d, client_addr).await.unwrap();
}
let _ = tokio::time::timeout(Duration::from_secs(2), client.recv_bytes())
.await
.expect("recv")
.expect("frame");
assert!(
!client.has_migration_candidate(),
"a raw recv must not commit a server-migration candidate (M-1)"
);
client.confirm_authenticated_source();
assert!(client.has_migration_candidate());
let mut blocked = false;
for _ in 0..100 {
if !client.send_to_candidate(b"challenge-frame").await.unwrap() {
blocked = true;
break;
}
}
assert!(
blocked,
"the 3× anti-amplification cap must block excessive challenges to a candidate"
);
let mut total_to_candidate = 0u64;
while let Ok(Ok((n, _))) =
tokio::time::timeout(Duration::from_millis(100), candidate.recv_from(&mut buf)).await
{
total_to_candidate += n as u64;
}
assert!(
total_to_candidate <= 30,
"the client must not send > 3× (30 bytes) to an unvalidated candidate; sent {total_to_candidate}"
);
client.send_bytes(b"app-data-to-server").await.unwrap();
let (sn, _) = tokio::time::timeout(Duration::from_secs(1), server.recv_from(&mut buf))
.await
.expect("app data must reach the established server")
.unwrap();
assert!(sn > 0);
assert!(
tokio::time::timeout(Duration::from_millis(200), candidate.recv_from(&mut buf))
.await
.is_err(),
"an unvalidated candidate must never receive app data — it is not the c2s send target"
);
}