Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: switch JA3 to use stuffer hex methods #4662

Merged
merged 2 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions tests/unit/s2n_fingerprint_test.c
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,6 @@ int main(int argc, char **argv)
EXPECT_EQUAL(fingerprint->method, &ja3_fingerprint);
EXPECT_TRUE(s2n_hash_is_ready_for_input(&fingerprint->hash));
EXPECT_NULL(fingerprint->client_hello);
EXPECT_FALSE(fingerprint->legacy_hash_format);
EXPECT_EQUAL(fingerprint->raw_size, 0);

/* Free cleans up the fingerprint */
Expand All @@ -313,15 +312,13 @@ int main(int argc, char **argv)
EXPECT_NOT_NULL(fingerprint.method);
EXPECT_TRUE(fingerprint.hash.is_ready_for_input);
EXPECT_NOT_NULL(fingerprint.client_hello);
EXPECT_TRUE(fingerprint.legacy_hash_format);
EXPECT_NOT_EQUAL(fingerprint.raw_size, 0);

/* Verify that wipe only clears the expected fields */
EXPECT_SUCCESS(s2n_fingerprint_wipe(&fingerprint));
EXPECT_NOT_NULL(fingerprint.method);
EXPECT_TRUE(fingerprint.hash.is_ready_for_input);
EXPECT_NULL(fingerprint.client_hello);
EXPECT_TRUE(fingerprint.legacy_hash_format);
EXPECT_EQUAL(fingerprint.raw_size, 0);
};

Expand Down
32 changes: 22 additions & 10 deletions tls/s2n_fingerprint.c
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,7 @@ int s2n_fingerprint_get_hash(struct s2n_fingerprint *fingerprint,
const struct s2n_fingerprint_method *method = fingerprint->method;
POSIX_ENSURE_REF(method);

size_t min_output_size = method->hash_str_size;
if (fingerprint->legacy_hash_format) {
min_output_size /= 2;
}

POSIX_ENSURE(max_output_size >= min_output_size, S2N_ERR_INSUFFICIENT_MEM_SIZE);
POSIX_ENSURE(max_output_size >= method->hash_str_size, S2N_ERR_INSUFFICIENT_MEM_SIZE);
POSIX_ENSURE(output, S2N_ERR_INVALID_ARGUMENT);
POSIX_ENSURE(output_size, S2N_ERR_INVALID_ARGUMENT);
*output_size = 0;
Expand All @@ -122,7 +117,6 @@ int s2n_fingerprint_get_hash(struct s2n_fingerprint *fingerprint,

struct s2n_fingerprint_hash hash = {
.hash = &fingerprint->hash,
.legacy_hash_format = fingerprint->legacy_hash_format,
};
POSIX_GUARD(s2n_hash_reset(&fingerprint->hash));

Expand Down Expand Up @@ -250,12 +244,30 @@ int s2n_client_hello_get_fingerprint_hash(struct s2n_client_hello *ch, s2n_finge
uint32_t max_output_size, uint8_t *output, uint32_t *output_size, uint32_t *str_size)
{
POSIX_ENSURE(type == S2N_FINGERPRINT_JA3, S2N_ERR_INVALID_ARGUMENT);
POSIX_ENSURE(max_output_size >= MD5_DIGEST_LENGTH, S2N_ERR_INSUFFICIENT_MEM_SIZE);
POSIX_ENSURE(str_size, S2N_ERR_INVALID_ARGUMENT);
DEFER_CLEANUP(struct s2n_fingerprint fingerprint = { .legacy_hash_format = true },
s2n_fingerprint_free_fields);
POSIX_ENSURE(output_size, S2N_ERR_INVALID_ARGUMENT);
POSIX_ENSURE(output, S2N_ERR_INVALID_ARGUMENT);

DEFER_CLEANUP(struct s2n_fingerprint fingerprint = { 0 }, s2n_fingerprint_free_fields);
POSIX_GUARD_RESULT(s2n_fingerprint_init(&fingerprint, type));
POSIX_GUARD(s2n_fingerprint_set_client_hello(&fingerprint, ch));
POSIX_GUARD(s2n_fingerprint_get_hash(&fingerprint, max_output_size, output, output_size));

uint32_t hex_hash_size = 0;
uint8_t hex_hash[S2N_JA3_HASH_STR_SIZE] = { 0 };
POSIX_GUARD(s2n_fingerprint_get_hash(&fingerprint, sizeof(hex_hash), hex_hash, &hex_hash_size));

/* s2n_client_hello_get_fingerprint_hash expects the raw bytes of the JA3 hash,
* but s2n_fingerprint_get_hash returns a hex string instead.
* We need to translate back to the raw bytes.
*/
struct s2n_stuffer bytes_out = { 0 };
POSIX_GUARD(s2n_blob_init(&bytes_out.blob, output, max_output_size));
struct s2n_blob hex_in = { 0 };
POSIX_GUARD(s2n_blob_init(&hex_in, hex_hash, hex_hash_size));
POSIX_GUARD_RESULT(s2n_stuffer_read_hex(&bytes_out, &hex_in));
*output_size = s2n_stuffer_data_available(&bytes_out);

POSIX_GUARD(s2n_fingerprint_get_raw_size(&fingerprint, str_size));
return S2N_SUCCESS;
}
Expand Down
10 changes: 2 additions & 8 deletions tls/s2n_fingerprint.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,19 @@
#include "tls/s2n_client_hello.h"
#include "utils/s2n_result.h"

#define S2N_JA3_HASH_STR_SIZE (MD5_DIGEST_LENGTH * 2)

struct s2n_fingerprint {
size_t raw_size;
const struct s2n_fingerprint_method *method;
struct s2n_client_hello *client_hello;
struct s2n_hash_state hash;
/* Originally we represented the hash as the raw bytes of the digest.
* However, the JA4 hash is composed of a string prefix and two digests,
* so cannot reasonably be represented as raw bytes.
* Keep the old behavior for the old method.
*/
unsigned int legacy_hash_format : 1;
};

struct s2n_fingerprint_hash {
uint32_t bytes_digested;
struct s2n_stuffer *buffer;
struct s2n_hash_state *hash;
/* See legacy_hash_format on s2n_fingerprint */
unsigned int legacy_hash_format : 1;
};
S2N_RESULT s2n_fingerprint_hash_add_char(struct s2n_fingerprint_hash *hash, char c);
S2N_RESULT s2n_fingerprint_hash_add_str(struct s2n_fingerprint_hash *hash, const char *str, size_t str_size);
Expand Down
26 changes: 6 additions & 20 deletions tls/s2n_fingerprint_ja3.c
Original file line number Diff line number Diff line change
Expand Up @@ -24,33 +24,19 @@
/* UINT16_MAX == 65535 */
#define S2N_UINT16_STR_MAX_SIZE 5

#define S2N_HEX_PER_BYTE 2

static S2N_RESULT s2n_fingerprint_ja3_digest(struct s2n_fingerprint_hash *hash,
struct s2n_stuffer *out)
{
if (!s2n_fingerprint_hash_do_digest(hash)) {
return S2N_RESULT_OK;
}

if (hash->legacy_hash_format) {
uint8_t *digest = s2n_stuffer_raw_write(out, MD5_DIGEST_LENGTH);
RESULT_GUARD_PTR(digest);
RESULT_GUARD(s2n_fingerprint_hash_digest(hash, digest, MD5_DIGEST_LENGTH));
return S2N_RESULT_OK;
}
uint8_t digest_bytes[MD5_DIGEST_LENGTH] = { 0 };
RESULT_GUARD(s2n_fingerprint_hash_digest(hash, digest_bytes, sizeof(digest_bytes)));

uint8_t digest[MD5_DIGEST_LENGTH] = { 0 };
RESULT_GUARD(s2n_fingerprint_hash_digest(hash, digest, sizeof(digest)));

/* We allocate enough memory for the trailing '\0', but it is not
* included in the return value of snprintf. */
char hex[S2N_HEX_PER_BYTE + 1] = { 0 };
for (size_t i = 0; i < sizeof(digest); i++) {
int written = snprintf(hex, sizeof(hex), "%.*x", S2N_HEX_PER_BYTE, digest[i]);
RESULT_ENSURE_EQ(written, S2N_HEX_PER_BYTE);
RESULT_GUARD_POSIX(s2n_stuffer_write_str(out, hex));
}
struct s2n_blob digest = { 0 };
RESULT_GUARD_POSIX(s2n_blob_init(&digest, digest_bytes, sizeof(digest_bytes)));
RESULT_GUARD(s2n_stuffer_write_hex(out, &digest));

return S2N_RESULT_OK;
}
Expand Down Expand Up @@ -222,6 +208,6 @@ struct s2n_fingerprint_method ja3_fingerprint = {
* so the weakness of MD5 shouldn't be a problem. */
.hash = S2N_HASH_MD5,
/* The hash string is a single MD5 digest represented as hex */
.hash_str_size = MD5_DIGEST_LENGTH * S2N_HEX_PER_BYTE,
.hash_str_size = S2N_JA3_HASH_STR_SIZE,
.fingerprint = s2n_fingerprint_ja3,
};
Loading