mirror of
https://github.com/telemt/telemt.git
synced 2026-06-09 20:41:44 +03:00
Align ServerHello cipher and opaque ALPN behavior in TLS-F
This commit is contained in:
@@ -1385,6 +1385,7 @@ fn emulated_server_hello_never_places_alpn_in_server_hello_extensions() {
|
||||
false,
|
||||
true,
|
||||
ClientHelloTlsVersion::Tls13,
|
||||
[0x13, 0x01],
|
||||
&rng,
|
||||
Some(b"h2".to_vec()),
|
||||
0,
|
||||
@@ -1509,12 +1510,22 @@ fn test_validate_tls_handshake_format() {
|
||||
}
|
||||
|
||||
fn build_client_hello_with_exts(exts: Vec<(u16, Vec<u8>)>, host: &str) -> Vec<u8> {
|
||||
build_client_hello_with_ciphers_and_exts(&[[0x13, 0x01]], exts, host)
|
||||
}
|
||||
|
||||
fn build_client_hello_with_ciphers_and_exts(
|
||||
cipher_suites: &[[u8; 2]],
|
||||
exts: Vec<(u16, Vec<u8>)>,
|
||||
host: &str,
|
||||
) -> Vec<u8> {
|
||||
let mut body = Vec::new();
|
||||
body.extend_from_slice(&TLS_VERSION);
|
||||
body.extend_from_slice(&[0u8; 32]);
|
||||
body.push(0);
|
||||
body.extend_from_slice(&2u16.to_be_bytes());
|
||||
body.extend_from_slice(&[0x13, 0x01]);
|
||||
body.extend_from_slice(&((cipher_suites.len() * 2) as u16).to_be_bytes());
|
||||
for suite in cipher_suites {
|
||||
body.extend_from_slice(suite);
|
||||
}
|
||||
body.push(1);
|
||||
body.push(0);
|
||||
|
||||
@@ -1654,6 +1665,51 @@ fn detect_client_hello_tls_version_rejects_malformed_supported_versions() {
|
||||
assert!(detect_client_hello_tls_version(&ch).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_server_hello_cipher_suite_keeps_profile_cipher_when_offered() {
|
||||
let ch = build_client_hello_with_ciphers_and_exts(
|
||||
&[[0x13, 0x01], [0x13, 0x03]],
|
||||
Vec::new(),
|
||||
"example.com",
|
||||
);
|
||||
assert_eq!(
|
||||
select_server_hello_cipher_suite(&ch, [0x13, 0x03]),
|
||||
[0x13, 0x03]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_server_hello_cipher_suite_ignores_profile_tls12_cipher() {
|
||||
let ch = build_client_hello_with_ciphers_and_exts(
|
||||
&[[0xc0, 0x2f], [0x13, 0x03]],
|
||||
Vec::new(),
|
||||
"example.com",
|
||||
);
|
||||
assert_eq!(
|
||||
select_server_hello_cipher_suite(&ch, [0xc0, 0x2f]),
|
||||
[0x13, 0x03]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_server_hello_cipher_suite_keeps_preferred_for_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]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_sni_rejects_zero_length_host_name() {
|
||||
let mut sni_ext = Vec::new();
|
||||
@@ -2179,7 +2235,7 @@ fn light_fuzz_boot_time_timestamp_matrix_with_short_replay_window_obeys_boot_cap
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_hello_application_data_contains_alpn_marker_when_selected() {
|
||||
fn server_hello_application_data_omits_alpn_marker_when_selected() {
|
||||
let secret = b"alpn_marker_test";
|
||||
let client_digest = [0x55u8; TLS_DIGEST_LEN];
|
||||
let session_id = vec![0xAB; 32];
|
||||
@@ -2206,8 +2262,8 @@ fn server_hello_application_data_contains_alpn_marker_when_selected() {
|
||||
assert!(
|
||||
app_payload
|
||||
.windows(expected.len())
|
||||
.any(|window| window == expected),
|
||||
"first application payload must carry ALPN marker for selected protocol"
|
||||
.all(|window| window != expected),
|
||||
"first application payload must not expose plaintext ALPN marker bytes"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2303,14 +2359,14 @@ fn server_hello_ignores_oversized_alpn_when_marker_would_not_fit() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_hello_embeds_full_alpn_marker_when_it_exactly_fits_fake_cert_len() {
|
||||
fn server_hello_omits_alpn_marker_even_when_it_would_fit_fake_cert_len() {
|
||||
let secret = b"alpn_exact_fit_test";
|
||||
let client_digest = [0x58u8; TLS_DIGEST_LEN];
|
||||
let session_id = vec![0xA5; 32];
|
||||
let rng = crate::crypto::SecureRandom::new();
|
||||
let proto = vec![b'z'; 57];
|
||||
|
||||
// marker_len = 4 + (2 + (1 + proto_len)) = 7 + proto_len = 64
|
||||
// marker_len = 4 + (2 + (1 + proto_len)) = 7 + proto_len = 64.
|
||||
let response = build_server_hello(
|
||||
secret,
|
||||
&client_digest,
|
||||
@@ -2336,7 +2392,7 @@ fn server_hello_embeds_full_alpn_marker_when_it_exactly_fits_fake_cert_len() {
|
||||
expected_marker.extend_from_slice(&proto);
|
||||
|
||||
assert_eq!(app_payload.len(), expected_marker.len());
|
||||
assert_eq!(app_payload, expected_marker.as_slice());
|
||||
assert_ne!(app_payload, expected_marker.as_slice());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -105,6 +105,8 @@ mod extension_type {
|
||||
/// TLS Cipher Suites
|
||||
mod cipher_suite {
|
||||
pub const TLS_AES_128_GCM_SHA256: [u8; 2] = [0x13, 0x01];
|
||||
pub const TLS_AES_256_GCM_SHA384: [u8; 2] = [0x13, 0x02];
|
||||
pub const TLS_CHACHA20_POLY1305_SHA256: [u8; 2] = [0x13, 0x03];
|
||||
}
|
||||
|
||||
/// TLS Named Curves
|
||||
@@ -241,6 +243,13 @@ impl ServerHelloBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
fn with_cipher_suite(mut self, cipher_suite: [u8; 2]) -> Self {
|
||||
if cipher_suite != [0, 0] {
|
||||
self.cipher_suite = cipher_suite;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Build ServerHello message (without record header)
|
||||
fn build_message(&self) -> Vec<u8> {
|
||||
let Ok(session_id_len) = u8::try_from(self.session_id.len()) else {
|
||||
@@ -520,6 +529,33 @@ pub fn build_server_hello(
|
||||
rng: &SecureRandom,
|
||||
alpn: Option<Vec<u8>>,
|
||||
new_session_tickets: u8,
|
||||
) -> Vec<u8> {
|
||||
build_server_hello_with_cipher(
|
||||
secret,
|
||||
client_digest,
|
||||
session_id,
|
||||
fake_cert_len,
|
||||
rng,
|
||||
cipher_suite::TLS_AES_128_GCM_SHA256,
|
||||
alpn,
|
||||
new_session_tickets,
|
||||
)
|
||||
}
|
||||
|
||||
/// Build TLS ServerHello response with a caller-selected cipher suite.
|
||||
///
|
||||
/// The caller is responsible for selecting a suite that is compatible with the
|
||||
/// already-authenticated ClientHello. Keeping the selection outside this
|
||||
/// builder avoids extra ClientHello parsing in the response construction path.
|
||||
pub(crate) fn build_server_hello_with_cipher(
|
||||
secret: &[u8],
|
||||
client_digest: &[u8; TLS_DIGEST_LEN],
|
||||
session_id: &[u8],
|
||||
fake_cert_len: usize,
|
||||
rng: &SecureRandom,
|
||||
selected_cipher_suite: [u8; 2],
|
||||
alpn: Option<Vec<u8>>,
|
||||
new_session_tickets: u8,
|
||||
) -> Vec<u8> {
|
||||
const MIN_APP_DATA: usize = 64;
|
||||
const MAX_APP_DATA: usize = MAX_TLS_CIPHERTEXT_SIZE;
|
||||
@@ -528,6 +564,7 @@ pub fn build_server_hello(
|
||||
|
||||
// Build ServerHello
|
||||
let server_hello = ServerHelloBuilder::new(session_id.to_vec())
|
||||
.with_cipher_suite(selected_cipher_suite)
|
||||
.with_x25519_key(&x25519_key)
|
||||
.with_tls13_version()
|
||||
.build_record();
|
||||
@@ -538,28 +575,14 @@ pub fn build_server_hello(
|
||||
TLS_VERSION[0],
|
||||
TLS_VERSION[1],
|
||||
0x00,
|
||||
0x01, // length = 1
|
||||
0x01, // CCS byte
|
||||
0x01,
|
||||
0x01,
|
||||
];
|
||||
|
||||
// Build first encrypted flight mimic as opaque ApplicationData bytes.
|
||||
// Embed a compact EncryptedExtensions-like ALPN block when selected.
|
||||
// ALPN belongs inside encrypted EncryptedExtensions in real TLS 1.3.
|
||||
let mut fake_cert = Vec::with_capacity(fake_cert_len);
|
||||
if let Some(proto) = alpn
|
||||
.as_ref()
|
||||
.filter(|p| !p.is_empty() && p.len() <= u8::MAX as usize)
|
||||
{
|
||||
let proto_list_len = 1usize + proto.len();
|
||||
let ext_data_len = 2usize + proto_list_len;
|
||||
let marker_len = 4usize + ext_data_len;
|
||||
if marker_len <= fake_cert_len {
|
||||
fake_cert.extend_from_slice(&0x0010u16.to_be_bytes());
|
||||
fake_cert.extend_from_slice(&(ext_data_len as u16).to_be_bytes());
|
||||
fake_cert.extend_from_slice(&(proto_list_len as u16).to_be_bytes());
|
||||
fake_cert.push(proto.len() as u8);
|
||||
fake_cert.extend_from_slice(proto);
|
||||
}
|
||||
}
|
||||
let _ = alpn;
|
||||
if fake_cert.len() < fake_cert_len {
|
||||
fake_cert.extend_from_slice(&rng.bytes(fake_cert_len - fake_cert.len()));
|
||||
} else if fake_cert.len() > fake_cert_len {
|
||||
@@ -580,7 +603,7 @@ pub fn build_server_hello(
|
||||
let ticket_count = new_session_tickets.min(4);
|
||||
if ticket_count > 0 {
|
||||
for _ in 0..ticket_count {
|
||||
let ticket_len: usize = rng.range(48) + 48; // 48-95 bytes
|
||||
let ticket_len: usize = rng.range(48) + 48;
|
||||
let mut record = Vec::with_capacity(5 + ticket_len);
|
||||
record.push(TLS_RECORD_APPLICATION);
|
||||
record.extend_from_slice(&TLS_VERSION);
|
||||
@@ -927,6 +950,112 @@ pub fn detect_client_hello_tls_version(handshake: &[u8]) -> Option<ClientHelloTl
|
||||
}
|
||||
}
|
||||
|
||||
fn client_hello_cipher_suites_range(handshake: &[u8]) -> Option<(usize, usize)> {
|
||||
if handshake.len() < 5 || handshake[0] != TLS_RECORD_HANDSHAKE {
|
||||
return None;
|
||||
}
|
||||
|
||||
let record_len = u16::from_be_bytes([handshake[3], handshake[4]]) as usize;
|
||||
let record_end = 5usize.checked_add(record_len)?;
|
||||
if record_end > handshake.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut pos = 5;
|
||||
if handshake.get(pos) != Some(&0x01) {
|
||||
return None;
|
||||
}
|
||||
pos += 1;
|
||||
|
||||
if pos + 3 > record_end {
|
||||
return None;
|
||||
}
|
||||
let handshake_len = ((handshake[pos] as usize) << 16)
|
||||
| ((handshake[pos + 1] as usize) << 8)
|
||||
| handshake[pos + 2] as usize;
|
||||
pos += 3;
|
||||
let handshake_end = pos.checked_add(handshake_len)?;
|
||||
if handshake_end > record_end {
|
||||
return None;
|
||||
}
|
||||
|
||||
if pos + 2 + 32 > handshake_end {
|
||||
return None;
|
||||
}
|
||||
pos += 2 + 32;
|
||||
|
||||
let session_id_len = *handshake.get(pos)? as usize;
|
||||
pos = pos.checked_add(1)?.checked_add(session_id_len)?;
|
||||
if pos + 2 > handshake_end {
|
||||
return None;
|
||||
}
|
||||
|
||||
let cipher_len = u16::from_be_bytes([handshake[pos], handshake[pos + 1]]) as usize;
|
||||
if cipher_len == 0 || cipher_len % 2 != 0 {
|
||||
return None;
|
||||
}
|
||||
pos += 2;
|
||||
let cipher_end = pos.checked_add(cipher_len)?;
|
||||
if cipher_end > handshake_end {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((pos, cipher_end))
|
||||
}
|
||||
|
||||
fn client_hello_offers_cipher_suite(
|
||||
handshake: &[u8],
|
||||
range: (usize, usize),
|
||||
suite: [u8; 2],
|
||||
) -> bool {
|
||||
let mut pos = range.0;
|
||||
while pos + 1 < range.1 {
|
||||
if handshake[pos] == suite[0] && handshake[pos + 1] == suite[1] {
|
||||
return true;
|
||||
}
|
||||
pos += 2;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn is_tls13_cipher_suite(suite: [u8; 2]) -> bool {
|
||||
suite == cipher_suite::TLS_AES_128_GCM_SHA256
|
||||
|| suite == cipher_suite::TLS_AES_256_GCM_SHA384
|
||||
|| suite == cipher_suite::TLS_CHACHA20_POLY1305_SHA256
|
||||
}
|
||||
|
||||
/// 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] {
|
||||
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;
|
||||
};
|
||||
|
||||
if client_hello_offers_cipher_suite(handshake, range, preferred) {
|
||||
return preferred;
|
||||
}
|
||||
|
||||
for fallback in [
|
||||
cipher_suite::TLS_AES_128_GCM_SHA256,
|
||||
cipher_suite::TLS_CHACHA20_POLY1305_SHA256,
|
||||
cipher_suite::TLS_AES_256_GCM_SHA384,
|
||||
] {
|
||||
if client_hello_offers_cipher_suite(handshake, range, fallback) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
preferred
|
||||
}
|
||||
|
||||
/// Check if bytes look like a TLS ClientHello
|
||||
pub fn is_tls_handshake(first_bytes: &[u8]) -> bool {
|
||||
if first_bytes.len() < 3 {
|
||||
|
||||
@@ -1504,6 +1504,13 @@ where
|
||||
let validation_session_id_slice = &validation_session_id[..validation_session_id_len];
|
||||
|
||||
let response = if let Some((cached_entry, use_full_cert_payload)) = cached {
|
||||
let preferred_cipher_suite = if cached_entry.server_hello_template.cipher_suite == [0, 0] {
|
||||
[0x13, 0x01]
|
||||
} else {
|
||||
cached_entry.server_hello_template.cipher_suite
|
||||
};
|
||||
let selected_cipher_suite =
|
||||
tls::select_server_hello_cipher_suite(handshake, preferred_cipher_suite);
|
||||
emulator::build_emulated_server_hello(
|
||||
&validated_secret,
|
||||
&validation_digest,
|
||||
@@ -1512,17 +1519,21 @@ where
|
||||
use_full_cert_payload,
|
||||
config.censorship.serverhello_compact,
|
||||
client_tls_version,
|
||||
selected_cipher_suite,
|
||||
rng,
|
||||
selected_alpn.clone(),
|
||||
config.censorship.tls_new_session_tickets,
|
||||
)
|
||||
} else {
|
||||
tls::build_server_hello(
|
||||
let selected_cipher_suite =
|
||||
tls::select_server_hello_cipher_suite(handshake, [0x13, 0x01]);
|
||||
tls::build_server_hello_with_cipher(
|
||||
&validated_secret,
|
||||
&validation_digest,
|
||||
validation_session_id_slice,
|
||||
config.censorship.fake_cert_len,
|
||||
rng,
|
||||
selected_cipher_suite,
|
||||
selected_alpn.clone(),
|
||||
config.censorship.tls_new_session_tickets,
|
||||
)
|
||||
|
||||
@@ -8,12 +8,17 @@ use crate::protocol::constants::{
|
||||
use crate::protocol::tls::{
|
||||
ClientHelloTlsVersion, TLS_DIGEST_LEN, TLS_DIGEST_POS, gen_fake_x25519_key,
|
||||
};
|
||||
use crate::tls_front::types::{CachedTlsData, ParsedCertificateInfo, TlsProfileSource};
|
||||
use crate::tls_front::types::{
|
||||
CachedTlsData, ParsedCertificateInfo, TlsExtension, TlsProfileSource,
|
||||
};
|
||||
use crc32fast::Hasher;
|
||||
|
||||
const MIN_APP_DATA: usize = 64;
|
||||
const MAX_APP_DATA: usize = MAX_TLS_CIPHERTEXT_SIZE;
|
||||
const MAX_TICKET_RECORDS: usize = 4;
|
||||
const EXT_SUPPORTED_VERSIONS: u16 = 0x002b;
|
||||
const EXT_KEY_SHARE: u16 = 0x0033;
|
||||
const EXT_ALPN: u16 = 0x0010;
|
||||
|
||||
fn jitter_and_clamp_sizes(sizes: &[usize], rng: &SecureRandom) -> Vec<usize> {
|
||||
sizes
|
||||
@@ -185,6 +190,77 @@ fn hash_compact_cert_info_payload(cert_payload: Vec<u8>) -> Option<Vec<u8>> {
|
||||
Some(hashed)
|
||||
}
|
||||
|
||||
fn push_supported_versions_extension(extensions: &mut Vec<u8>) {
|
||||
extensions.extend_from_slice(&EXT_SUPPORTED_VERSIONS.to_be_bytes());
|
||||
extensions.extend_from_slice(&(2u16).to_be_bytes());
|
||||
extensions.extend_from_slice(&0x0304u16.to_be_bytes());
|
||||
}
|
||||
|
||||
fn push_key_share_extension(extensions: &mut Vec<u8>, rng: &SecureRandom) {
|
||||
let key = gen_fake_x25519_key(rng);
|
||||
extensions.extend_from_slice(&EXT_KEY_SHARE.to_be_bytes());
|
||||
extensions.extend_from_slice(&(2 + 2 + 32u16).to_be_bytes());
|
||||
extensions.extend_from_slice(&0x001du16.to_be_bytes());
|
||||
extensions.extend_from_slice(&(32u16).to_be_bytes());
|
||||
extensions.extend_from_slice(&key);
|
||||
}
|
||||
|
||||
fn replay_profiled_server_hello_extension(
|
||||
ext: &TlsExtension,
|
||||
extensions: &mut Vec<u8>,
|
||||
rng: &SecureRandom,
|
||||
saw_supported_versions: &mut bool,
|
||||
saw_key_share: &mut bool,
|
||||
) {
|
||||
match ext.ext_type {
|
||||
EXT_SUPPORTED_VERSIONS if !*saw_supported_versions => {
|
||||
push_supported_versions_extension(extensions);
|
||||
*saw_supported_versions = true;
|
||||
}
|
||||
EXT_KEY_SHARE if !*saw_key_share => {
|
||||
push_key_share_extension(extensions, rng);
|
||||
*saw_key_share = true;
|
||||
}
|
||||
EXT_ALPN => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_profiled_server_hello_extensions(
|
||||
cached: &CachedTlsData,
|
||||
rng: &SecureRandom,
|
||||
) -> Vec<u8> {
|
||||
let capacity = cached
|
||||
.server_hello_template
|
||||
.extensions
|
||||
.iter()
|
||||
.map(|ext| 4 + ext.data.len())
|
||||
.sum::<usize>()
|
||||
.max(44);
|
||||
let mut extensions = Vec::with_capacity(capacity);
|
||||
let mut saw_supported_versions = false;
|
||||
let mut saw_key_share = false;
|
||||
|
||||
for ext in &cached.server_hello_template.extensions {
|
||||
replay_profiled_server_hello_extension(
|
||||
ext,
|
||||
&mut extensions,
|
||||
rng,
|
||||
&mut saw_supported_versions,
|
||||
&mut saw_key_share,
|
||||
);
|
||||
}
|
||||
|
||||
if !saw_key_share {
|
||||
push_key_share_extension(&mut extensions, rng);
|
||||
}
|
||||
if !saw_supported_versions {
|
||||
push_supported_versions_extension(&mut extensions);
|
||||
}
|
||||
|
||||
extensions
|
||||
}
|
||||
|
||||
/// Build a ServerHello + CCS + ApplicationData sequence using cached TLS metadata.
|
||||
pub fn build_emulated_server_hello(
|
||||
secret: &[u8],
|
||||
@@ -194,39 +270,28 @@ pub fn build_emulated_server_hello(
|
||||
use_full_cert_payload: bool,
|
||||
serverhello_compact: bool,
|
||||
client_tls_version: ClientHelloTlsVersion,
|
||||
selected_cipher_suite: [u8; 2],
|
||||
rng: &SecureRandom,
|
||||
alpn: Option<Vec<u8>>,
|
||||
new_session_tickets: u8,
|
||||
) -> Vec<u8> {
|
||||
// --- ServerHello ---
|
||||
let mut extensions = Vec::new();
|
||||
let key = gen_fake_x25519_key(rng);
|
||||
extensions.extend_from_slice(&0x0033u16.to_be_bytes());
|
||||
extensions.extend_from_slice(&(2 + 2 + 32u16).to_be_bytes());
|
||||
extensions.extend_from_slice(&0x001du16.to_be_bytes());
|
||||
extensions.extend_from_slice(&(32u16).to_be_bytes());
|
||||
extensions.extend_from_slice(&key);
|
||||
extensions.extend_from_slice(&0x002bu16.to_be_bytes());
|
||||
extensions.extend_from_slice(&(2u16).to_be_bytes());
|
||||
extensions.extend_from_slice(&0x0304u16.to_be_bytes());
|
||||
let extensions = build_profiled_server_hello_extensions(cached, rng);
|
||||
let extensions_len = extensions.len() as u16;
|
||||
|
||||
let body_len = 2 + // version
|
||||
32 + // random
|
||||
1 + session_id.len() + // session id
|
||||
2 + // cipher
|
||||
1 + // compression
|
||||
2 + extensions.len(); // extensions
|
||||
let body_len = 2 + 32 + 1 + session_id.len() + 2 + 1 + 2 + extensions.len();
|
||||
|
||||
let mut message = Vec::with_capacity(4 + body_len);
|
||||
message.push(0x02); // ServerHello
|
||||
message.push(0x02);
|
||||
let len_bytes = (body_len as u32).to_be_bytes();
|
||||
message.extend_from_slice(&len_bytes[1..4]);
|
||||
message.extend_from_slice(&cached.server_hello_template.version); // 0x0303
|
||||
message.extend_from_slice(&[0u8; 32]); // random placeholder
|
||||
message.extend_from_slice(&cached.server_hello_template.version);
|
||||
message.extend_from_slice(&[0u8; 32]);
|
||||
message.push(session_id.len() as u8);
|
||||
message.extend_from_slice(session_id);
|
||||
let cipher = if cached.server_hello_template.cipher_suite == [0, 0] {
|
||||
let cipher = if selected_cipher_suite != [0, 0] {
|
||||
selected_cipher_suite
|
||||
} else if cached.server_hello_template.cipher_suite == [0, 0] {
|
||||
[0x13, 0x01]
|
||||
} else {
|
||||
cached.server_hello_template.cipher_suite
|
||||
@@ -303,21 +368,10 @@ pub fn build_emulated_server_hello(
|
||||
}
|
||||
|
||||
let mut app_data = Vec::new();
|
||||
let alpn_marker = alpn
|
||||
.as_ref()
|
||||
.filter(|p| !p.is_empty() && p.len() <= u8::MAX as usize)
|
||||
.map(|proto| {
|
||||
let proto_list_len = 1usize + proto.len();
|
||||
let ext_data_len = 2usize + proto_list_len;
|
||||
let mut marker = Vec::with_capacity(4 + ext_data_len);
|
||||
marker.extend_from_slice(&0x0010u16.to_be_bytes());
|
||||
marker.extend_from_slice(&(ext_data_len as u16).to_be_bytes());
|
||||
marker.extend_from_slice(&(proto_list_len as u16).to_be_bytes());
|
||||
marker.push(proto.len() as u8);
|
||||
marker.extend_from_slice(proto);
|
||||
marker
|
||||
});
|
||||
for (idx, size) in sizes.into_iter().enumerate() {
|
||||
// ALPN selection is encrypted inside EncryptedExtensions in real TLS 1.3.
|
||||
// Keeping the FakeTLS record body opaque avoids a stable plaintext marker.
|
||||
let _ = alpn;
|
||||
for size in sizes {
|
||||
let mut rec = Vec::with_capacity(5 + size);
|
||||
rec.push(TLS_RECORD_APPLICATION);
|
||||
rec.extend_from_slice(&TLS_VERSION);
|
||||
@@ -334,31 +388,18 @@ pub fn build_emulated_server_hello(
|
||||
if body_len > copy_len {
|
||||
rec.extend_from_slice(&rng.bytes(body_len - copy_len));
|
||||
}
|
||||
rec.push(0x16); // inner content type marker (handshake)
|
||||
rec.extend_from_slice(&rng.bytes(16)); // AEAD-like tag
|
||||
rec.push(0x16);
|
||||
rec.extend_from_slice(&rng.bytes(16));
|
||||
} else {
|
||||
rec.extend_from_slice(&rng.bytes(size));
|
||||
}
|
||||
} else if size > 17 {
|
||||
let body_len = size - 17;
|
||||
let mut body = Vec::with_capacity(body_len);
|
||||
if idx == 0
|
||||
&& let Some(marker) = &alpn_marker
|
||||
{
|
||||
if marker.len() <= body_len {
|
||||
body.extend_from_slice(marker);
|
||||
if body_len > marker.len() {
|
||||
body.extend_from_slice(&rng.bytes(body_len - marker.len()));
|
||||
}
|
||||
} else {
|
||||
body.extend_from_slice(&rng.bytes(body_len));
|
||||
}
|
||||
} else {
|
||||
body.extend_from_slice(&rng.bytes(body_len));
|
||||
}
|
||||
body.extend_from_slice(&rng.bytes(body_len));
|
||||
rec.extend_from_slice(&body);
|
||||
rec.push(0x16); // inner content type marker (handshake)
|
||||
rec.extend_from_slice(&rng.bytes(16)); // AEAD-like tag
|
||||
rec.push(0x16);
|
||||
rec.extend_from_slice(&rng.bytes(16));
|
||||
} else {
|
||||
rec.extend_from_slice(&rng.bytes(size));
|
||||
}
|
||||
@@ -408,7 +449,8 @@ mod tests {
|
||||
use std::time::SystemTime;
|
||||
|
||||
use crate::tls_front::types::{
|
||||
CachedTlsData, ParsedServerHello, TlsBehaviorProfile, TlsCertPayload, TlsProfileSource,
|
||||
CachedTlsData, ParsedServerHello, TlsBehaviorProfile, TlsCertPayload, TlsExtension,
|
||||
TlsProfileSource,
|
||||
};
|
||||
|
||||
use super::{
|
||||
@@ -432,6 +474,38 @@ mod tests {
|
||||
&response[app_start + 5..app_start + 5 + app_len]
|
||||
}
|
||||
|
||||
fn server_hello_cipher_suite(response: &[u8]) -> [u8; 2] {
|
||||
let mut pos = 5 + 4 + 2 + 32;
|
||||
let session_id_len = response[pos] as usize;
|
||||
pos += 1 + session_id_len;
|
||||
[response[pos], response[pos + 1]]
|
||||
}
|
||||
|
||||
fn server_hello_extension_types(response: &[u8]) -> Vec<u16> {
|
||||
let record_len = u16::from_be_bytes([response[3], response[4]]) as usize;
|
||||
let handshake_end = 5 + record_len;
|
||||
let mut pos = 5 + 4 + 2 + 32;
|
||||
let session_id_len = response[pos] as usize;
|
||||
pos += 1 + session_id_len + 2 + 1;
|
||||
let extensions_len = u16::from_be_bytes([response[pos], response[pos + 1]]) as usize;
|
||||
pos += 2;
|
||||
let extensions_end = (pos + extensions_len).min(handshake_end);
|
||||
let mut out = Vec::new();
|
||||
|
||||
while pos + 4 <= extensions_end {
|
||||
let ext_type = u16::from_be_bytes([response[pos], response[pos + 1]]);
|
||||
let ext_len = u16::from_be_bytes([response[pos + 2], response[pos + 3]]) as usize;
|
||||
pos += 4;
|
||||
if pos + ext_len > extensions_end {
|
||||
break;
|
||||
}
|
||||
out.push(ext_type);
|
||||
pos += ext_len;
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
fn make_cached(cert_payload: Option<TlsCertPayload>) -> CachedTlsData {
|
||||
CachedTlsData {
|
||||
server_hello_template: ParsedServerHello {
|
||||
@@ -468,6 +542,7 @@ mod tests {
|
||||
true,
|
||||
true,
|
||||
ClientHelloTlsVersion::Tls12,
|
||||
[0x13, 0x01],
|
||||
&rng,
|
||||
None,
|
||||
0,
|
||||
@@ -484,6 +559,62 @@ mod tests {
|
||||
assert!(payload.starts_with(&cert_msg));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_emulated_server_hello_uses_selected_cipher_suite() {
|
||||
let cached = make_cached(None);
|
||||
let rng = SecureRandom::new();
|
||||
let response = build_emulated_server_hello(
|
||||
b"secret",
|
||||
&[0x10; 32],
|
||||
&[0x20; 16],
|
||||
&cached,
|
||||
false,
|
||||
true,
|
||||
ClientHelloTlsVersion::Tls13,
|
||||
[0x13, 0x03],
|
||||
&rng,
|
||||
None,
|
||||
0,
|
||||
);
|
||||
|
||||
assert_eq!(server_hello_cipher_suite(&response), [0x13, 0x03]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_emulated_server_hello_replays_profiled_safe_extension_order() {
|
||||
let mut cached = make_cached(None);
|
||||
cached.server_hello_template.extensions = vec![
|
||||
TlsExtension {
|
||||
ext_type: 0x002b,
|
||||
data: vec![0x03, 0x04],
|
||||
},
|
||||
TlsExtension {
|
||||
ext_type: 0x0010,
|
||||
data: vec![0x00, 0x03, 0x02, b'h', b'2'],
|
||||
},
|
||||
TlsExtension {
|
||||
ext_type: 0x0033,
|
||||
data: vec![0; 36],
|
||||
},
|
||||
];
|
||||
let rng = SecureRandom::new();
|
||||
let response = build_emulated_server_hello(
|
||||
b"secret",
|
||||
&[0x21; 32],
|
||||
&[0x22; 16],
|
||||
&cached,
|
||||
false,
|
||||
true,
|
||||
ClientHelloTlsVersion::Tls13,
|
||||
[0x13, 0x01],
|
||||
&rng,
|
||||
Some(b"h2".to_vec()),
|
||||
0,
|
||||
);
|
||||
|
||||
assert_eq!(server_hello_extension_types(&response), vec![0x002b, 0x0033]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_emulated_server_hello_random_fallback_when_no_cert_payload() {
|
||||
let cached = make_cached(None);
|
||||
@@ -496,6 +627,7 @@ mod tests {
|
||||
true,
|
||||
true,
|
||||
ClientHelloTlsVersion::Tls12,
|
||||
[0x13, 0x01],
|
||||
&rng,
|
||||
None,
|
||||
0,
|
||||
@@ -530,6 +662,7 @@ mod tests {
|
||||
false,
|
||||
true,
|
||||
ClientHelloTlsVersion::Tls12,
|
||||
[0x13, 0x01],
|
||||
&rng,
|
||||
None,
|
||||
0,
|
||||
@@ -570,6 +703,7 @@ mod tests {
|
||||
true,
|
||||
true,
|
||||
ClientHelloTlsVersion::Tls13,
|
||||
[0x13, 0x01],
|
||||
&rng,
|
||||
None,
|
||||
0,
|
||||
@@ -583,7 +717,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_emulated_server_hello_compact_disabled_skips_compact_payload() {
|
||||
fn test_build_emulated_server_hello_keeps_alpn_marker_out_of_random_payload() {
|
||||
let mut cached = make_cached(None);
|
||||
cached.cert_info = Some(crate::tls_front::types::ParsedCertificateInfo {
|
||||
not_after_unix: Some(1_900_000_000),
|
||||
@@ -602,6 +736,7 @@ mod tests {
|
||||
false,
|
||||
false,
|
||||
ClientHelloTlsVersion::Tls12,
|
||||
[0x13, 0x01],
|
||||
&rng,
|
||||
Some(b"h2".to_vec()),
|
||||
0,
|
||||
@@ -610,8 +745,8 @@ mod tests {
|
||||
let payload = first_app_data_payload(&response);
|
||||
let expected_alpn_marker = [0x00u8, 0x10, 0x00, 0x05, 0x00, 0x03, 0x02, b'h', b'2'];
|
||||
assert!(
|
||||
payload.starts_with(&expected_alpn_marker),
|
||||
"when compact mode is disabled and no full cert payload exists, the random/alpn path must be used"
|
||||
!payload.starts_with(&expected_alpn_marker),
|
||||
"random fallback payload must not expose plaintext ALPN marker bytes"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -633,6 +768,7 @@ mod tests {
|
||||
false,
|
||||
true,
|
||||
ClientHelloTlsVersion::Tls13,
|
||||
[0x13, 0x01],
|
||||
&rng,
|
||||
None,
|
||||
0,
|
||||
|
||||
@@ -65,6 +65,7 @@ fn emulated_server_hello_keeps_single_change_cipher_spec_for_client_compatibilit
|
||||
false,
|
||||
true,
|
||||
ClientHelloTlsVersion::Tls13,
|
||||
[0x13, 0x01],
|
||||
&rng,
|
||||
None,
|
||||
0,
|
||||
@@ -89,6 +90,7 @@ fn emulated_server_hello_does_not_emit_profile_ticket_tail_when_disabled() {
|
||||
false,
|
||||
true,
|
||||
ClientHelloTlsVersion::Tls13,
|
||||
[0x13, 0x01],
|
||||
&rng,
|
||||
None,
|
||||
0,
|
||||
@@ -111,6 +113,7 @@ fn emulated_server_hello_uses_profile_ticket_lengths_when_enabled() {
|
||||
false,
|
||||
true,
|
||||
ClientHelloTlsVersion::Tls13,
|
||||
[0x13, 0x01],
|
||||
&rng,
|
||||
None,
|
||||
2,
|
||||
|
||||
@@ -58,6 +58,7 @@ fn emulated_server_hello_ignores_oversized_alpn_when_marker_would_not_fit() {
|
||||
true,
|
||||
true,
|
||||
ClientHelloTlsVersion::Tls13,
|
||||
[0x13, 0x01],
|
||||
&rng,
|
||||
Some(oversized_alpn),
|
||||
0,
|
||||
@@ -84,7 +85,7 @@ fn emulated_server_hello_ignores_oversized_alpn_when_marker_would_not_fit() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emulated_server_hello_embeds_full_alpn_marker_when_body_can_fit() {
|
||||
fn emulated_server_hello_keeps_alpn_marker_out_of_appdata() {
|
||||
let cached = make_cached(None);
|
||||
let rng = SecureRandom::new();
|
||||
|
||||
@@ -96,6 +97,7 @@ fn emulated_server_hello_embeds_full_alpn_marker_when_body_can_fit() {
|
||||
true,
|
||||
true,
|
||||
ClientHelloTlsVersion::Tls13,
|
||||
[0x13, 0x01],
|
||||
&rng,
|
||||
Some(b"h2".to_vec()),
|
||||
0,
|
||||
@@ -104,8 +106,8 @@ fn emulated_server_hello_embeds_full_alpn_marker_when_body_can_fit() {
|
||||
let payload = first_app_data_payload(&response);
|
||||
let expected = [0x00u8, 0x10, 0x00, 0x05, 0x00, 0x03, 0x02, b'h', b'2'];
|
||||
assert!(
|
||||
payload.starts_with(&expected),
|
||||
"when body has enough capacity, emulated first application record must include full ALPN marker"
|
||||
!payload.starts_with(&expected),
|
||||
"emulated ApplicationData must not expose plaintext ALPN marker bytes"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -126,6 +128,7 @@ fn emulated_server_hello_prefers_cert_payload_over_alpn_marker() {
|
||||
true,
|
||||
true,
|
||||
ClientHelloTlsVersion::Tls12,
|
||||
[0x13, 0x01],
|
||||
&rng,
|
||||
Some(b"h2".to_vec()),
|
||||
0,
|
||||
|
||||
Reference in New Issue
Block a user