forked from mirrors/gecko-dev
472 lines
17 KiB
Rust
472 lines
17 KiB
Rust
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
|
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
|
// option. This file may not be copied, modified, or distributed
|
|
// except according to those terms.
|
|
|
|
#![cfg(not(feature = "disable-encryption"))]
|
|
|
|
mod common;
|
|
|
|
use std::{
|
|
mem,
|
|
net::{IpAddr, Ipv4Addr, SocketAddr},
|
|
time::Duration,
|
|
};
|
|
|
|
use common::{
|
|
apply_header_protection, connected_server, decode_initial_header, default_server,
|
|
generate_ticket, initial_aead_and_hp, remove_header_protection,
|
|
};
|
|
use neqo_common::{hex_with_len, qdebug, qtrace, Datagram, Encoder, Role};
|
|
use neqo_crypto::AuthenticationStatus;
|
|
use neqo_transport::{server::ValidateAddress, ConnectionError, Error, State, StreamType};
|
|
use test_fixture::{assertions, datagram, default_client, now, split_datagram};
|
|
|
|
#[test]
|
|
fn retry_basic() {
|
|
let mut server = default_server();
|
|
server.set_validation(ValidateAddress::Always);
|
|
let mut client = default_client();
|
|
|
|
let dgram = client.process(None, now()).dgram(); // Initial
|
|
assert!(dgram.is_some());
|
|
let dgram = server.process(dgram.as_ref(), now()).dgram(); // Retry
|
|
assert!(dgram.is_some());
|
|
|
|
assertions::assert_retry(dgram.as_ref().unwrap());
|
|
|
|
let dgram = client.process(dgram.as_ref(), now()).dgram(); // Initial w/token
|
|
assert!(dgram.is_some());
|
|
let dgram = server.process(dgram.as_ref(), now()).dgram(); // Initial, HS
|
|
assert!(dgram.is_some());
|
|
mem::drop(client.process(dgram.as_ref(), now()).dgram()); // Ingest, drop any ACK.
|
|
client.authenticated(AuthenticationStatus::Ok, now());
|
|
let dgram = client.process(None, now()).dgram(); // Send Finished
|
|
assert!(dgram.is_some());
|
|
assert_eq!(*client.state(), State::Connected);
|
|
let dgram = server.process(dgram.as_ref(), now()).dgram(); // (done)
|
|
assert!(dgram.is_some()); // Note that this packet will be dropped...
|
|
connected_server(&mut server);
|
|
}
|
|
|
|
/// Receiving a Retry is enough to infer something about the RTT.
|
|
/// Probably.
|
|
#[test]
|
|
fn implicit_rtt_retry() {
|
|
const RTT: Duration = Duration::from_secs(2);
|
|
let mut server = default_server();
|
|
server.set_validation(ValidateAddress::Always);
|
|
let mut client = default_client();
|
|
let mut now = now();
|
|
|
|
let dgram = client.process(None, now).dgram();
|
|
now += RTT / 2;
|
|
let dgram = server.process(dgram.as_ref(), now).dgram();
|
|
assertions::assert_retry(dgram.as_ref().unwrap());
|
|
now += RTT / 2;
|
|
client.process_input(&dgram.unwrap(), now);
|
|
|
|
assert_eq!(client.stats().rtt, RTT);
|
|
}
|
|
|
|
#[test]
|
|
fn retry_expired() {
|
|
let mut server = default_server();
|
|
server.set_validation(ValidateAddress::Always);
|
|
let mut client = default_client();
|
|
let mut now = now();
|
|
|
|
let dgram = client.process(None, now).dgram(); // Initial
|
|
assert!(dgram.is_some());
|
|
let dgram = server.process(dgram.as_ref(), now).dgram(); // Retry
|
|
assert!(dgram.is_some());
|
|
|
|
assertions::assert_retry(dgram.as_ref().unwrap());
|
|
|
|
let dgram = client.process(dgram.as_ref(), now).dgram(); // Initial w/token
|
|
assert!(dgram.is_some());
|
|
|
|
now += Duration::from_secs(60); // Too long for Retry.
|
|
let dgram = server.process(dgram.as_ref(), now).dgram(); // Initial, HS
|
|
assert!(dgram.is_none());
|
|
}
|
|
|
|
// Attempt a retry with 0-RTT, and have 0-RTT packets sent with the second ClientHello.
|
|
#[test]
|
|
fn retry_0rtt() {
|
|
let mut server = default_server();
|
|
let token = generate_ticket(&mut server);
|
|
server.set_validation(ValidateAddress::Always);
|
|
|
|
let mut client = default_client();
|
|
client.enable_resumption(now(), &token).unwrap();
|
|
|
|
let client_stream = client.stream_create(StreamType::UniDi).unwrap();
|
|
client.stream_send(client_stream, &[1, 2, 3]).unwrap();
|
|
|
|
let dgram = client.process(None, now()).dgram(); // Initial w/0-RTT
|
|
assert!(dgram.is_some());
|
|
assertions::assert_coalesced_0rtt(dgram.as_ref().unwrap());
|
|
let dgram = server.process(dgram.as_ref(), now()).dgram(); // Retry
|
|
assert!(dgram.is_some());
|
|
assertions::assert_retry(dgram.as_ref().unwrap());
|
|
|
|
// After retry, there should be a token and still coalesced 0-RTT.
|
|
let dgram = client.process(dgram.as_ref(), now()).dgram();
|
|
assert!(dgram.is_some());
|
|
assertions::assert_coalesced_0rtt(dgram.as_ref().unwrap());
|
|
|
|
let dgram = server.process(dgram.as_ref(), now()).dgram(); // Initial, HS
|
|
assert!(dgram.is_some());
|
|
let dgram = client.process(dgram.as_ref(), now()).dgram();
|
|
// Note: the client doesn't need to authenticate the server here
|
|
// as there is no certificate; authentication is based on the ticket.
|
|
assert!(dgram.is_some());
|
|
assert_eq!(*client.state(), State::Connected);
|
|
let dgram = server.process(dgram.as_ref(), now()).dgram(); // (done)
|
|
assert!(dgram.is_some());
|
|
connected_server(&mut server);
|
|
assert!(client.tls_info().unwrap().resumed());
|
|
}
|
|
|
|
#[test]
|
|
fn retry_different_ip() {
|
|
let mut server = default_server();
|
|
server.set_validation(ValidateAddress::Always);
|
|
let mut client = default_client();
|
|
|
|
let dgram = client.process(None.as_ref(), now()).dgram(); // Initial
|
|
assert!(dgram.is_some());
|
|
let dgram = server.process(dgram.as_ref(), now()).dgram(); // Retry
|
|
assert!(dgram.is_some());
|
|
|
|
assertions::assert_retry(dgram.as_ref().unwrap());
|
|
|
|
let dgram = client.process(dgram.as_ref(), now()).dgram(); // Initial w/token
|
|
assert!(dgram.is_some());
|
|
|
|
// Change the source IP on the address from the client.
|
|
let dgram = dgram.unwrap();
|
|
let other_v4 = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2));
|
|
let other_addr = SocketAddr::new(other_v4, 443);
|
|
let from_other = Datagram::new(
|
|
other_addr,
|
|
dgram.destination(),
|
|
dgram.tos(),
|
|
dgram.ttl(),
|
|
&dgram[..],
|
|
);
|
|
let dgram = server.process(Some(&from_other), now()).dgram();
|
|
assert!(dgram.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn new_token_different_ip() {
|
|
let mut server = default_server();
|
|
let token = generate_ticket(&mut server);
|
|
server.set_validation(ValidateAddress::NoToken);
|
|
|
|
let mut client = default_client();
|
|
client.enable_resumption(now(), &token).unwrap();
|
|
|
|
let dgram = client.process(None, now()).dgram(); // Initial
|
|
assert!(dgram.is_some());
|
|
assertions::assert_initial(dgram.as_ref().unwrap(), true);
|
|
|
|
// Now rewrite the source address.
|
|
let d = dgram.unwrap();
|
|
let src = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)), d.source().port());
|
|
let dgram = Some(Datagram::new(
|
|
src,
|
|
d.destination(),
|
|
d.tos(),
|
|
d.ttl(),
|
|
&d[..],
|
|
));
|
|
let dgram = server.process(dgram.as_ref(), now()).dgram(); // Retry
|
|
assert!(dgram.is_some());
|
|
assertions::assert_retry(dgram.as_ref().unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn new_token_expired() {
|
|
let mut server = default_server();
|
|
let token = generate_ticket(&mut server);
|
|
server.set_validation(ValidateAddress::NoToken);
|
|
|
|
let mut client = default_client();
|
|
client.enable_resumption(now(), &token).unwrap();
|
|
|
|
let dgram = client.process(None, now()).dgram(); // Initial
|
|
assert!(dgram.is_some());
|
|
assertions::assert_initial(dgram.as_ref().unwrap(), true);
|
|
|
|
// Now move into the future.
|
|
// We can't go too far or we'll overflow our field. Not when checking,
|
|
// but when trying to generate another Retry. A month is fine.
|
|
let the_future = now() + Duration::from_secs(60 * 60 * 24 * 30);
|
|
let d = dgram.unwrap();
|
|
let src = SocketAddr::new(d.source().ip(), d.source().port() + 1);
|
|
let dgram = Some(Datagram::new(
|
|
src,
|
|
d.destination(),
|
|
d.tos(),
|
|
d.ttl(),
|
|
&d[..],
|
|
));
|
|
let dgram = server.process(dgram.as_ref(), the_future).dgram(); // Retry
|
|
assert!(dgram.is_some());
|
|
assertions::assert_retry(dgram.as_ref().unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn retry_after_initial() {
|
|
let mut server = default_server();
|
|
let mut retry_server = default_server();
|
|
retry_server.set_validation(ValidateAddress::Always);
|
|
let mut client = default_client();
|
|
|
|
let cinit = client.process(None, now()).dgram(); // Initial
|
|
assert!(cinit.is_some());
|
|
let server_flight = server.process(cinit.as_ref(), now()).dgram(); // Initial
|
|
assert!(server_flight.is_some());
|
|
|
|
// We need to have the client just process the Initial.
|
|
let (server_initial, _other) = split_datagram(server_flight.as_ref().unwrap());
|
|
let dgram = client.process(Some(&server_initial), now()).dgram();
|
|
assert!(dgram.is_some());
|
|
assert!(*client.state() != State::Connected);
|
|
|
|
let retry = retry_server.process(cinit.as_ref(), now()).dgram(); // Retry!
|
|
assert!(retry.is_some());
|
|
assertions::assert_retry(retry.as_ref().unwrap());
|
|
|
|
// The client should ignore the retry.
|
|
let junk = client.process(retry.as_ref(), now()).dgram();
|
|
assert!(junk.is_none());
|
|
|
|
// Either way, the client should still be able to process the server flight and connect.
|
|
let dgram = client.process(server_flight.as_ref(), now()).dgram();
|
|
assert!(dgram.is_some()); // Drop this one.
|
|
assert!(test_fixture::maybe_authenticate(&mut client));
|
|
let dgram = client.process(None, now()).dgram();
|
|
assert!(dgram.is_some());
|
|
|
|
assert_eq!(*client.state(), State::Connected);
|
|
let dgram = server.process(dgram.as_ref(), now()).dgram(); // (done)
|
|
assert!(dgram.is_some());
|
|
connected_server(&mut server);
|
|
}
|
|
|
|
#[test]
|
|
fn retry_bad_integrity() {
|
|
let mut server = default_server();
|
|
server.set_validation(ValidateAddress::Always);
|
|
let mut client = default_client();
|
|
|
|
let dgram = client.process(None, now()).dgram(); // Initial
|
|
assert!(dgram.is_some());
|
|
let dgram = server.process(dgram.as_ref(), now()).dgram(); // Retry
|
|
assert!(dgram.is_some());
|
|
|
|
let retry = &dgram.as_ref().unwrap();
|
|
assertions::assert_retry(retry);
|
|
|
|
let mut tweaked = retry.to_vec();
|
|
tweaked[retry.len() - 1] ^= 0x45; // damage the auth tag
|
|
let tweaked_packet = Datagram::new(
|
|
retry.source(),
|
|
retry.destination(),
|
|
retry.tos(),
|
|
retry.ttl(),
|
|
tweaked,
|
|
);
|
|
|
|
// The client should ignore this packet.
|
|
let dgram = client.process(Some(&tweaked_packet), now()).dgram();
|
|
assert!(dgram.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn retry_bad_token() {
|
|
let mut client = default_client();
|
|
let mut retry_server = default_server();
|
|
retry_server.set_validation(ValidateAddress::Always);
|
|
let mut server = default_server();
|
|
|
|
// Send a retry to one server, then replay it to the other.
|
|
let client_initial1 = client.process(None, now()).dgram();
|
|
assert!(client_initial1.is_some());
|
|
let retry = retry_server
|
|
.process(client_initial1.as_ref(), now())
|
|
.dgram();
|
|
assert!(retry.is_some());
|
|
let client_initial2 = client.process(retry.as_ref(), now()).dgram();
|
|
assert!(client_initial2.is_some());
|
|
|
|
let dgram = server.process(client_initial2.as_ref(), now()).dgram();
|
|
assert!(dgram.is_none());
|
|
}
|
|
|
|
// This is really a client test, but we need a server with Retry to test it.
|
|
// In this test, the client sends Initial on PTO. The Retry should cause
|
|
// all loss recovery timers to be reset, but we had a bug where the PTO timer
|
|
// was not properly reset. This tests that the client generates a new Initial
|
|
// in response to receiving a Retry, even after it sends the Initial on PTO.
|
|
#[test]
|
|
fn retry_after_pto() {
|
|
let mut client = default_client();
|
|
let mut server = default_server();
|
|
server.set_validation(ValidateAddress::Always);
|
|
let mut now = now();
|
|
|
|
let ci = client.process(None, now).dgram();
|
|
assert!(ci.is_some()); // sit on this for a bit.RefCell
|
|
|
|
// Let PTO fire on the client and then let it exhaust its PTO packets.
|
|
now += Duration::from_secs(1);
|
|
let pto = client.process(None, now).dgram();
|
|
assert!(pto.unwrap().len() >= 1200);
|
|
let cb = client.process(None, now).callback();
|
|
assert_ne!(cb, Duration::new(0, 0));
|
|
|
|
let retry = server.process(ci.as_ref(), now).dgram();
|
|
assertions::assert_retry(retry.as_ref().unwrap());
|
|
|
|
let ci2 = client.process(retry.as_ref(), now).dgram();
|
|
assert!(ci2.unwrap().len() >= 1200);
|
|
}
|
|
|
|
#[test]
|
|
fn vn_after_retry() {
|
|
let mut server = default_server();
|
|
server.set_validation(ValidateAddress::Always);
|
|
let mut client = default_client();
|
|
|
|
let dgram = client.process(None, now()).dgram(); // Initial
|
|
assert!(dgram.is_some());
|
|
let dgram = server.process(dgram.as_ref(), now()).dgram(); // Retry
|
|
assert!(dgram.is_some());
|
|
|
|
assertions::assert_retry(dgram.as_ref().unwrap());
|
|
|
|
let dgram = client.process(dgram.as_ref(), now()).dgram(); // Initial w/token
|
|
assert!(dgram.is_some());
|
|
|
|
let mut encoder = Encoder::default();
|
|
encoder.encode_byte(0x80);
|
|
encoder.encode(&[0; 4]); // Zero version == VN.
|
|
encoder.encode_vec(1, &client.odcid().unwrap()[..]);
|
|
encoder.encode_vec(1, &[]);
|
|
encoder.encode_uint(4, 0x5a5a_6a6a_u64);
|
|
let vn = datagram(encoder.into());
|
|
|
|
assert_ne!(
|
|
client.process(Some(&vn), now()).callback(),
|
|
Duration::from_secs(0)
|
|
);
|
|
}
|
|
|
|
// This tests a simulated on-path attacker that intercepts the first
|
|
// client Initial packet and spoofs a retry.
|
|
// The tricky part is in rewriting the second client Initial so that
|
|
// the server doesn't reject the Initial for having a bad token.
|
|
// The client is the only one that can detect this, and that is because
|
|
// the original connection ID is not in transport parameters.
|
|
//
|
|
// Note that this depends on having the server produce a CID that is
|
|
// at least 8 bytes long. Otherwise, the second Initial won't have a
|
|
// long enough connection ID.
|
|
#[test]
|
|
#[allow(clippy::shadow_unrelated)]
|
|
fn mitm_retry() {
|
|
let mut client = default_client();
|
|
let mut retry_server = default_server();
|
|
retry_server.set_validation(ValidateAddress::Always);
|
|
let mut server = default_server();
|
|
|
|
// Trigger initial and a second client Initial.
|
|
let client_initial1 = client.process(None, now()).dgram();
|
|
assert!(client_initial1.is_some());
|
|
let retry = retry_server
|
|
.process(client_initial1.as_ref(), now())
|
|
.dgram();
|
|
assert!(retry.is_some());
|
|
let client_initial2 = client.process(retry.as_ref(), now()).dgram();
|
|
assert!(client_initial2.is_some());
|
|
|
|
// Now to start the epic process of decrypting the packet,
|
|
// rewriting the header to remove the token, and then re-encrypting.
|
|
let client_initial2 = client_initial2.unwrap();
|
|
let (protected_header, d_cid, s_cid, payload) =
|
|
decode_initial_header(&client_initial2, Role::Client);
|
|
|
|
// Now we have enough information to make keys.
|
|
let (aead, hp) = initial_aead_and_hp(d_cid, Role::Client);
|
|
let (header, pn) = remove_header_protection(&hp, protected_header, payload);
|
|
let pn_len = header.len() - protected_header.len();
|
|
|
|
// Decrypt.
|
|
assert_eq!(pn, 1);
|
|
let mut plaintext_buf = vec![0; client_initial2.len()];
|
|
let plaintext = aead
|
|
.decrypt(pn, &header, &payload[pn_len..], &mut plaintext_buf)
|
|
.unwrap();
|
|
|
|
// Now re-encode without the token.
|
|
let mut enc = Encoder::with_capacity(header.len());
|
|
enc.encode(&header[..5])
|
|
.encode_vec(1, d_cid)
|
|
.encode_vec(1, s_cid)
|
|
.encode_vvec(&[])
|
|
.encode_varint(u64::try_from(payload.len()).unwrap());
|
|
let pn_offset = enc.len();
|
|
let notoken_header = enc.encode_uint(pn_len, pn).as_ref().to_vec();
|
|
qtrace!("notoken_header={}", hex_with_len(¬oken_header));
|
|
|
|
// Encrypt.
|
|
let mut notoken_packet = Encoder::with_capacity(1200)
|
|
.encode(¬oken_header)
|
|
.as_ref()
|
|
.to_vec();
|
|
notoken_packet.resize_with(1200, u8::default);
|
|
aead.encrypt(
|
|
pn,
|
|
¬oken_header,
|
|
plaintext,
|
|
&mut notoken_packet[notoken_header.len()..],
|
|
)
|
|
.unwrap();
|
|
// Unlike with decryption, don't truncate.
|
|
// All 1200 bytes are needed to reach the minimum datagram size.
|
|
|
|
apply_header_protection(&hp, &mut notoken_packet, pn_offset..(pn_offset + pn_len));
|
|
qtrace!("packet={}", hex_with_len(¬oken_packet));
|
|
|
|
let new_datagram = Datagram::new(
|
|
client_initial2.source(),
|
|
client_initial2.destination(),
|
|
client_initial2.tos(),
|
|
client_initial2.ttl(),
|
|
notoken_packet,
|
|
);
|
|
qdebug!("passing modified Initial to the main server");
|
|
let dgram = server.process(Some(&new_datagram), now()).dgram();
|
|
assert!(dgram.is_some());
|
|
|
|
let dgram = client.process(dgram.as_ref(), now()).dgram(); // Generate an ACK.
|
|
assert!(dgram.is_some());
|
|
let dgram = server.process(dgram.as_ref(), now()).dgram();
|
|
assert!(dgram.is_none());
|
|
assert!(test_fixture::maybe_authenticate(&mut client));
|
|
let dgram = client.process(dgram.as_ref(), now()).dgram();
|
|
assert!(dgram.is_some()); // Client sending CLOSE_CONNECTIONs
|
|
assert!(matches!(
|
|
*client.state(),
|
|
State::Closing {
|
|
error: ConnectionError::Transport(Error::ProtocolViolation),
|
|
..
|
|
}
|
|
));
|
|
}
|