use crate::{
client_hello::ClientHello,
error::{Error, Fallible},
ffi::*,
};
use core::ptr::NonNull;
#[non_exhaustive]
#[derive(Copy, Clone)]
pub enum FingerprintType {
JA3,
JA4,
}
impl From<FingerprintType> 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,
FingerprintType::JA4 => s2n_tls_sys::s2n_fingerprint_type::FINGERPRINT_JA4,
}
}
}
pub struct Fingerprint<'a>(&'a mut Builder);
impl Fingerprint<'_> {
pub fn hash_size(&self) -> Result<usize, Error> {
self.0.hash_size()
}
pub fn hash(&mut self) -> Result<&str, Error> {
if self.0.hash.is_empty() {
let mut output_size = 0;
unsafe {
s2n_fingerprint_get_hash(
self.0.ptr.as_ptr(),
self.0.hash.capacity() as u32,
self.0.hash.as_mut_ptr(),
&mut output_size,
)
.into_result()?;
self.0.hash.as_mut_vec().set_len(output_size as usize);
}
}
Ok(&self.0.hash)
}
pub fn raw_size(&self) -> Result<usize, Error> {
let mut raw_size = 0;
unsafe { s2n_fingerprint_get_raw_size(self.0.ptr.as_ptr(), &mut raw_size).into_result() }?;
Ok(raw_size as usize)
}
pub fn raw(&mut self) -> Result<&str, Error> {
if self.0.raw.is_empty() {
if self.0.raw_size.is_none() {
self.0.raw.reserve_exact(self.raw_size()?);
};
let mut output_size = 0;
unsafe {
s2n_fingerprint_get_raw(
self.0.ptr.as_ptr(),
self.0.raw.capacity() as u32,
self.0.raw.as_mut_ptr(),
&mut output_size,
)
.into_result()?;
self.0.raw.as_mut_vec().set_len(output_size as usize);
};
}
Ok(&self.0.raw)
}
pub fn builder(method: FingerprintType) -> Result<Builder, Error> {
Builder::new(method)
}
}
impl Drop for Fingerprint<'_> {
fn drop(&mut self) {
unsafe {
s2n_fingerprint_wipe(self.0.ptr.as_ptr())
.into_result()
.unwrap()
};
self.0.hash.clear();
self.0.raw.clear();
}
}
pub struct Builder {
ptr: NonNull<s2n_fingerprint>,
hash: String,
raw: String,
raw_size: Option<usize>,
}
impl Builder {
pub fn new(method: FingerprintType) -> Result<Self, Error> {
crate::init::init();
let ptr = unsafe { s2n_fingerprint_new(method.into()).into_result() }?;
let hash = String::with_capacity(Self::ptr_hash_size(&ptr)?);
let raw = String::new();
Ok(Builder {
ptr,
hash,
raw,
raw_size: None,
})
}
fn ptr_hash_size(ptr: &NonNull<s2n_fingerprint>) -> Result<usize, Error> {
let mut hash_size = 0;
unsafe { s2n_fingerprint_get_hash_size(ptr.as_ptr(), &mut hash_size).into_result() }?;
Ok(hash_size as usize)
}
pub fn hash_size(&self) -> Result<usize, Error> {
Self::ptr_hash_size(&self.ptr)
}
pub fn set_raw_size(&mut self, size: usize) -> Result<&mut Self, Error> {
self.raw_size = Some(size);
self.raw.reserve_exact(size);
Ok(self)
}
pub fn build<'a>(
&'a mut self,
client_hello: &'a ClientHello,
) -> Result<Fingerprint<'a>, Error> {
unsafe {
s2n_fingerprint_set_client_hello(self.ptr.as_ptr(), client_hello.deref_mut_ptr())
.into_result()
}?;
Ok(Fingerprint(self))
}
}
impl Drop for Builder {
fn drop(&mut self) {
let mut ptr: *mut s2n_fingerprint = unsafe { self.ptr.as_mut() };
unsafe {
let _ = s2n_fingerprint_free(std::ptr::addr_of_mut!(ptr));
}
}
}
const MD5_HASH_SIZE: u32 = 16;
impl ClientHello {
#[deprecated = "Users should prefer the Fingerprint::hash() method"]
pub fn fingerprint_hash(
&self,
hash: FingerprintType,
output: &mut Vec<u8>,
) -> Result<u32, Error> {
let mut hash_size: u32 = 0;
let mut str_size: u32 = 0;
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()?;
output.set_len(hash_size as usize);
};
Ok(str_size)
}
#[deprecated = "Users should prefer the Fingerprint::raw() method"]
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()?;
output.as_mut_vec().set_len(output_size as usize);
};
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
error::ErrorType,
security,
security::Policy,
testing::{build_config, TestPair},
};
use std::{collections::HashSet, error::Error};
const CLIENT_HELLO_BYTES: &[u8] = &[
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,
];
const JA3_FULL_STRING: &str = "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";
const JA3_HASH: &str = "839bbe3ed07fed922ded5aaf714d6842";
fn simple_handshake() -> Result<TestPair, Box<dyn Error>> {
let config = build_config(&security::DEFAULT)?;
let mut pair = TestPair::from_config(&config);
pair.handshake()?;
Ok(pair)
}
#[test]
fn connection_fingerprint() -> Result<(), Box<dyn Error>> {
let pair = simple_handshake()?;
let client_hello = pair.server.client_hello()?;
let mut builder = Fingerprint::builder(FingerprintType::JA3)?;
let mut fingerprint = builder.build(client_hello)?;
let hash_size = fingerprint.hash_size()?;
let hash = fingerprint.hash()?;
assert_eq!(hash.len(), hash_size);
hex::decode(hash)?;
let raw_size = fingerprint.raw_size()?;
let raw = fingerprint.raw()?;
assert_eq!(raw.len(), raw_size);
Ok(())
}
#[test]
fn known_value() -> Result<(), Box<dyn Error>> {
let client_hello = ClientHello::parse_client_hello(CLIENT_HELLO_BYTES)?;
let mut builder = Fingerprint::builder(FingerprintType::JA3)?;
let mut fingerprint = builder.build(&client_hello)?;
let hash_size = fingerprint.hash_size()?;
let hash = fingerprint.hash()?;
assert_eq!(hash.len(), hash_size);
assert_eq!(hash, JA3_HASH);
let raw_size = fingerprint.raw_size()?;
let raw = fingerprint.raw()?;
assert_eq!(raw.len(), raw_size);
assert_eq!(raw, JA3_FULL_STRING);
Ok(())
}
#[test]
fn multiple_fingerprints() -> Result<(), Box<dyn Error>> {
let mut builder = Fingerprint::builder(FingerprintType::JA3)?;
for _ in 1..10 {
let client_hello = ClientHello::parse_client_hello(CLIENT_HELLO_BYTES)?;
let mut fingerprint = builder.build(&client_hello)?;
let hash = fingerprint.hash()?;
assert_eq!(hash, JA3_HASH);
}
Ok(())
}
#[test]
fn multiple_fingerprints_reset() -> Result<(), Box<dyn Error>> {
let mut builder = Fingerprint::builder(FingerprintType::JA3)?;
let client_hello = ClientHello::parse_client_hello(CLIENT_HELLO_BYTES)?;
{
let mut fingerprint = builder.build(&client_hello)?;
fingerprint
.raw_size()
.expect_err("Raw size unexpectedly set");
fingerprint.hash()?;
fingerprint.raw_size()?;
}
let client_hello = ClientHello::parse_client_hello(CLIENT_HELLO_BYTES)?;
let fingerprint = builder.build(&client_hello)?;
fingerprint
.raw_size()
.expect_err("Fingerprint state not reset");
Ok(())
}
#[test]
fn multiple_connection_fingerprints() -> Result<(), Box<dyn Error>> {
let mut builder = Fingerprint::builder(FingerprintType::JA3)?;
let configs = [
build_config(&Policy::from_version("20240501")?)?,
build_config(&Policy::from_version("20240502")?)?,
build_config(&Policy::from_version("20240503")?)?,
build_config(&Policy::from_version("test_all")?)?,
];
let mut fingerprints: Vec<String> = Vec::new();
let handshake_count = configs.len() * 5;
for i in 0..handshake_count {
let i = i % configs.len();
let config = &configs[i];
let mut pair = TestPair::from_config(config);
pair.handshake()?;
let client_hello = pair.server.client_hello()?;
let mut fingerprint = builder.build(client_hello)?;
let hash = fingerprint.hash()?;
hex::decode(hash)?;
if let Some(previous) = fingerprints.get(i) {
assert_eq!(previous.as_str(), hash);
} else {
fingerprints.push(hash.to_string());
}
}
assert_eq!(configs.len(), fingerprints.len());
let unique: HashSet<&String> = HashSet::from_iter(fingerprints.iter());
assert_eq!(unique.len(), fingerprints.len());
Ok(())
}
#[test]
fn raw_sufficient_memory() -> Result<(), Box<dyn Error>> {
let client_hello = ClientHello::parse_client_hello(CLIENT_HELLO_BYTES)?;
let mut builder = Fingerprint::builder(FingerprintType::JA3)?;
builder.set_raw_size(JA3_FULL_STRING.len())?;
let mut fingerprint = builder.build(&client_hello)?;
let raw = fingerprint.raw()?;
assert_eq!(raw, JA3_FULL_STRING);
Ok(())
}
#[test]
fn raw_insufficient_memory() -> Result<(), Box<dyn Error>> {
let client_hello = ClientHello::parse_client_hello(CLIENT_HELLO_BYTES)?;
let mut builder = Fingerprint::builder(FingerprintType::JA3)?;
builder.set_raw_size(JA3_FULL_STRING.len() - 1)?;
let mut fingerprint = builder.build(&client_hello)?;
let error = fingerprint
.raw()
.expect_err("Calculated raw string despite insufficient memory");
assert_eq!(error.kind(), ErrorType::UsageError);
assert_eq!(error.name(), "S2N_ERR_INSUFFICIENT_MEM_SIZE");
Ok(())
}
#[test]
fn hash_does_not_allocate_memory() -> Result<(), Box<dyn Error>> {
let client_hello = ClientHello::parse_client_hello(CLIENT_HELLO_BYTES)?;
let mut builder = Fingerprint::builder(FingerprintType::JA3)?;
for _ in 0..10 {
let snapshot = checkers::with(|| {
let mut fingerprint = builder.build(&client_hello).unwrap();
fingerprint.hash().unwrap();
});
assert!(snapshot.events.is_empty());
}
Ok(())
}
#[test]
fn raw_may_allocate_memory() -> Result<(), Box<dyn Error>> {
let client_hello = ClientHello::parse_client_hello(CLIENT_HELLO_BYTES)?;
let mut builder = Fingerprint::builder(FingerprintType::JA3)?;
let minimum_size = JA3_FULL_STRING.len();
let large_size = minimum_size + 100;
let snapshot = checkers::with(|| {
let mut fingerprint = builder.build(&client_hello).unwrap();
fingerprint.hash().unwrap();
fingerprint.raw().unwrap();
});
assert_eq!(snapshot.events.allocs(), 1);
assert_eq!(snapshot.events.reallocs(), 0);
assert_eq!(snapshot.events.frees(), 0);
assert_eq!(snapshot.events.max_memory_used().unwrap(), minimum_size);
let mut full_snapshot = snapshot;
let snapshot = checkers::with(|| {
for _ in 0..10 {
let mut fingerprint = builder.build(&client_hello).unwrap();
fingerprint.hash().unwrap();
for _ in 0..10 {
fingerprint.raw().unwrap();
}
}
});
assert!(snapshot.events.is_empty());
let snapshot = checkers::with(|| {
builder.set_raw_size(large_size).unwrap();
});
assert_eq!(snapshot.events.allocs(), 0);
assert_eq!(snapshot.events.reallocs(), 1);
assert_eq!(snapshot.events.frees(), 0);
for event in snapshot.events.as_slice() {
full_snapshot.events.push(event.clone());
}
assert_eq!(full_snapshot.events.max_memory_used().unwrap(), large_size);
let snapshot = checkers::with(|| {
let mut fingerprint = builder.build(&client_hello).unwrap();
fingerprint.raw().unwrap();
});
assert!(snapshot.events.is_empty());
let snapshot = checkers::with(|| {
builder.set_raw_size(minimum_size).unwrap();
});
assert!(snapshot.events.is_empty());
let snapshot = checkers::with(|| {
for _ in 0..10 {
let mut fingerprint = builder.build(&client_hello).unwrap();
fingerprint.raw().unwrap();
}
});
assert!(snapshot.events.is_empty());
Ok(())
}
#[test]
#[allow(deprecated)]
fn legacy_connection_fingerprint() -> Result<(), Box<dyn Error>> {
let pair = simple_handshake()?;
let client_hello = pair.server.client_hello()?;
let mut hash = Vec::with_capacity(MD5_HASH_SIZE.try_into()?);
let str_size = client_hello.fingerprint_hash(FingerprintType::JA3, &mut hash)?;
assert_eq!(hash.len(), MD5_HASH_SIZE.try_into()?);
let mut full_str = String::with_capacity(str_size.try_into()?);
client_hello.fingerprint_string(FingerprintType::JA3, &mut full_str)?;
assert_eq!(full_str.len(), str_size.try_into()?);
Ok(())
}
#[test]
#[allow(deprecated)]
fn legacy_known_value() -> Result<(), Box<dyn Error>> {
let client_hello = ClientHello::parse_client_hello(CLIENT_HELLO_BYTES)?;
let mut hash = Vec::with_capacity(MD5_HASH_SIZE.try_into()?);
let str_size = client_hello.fingerprint_hash(FingerprintType::JA3, &mut hash)?;
assert_eq!(hash.len(), MD5_HASH_SIZE.try_into()?);
assert_eq!(hash, hex::decode(JA3_HASH)?);
let mut full_str = String::with_capacity(str_size.try_into()?);
client_hello.fingerprint_string(FingerprintType::JA3, &mut full_str)?;
assert_eq!(full_str.len(), str_size.try_into()?);
assert_eq!(full_str, JA3_FULL_STRING);
Ok(())
}
#[test]
#[allow(deprecated)]
fn legacy_hash_output_resizing() -> Result<(), Box<dyn Error>> {
let client_hello = ClientHello::parse_client_hello(CLIENT_HELLO_BYTES)?;
let hash_capacities = vec![0, MD5_HASH_SIZE, 1_000];
for initial_size in hash_capacities {
let mut hash = Vec::with_capacity(initial_size.try_into()?);
client_hello.fingerprint_hash(FingerprintType::JA3, &mut hash)?;
assert_eq!(hash.len(), MD5_HASH_SIZE.try_into()?);
}
Ok(())
}
#[test]
#[allow(deprecated)]
fn legacy_string_output_too_small() -> Result<(), Box<dyn Error>> {
let client_hello = ClientHello::parse_client_hello(CLIENT_HELLO_BYTES)?;
let mut fingerprint_string = String::with_capacity(JA3_FULL_STRING.len() - 1);
let fingerprint_err = client_hello
.fingerprint_string(FingerprintType::JA3, &mut fingerprint_string)
.unwrap_err();
assert_eq!(fingerprint_err.kind(), ErrorType::UsageError);
assert_eq!(fingerprint_err.name(), "S2N_ERR_INSUFFICIENT_MEM_SIZE");
Ok(())
}
}