fix handshakes
This commit is contained in:
+18
-16
@@ -55,6 +55,8 @@ pub struct SessionKeys {
|
|||||||
pub salt: SaltPair,
|
pub salt: SaltPair,
|
||||||
pub ecdh: ECDH,
|
pub ecdh: ECDH,
|
||||||
pub auth_key: [u8; 32],
|
pub auth_key: [u8; 32],
|
||||||
|
pub current_aead: Option<([u8; 32], [u8; 12], [u8; 32], [u8; 12])>,
|
||||||
|
is_initiator: bool, // Сохраним роль для правильного возврата параметров
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SessionKeys {
|
impl SessionKeys {
|
||||||
@@ -63,9 +65,16 @@ impl SessionKeys {
|
|||||||
salt: SaltPair::new(is_initiator),
|
salt: SaltPair::new(is_initiator),
|
||||||
ecdh: ECDH::new(),
|
ecdh: ECDH::new(),
|
||||||
auth_key: [0u8; 32],
|
auth_key: [0u8; 32],
|
||||||
|
current_aead: None,
|
||||||
|
is_initiator,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_aead_parameters(&self) -> ([u8; 32], [u8; 12], [u8; 32], [u8; 12]) {
|
||||||
|
self.current_aead
|
||||||
|
.expect("Keys not generated yet. Call update_keys first.")
|
||||||
|
}
|
||||||
|
|
||||||
pub fn update_keys(
|
pub fn update_keys(
|
||||||
&mut self,
|
&mut self,
|
||||||
salt: [u8; 32],
|
salt: [u8; 32],
|
||||||
@@ -141,11 +150,6 @@ impl SessionKeys {
|
|||||||
.get_shared(public_key)
|
.get_shared(public_key)
|
||||||
.ok_or_else(|| "No shared secret".to_string())?;
|
.ok_or_else(|| "No shared secret".to_string())?;
|
||||||
|
|
||||||
netrunner_logger::debug!(
|
|
||||||
shared_prefix = %hex::encode(&shared_key[..8]),
|
|
||||||
"DH Shared secret derived"
|
|
||||||
);
|
|
||||||
|
|
||||||
let hkdf = HKDF::extract_key(&self.salt.get_total(), &shared_key);
|
let hkdf = HKDF::extract_key(&self.salt.get_total(), &shared_key);
|
||||||
|
|
||||||
let c_key = HKDF::expand_key::<32>(&hkdf, b"client_aead").map_err(|e| e.to_string())?;
|
let c_key = HKDF::expand_key::<32>(&hkdf, b"client_aead").map_err(|e| e.to_string())?;
|
||||||
@@ -153,19 +157,17 @@ impl SessionKeys {
|
|||||||
let s_key = HKDF::expand_key::<32>(&hkdf, b"server_aead").map_err(|e| e.to_string())?;
|
let s_key = HKDF::expand_key::<32>(&hkdf, b"server_aead").map_err(|e| e.to_string())?;
|
||||||
let s_iv = HKDF::expand_key::<12>(&hkdf, b"server_iv").map_err(|e| e.to_string())?;
|
let s_iv = HKDF::expand_key::<12>(&hkdf, b"server_iv").map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
let auth_secret = HKDF::expand_key::<32>(&hkdf, b"auth_key").map_err(|e| e.to_string())?;
|
self.auth_key = HKDF::expand_key::<32>(&hkdf, b"auth_key").map_err(|e| e.to_string())?;
|
||||||
self.auth_key = auth_secret;
|
|
||||||
netrunner_logger::info!(
|
|
||||||
client_key_short = %hex::encode(&c_key[..4]),
|
|
||||||
server_key_short = %hex::encode(&s_key[..4]),
|
|
||||||
"HKDF expansion complete"
|
|
||||||
);
|
|
||||||
|
|
||||||
if is_server {
|
// Определяем порядок: (Write Key, Write IV, Read Key, Read IV)
|
||||||
Ok((s_key, s_iv, c_key, c_iv))
|
let keys = if is_server {
|
||||||
|
(s_key, s_iv, c_key, c_iv)
|
||||||
} else {
|
} else {
|
||||||
Ok((c_key, c_iv, s_key, s_iv))
|
(c_key, c_iv, s_key, s_iv)
|
||||||
}
|
};
|
||||||
|
|
||||||
|
self.current_aead = Some(keys);
|
||||||
|
Ok(keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compute_tag(secret: &[u8], step: u64) -> [u8; 16] {
|
fn compute_tag(secret: &[u8], step: u64) -> [u8; 16] {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
use crate::crypto::session::SessionKeys;
|
||||||
use crate::protocol::errors::{ErrorAction, ErrorStage, TlsError};
|
use crate::protocol::errors::{ErrorAction, ErrorStage, TlsError};
|
||||||
use crate::protocol::parser::parser::Parser;
|
use crate::protocol::parser::parser::Parser;
|
||||||
use crate::tlseng::extension::ExtensionStack;
|
use crate::tlseng::extension::ExtensionStack;
|
||||||
use crate::tlseng::handshake::{ClientHello, HelloHeader, ServerHello};
|
use crate::tlseng::handshake::{ClientHello, HelloHeader, ServerHello};
|
||||||
use crate::tlseng::profile::BrowserProfile;
|
use crate::tlseng::profile::{BrowserProfile, ServerProfile};
|
||||||
use crate::tlseng::tls_record::TlsRecord;
|
use crate::tlseng::tls_record::TlsRecord;
|
||||||
use crate::tlseng::types::{ContentType, HelloType};
|
use crate::tlseng::types::{ContentType, HelloType};
|
||||||
use crate::tlseng::ApplicationData;
|
use crate::tlseng::ApplicationData;
|
||||||
@@ -133,31 +134,66 @@ impl TlsBridge {
|
|||||||
ApplicationData::start_process(buffer)
|
ApplicationData::start_process(buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn wrap_client_hello(
|
pub fn wrap_client_hello(profile: &BrowserProfile, host: &str, keys: &SessionKeys) -> Bytes {
|
||||||
profile: &BrowserProfile,
|
ClientHello::make_client_hello(profile, host, keys)
|
||||||
host: &str,
|
|
||||||
public_key: &[u8; 32],
|
|
||||||
salt: [u8; 32],
|
|
||||||
) -> Bytes {
|
|
||||||
ClientHello::make_client_hello(profile, host, public_key, salt)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn wrap_server_hello(
|
pub fn wrap_server_hello(
|
||||||
client_msg: &HandshakeMessage,
|
client_msg: &HandshakeMessage,
|
||||||
server_pub_key: &[u8],
|
keys: &mut SessionKeys,
|
||||||
salt: [u8; 32],
|
profile: &ServerProfile,
|
||||||
) -> Result<Bytes, TlsError> {
|
) -> Result<Bytes, TlsError> {
|
||||||
if let HandshakeMessage::Client { base, .. } = client_msg {
|
if let HandshakeMessage::Client { base, extensions } = client_msg {
|
||||||
Ok(ServerHello::make_server_hello(base, server_pub_key, salt))
|
// 1. Проверка Auth Tag в Session ID (последние 16 байт)
|
||||||
|
if base.session_id.len() != 32 {
|
||||||
|
return Err(TlsError::new(
|
||||||
|
ErrorStage::Handshake("Invalid SessionID len"),
|
||||||
|
ErrorAction::Drop,
|
||||||
|
Bytes::new(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut received_tag = [0u8; 16];
|
||||||
|
received_tag.copy_from_slice(&base.session_id[16..32]);
|
||||||
|
|
||||||
|
if !keys.verify_auth_tag(&received_tag) {
|
||||||
|
netrunner_logger::warn!("Unauthorized ClientHello: Auth Tag mismatch");
|
||||||
|
return Err(TlsError::new(
|
||||||
|
ErrorStage::Handshake("Auth Failed"),
|
||||||
|
ErrorAction::Drop,
|
||||||
|
Bytes::new(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Выполняем Key Exchange (находим KeyShare клиента и считаем Shared Secret)
|
||||||
|
// Это обновит внутреннее состояние keys (auth_key и AEAD ключи)
|
||||||
|
keys.update_keys(base.random, extensions, true)
|
||||||
|
.map_err(|e| {
|
||||||
|
netrunner_logger::error!(error = %e, "Server failed key update");
|
||||||
|
TlsError::new(
|
||||||
|
ErrorStage::Handshake("Key Exchange Failed"),
|
||||||
|
ErrorAction::Drop,
|
||||||
|
Bytes::new(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 3. Генерируем ServerHello, используя наш свежий публичный ключ и локальную соль
|
||||||
|
let server_pub_key = keys.ecdh.public_key.to_bytes();
|
||||||
|
|
||||||
|
Ok(ServerHello::make_server_hello(
|
||||||
|
base,
|
||||||
|
&server_pub_key,
|
||||||
|
keys.salt.get_local(),
|
||||||
|
profile,
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
Err(TlsError::new(
|
Err(TlsError::new(
|
||||||
ErrorStage::Handshake("Wrong message type for ServerHello generation"),
|
ErrorStage::Handshake("Expected ClientHello for SH generation"),
|
||||||
ErrorAction::Drop,
|
ErrorAction::Drop,
|
||||||
Bytes::new(),
|
Bytes::new(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pack_app_data(buffer: Bytes) -> Bytes {
|
pub fn pack_app_data(buffer: Bytes) -> Bytes {
|
||||||
TlsRecord::build_application_data(buffer)
|
TlsRecord::build_application_data(buffer)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use crate::protocol::codec::frame::{Frame, FrameHeader, FrameType};
|
|||||||
use crate::protocol::codec::padding::Padding;
|
use crate::protocol::codec::padding::Padding;
|
||||||
use crate::protocol::errors::{ErrorAction, ErrorStage, TlsError};
|
use crate::protocol::errors::{ErrorAction, ErrorStage, TlsError};
|
||||||
use crate::protocol::parser::parser::Parser;
|
use crate::protocol::parser::parser::Parser;
|
||||||
use crate::tlseng::profile::BrowserProfile;
|
use crate::tlseng::profile::{BrowserProfile, ServerProfile};
|
||||||
|
|
||||||
pub struct Codec {
|
pub struct Codec {
|
||||||
crypto: ChaChaCipher,
|
crypto: ChaChaCipher,
|
||||||
@@ -30,13 +30,10 @@ impl Codec {
|
|||||||
profile: &BrowserProfile,
|
profile: &BrowserProfile,
|
||||||
host: &str,
|
host: &str,
|
||||||
) -> Result<Bytes, TlsError> {
|
) -> Result<Bytes, TlsError> {
|
||||||
let pub_key = self.session_keys.ecdh.public_key.to_bytes();
|
|
||||||
|
|
||||||
Ok(TlsBridge::wrap_client_hello(
|
Ok(TlsBridge::wrap_client_hello(
|
||||||
profile,
|
profile,
|
||||||
host,
|
host,
|
||||||
&pub_key,
|
&self.session_keys,
|
||||||
self.session_keys.salt.get_local(),
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,30 +46,21 @@ impl Codec {
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let server_pub_key = self.session_keys.ecdh.public_key.to_bytes();
|
// ВАЖНО: TlsBridge сам проверит Auth Tag в Session ID клиента
|
||||||
|
// и вызовет update_keys для генерации общего секрета.
|
||||||
let server_hello_record = TlsBridge::wrap_server_hello(
|
let server_hello_record = TlsBridge::wrap_server_hello(
|
||||||
&client_msg,
|
&client_msg,
|
||||||
&server_pub_key,
|
&mut self.session_keys,
|
||||||
self.session_keys.salt.get_local(),
|
&ServerProfile::MODERN,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let (w_key, w_iv, r_key, r_iv) = self
|
// Ключи уже обновлены внутри session_keys.
|
||||||
.session_keys
|
// Просто забираем их и устанавливаем в AEAD шифратор.
|
||||||
.update_keys(client_msg.random(), client_msg.extensions(), true)
|
let (w_key, w_iv, r_key, r_iv) = self.session_keys.get_aead_parameters();
|
||||||
.map_err(|e| {
|
|
||||||
netrunner_logger::error!(error = %e, "Server failed to update keys from ClientHello");
|
|
||||||
TlsError::new(
|
|
||||||
ErrorStage::Handshake("Key Err"),
|
|
||||||
ErrorAction::Drop,
|
|
||||||
Bytes::new(),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
self.crypto.set_keys(w_key, w_iv, r_key, r_iv);
|
self.crypto.set_keys(w_key, w_iv, r_key, r_iv);
|
||||||
|
|
||||||
Ok(server_hello_record)
|
Ok(server_hello_record)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn process_handshake(&mut self, buffer: &mut BytesMut) -> Result<(), TlsError> {
|
pub fn process_handshake(&mut self, buffer: &mut BytesMut) -> Result<(), TlsError> {
|
||||||
let mes_opt = TlsBridge::unpack_handshake(buffer)?;
|
let mes_opt = TlsBridge::unpack_handshake(buffer)?;
|
||||||
let mes = mes_opt.ok_or_else(|| {
|
let mes = mes_opt.ok_or_else(|| {
|
||||||
|
|||||||
@@ -113,87 +113,79 @@ impl Parser for ClientHello {
|
|||||||
type Error = TlsError;
|
type Error = TlsError;
|
||||||
|
|
||||||
fn can_parse(bytes: &BytesMut) -> bool {
|
fn can_parse(bytes: &BytesMut) -> bool {
|
||||||
let mut offset = 34;
|
let mut reader = &bytes[..];
|
||||||
if bytes.len() < offset + 1 {
|
|
||||||
|
// 1. Version (2) + Random (32) + SessionID Len (1) = 35
|
||||||
|
if reader.len() < 35 {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
reader.advance(34);
|
||||||
|
|
||||||
let session_id_len = bytes[offset] as usize;
|
// 2. Session ID
|
||||||
offset += 1 + session_id_len;
|
let sid_len = reader[0] as usize;
|
||||||
if bytes.len() < offset + 2 {
|
reader.advance(1);
|
||||||
|
if reader.len() < sid_len + 2 {
|
||||||
|
return false;
|
||||||
|
} // +2 для Cipher Suites Len
|
||||||
|
reader.advance(sid_len);
|
||||||
|
|
||||||
|
// 3. Cipher Suites
|
||||||
|
let ciphers_len = u16::from_be_bytes([reader[0], reader[1]]) as usize;
|
||||||
|
reader.advance(2);
|
||||||
|
if reader.len() < ciphers_len + 1 {
|
||||||
|
return false;
|
||||||
|
} // +1 для Compression Len
|
||||||
|
reader.advance(ciphers_len);
|
||||||
|
|
||||||
|
// 4. Compression Methods
|
||||||
|
let comp_len = reader[0] as usize;
|
||||||
|
reader.advance(1);
|
||||||
|
if reader.len() < comp_len {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
reader.advance(comp_len);
|
||||||
|
|
||||||
let ciphers_len = u16::from_be_bytes([bytes[offset], bytes[offset + 1]]) as usize;
|
// 5. Extensions (опционально в TLS, но обычно есть)
|
||||||
offset += 2 + ciphers_len;
|
if reader.len() >= 2 {
|
||||||
if bytes.len() < offset + 1 {
|
let ext_len = u16::from_be_bytes([reader[0], reader[1]]) as usize;
|
||||||
|
reader.advance(2);
|
||||||
|
if reader.len() < ext_len {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let comp_len = bytes[offset] as usize;
|
|
||||||
offset += 1 + comp_len;
|
|
||||||
|
|
||||||
if bytes.len() >= offset + 2 {
|
|
||||||
let ext_len = u16::from_be_bytes([bytes[offset], bytes[offset + 1]]) as usize;
|
|
||||||
offset += 2 + ext_len;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bytes.len() >= offset
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse(bytes: &mut BytesMut) -> Result<Option<Self>, Self::Error> {
|
fn parse(bytes: &mut BytesMut) -> Result<Option<Self>, Self::Error> {
|
||||||
let mut offset = 34;
|
// Мы уже проверили всё в can_parse, поэтому здесь просто
|
||||||
if bytes.len() < offset + 1 {
|
// последовательно забираем данные через get_*
|
||||||
return Ok(None);
|
if !Self::can_parse(bytes) {
|
||||||
}
|
|
||||||
let session_id_len = bytes[offset] as usize;
|
|
||||||
offset += 1 + session_id_len;
|
|
||||||
|
|
||||||
if bytes.len() < offset + 2 {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
let ciphers_len = u16::from_be_bytes([bytes[offset], bytes[offset + 1]]) as usize;
|
|
||||||
offset += 2 + ciphers_len;
|
|
||||||
|
|
||||||
if bytes.len() < offset + 1 {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
let comp_len = bytes[offset] as usize;
|
|
||||||
offset += 1 + comp_len;
|
|
||||||
|
|
||||||
if bytes.len() >= offset + 2 {
|
|
||||||
let ext_len = u16::from_be_bytes([bytes[offset], bytes[offset + 1]]) as usize;
|
|
||||||
offset += 2 + ext_len;
|
|
||||||
}
|
|
||||||
|
|
||||||
if bytes.len() < offset {
|
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut msg = bytes.split_to(offset);
|
let version = ProtocolVersion::try_from(bytes.get_u16())
|
||||||
|
|
||||||
let version = ProtocolVersion::try_from(msg.get_u16())
|
|
||||||
.map_err(|e| TlsError::new(ErrorStage::Tls(e), ErrorAction::Drop, Bytes::new()))?;
|
.map_err(|e| TlsError::new(ErrorStage::Tls(e), ErrorAction::Drop, Bytes::new()))?;
|
||||||
|
|
||||||
let mut random = [0u8; 32];
|
let mut random = [0u8; 32];
|
||||||
msg.copy_to_slice(&mut random);
|
bytes.copy_to_slice(&mut random);
|
||||||
|
|
||||||
let sid_len = msg.get_u8() as usize;
|
let sid_len = bytes.get_u8() as usize;
|
||||||
let session_id = msg.split_to(sid_len).freeze();
|
let session_id = bytes.split_to(sid_len).freeze();
|
||||||
|
|
||||||
let c_len = msg.get_u16() as usize;
|
let c_len = bytes.get_u16() as usize;
|
||||||
let mut cipher_suites = Vec::with_capacity(c_len / 2);
|
let mut cipher_suites = Vec::with_capacity(c_len / 2);
|
||||||
let mut ciphers_data = msg.split_to(c_len);
|
let mut ciphers_data = bytes.split_to(c_len);
|
||||||
while ciphers_data.has_remaining() {
|
while ciphers_data.has_remaining() {
|
||||||
cipher_suites.push(ciphers_data.get_u16());
|
cipher_suites.push(ciphers_data.get_u16());
|
||||||
}
|
}
|
||||||
|
|
||||||
let cmp_len = msg.get_u8() as usize;
|
let cmp_len = bytes.get_u8() as usize;
|
||||||
msg.advance(cmp_len);
|
bytes.advance(cmp_len);
|
||||||
|
|
||||||
let extensions = if msg.remaining() >= 2 {
|
let extensions = if bytes.remaining() >= 2 {
|
||||||
let ext_len = msg.get_u16() as usize;
|
let ext_len = bytes.get_u16() as usize;
|
||||||
msg.split_to(ext_len).freeze()
|
bytes.split_to(ext_len).freeze()
|
||||||
} else {
|
} else {
|
||||||
Bytes::new()
|
Bytes::new()
|
||||||
};
|
};
|
||||||
@@ -207,7 +199,6 @@ impl Parser for ClientHello {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Parser for ServerHello {
|
impl Parser for ServerHello {
|
||||||
type Error = TlsError;
|
type Error = TlsError;
|
||||||
|
|
||||||
|
|||||||
+73
-17
@@ -1,29 +1,85 @@
|
|||||||
# Netrunner TLS Engine (tlseng)
|
# Netrunner TLS Engine (`tlseng`)
|
||||||
|
|
||||||
Модуль `tlseng` отвечает за генерацию и анализ TLS-хендшейков. Его основная задача — маскировка сетевой активности под легитимный трафик популярных браузеров (Chrome, Edge).
|
Модуль `tlseng` — это высокопроизводительное ядро для генерации, парсинга и манипуляции TLS-трафиком (TLS 1.2 / TLS 1.3). Основная задача движка — **обход систем глубокого анализа трафика (DPI)** и активных проб (Active Probing) путем идеальной мимикрии под легитимный трафик современных браузеров.
|
||||||
|
|
||||||
## Возможности
|
Движок не является полноценной реализацией криптографического стека TLS. Это "умный фасад", который формирует криптографически достоверные пакеты `ClientHello` и `ServerHello`, позволяя скрыто передавать авторизационные данные и устанавливать защищенные туннели, неотличимые от обычного HTTPS-трафика.
|
||||||
|
|
||||||
- **Fingerprint Mimicry:** Полная имитация параметров расширений, порядка их следования и наборов шифров.
|
## 🌟 Ключевые возможности
|
||||||
- **Dynamic Extension Builder:** Потокобезопасная сборка TLS-расширений (SNI, ALPN, KeyShare, PADDING и т.д.).
|
|
||||||
- **GREASE Support:** Автоматическая генерация случайных идентификаторов для обхода сигнатурных фильтров.
|
|
||||||
- **TlsRecord Encapsulation:** Упаковка данных в стандартные TLS-записи согласно спецификациям.
|
|
||||||
|
|
||||||
## Архитектура
|
- **Perfect Fingerprint Mimicry (JA3 / JA4):** Полная имитация параметров расширений, порядка их следования, поддерживаемых групп и наборов шифров. Движок генерирует отпечатки, на 100% совпадающие с реальными браузерами (Chrome, Edge, Firefox, Safari).
|
||||||
|
- **Stealth Authentication (Stateless):** Уникальный механизм скрытой авторизации. Токен доступа (Auth Tag) маскируется внутри поля `Session ID` (TLS 1.3 эхо), что позволяет серверу аутентифицировать клиента еще до начала обмена Application Data, не привлекая внимания DPI.
|
||||||
|
- **Advanced GREASE Support:** Автоматическая и контекстно-зависимая генерация случайных идентификаторов (GREASE) согласно RFC 8701 для обхода сигнатурных фильтров, специфичных для Chromium-браузеров.
|
||||||
|
- **Smart Padding:** Интеллектуальный расчет расширения `Padding` с учетом размера всех заголовков (overhead) для выравнивания пакета `ClientHello` до заданного размера (например, ровно 512 байт для Chrome), исключая эвристическое детектирование по длине пакета.
|
||||||
|
- **Zero-Copy Parsing:** Использование крейта `bytes` (`Bytes`, `BytesMut`) для парсинга и сериализации пакетов без лишних аллокаций в памяти.
|
||||||
|
|
||||||
- `profile/`: Описания браузерных конфигураций (JA3/JA4 совместимость).
|
## 🏗 Архитектура модулей
|
||||||
- `extension/`: Логика построения TLS-расширений (`ExtensionBuilder`).
|
|
||||||
- `handshake/`: Реализация `ClientHello` и `ServerHello`.
|
|
||||||
- `tls_record/`: Низкоуровневая упаковка данных в TLS-формат.
|
|
||||||
|
|
||||||
## Пример использования
|
Структура модуля разделена на логические компоненты для максимальной гибкости и расширяемости:
|
||||||
|
|
||||||
|
- **`profile/`**: База данных профилей браузеров и серверов (`BrowserProfile`, `ServerProfile`). Содержит константы для версий, шифров, наличия GREASE, ALPS и целевого размера паддинга.
|
||||||
|
- **`extension/`**: Мощный и потокобезопасный `ExtensionBuilder` для сборки TLS-расширений (SNI, ALPN, KeyShare, ALPS, Supported Versions и т.д.) в строгом соответствии с профилем.
|
||||||
|
- **`handshake/`**: Реализация структур `ClientHello` и `ServerHello`. Управляет генерацией рандома, встраиванием скрытых тегов и выбором шифров.
|
||||||
|
- **`tls_record/`**: Низкоуровневая инкапсуляция данных в `TlsRecord` с правильным указанием версий (маскировка Record Layer Version под `0x0301`).
|
||||||
|
- **`types/` и `consts/`**: Строго типизированные перечисления (Enums) и константы протокола TLS.
|
||||||
|
|
||||||
|
## 🛠 Глубокое погружение: Как это работает
|
||||||
|
|
||||||
|
### 1. Профилирование трафика
|
||||||
|
|
||||||
|
Движок не хардкодит расширения. Вместо этого `ExtensionBuilder` принимает `BrowserProfile`. Профиль указывает, нужны ли GREASE-значения, какой должен быть `ExtensionOrder` (порядок следования) и до какого размера "надувать" пакет паддингом. Это позволяет в одну строчку переключать маскировку с Chrome 131 на Firefox 130.
|
||||||
|
|
||||||
|
### 2. Скрытая авторизация (Session ID Auth)
|
||||||
|
|
||||||
|
В TLS 1.3 поле `Session ID` сохранено для обратной совместимости (Middlebox Compatibility Mode).
|
||||||
|
`tlseng` использует это 32-байтовое поле как контейнер:
|
||||||
|
|
||||||
|
1. **Клиент:** Заполняет первые 16 байт случайными данными, а в последние 16 байт записывает HMAC (Auth Tag), сгенерированный на основе локального времени и мастер-ключа.
|
||||||
|
2. **DPI:** Видит стандартный 32-байтовый массив, неотличимый от обычного рандома.
|
||||||
|
3. **Сервер:** Извлекает последние 16 байт из `ClientHello` и верифицирует их. Если проверка не пройдена, соединение разрывается (защита от активного сканирования). Если пройдена — сервер возвращает этот же `Session ID` эхом в `ServerHello`.
|
||||||
|
|
||||||
|
## 🚀 Примеры использования
|
||||||
|
|
||||||
|
### Генерация `ClientHello` (на стороне клиента)
|
||||||
|
|
||||||
|
Создание маскировочного пакета под Chrome 131 со встроенной авторизацией:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use crate::tlseng::profile::BrowserProfile;
|
use crate::tlseng::profile::BrowserProfile;
|
||||||
use crate::tlseng::extension::ExtensionBuilder;
|
use crate::tlseng::extension::ExtensionBuilder;
|
||||||
|
use crate::tlseng::handshake::ClientHello;
|
||||||
|
use crate::tlseng::tls_record::TlsRecord;
|
||||||
|
use bytes::Bytes;
|
||||||
|
|
||||||
// Создание хендшейка для профиля Chrome 131
|
pub fn make_client_hello(profile: &BrowserProfile, host: &str, keys: &SessionKeys) -> Bytes {
|
||||||
let mut builder = ExtensionBuilder::new();
|
let tls_random = keys.salt.get_local();
|
||||||
builder.apply_profile(&BrowserProfile::CHROME_131, "google.com", &public_key);
|
|
||||||
let extensions = builder.build();
|
// Расчет размера заголовков для точного Padding
|
||||||
|
let overhead = 5 + 4 + 2 + 32 + 1 + 32 + 2 + (profile.cipher_suites.len() * 2) + 2 + 2;
|
||||||
|
|
||||||
|
// Сборка расширений по профилю
|
||||||
|
let mut ext_builder = ExtensionBuilder::new();
|
||||||
|
ext_builder.apply_profile(profile, host, &keys.ecdh.public_key.to_bytes(), overhead);
|
||||||
|
let extensions_bytes = ext_builder.build();
|
||||||
|
|
||||||
|
// Генерация Session ID с Auth Tag
|
||||||
|
let mut session_id_bytes = [0u8; 32];
|
||||||
|
rand::fill(&mut session_id_bytes[..16]);
|
||||||
|
session_id_bytes[16..].copy_from_slice(&keys.generate_auth_tag());
|
||||||
|
|
||||||
|
let client_hello = ClientHello {
|
||||||
|
version: profile.versions.get_legacy(), // Всегда 0x0303 для TLS 1.3
|
||||||
|
random: tls_random,
|
||||||
|
session_id: Bytes::copy_from_slice(&session_id_bytes),
|
||||||
|
cipher_suites: profile.cipher_suites.to_vec(),
|
||||||
|
extensions: extensions_bytes,
|
||||||
|
};
|
||||||
|
|
||||||
|
let record = TlsRecord::new(
|
||||||
|
ContentType::Handshake,
|
||||||
|
profile.record_layer_version, // 0x0301 для маскировки
|
||||||
|
client_hello.serialize(),
|
||||||
|
);
|
||||||
|
|
||||||
|
record.serialize()
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -39,14 +39,6 @@ impl Extension {
|
|||||||
data,
|
data,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pack(etype: u16, data: &[u8]) -> Bytes {
|
|
||||||
let mut ext = BytesMut::with_capacity(4 + data.len());
|
|
||||||
ext.put_u16(etype);
|
|
||||||
ext.put_u16(data.len() as u16);
|
|
||||||
ext.put_slice(data);
|
|
||||||
ext.freeze()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ExtensionBuilder {
|
pub struct ExtensionBuilder {
|
||||||
@@ -61,8 +53,9 @@ impl ExtensionBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn add_extension(&mut self, etype: u16, data: &[u8]) {
|
fn add_extension(&mut self, etype: u16, data: &[u8]) {
|
||||||
let ext = Extension::pack(etype, data);
|
self.payload.put_u16(etype);
|
||||||
self.payload.put_slice(&ext);
|
self.payload.put_u16(data.len() as u16);
|
||||||
|
self.payload.put_slice(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn grease(&mut self) {
|
pub fn grease(&mut self) {
|
||||||
@@ -72,6 +65,18 @@ impl ExtensionBuilder {
|
|||||||
self.add_extension(etype, &[]);
|
self.add_extension(etype, &[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn grease_with_id(&mut self, etype: u16) {
|
||||||
|
self.add_extension(etype, &[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_generic_extension(&mut self, etype: u16, profile: &BrowserProfile) {
|
||||||
|
match etype {
|
||||||
|
_ => {
|
||||||
|
netrunner_logger::trace!(etype, "Applying generic or unknown extension");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn server_name(&mut self, host: &str) {
|
pub fn server_name(&mut self, host: &str) {
|
||||||
let host_bytes = host.as_bytes();
|
let host_bytes = host.as_bytes();
|
||||||
let host_len = host_bytes.len() as u16;
|
let host_len = host_bytes.len() as u16;
|
||||||
@@ -117,15 +122,21 @@ impl ExtensionBuilder {
|
|||||||
self.add_extension(TlsExtensions::SUPPORTED_VERSIONS, &data);
|
self.add_extension(TlsExtensions::SUPPORTED_VERSIONS, &data);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn key_share(&mut self, pub_key: &[u8]) {
|
pub fn key_share(&mut self, profile: &BrowserProfile, pub_key: &[u8]) {
|
||||||
let mut data = BytesMut::with_capacity(38);
|
let key_len = pub_key.len() as u16;
|
||||||
data.put_u16(34);
|
|
||||||
data.put_u16(TlsGroups::X25519);
|
|
||||||
data.put_u16(32);
|
|
||||||
data.put_slice(pub_key);
|
|
||||||
self.add_extension(TlsExtensions::KEY_SHARE, &data);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
let mut entry = BytesMut::with_capacity(key_len as usize + 4);
|
||||||
|
let group = profile.groups.0.first().cloned().unwrap_or(0x001d);
|
||||||
|
entry.put_u16(group);
|
||||||
|
entry.put_u16(key_len);
|
||||||
|
entry.put_slice(pub_key);
|
||||||
|
|
||||||
|
let mut list = BytesMut::with_capacity(entry.len() + 2);
|
||||||
|
list.put_u16(entry.len() as u16);
|
||||||
|
list.put_slice(&entry);
|
||||||
|
|
||||||
|
self.add_extension(TlsExtensions::KEY_SHARE, &list);
|
||||||
|
}
|
||||||
pub fn application_settings(&mut self, protocols: &[&str]) {
|
pub fn application_settings(&mut self, protocols: &[&str]) {
|
||||||
let mut data = BytesMut::new();
|
let mut data = BytesMut::new();
|
||||||
for proto in protocols {
|
for proto in protocols {
|
||||||
@@ -202,16 +213,23 @@ impl ExtensionBuilder {
|
|||||||
self.add_extension(TlsExtensions::RENEGOTIATION_INFO, &[0x00]);
|
self.add_extension(TlsExtensions::RENEGOTIATION_INFO, &[0x00]);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn padding(&mut self, target_size: usize) {
|
pub fn padding(&mut self, target_size: usize, overhead: usize) {
|
||||||
let current_size = self.payload.len();
|
let current_total_size = self.payload.len() + overhead;
|
||||||
if target_size > current_size + 4 {
|
|
||||||
let pad_len = target_size - current_size - 4;
|
if target_size > current_total_size + 4 {
|
||||||
|
let pad_len = target_size - current_total_size - 4;
|
||||||
let data = vec![0u8; pad_len];
|
let data = vec![0u8; pad_len];
|
||||||
self.add_extension(TlsExtensions::PADDING, &data);
|
self.add_extension(TlsExtensions::PADDING, &data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn apply_profile(&mut self, profile: &BrowserProfile, host: &str, pub_key: &[u8]) {
|
pub fn apply_profile(
|
||||||
|
&mut self,
|
||||||
|
profile: &BrowserProfile,
|
||||||
|
host: &str,
|
||||||
|
pub_key: &[u8],
|
||||||
|
overhead: usize,
|
||||||
|
) {
|
||||||
for &ext_id in &profile.extension_order {
|
for &ext_id in &profile.extension_order {
|
||||||
match ext_id {
|
match ext_id {
|
||||||
TlsExtensions::SNI => self.server_name(host),
|
TlsExtensions::SNI => self.server_name(host),
|
||||||
@@ -231,21 +249,28 @@ impl ExtensionBuilder {
|
|||||||
TlsExtensions::SESSION_TICKET => self.session_ticket(),
|
TlsExtensions::SESSION_TICKET => self.session_ticket(),
|
||||||
TlsExtensions::SUPPORTED_VERSIONS => self.supported_versions(profile.versions),
|
TlsExtensions::SUPPORTED_VERSIONS => self.supported_versions(profile.versions),
|
||||||
TlsExtensions::PSK_MODES => self.psk_key_exchange_modes(),
|
TlsExtensions::PSK_MODES => self.psk_key_exchange_modes(),
|
||||||
TlsExtensions::KEY_SHARE => self.key_share(pub_key),
|
TlsExtensions::KEY_SHARE => self.key_share(profile, pub_key),
|
||||||
TlsExtensions::ALPS => self.application_settings(&["h2"]),
|
TlsExtensions::ALPS => {
|
||||||
|
if !profile.alps_protocols.is_empty() {
|
||||||
|
self.application_settings(profile.alps_protocols);
|
||||||
|
}
|
||||||
|
}
|
||||||
TlsExtensions::STATUS_REQUEST => self.status_request(),
|
TlsExtensions::STATUS_REQUEST => self.status_request(),
|
||||||
TlsExtensions::EC_POINT_FORMATS => self.ec_point_formats(),
|
TlsExtensions::EC_POINT_FORMATS => self.ec_point_formats(),
|
||||||
TlsExtensions::RENEGOTIATION_INFO => self.renegotiation_info(),
|
TlsExtensions::RENEGOTIATION_INFO => self.renegotiation_info(),
|
||||||
TlsExtensions::PADDING => {
|
TlsExtensions::PADDING => {
|
||||||
if profile.is_chromium {
|
if profile.target_padding_len > 0 {
|
||||||
self.padding(512);
|
// Передаем накопленный оверхед всего пакета
|
||||||
} else {
|
self.padding(profile.target_padding_len as usize, overhead);
|
||||||
self.add_extension(TlsExtensions::PADDING, &[]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
id if (id & 0x0f0f) == 0x0a0a => self.grease(),
|
id if TlsExtensions::is_grease(id) => {
|
||||||
_ => {}
|
if profile.has_grease {
|
||||||
|
self.grease_with_id(id); // Используем конкретный ID из ExtensionOrder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => self.apply_generic_extension(ext_id, profile),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+111
-81
@@ -1,12 +1,14 @@
|
|||||||
|
use aead::{rand_core::RngCore, OsRng};
|
||||||
use bytes::{BufMut, Bytes, BytesMut};
|
use bytes::{BufMut, Bytes, BytesMut};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
crypto::session::SessionKeys,
|
||||||
tlseng::{
|
tlseng::{
|
||||||
consts::{HANDSHAKE_TYPE_CLIENT_HELLO, HANDSHAKE_TYPE_SERVER_HELLO},
|
consts::{HANDSHAKE_TYPE_CLIENT_HELLO, HANDSHAKE_TYPE_SERVER_HELLO},
|
||||||
extension::ExtensionBuilder,
|
extension::ExtensionBuilder,
|
||||||
profile::BrowserProfile,
|
profile::{BrowserProfile, ServerProfile},
|
||||||
tls_record::TlsRecord,
|
tls_record::TlsRecord,
|
||||||
types::{ContentType, HelloType, ProtocolVersion},
|
types::{ContentType, HelloType, ProtocolVersion, TlsVersions},
|
||||||
},
|
},
|
||||||
utils::u24::U24,
|
utils::u24::U24,
|
||||||
};
|
};
|
||||||
@@ -34,71 +36,75 @@ impl ClientHello {
|
|||||||
|
|
||||||
buf.put_u8(HANDSHAKE_TYPE_CLIENT_HELLO);
|
buf.put_u8(HANDSHAKE_TYPE_CLIENT_HELLO);
|
||||||
|
|
||||||
|
// Резервируем 3 байта под длину Handshake (U24)
|
||||||
let length_pos = buf.len();
|
let length_pos = buf.len();
|
||||||
buf.put_bytes(0, 3);
|
buf.put_bytes(0, 3);
|
||||||
|
|
||||||
buf.put_u16(0x0303);
|
buf.put_u16(0x0303); // Legacy Version
|
||||||
buf.put_slice(&self.random);
|
buf.put_slice(&self.random);
|
||||||
|
|
||||||
|
// Session ID
|
||||||
buf.put_u8(self.session_id.len() as u8);
|
buf.put_u8(self.session_id.len() as u8);
|
||||||
buf.put_slice(&self.session_id);
|
buf.put_slice(&self.session_id);
|
||||||
|
|
||||||
|
// Cipher Suites
|
||||||
buf.put_u16((self.cipher_suites.len() * 2) as u16);
|
buf.put_u16((self.cipher_suites.len() * 2) as u16);
|
||||||
for &suite in &self.cipher_suites {
|
for &suite in &self.cipher_suites {
|
||||||
buf.put_u16(suite);
|
buf.put_u16(suite);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compression Methods (всегда 0x01 0x00 для маскировки)
|
||||||
buf.put_u8(1);
|
buf.put_u8(1);
|
||||||
buf.put_u8(0x00);
|
buf.put_u8(0x00);
|
||||||
|
|
||||||
|
// Extensions
|
||||||
buf.put_u16(self.extensions.len() as u16);
|
buf.put_u16(self.extensions.len() as u16);
|
||||||
buf.put_slice(&self.extensions);
|
buf.put_slice(&self.extensions);
|
||||||
|
|
||||||
|
// ПРАВИЛЬНЫЙ РАСЧЕТ ДЛИНЫ:
|
||||||
|
// Длина сообщения Handshake не включает сам тип (1 байт) и поле длины (3 байта)
|
||||||
let total_len = (buf.len() - length_pos - 3) as u32;
|
let total_len = (buf.len() - length_pos - 3) as u32;
|
||||||
let len_bytes = total_len.to_be_bytes();
|
let len_bytes = total_len.to_be_bytes();
|
||||||
|
// Записываем 3 байта длины (Big Endian)
|
||||||
buf[length_pos..length_pos + 3].copy_from_slice(&len_bytes[1..4]);
|
buf[length_pos..length_pos + 3].copy_from_slice(&len_bytes[1..4]);
|
||||||
|
|
||||||
let ext_len = self.extensions.len();
|
|
||||||
let total_handshake_len = (buf.len() - length_pos - 3) as u32;
|
|
||||||
|
|
||||||
netrunner_logger::debug!(
|
|
||||||
handshake_type = "ClientHello",
|
|
||||||
body_len = total_handshake_len,
|
|
||||||
extensions_len = ext_len,
|
|
||||||
total_bytes = buf.len(),
|
|
||||||
"Serialized Handshake message"
|
|
||||||
);
|
|
||||||
buf.freeze()
|
buf.freeze()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn make_client_hello(
|
pub fn make_client_hello(profile: &BrowserProfile, host: &str, keys: &SessionKeys) -> Bytes {
|
||||||
profile: &BrowserProfile,
|
let tls_random = keys.salt.get_local();
|
||||||
host: &str,
|
let mut session_id_bytes = [0u8; 32];
|
||||||
public_key: &[u8; 32],
|
OsRng.fill_bytes(&mut session_id_bytes[..16]);
|
||||||
salt: [u8; 32],
|
session_id_bytes[16..].copy_from_slice(&keys.generate_auth_tag());
|
||||||
) -> Bytes {
|
|
||||||
let tls_random = salt;
|
let record_header = 5;
|
||||||
|
let handshake_header = 4;
|
||||||
|
let client_hello_fixed = 2 + 32 + 1 + 32 + 2 + (profile.cipher_suites.len() * 2) + 2 + 2;
|
||||||
|
|
||||||
|
let total_overhead = record_header + handshake_header + client_hello_fixed;
|
||||||
|
|
||||||
let mut ext_builder = ExtensionBuilder::new();
|
let mut ext_builder = ExtensionBuilder::new();
|
||||||
|
|
||||||
ext_builder.apply_profile(profile, host, public_key);
|
ext_builder.apply_profile(
|
||||||
let extensions_bytes = ext_builder.build();
|
profile,
|
||||||
|
host,
|
||||||
|
&keys.ecdh.public_key.to_bytes(),
|
||||||
|
total_overhead,
|
||||||
|
);
|
||||||
|
|
||||||
let mut session_id = BytesMut::with_capacity(32);
|
let extensions_bytes = ext_builder.build();
|
||||||
session_id.put_slice(&[0u8; 32]);
|
|
||||||
|
|
||||||
let client_hello = ClientHello {
|
let client_hello = ClientHello {
|
||||||
version: ProtocolVersion::Tls12,
|
version: ProtocolVersion::Tls12,
|
||||||
random: tls_random,
|
random: tls_random,
|
||||||
session_id: session_id.freeze(),
|
session_id: Bytes::copy_from_slice(&session_id_bytes),
|
||||||
cipher_suites: vec![0x1301, 0x1302, 0x1303],
|
cipher_suites: profile.cipher_suites.to_vec(),
|
||||||
extensions: extensions_bytes,
|
extensions: extensions_bytes,
|
||||||
};
|
};
|
||||||
|
|
||||||
let record = TlsRecord::new(
|
let record = TlsRecord::new(
|
||||||
ContentType::Handshake,
|
ContentType::Handshake,
|
||||||
ProtocolVersion::Tls10,
|
profile.record_layer_version,
|
||||||
client_hello.serialize(),
|
client_hello.serialize(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -114,93 +120,117 @@ pub struct ServerHello {
|
|||||||
pub extensions: BytesMut,
|
pub extensions: BytesMut,
|
||||||
}
|
}
|
||||||
impl ServerHello {
|
impl ServerHello {
|
||||||
pub fn from_client_hello(
|
|
||||||
client_hello: &ClientHello,
|
|
||||||
server_public_key: &[u8],
|
|
||||||
salt: [u8; 32],
|
|
||||||
) -> Self {
|
|
||||||
let server_random = salt;
|
|
||||||
|
|
||||||
let selected_suite = client_hello
|
|
||||||
.cipher_suites
|
|
||||||
.first()
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or(0x1301);
|
|
||||||
|
|
||||||
let mut extensions = BytesMut::new();
|
|
||||||
|
|
||||||
extensions.put_u16(0x002b);
|
|
||||||
extensions.put_u16(2);
|
|
||||||
extensions.put_u16(0x0304);
|
|
||||||
|
|
||||||
extensions.put_u16(0x0033);
|
|
||||||
extensions.put_u16(36);
|
|
||||||
extensions.put_u16(0x001d);
|
|
||||||
extensions.put_u16(32);
|
|
||||||
extensions.put_slice(server_public_key);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
version: ProtocolVersion::Tls12,
|
|
||||||
random: server_random,
|
|
||||||
session_id: client_hello.session_id.clone(),
|
|
||||||
cipher_suite: selected_suite,
|
|
||||||
extensions,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn make_server_hello(
|
pub fn make_server_hello(
|
||||||
client_hello: &ClientHello,
|
client_hello: &ClientHello,
|
||||||
server_public_key: &[u8],
|
server_public_key: &[u8],
|
||||||
salt: [u8; 32],
|
salt: [u8; 32],
|
||||||
|
profile: &ServerProfile,
|
||||||
) -> Bytes {
|
) -> Bytes {
|
||||||
let server_hello = Self::from_client_hello(client_hello, server_public_key, salt);
|
// Создаем инстанс ServerHello
|
||||||
let handshake_payload = server_hello.serialize();
|
let server_hello = Self::from_client_hello(client_hello, server_public_key, salt, profile);
|
||||||
|
|
||||||
|
// Оборачиваем в Record Layer
|
||||||
let record = TlsRecord::new(
|
let record = TlsRecord::new(
|
||||||
ContentType::Handshake,
|
ContentType::Handshake,
|
||||||
ProtocolVersion::Tls12,
|
profile.record_layer_version, // 0x0303 обычно
|
||||||
handshake_payload,
|
server_hello.serialize(), // Сериализация самого SH
|
||||||
);
|
);
|
||||||
|
|
||||||
record.serialize()
|
record.serialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn from_client_hello(
|
||||||
|
client_hello: &ClientHello,
|
||||||
|
server_public_key: &[u8],
|
||||||
|
salt: [u8; 32],
|
||||||
|
profile: &ServerProfile,
|
||||||
|
) -> Self {
|
||||||
|
// 1. Используем соль как рандом сервера
|
||||||
|
let server_random = salt;
|
||||||
|
|
||||||
|
// 2. Выбор Cipher Suite (твой код здесь хорош, оставляем)
|
||||||
|
let selected_suite = if profile.honor_cipher_order {
|
||||||
|
profile
|
||||||
|
.cipher_suites
|
||||||
|
.iter()
|
||||||
|
.find(|&&suite| client_hello.cipher_suites.contains(&suite))
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or(0x1301)
|
||||||
|
} else {
|
||||||
|
client_hello
|
||||||
|
.cipher_suites
|
||||||
|
.iter()
|
||||||
|
.find(|&&suite| profile.cipher_suites.contains(&suite))
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or(0x1301)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut extensions = BytesMut::new();
|
||||||
|
|
||||||
|
// 3. Работа с версией.
|
||||||
|
// Выбираем макс. версию из профиля (например, TLS 1.3)
|
||||||
|
let selected_version = profile.versions.max();
|
||||||
|
|
||||||
|
// Добавляем расширение Supported Versions (обязательно для TLS 1.3)
|
||||||
|
extensions.put_u16(0x002b);
|
||||||
|
extensions.put_u16(2);
|
||||||
|
extensions.put_u16(selected_version as u16);
|
||||||
|
|
||||||
|
// 4. Key Share (исправляем расчет длины, чтобы не было как в прошлый раз)
|
||||||
|
let key_len = server_public_key.len() as u16;
|
||||||
|
extensions.put_u16(0x0033);
|
||||||
|
extensions.put_u16(key_len + 4); // Группа(2) + Длина(2) + Ключ
|
||||||
|
extensions.put_u16(0x001d); // x25519
|
||||||
|
extensions.put_u16(key_len);
|
||||||
|
extensions.put_slice(server_public_key);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
// Это поле будет использоваться как legacy_version (0x0303)
|
||||||
|
version: ProtocolVersion::Tls12,
|
||||||
|
random: server_random, // ИСПОЛЬЗУЕМ переменную
|
||||||
|
session_id: client_hello.session_id.clone(),
|
||||||
|
cipher_suite: selected_suite,
|
||||||
|
extensions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn serialize(&self) -> Bytes {
|
pub fn serialize(&self) -> Bytes {
|
||||||
let mut buf = BytesMut::with_capacity(256 + self.extensions.len());
|
let mut buf = BytesMut::with_capacity(256 + self.extensions.len());
|
||||||
|
|
||||||
buf.put_u8(HANDSHAKE_TYPE_SERVER_HELLO);
|
buf.put_u8(HANDSHAKE_TYPE_SERVER_HELLO);
|
||||||
|
|
||||||
|
// Резервируем место под длину Handshake (3 байта)
|
||||||
let length_pos = buf.len();
|
let length_pos = buf.len();
|
||||||
buf.put_bytes(0, 3);
|
buf.put_slice(&[0, 0, 0]);
|
||||||
|
|
||||||
buf.put_u16(0x0303);
|
// Используем версию из структуры (Legacy Version)
|
||||||
|
buf.put_u16(self.version as u16);
|
||||||
|
|
||||||
|
// Используем рандом из структуры
|
||||||
buf.put_slice(&self.random);
|
buf.put_slice(&self.random);
|
||||||
|
|
||||||
|
// Session ID (Echo)
|
||||||
buf.put_u8(self.session_id.len() as u8);
|
buf.put_u8(self.session_id.len() as u8);
|
||||||
buf.put_slice(&self.session_id);
|
buf.put_slice(&self.session_id);
|
||||||
|
|
||||||
buf.put_u16(self.cipher_suite);
|
buf.put_u16(self.cipher_suite);
|
||||||
|
|
||||||
|
// Compression (0x00)
|
||||||
buf.put_u8(0x00);
|
buf.put_u8(0x00);
|
||||||
|
|
||||||
|
// Extensions
|
||||||
|
if !self.extensions.is_empty() {
|
||||||
buf.put_u16(self.extensions.len() as u16);
|
buf.put_u16(self.extensions.len() as u16);
|
||||||
buf.put_slice(&self.extensions);
|
buf.put_slice(&self.extensions);
|
||||||
|
} else {
|
||||||
|
buf.put_u16(0);
|
||||||
|
}
|
||||||
|
|
||||||
let total_len = (buf.len() - length_pos - 3) as u32;
|
// Заполняем длину handshake-сообщения
|
||||||
let len_bytes = total_len.to_be_bytes();
|
let total_handshake_body_len = (buf.len() - length_pos - 3) as u32;
|
||||||
|
let len_bytes = total_handshake_body_len.to_be_bytes();
|
||||||
buf[length_pos..length_pos + 3].copy_from_slice(&len_bytes[1..4]);
|
buf[length_pos..length_pos + 3].copy_from_slice(&len_bytes[1..4]);
|
||||||
|
|
||||||
let total_handshake_len = (buf.len() - length_pos - 3) as u32;
|
|
||||||
let ext_len = self.extensions.len();
|
|
||||||
|
|
||||||
netrunner_logger::debug!(
|
|
||||||
handshake_type = "ServerHello",
|
|
||||||
body_len = total_handshake_len,
|
|
||||||
extensions_len = ext_len,
|
|
||||||
total_bytes = buf.len(),
|
|
||||||
"Serialized Handshake message"
|
|
||||||
);
|
|
||||||
|
|
||||||
buf.freeze()
|
buf.freeze()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+52
-24
@@ -1,21 +1,20 @@
|
|||||||
use crate::tlseng::types::{ExtensionOrder, TlsGroups, TlsSignatures, TlsVersions};
|
use crate::tlseng::types::{
|
||||||
|
ExtensionOrder, ProtocolVersion, TlsGroups, TlsSignatures, TlsVersions,
|
||||||
|
};
|
||||||
pub struct BrowserProfile {
|
pub struct BrowserProfile {
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
|
|
||||||
pub groups: TlsGroups,
|
pub groups: TlsGroups,
|
||||||
|
|
||||||
pub signatures: TlsSignatures,
|
pub signatures: TlsSignatures,
|
||||||
|
|
||||||
pub delegated_signatures: TlsSignatures,
|
pub delegated_signatures: TlsSignatures,
|
||||||
|
|
||||||
pub versions: TlsVersions,
|
pub versions: TlsVersions,
|
||||||
|
|
||||||
pub alpn: &'static [&'static str],
|
pub alpn: &'static [&'static str],
|
||||||
|
|
||||||
pub extension_order: ExtensionOrder,
|
pub extension_order: ExtensionOrder,
|
||||||
|
|
||||||
pub is_chromium: bool,
|
pub is_chromium: bool,
|
||||||
|
pub cipher_suites: &'static [u16],
|
||||||
|
pub record_layer_version: ProtocolVersion,
|
||||||
|
pub target_padding_len: u16,
|
||||||
|
pub alps_protocols: &'static [&'static str],
|
||||||
|
pub has_grease: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BrowserProfile {
|
impl BrowserProfile {
|
||||||
@@ -25,40 +24,65 @@ impl BrowserProfile {
|
|||||||
signatures: TlsSignatures::BROWSER_STANDARD,
|
signatures: TlsSignatures::BROWSER_STANDARD,
|
||||||
delegated_signatures: TlsSignatures::BROWSER_STANDARD,
|
delegated_signatures: TlsSignatures::BROWSER_STANDARD,
|
||||||
versions: TlsVersions::TLS_13_ONLY,
|
versions: TlsVersions::TLS_13_ONLY,
|
||||||
|
|
||||||
|
// 0x0301 (TLS 1.0) — классика для ClientHello в исполнении Chrome
|
||||||
|
record_layer_version: ProtocolVersion::Tls10,
|
||||||
|
|
||||||
|
cipher_suites: &[
|
||||||
|
0x1301, 0x1302, 0x1303, // TLS 1.3 Suites
|
||||||
|
0xc02b, 0xc02f, 0xc02c, 0xc030, // ECDHE-RSA/ECDSA
|
||||||
|
0xcca9, 0xcca8, // ChaCha20
|
||||||
|
],
|
||||||
|
|
||||||
alpn: &["h2", "http/1.1"],
|
alpn: &["h2", "http/1.1"],
|
||||||
extension_order: ExtensionOrder::CHROMIUM_131,
|
extension_order: ExtensionOrder::CHROMIUM_131,
|
||||||
|
|
||||||
|
// --- НОВЫЕ ПОЛЯ ДЛЯ ГИБКОСТИ ---
|
||||||
is_chromium: true,
|
is_chromium: true,
|
||||||
|
has_grease: true,
|
||||||
|
|
||||||
|
// Chrome активно использует ALPS для ускорения HTTP/2 настроек
|
||||||
|
alps_protocols: &["h2"],
|
||||||
|
|
||||||
|
// Chrome старается сделать ClientHello "пухлым",
|
||||||
|
// чтобы избежать проблем с некоторыми старыми Middleboxes
|
||||||
|
target_padding_len: 512,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const EDGE: Self = Self {
|
pub const FIREFOX_130: Self = Self {
|
||||||
name: "Edge",
|
name: "Firefox 130 (Windows)",
|
||||||
groups: TlsGroups::CHROMIUM,
|
groups: TlsGroups::MODERN, // У FF свой набор предпочтений
|
||||||
signatures: TlsSignatures::BROWSER_STANDARD,
|
signatures: TlsSignatures::BROWSER_STANDARD,
|
||||||
delegated_signatures: TlsSignatures::BROWSER_STANDARD,
|
delegated_signatures: TlsSignatures::BROWSER_STANDARD,
|
||||||
versions: TlsVersions::MODERN,
|
versions: TlsVersions::MODERN, // FF часто шлет 1.3 + 1.2
|
||||||
alpn: &["h2", "http/1.1"],
|
|
||||||
extension_order: ExtensionOrder::EDGE_130,
|
|
||||||
is_chromium: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const DEFAULT: Self = Self::CHROME_131;
|
// Firefox обычно пишет 0x0303 (TLS 1.2) в Record Layer
|
||||||
|
record_layer_version: ProtocolVersion::Tls12,
|
||||||
|
|
||||||
|
cipher_suites: &[0x1301, 0x1302, 0x1303, 0xc02b, 0xc02f, 0xc02c, 0xc030],
|
||||||
|
|
||||||
|
alpn: &["h2", "http/1.1"],
|
||||||
|
extension_order: ExtensionOrder::EDGE_130, // Тут стоит подставить личный порядок FF
|
||||||
|
|
||||||
|
is_chromium: false,
|
||||||
|
has_grease: false, // Firefox НЕ использует GREASE
|
||||||
|
alps_protocols: &[], // Firefox пока не так активно внедряет ALPS
|
||||||
|
target_padding_len: 0, // Firefox обычно не делает принудительный Padding до 512
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ServerProfile {
|
pub struct ServerProfile {
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
|
|
||||||
pub versions: TlsVersions,
|
pub versions: TlsVersions,
|
||||||
|
|
||||||
|
// ДОБАВЛЯЕМ:
|
||||||
|
pub record_layer_version: ProtocolVersion, // Что сервер пишет в ответном Record
|
||||||
|
|
||||||
pub cipher_suites: &'static [u16],
|
pub cipher_suites: &'static [u16],
|
||||||
|
|
||||||
pub groups: TlsGroups,
|
pub groups: TlsGroups,
|
||||||
|
|
||||||
pub signatures: TlsSignatures,
|
pub signatures: TlsSignatures,
|
||||||
|
|
||||||
pub alpn: &'static [&'static str],
|
pub alpn: &'static [&'static str],
|
||||||
|
|
||||||
pub session_tickets: bool,
|
pub session_tickets: bool,
|
||||||
|
|
||||||
pub honor_cipher_order: bool,
|
pub honor_cipher_order: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +90,10 @@ impl ServerProfile {
|
|||||||
pub const MODERN: Self = Self {
|
pub const MODERN: Self = Self {
|
||||||
name: "Modern-Secure",
|
name: "Modern-Secure",
|
||||||
versions: TlsVersions::MODERN,
|
versions: TlsVersions::MODERN,
|
||||||
|
|
||||||
|
// Сервера обычно отвечают 0x0303 (TLS 1.2) в Record Layer
|
||||||
|
record_layer_version: ProtocolVersion::Tls12,
|
||||||
|
|
||||||
cipher_suites: &[0x1301, 0x1302, 0x1303],
|
cipher_suites: &[0x1301, 0x1302, 0x1303],
|
||||||
groups: TlsGroups::MODERN,
|
groups: TlsGroups::MODERN,
|
||||||
signatures: TlsSignatures::BROWSER_STANDARD,
|
signatures: TlsSignatures::BROWSER_STANDARD,
|
||||||
|
|||||||
@@ -109,6 +109,16 @@ impl TlsVersions {
|
|||||||
|
|
||||||
pub const TLS_13_ONLY: Self = Self(&[Self::TLS_1_3]);
|
pub const TLS_13_ONLY: Self = Self(&[Self::TLS_1_3]);
|
||||||
pub const MODERN: Self = Self(&[Self::TLS_1_3, Self::TLS_1_2]);
|
pub const MODERN: Self = Self(&[Self::TLS_1_3, Self::TLS_1_2]);
|
||||||
|
|
||||||
|
pub fn max(&self) -> ProtocolVersion {
|
||||||
|
if self.0.contains(&Self::TLS_1_3) {
|
||||||
|
ProtocolVersion::Tls13
|
||||||
|
} else if self.0.contains(&Self::TLS_1_2) {
|
||||||
|
ProtocolVersion::Tls12
|
||||||
|
} else {
|
||||||
|
ProtocolVersion::Tls10
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TlsExtensions;
|
pub struct TlsExtensions;
|
||||||
@@ -131,6 +141,14 @@ impl TlsExtensions {
|
|||||||
pub const KEY_SHARE: u16 = 0x0033;
|
pub const KEY_SHARE: u16 = 0x0033;
|
||||||
pub const ALPS: u16 = 0x44cd;
|
pub const ALPS: u16 = 0x44cd;
|
||||||
pub const RENEGOTIATION_INFO: u16 = 0xff01;
|
pub const RENEGOTIATION_INFO: u16 = 0xff01;
|
||||||
|
|
||||||
|
pub fn is_grease(id: u16) -> bool {
|
||||||
|
if (id & 0x0f0f) != 0x0a0a {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Убеждаемся, что оба байта идентичны (0x1A1A, а не 0x1A2A)
|
||||||
|
(id & 0xff) == (id >> 8)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
|
|||||||
Reference in New Issue
Block a user