From 80b9242000b8d86cfa13bbf1a2c5bcb9dccbdcfa Mon Sep 17 00:00:00 2001 From: maddeleine <59030281+maddeleine@users.noreply.github.com> Date: Fri, 29 Mar 2024 13:43:30 -0700 Subject: [PATCH] fix: refactor rust bindings fingerprint methods (#4474) * Made Client Hello methods public --- bindings/rust/s2n-tls/src/client_hello.rs | 479 ++++++++++++---------- bindings/rust/s2n-tls/src/connection.rs | 1 - bindings/rust/s2n-tls/src/lib.rs | 1 - 3 files changed, 259 insertions(+), 222 deletions(-) diff --git a/bindings/rust/s2n-tls/src/client_hello.rs b/bindings/rust/s2n-tls/src/client_hello.rs index dab6e2444db..0414c15c73e 100644 --- a/bindings/rust/s2n-tls/src/client_hello.rs +++ b/bindings/rust/s2n-tls/src/client_hello.rs @@ -5,22 +5,6 @@ use crate::error::{Error, Fallible}; use s2n_tls_sys::*; use std::fmt; -#[derive(Copy, Clone)] -pub enum FingerprintType { - JA3, -} - -// this is the size of the MD5 hash digest that is used for the JA3 fingerprint -const MD5_HASH_SIZE: u32 = 16; - -impl From for s2n_tls_sys::s2n_fingerprint_type::Type { - fn from(value: FingerprintType) -> Self { - match value { - FingerprintType::JA3 => s2n_tls_sys::s2n_fingerprint_type::FINGERPRINT_JA3, - } - } -} - // ClientHello is an opaque wrapper struct around `s2n_client_hello`. Note that // the size of this type is not known, and as such it can only be used through // references and pointers. @@ -89,65 +73,7 @@ impl ClientHello { &self.0 as *const s2n_client_hello as *mut s2n_client_hello } - /// `fingerprint_hash` calculates the hash, and also returns the size - /// required for the full fingerprint string. The return value can be used - /// to construct a string of appropriate capacity to call - /// `fingerprint_string`. `output` will be extended if necessary to store - /// the full hash. - pub fn fingerprint_hash( - &self, - hash: FingerprintType, - output: &mut Vec, - ) -> Result { - let mut hash_size: u32 = 0; - let mut str_size: u32 = 0; - // make sure the vec has sufficient space for the hash - if output.capacity() < MD5_HASH_SIZE as usize { - output.reserve_exact(MD5_HASH_SIZE as usize - output.len()); - } - unsafe { - s2n_client_hello_get_fingerprint_hash( - self.deref_mut_ptr(), - hash.into(), - MD5_HASH_SIZE, - output.as_mut_ptr(), - &mut hash_size, - &mut str_size, - ) - .into_result()?; - // SAFETY: we wrote to the raw vec (using the mut pointer), and need - // to update the state of the vec to reflect the changes we made. - output.set_len(hash_size as usize); - }; - Ok(str_size) - } - - /// `fingerprint_string` will try to calculate the fingerprint and store the - /// resulting string in `output`. If `output` does not have sufficient - /// capacity an Error of `ErrorType::UsageError` will be returned. - pub fn fingerprint_string( - &self, - hash: FingerprintType, - output: &mut String, - ) -> Result<(), Error> { - let mut output_size = 0; - unsafe { - s2n_tls_sys::s2n_client_hello_get_fingerprint_string( - self.deref_mut_ptr(), - hash.into(), - output.capacity() as u32, - output.as_mut_ptr(), - &mut output_size, - ) - .into_result()?; - // SAFETY: update internal state of string to match the data written - // into it. - output.as_mut_vec().set_len(output_size as usize); - }; - Ok(()) - } - - fn session_id(&self) -> Result, Error> { + pub fn session_id(&self) -> Result, Error> { let mut session_id_length = 0; unsafe { s2n_client_hello_get_session_id_length(self.deref_mut_ptr(), &mut session_id_length) @@ -168,7 +94,7 @@ impl ClientHello { Ok(session_id) } - fn server_name(&self) -> Result, Error> { + pub fn server_name(&self) -> Result, Error> { let mut server_name_length = 0; unsafe { s2n_client_hello_get_server_name_length(self.deref_mut_ptr(), &mut server_name_length) @@ -189,7 +115,7 @@ impl ClientHello { Ok(server_name) } - fn raw_message(&self) -> Result, Error> { + pub fn raw_message(&self) -> Result, Error> { let message_length = unsafe { s2n_client_hello_get_raw_message_length(self.deref_mut_ptr()).into_result()? }; @@ -206,6 +132,256 @@ impl ClientHello { } } +#[cfg(feature = "unstable-fingerprint")] +pub use self::fingerprint::*; + +// Fingerprinting is an unstable feature. This module can be removed and added +// to the client_hello module once we have settled on an implementation. +#[cfg(feature = "unstable-fingerprint")] +pub mod fingerprint { + use crate::error::{Error, Fallible}; + use s2n_tls_sys::*; + + use super::ClientHello; + + #[derive(Copy, Clone)] + pub enum FingerprintType { + JA3, + } + + // this is the size of the MD5 hash digest that is used for the JA3 fingerprint + const MD5_HASH_SIZE: u32 = 16; + + impl From for s2n_tls_sys::s2n_fingerprint_type::Type { + fn from(value: FingerprintType) -> Self { + match value { + FingerprintType::JA3 => s2n_tls_sys::s2n_fingerprint_type::FINGERPRINT_JA3, + } + } + } + + impl ClientHello { + /// `fingerprint_hash` calculates the hash, and also returns the size + /// required for the full fingerprint string. The return value can be used + /// to construct a string of appropriate capacity to call + /// `fingerprint_string`. `output` will be extended if necessary to store + /// the full hash. + pub fn fingerprint_hash( + &self, + hash: FingerprintType, + output: &mut Vec, + ) -> Result { + let mut hash_size: u32 = 0; + let mut str_size: u32 = 0; + // make sure the vec has sufficient space for the hash + if output.capacity() < MD5_HASH_SIZE as usize { + output.reserve_exact(MD5_HASH_SIZE as usize - output.len()); + } + unsafe { + s2n_client_hello_get_fingerprint_hash( + self.deref_mut_ptr(), + hash.into(), + MD5_HASH_SIZE, + output.as_mut_ptr(), + &mut hash_size, + &mut str_size, + ) + .into_result()?; + // SAFETY: we wrote to the raw vec (using the mut pointer), and need + // to update the state of the vec to reflect the changes we made. + output.set_len(hash_size as usize); + }; + Ok(str_size) + } + + /// `fingerprint_string` will try to calculate the fingerprint and store the + /// resulting string in `output`. If `output` does not have sufficient + /// capacity an Error of `ErrorType::UsageError` will be returned. + pub fn fingerprint_string( + &self, + hash: FingerprintType, + output: &mut String, + ) -> Result<(), Error> { + let mut output_size = 0; + unsafe { + s2n_tls_sys::s2n_client_hello_get_fingerprint_string( + self.deref_mut_ptr(), + hash.into(), + output.capacity() as u32, + output.as_mut_ptr(), + &mut output_size, + ) + .into_result()?; + // SAFETY: update internal state of string to match the data written + // into it. + output.as_mut_vec().set_len(output_size as usize); + }; + Ok(()) + } + } + + #[cfg(test)] + pub mod fingerprint_tests { + use crate::{ + client_hello::{ + fingerprint::{FingerprintType, MD5_HASH_SIZE}, + ClientHello, + }, + connection::Connection, + error::{Error, ErrorType}, + security, + testing::{poll_tls_pair, tls_pair}, + }; + + /// This function is a test fixture used a generate a valid ClientHello so + /// that we don't have to copy and paste the raw bytes for test fixtures + fn get_client_hello_bytes() -> Vec { + let config = crate::testing::config_builder(&security::DEFAULT_TLS13) + .unwrap() + .build() + .unwrap(); + let pair = tls_pair(config); + let pair = poll_tls_pair(pair); + // this doesn't have the handshake header + let client_hello_message = pair + .server + .0 + .connection() + .client_hello() + .unwrap() + .raw_message() + .unwrap(); + // handshake header is {tag: u8, client_hello_length: u24} + let mut client_hello = vec![0; 4]; + // As long as the client hello is small, no bit fiddling is required + assert!(client_hello_message.len() < u8::MAX as usize); + // tag for handshake header + client_hello[0] = 1; + client_hello[3] = client_hello_message.len() as u8; + client_hello.extend(client_hello_message.iter()); + client_hello + } + + fn known_test_case( + raw_client_hello: Vec, + expected_string: &str, + expected_hash_hex: &str, + ) -> Result<(), Error> { + let expected_hash: Vec = hex::decode(expected_hash_hex).unwrap(); + let client_hello = + ClientHello::parse_client_hello(raw_client_hello.as_slice()).unwrap(); + + let mut hash = Vec::new(); + let string_size = client_hello + .fingerprint_hash(FingerprintType::JA3, &mut hash) + .unwrap(); + assert_eq!(hash, expected_hash); + + let mut string = String::with_capacity(string_size as usize); + client_hello + .fingerprint_string(FingerprintType::JA3, &mut string) + .unwrap(); + assert_eq!(string, expected_string); + Ok(()) + } + + pub fn get_client_hello() -> Box { + // sets up connection and handshakes + let raw_client_hello = get_client_hello_bytes(); + ClientHello::parse_client_hello(raw_client_hello.as_slice()).unwrap() + } + + pub fn client_hello_bytes() -> Vec { + vec![ + 0x01, 0x00, 0x00, 0xEC, 0x03, 0x03, 0x90, 0xe8, 0xcc, 0xee, 0xe5, 0x70, 0xa2, 0xa1, + 0x2f, 0x6b, 0x69, 0xd2, 0x66, 0x96, 0x0f, 0xcf, 0x20, 0xd5, 0x32, 0x6e, 0xc4, 0xb2, + 0x8c, 0xc7, 0xbd, 0x0a, 0x06, 0xc2, 0xa5, 0x14, 0xfc, 0x34, 0x20, 0xaf, 0x72, 0xbf, + 0x39, 0x99, 0xfb, 0x20, 0x70, 0xc3, 0x10, 0x83, 0x0c, 0xee, 0xfb, 0xfa, 0x72, 0xcc, + 0x5d, 0xa8, 0x99, 0xb4, 0xc5, 0x53, 0xd6, 0x3d, 0xa0, 0x53, 0x7a, 0x5c, 0xbc, 0xf5, + 0x0b, 0x00, 0x1e, 0xc0, 0x2b, 0xc0, 0x2f, 0xcc, 0xa9, 0xcc, 0xa8, 0xc0, 0x2c, 0xc0, + 0x30, 0xc0, 0x0a, 0xc0, 0x09, 0xc0, 0x13, 0xc0, 0x14, 0x00, 0x33, 0x00, 0x39, 0x00, + 0x2f, 0x00, 0x35, 0x00, 0x0a, 0x01, 0x00, 0x00, 0x85, 0x00, 0x00, 0x00, 0x23, 0x00, + 0x21, 0x00, 0x00, 0x1e, 0x69, 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x2e, 0x74, + 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x2e, 0x6d, 0x6f, 0x7a, 0x69, 0x6c, + 0x6c, 0x61, 0x2e, 0x6f, 0x72, 0x67, 0x00, 0x17, 0x00, 0x00, 0xff, 0x01, 0x00, 0x01, + 0x00, 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x00, + 0x19, 0x00, 0x0b, 0x00, 0x02, 0x01, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x10, 0x00, + 0x0e, 0x00, 0x0c, 0x02, 0x68, 0x32, 0x08, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x31, 0x2e, + 0x31, 0x00, 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x18, + 0x00, 0x16, 0x04, 0x03, 0x05, 0x03, 0x06, 0x03, 0x08, 0x04, 0x08, 0x05, 0x08, 0x06, + 0x04, 0x01, 0x05, 0x01, 0x06, 0x01, 0x02, 0x03, 0x02, 0x01, 0x00, 0x1c, 0x00, 0x02, + 0x40, 0x00, + ] + } + + // test that a fingerprint can successfully be calculated from ClientHellos + // returned from a connection + #[checkers::test] + fn io_fingerprint_test() { + let config = crate::testing::config_builder(&security::DEFAULT_TLS13) + .unwrap() + .build() + .unwrap(); + let pair = crate::testing::tls_pair(config); + + // client_hellos can not be accessed before the handshake + assert!(pair.client.0.connection().client_hello().is_err()); + assert!(pair.server.0.connection().client_hello().is_err()); + + let pair = poll_tls_pair(pair); + let server_conn = pair.server.0.connection(); + let client_conn = pair.server.0.connection(); + + let check_client_hello = |conn: &Connection| -> Result<(), Error> { + let client_hello = conn.client_hello().unwrap(); + let mut hash = Vec::new(); + let fingerprint_size = + client_hello.fingerprint_hash(FingerprintType::JA3, &mut hash)?; + let mut string = String::with_capacity(fingerprint_size as usize); + client_hello.fingerprint_string(FingerprintType::JA3, &mut string)?; + Ok(()) + }; + + assert!(check_client_hello(server_conn).is_ok()); + assert!(check_client_hello(client_conn).is_ok()); + } + + // known value test case copied from s2n_fingerprint_ja3_test.c + #[checkers::test] + fn valid_client_bytes() { + let raw_client_hello = client_hello_bytes(); + let expected_fingerprint = "771,49195-49199-52393-52392-49196-49200-\ + 49162-49161-49171-49172-51-57-47-53-10,0-\ + 23-65281-10-11-35-16-5-13-28,29-23-24-25,0"; + let expected_hash_hex = "839bbe3ed07fed922ded5aaf714d6842"; + known_test_case(raw_client_hello, expected_fingerprint, expected_hash_hex).unwrap(); + } + + #[test] + fn hash_output_resizing() { + let client_hello = get_client_hello(); + let hash_capacities = vec![0, MD5_HASH_SIZE, 1_000]; + for initial_size in hash_capacities { + let mut hash = Vec::with_capacity(initial_size as usize); + client_hello + .fingerprint_hash(FingerprintType::JA3, &mut hash) + .unwrap(); + assert_eq!(hash.len(), MD5_HASH_SIZE as usize); + } + } + + #[test] + fn string_output_too_small() { + let client_hello = get_client_hello(); + let mut fingerprint_string = String::with_capacity(0); + let fingerprint_err = client_hello + .fingerprint_string(FingerprintType::JA3, &mut fingerprint_string) + .unwrap_err(); + assert_eq!(fingerprint_err.kind(), ErrorType::UsageError); + } + } +} + impl Drop for ClientHello { fn drop(&mut self) { let mut client_hello: *mut s2n_client_hello = &mut self.0; @@ -222,115 +398,16 @@ impl fmt::Debug for ClientHello { let session_id = self.session_id().map_err(|_| fmt::Error)?; let session_id = hex::encode(session_id); let message_head = self.raw_message().map_err(|_| fmt::Error)?; - let mut hash = Vec::new(); - self.fingerprint_hash(FingerprintType::JA3, &mut hash) - .map_err(|_| fmt::Error)?; f.debug_struct("ClientHello") .field("session_id", &session_id) .field("message_len", &(message_head.len())) - .field("ja3_fingerprint", &hex::encode(hash)) .finish_non_exhaustive() } } #[cfg(test)] mod tests { - use super::*; - use crate::{ - connection::Connection, - error::{Error, ErrorType}, - security, - testing::{poll_tls_pair, tls_pair}, - }; - - /// This function is a test fixture used a generate a valid ClientHello so - /// that we don't have to copy and paste the raw bytes for test fixtures - fn get_client_hello_bytes() -> Vec { - let config = crate::testing::config_builder(&security::DEFAULT_TLS13) - .unwrap() - .build() - .unwrap(); - let pair = tls_pair(config); - let pair = poll_tls_pair(pair); - // this doesn't have the handshake header - let client_hello_message = pair - .server - .0 - .connection() - .client_hello() - .unwrap() - .raw_message() - .unwrap(); - // handshake header is {tag: u8, client_hello_length: u24} - let mut client_hello = vec![0; 4]; - // As long as the client hello is small, no bit fiddling is required - assert!(client_hello_message.len() < u8::MAX as usize); - // tag for handshake header - client_hello[0] = 1; - client_hello[3] = client_hello_message.len() as u8; - client_hello.extend(client_hello_message.iter()); - client_hello - } - - fn get_client_hello() -> Box { - // sets up connection and handshakes - let raw_client_hello = get_client_hello_bytes(); - ClientHello::parse_client_hello(raw_client_hello.as_slice()).unwrap() - } - - // test that a fingerprint can successfully be calculated from ClientHellos - // returned from a connection - #[checkers::test] - fn io_fingerprint_test() { - let config = crate::testing::config_builder(&security::DEFAULT_TLS13) - .unwrap() - .build() - .unwrap(); - let pair = crate::testing::tls_pair(config); - - // client_hellos can not be accessed before the handshake - assert!(pair.client.0.connection().client_hello().is_err()); - assert!(pair.server.0.connection().client_hello().is_err()); - - let pair = poll_tls_pair(pair); - let server_conn = pair.server.0.connection(); - let client_conn = pair.server.0.connection(); - - let check_client_hello = |conn: &Connection| -> Result<(), Error> { - let client_hello = conn.client_hello().unwrap(); - let mut hash = Vec::new(); - let fingerprint_size = - client_hello.fingerprint_hash(FingerprintType::JA3, &mut hash)?; - let mut string = String::with_capacity(fingerprint_size as usize); - client_hello.fingerprint_string(FingerprintType::JA3, &mut string)?; - Ok(()) - }; - - assert!(check_client_hello(server_conn).is_ok()); - assert!(check_client_hello(client_conn).is_ok()); - } - - fn known_test_case( - raw_client_hello: Vec, - expected_string: &str, - expected_hash_hex: &str, - ) -> Result<(), Error> { - let expected_hash: Vec = hex::decode(expected_hash_hex).unwrap(); - let client_hello = ClientHello::parse_client_hello(raw_client_hello.as_slice()).unwrap(); - - let mut hash = Vec::new(); - let string_size = client_hello - .fingerprint_hash(FingerprintType::JA3, &mut hash) - .unwrap(); - assert_eq!(hash, expected_hash); - - let mut string = String::with_capacity(string_size as usize); - client_hello - .fingerprint_string(FingerprintType::JA3, &mut string) - .unwrap(); - assert_eq!(string, expected_string); - Ok(()) - } + use crate::client_hello::ClientHello; #[test] fn invalid_client_bytes() { @@ -340,9 +417,8 @@ mod tests { assert!(result.is_err()); } - // known value test case copied from s2n_fingerprint_ja3_test.c - #[checkers::test] - fn valid_client_bytes() { + #[test] + fn server_name() { let raw_client_hello = vec![ 0x01, 0x00, 0x00, 0xEC, 0x03, 0x03, 0x90, 0xe8, 0xcc, 0xee, 0xe5, 0x70, 0xa2, 0xa1, 0x2f, 0x6b, 0x69, 0xd2, 0x66, 0x96, 0x0f, 0xcf, 0x20, 0xd5, 0x32, 0x6e, 0xc4, 0xb2, @@ -363,45 +439,8 @@ mod tests { 0x04, 0x01, 0x05, 0x01, 0x06, 0x01, 0x02, 0x03, 0x02, 0x01, 0x00, 0x1c, 0x00, 0x02, 0x40, 0x00, ]; - let expected_fingerprint = "771,49195-49199-52393-52392-49196-49200-\ - 49162-49161-49171-49172-51-57-47-53-10,0-\ - 23-65281-10-11-35-16-5-13-28,29-23-24-25,0"; - let expected_hash_hex = "839bbe3ed07fed922ded5aaf714d6842"; - known_test_case(raw_client_hello, expected_fingerprint, expected_hash_hex).unwrap(); - } - - #[test] - fn hash_output_resizing() { - let client_hello = get_client_hello(); - let hash_capacities = vec![0, MD5_HASH_SIZE, 1_000]; - for initial_size in hash_capacities { - let mut hash = Vec::with_capacity(initial_size as usize); - client_hello - .fingerprint_hash(FingerprintType::JA3, &mut hash) - .unwrap(); - assert_eq!(hash.len(), MD5_HASH_SIZE as usize); - } - } - - #[test] - fn string_output_too_small() { - let client_hello = get_client_hello(); - let mut fingerprint_string = String::with_capacity(0); - let fingerprint_err = client_hello - .fingerprint_string(FingerprintType::JA3, &mut fingerprint_string) - .unwrap_err(); - assert_eq!(fingerprint_err.kind(), ErrorType::UsageError); - } - - // make sure that debug doesn't panic and seems reasonable - #[test] - fn debug() { - let client_hello = get_client_hello(); - let mut hash = Vec::new(); - client_hello - .fingerprint_hash(FingerprintType::JA3, &mut hash) - .unwrap(); - let client_hello_debug = format!("{:?}", client_hello); - assert!(client_hello_debug.contains(&hex::encode(hash))); + let client_hello = ClientHello::parse_client_hello(raw_client_hello.as_slice()).unwrap(); + let server_name = client_hello.server_name().unwrap(); + assert_eq!("incoming.telemetry.mozilla.org".as_bytes(), server_name); } } diff --git a/bindings/rust/s2n-tls/src/connection.rs b/bindings/rust/s2n-tls/src/connection.rs index 48e5ce9fdb5..c0754e7388d 100644 --- a/bindings/rust/s2n-tls/src/connection.rs +++ b/bindings/rust/s2n-tls/src/connection.rs @@ -752,7 +752,6 @@ impl Connection { /// client_hello.fingerprint_hash(FingerprintType::JA3, &mut hash); /// drop(conn); /// ``` - #[cfg(feature = "unstable-fingerprint")] pub fn client_hello(&self) -> Result<&crate::client_hello::ClientHello, Error> { let mut handle = unsafe { s2n_connection_get_client_hello(self.connection.as_ptr()).into_result()? }; diff --git a/bindings/rust/s2n-tls/src/lib.rs b/bindings/rust/s2n-tls/src/lib.rs index 78b4e81c572..40bbb6dd1ff 100644 --- a/bindings/rust/s2n-tls/src/lib.rs +++ b/bindings/rust/s2n-tls/src/lib.rs @@ -15,7 +15,6 @@ pub mod error; pub mod callbacks; pub mod cert_chain; -#[cfg(feature = "unstable-fingerprint")] pub mod client_hello; pub mod config; pub mod connection;