mirror of
https://github.com/telemt/telemt.git
synced 2026-06-18 00:48:31 +03:00
Hardened TLS-F ServerHello selection
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
@@ -1457,7 +1457,6 @@ fn emulated_server_hello_never_places_alpn_in_server_hello_extensions() {
|
||||
true,
|
||||
ClientHelloTlsVersion::Tls13,
|
||||
[0x13, 0x01],
|
||||
TLS_NAMED_GROUP_X25519MLKEM768,
|
||||
&rng,
|
||||
Some(b"h2".to_vec()),
|
||||
0,
|
||||
@@ -1467,14 +1466,21 @@ fn emulated_server_hello_never_places_alpn_in_server_hello_extensions() {
|
||||
!exts.contains(&0x0010),
|
||||
"ALPN extension must not appear in emulated ServerHello"
|
||||
);
|
||||
assert_eq!(
|
||||
server_hello_key_share(&response),
|
||||
Some((
|
||||
TLS_NAMED_GROUP_X25519MLKEM768,
|
||||
X25519MLKEM768_SERVER_KEY_SHARE_LEN
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tls_extension_builder() {
|
||||
let key = [0x42u8; 32];
|
||||
let key = vec![0x42u8; X25519MLKEM768_SERVER_KEY_SHARE_LEN];
|
||||
|
||||
let mut builder = TlsExtensionBuilder::new();
|
||||
builder.add_key_share(TLS_NAMED_GROUP_X25519, &key);
|
||||
builder.add_key_share(TLS_NAMED_GROUP_X25519MLKEM768, &key);
|
||||
builder.add_supported_versions(0x0304);
|
||||
|
||||
let result = builder.build();
|
||||
@@ -1487,10 +1493,10 @@ fn test_tls_extension_builder() {
|
||||
#[test]
|
||||
fn test_server_hello_builder() {
|
||||
let session_id = vec![0x01, 0x02, 0x03, 0x04];
|
||||
let key = [0x55u8; 32];
|
||||
let key = vec![0x55u8; X25519MLKEM768_SERVER_KEY_SHARE_LEN];
|
||||
|
||||
let builder = ServerHelloBuilder::new(session_id.clone())
|
||||
.with_key_share(TLS_NAMED_GROUP_X25519, &key)
|
||||
.with_key_share(TLS_NAMED_GROUP_X25519MLKEM768, &key)
|
||||
.with_tls13_version();
|
||||
|
||||
let record = builder.build_record();
|
||||
@@ -1535,7 +1541,7 @@ fn test_build_server_hello_structure() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_server_hello_with_cipher_can_keep_x25519_key_share() {
|
||||
fn test_build_server_hello_with_cipher_always_uses_hybrid_key_share() {
|
||||
let secret = b"test secret";
|
||||
let client_digest = [0x42u8; 32];
|
||||
let session_id = vec![0xAA; 32];
|
||||
@@ -1548,14 +1554,16 @@ fn test_build_server_hello_with_cipher_can_keep_x25519_key_share() {
|
||||
2048,
|
||||
&rng,
|
||||
[0x13, 0x01],
|
||||
TLS_NAMED_GROUP_X25519,
|
||||
None,
|
||||
0,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
server_hello_key_share(&response),
|
||||
Some((TLS_NAMED_GROUP_X25519, X25519_KEY_SHARE_LEN))
|
||||
Some((
|
||||
TLS_NAMED_GROUP_X25519MLKEM768,
|
||||
X25519MLKEM768_SERVER_KEY_SHARE_LEN
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1579,10 +1587,10 @@ fn test_build_server_hello_digest() {
|
||||
#[test]
|
||||
fn test_server_hello_extensions_length() {
|
||||
let session_id = vec![0x01; 32];
|
||||
let key = [0x55u8; 32];
|
||||
let key = vec![0x55u8; X25519MLKEM768_SERVER_KEY_SHARE_LEN];
|
||||
|
||||
let builder = ServerHelloBuilder::new(session_id)
|
||||
.with_key_share(TLS_NAMED_GROUP_X25519, &key)
|
||||
.with_key_share(TLS_NAMED_GROUP_X25519MLKEM768, &key)
|
||||
.with_tls13_version();
|
||||
|
||||
let record = builder.build_record();
|
||||
@@ -1796,7 +1804,7 @@ fn select_server_hello_cipher_suite_keeps_profile_cipher_when_offered() {
|
||||
);
|
||||
assert_eq!(
|
||||
select_server_hello_cipher_suite(&ch, [0x13, 0x03]),
|
||||
[0x13, 0x03]
|
||||
Some([0x13, 0x03])
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1809,7 +1817,16 @@ fn select_server_hello_cipher_suite_ignores_profile_tls12_cipher() {
|
||||
);
|
||||
assert_eq!(
|
||||
select_server_hello_cipher_suite(&ch, [0xc0, 0x2f]),
|
||||
[0x13, 0x03]
|
||||
Some([0x13, 0x03])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_server_hello_cipher_suite_rejects_without_offered_tls13_suite() {
|
||||
let ch = build_client_hello_with_ciphers_and_exts(&[[0xc0, 0x2f]], Vec::new(), "example.com");
|
||||
assert_eq!(
|
||||
select_server_hello_cipher_suite(&ch, [0x13, 0x01]),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1818,18 +1835,18 @@ fn select_server_hello_cipher_suite_falls_back_to_offered_tls13_suite() {
|
||||
let ch = build_client_hello_with_ciphers_and_exts(&[[0x13, 0x03]], Vec::new(), "example.com");
|
||||
assert_eq!(
|
||||
select_server_hello_cipher_suite(&ch, [0x13, 0x01]),
|
||||
[0x13, 0x03]
|
||||
Some([0x13, 0x03])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_server_hello_cipher_suite_keeps_preferred_for_malformed_clienthello() {
|
||||
fn select_server_hello_cipher_suite_rejects_malformed_clienthello() {
|
||||
let mut ch =
|
||||
build_client_hello_with_ciphers_and_exts(&[[0x13, 0x03]], Vec::new(), "example.com");
|
||||
ch.truncate(12);
|
||||
assert_eq!(
|
||||
select_server_hello_cipher_suite(&ch, [0x13, 0x01]),
|
||||
[0x13, 0x01]
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1847,38 +1864,32 @@ fn select_server_hello_key_share_group_prefers_hybrid_when_valid_share_is_offere
|
||||
|
||||
assert_eq!(
|
||||
select_server_hello_key_share_group(&ch),
|
||||
TLS_NAMED_GROUP_X25519MLKEM768
|
||||
Some(TLS_NAMED_GROUP_X25519MLKEM768)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_server_hello_key_share_group_falls_back_without_hybrid_share() {
|
||||
fn select_server_hello_key_share_group_rejects_without_hybrid_share() {
|
||||
let key_share =
|
||||
client_key_share_extension(&[(TLS_NAMED_GROUP_X25519, X25519_KEY_SHARE_LEN)]);
|
||||
let ch = build_client_hello_with_exts(vec![(0x0033, key_share)], "example.com");
|
||||
|
||||
assert_eq!(
|
||||
select_server_hello_key_share_group(&ch),
|
||||
TLS_NAMED_GROUP_X25519
|
||||
);
|
||||
assert_eq!(select_server_hello_key_share_group(&ch), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_server_hello_key_share_group_falls_back_for_malformed_hybrid_len() {
|
||||
fn select_server_hello_key_share_group_rejects_malformed_hybrid_len() {
|
||||
let key_share = client_key_share_extension(&[(
|
||||
TLS_NAMED_GROUP_X25519MLKEM768,
|
||||
X25519MLKEM768_CLIENT_KEY_SHARE_LEN - 1,
|
||||
)]);
|
||||
let ch = build_client_hello_with_exts(vec![(0x0033, key_share)], "example.com");
|
||||
|
||||
assert_eq!(
|
||||
select_server_hello_key_share_group(&ch),
|
||||
TLS_NAMED_GROUP_X25519
|
||||
);
|
||||
assert_eq!(select_server_hello_key_share_group(&ch), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_server_hello_key_share_group_falls_back_for_malformed_key_share_tail() {
|
||||
fn select_server_hello_key_share_group_rejects_malformed_key_share_tail() {
|
||||
let mut key_share = client_key_share_extension(&[(
|
||||
TLS_NAMED_GROUP_X25519MLKEM768,
|
||||
X25519MLKEM768_CLIENT_KEY_SHARE_LEN,
|
||||
@@ -1888,10 +1899,7 @@ fn select_server_hello_key_share_group_falls_back_for_malformed_key_share_tail()
|
||||
key_share.push(0);
|
||||
let ch = build_client_hello_with_exts(vec![(0x0033, key_share)], "example.com");
|
||||
|
||||
assert_eq!(
|
||||
select_server_hello_key_share_group(&ch),
|
||||
TLS_NAMED_GROUP_X25519
|
||||
);
|
||||
assert_eq!(select_server_hello_key_share_group(&ch), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -561,7 +561,6 @@ pub fn build_server_hello(
|
||||
fake_cert_len,
|
||||
rng,
|
||||
cipher_suite::TLS_AES_128_GCM_SHA256,
|
||||
TLS_NAMED_GROUP_X25519MLKEM768,
|
||||
alpn,
|
||||
new_session_tickets,
|
||||
)
|
||||
@@ -579,7 +578,6 @@ pub(crate) fn build_server_hello_with_cipher(
|
||||
fake_cert_len: usize,
|
||||
rng: &SecureRandom,
|
||||
selected_cipher_suite: [u8; 2],
|
||||
selected_key_share_group: u16,
|
||||
alpn: Option<Vec<u8>>,
|
||||
new_session_tickets: u8,
|
||||
) -> Vec<u8> {
|
||||
@@ -588,21 +586,12 @@ pub(crate) fn build_server_hello_with_cipher(
|
||||
let fake_cert_len = fake_cert_len.clamp(MIN_APP_DATA, MAX_APP_DATA);
|
||||
|
||||
// Build ServerHello
|
||||
let server_hello = if selected_key_share_group == TLS_NAMED_GROUP_X25519MLKEM768 {
|
||||
let key_share = gen_fake_x25519mlkem768_server_key_share(rng);
|
||||
ServerHelloBuilder::new(session_id.to_vec())
|
||||
.with_cipher_suite(selected_cipher_suite)
|
||||
.with_key_share(TLS_NAMED_GROUP_X25519MLKEM768, &key_share)
|
||||
.with_tls13_version()
|
||||
.build_record()
|
||||
} else {
|
||||
let key_share = gen_fake_x25519_key(rng);
|
||||
ServerHelloBuilder::new(session_id.to_vec())
|
||||
.with_cipher_suite(selected_cipher_suite)
|
||||
.with_key_share(TLS_NAMED_GROUP_X25519, &key_share)
|
||||
.with_tls13_version()
|
||||
.build_record()
|
||||
};
|
||||
let key_share = gen_fake_x25519mlkem768_server_key_share(rng);
|
||||
let server_hello = ServerHelloBuilder::new(session_id.to_vec())
|
||||
.with_cipher_suite(selected_cipher_suite)
|
||||
.with_key_share(TLS_NAMED_GROUP_X25519MLKEM768, &key_share)
|
||||
.with_tls13_version()
|
||||
.build_record();
|
||||
|
||||
// Build Change Cipher Spec record
|
||||
let change_cipher_spec = [
|
||||
@@ -1201,20 +1190,23 @@ fn is_tls13_cipher_suite(suite: [u8; 2]) -> bool {
|
||||
/// Select the ServerHello cipher suite from the already-received ClientHello.
|
||||
///
|
||||
/// This is intentionally a borrowed, zero-allocation scan. It runs only for an
|
||||
/// authenticated success response and keeps malformed or unexpected ClientHello
|
||||
/// shapes on the previous fallback behavior.
|
||||
pub(crate) fn select_server_hello_cipher_suite(handshake: &[u8], preferred: [u8; 2]) -> [u8; 2] {
|
||||
/// authenticated success response and fails closed for malformed or unsupported
|
||||
/// ClientHello shapes that cannot produce a DPI-consistent ServerHello.
|
||||
pub(crate) fn select_server_hello_cipher_suite(
|
||||
handshake: &[u8],
|
||||
preferred: [u8; 2],
|
||||
) -> Option<[u8; 2]> {
|
||||
let preferred = if is_tls13_cipher_suite(preferred) {
|
||||
preferred
|
||||
} else {
|
||||
cipher_suite::TLS_AES_128_GCM_SHA256
|
||||
};
|
||||
let Some(range) = client_hello_cipher_suites_range(handshake) else {
|
||||
return preferred;
|
||||
return None;
|
||||
};
|
||||
|
||||
if client_hello_offers_cipher_suite(handshake, range, preferred) {
|
||||
return preferred;
|
||||
return Some(preferred);
|
||||
}
|
||||
|
||||
for fallback in [
|
||||
@@ -1223,26 +1215,26 @@ pub(crate) fn select_server_hello_cipher_suite(handshake: &[u8], preferred: [u8;
|
||||
cipher_suite::TLS_AES_256_GCM_SHA384,
|
||||
] {
|
||||
if client_hello_offers_cipher_suite(handshake, range, fallback) {
|
||||
return fallback;
|
||||
return Some(fallback);
|
||||
}
|
||||
}
|
||||
|
||||
preferred
|
||||
None
|
||||
}
|
||||
|
||||
/// Select the ServerHello key_share named group from the authenticated ClientHello.
|
||||
/// Select the hybrid ServerHello key_share named group from the authenticated ClientHello.
|
||||
///
|
||||
/// Malformed key_share structures intentionally keep the legacy X25519 response
|
||||
/// to avoid breaking older clients that do not advertise the hybrid group.
|
||||
pub(crate) fn select_server_hello_key_share_group(handshake: &[u8]) -> u16 {
|
||||
/// Malformed or non-hybrid key_share structures fail closed so authenticated
|
||||
/// but DPI-inconsistent ClientHellos take the ordinary masking fallback path.
|
||||
pub(crate) fn select_server_hello_key_share_group(handshake: &[u8]) -> Option<u16> {
|
||||
if client_hello_offers_key_share_group(
|
||||
handshake,
|
||||
TLS_NAMED_GROUP_X25519MLKEM768,
|
||||
X25519MLKEM768_CLIENT_KEY_SHARE_LEN,
|
||||
) {
|
||||
TLS_NAMED_GROUP_X25519MLKEM768
|
||||
Some(TLS_NAMED_GROUP_X25519MLKEM768)
|
||||
} else {
|
||||
TLS_NAMED_GROUP_X25519
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user