Branch data Line data Source code
1 : : /*
2 : : * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 : : *
4 : : * Licensed under the Apache License, Version 2.0 (the "License").
5 : : * You may not use this file except in compliance with the License.
6 : : * A copy of the License is located at
7 : : *
8 : : * http://aws.amazon.com/apache2.0
9 : : *
10 : : * or in the "license" file accompanying this file. This file is distributed
11 : : * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12 : : * express or implied. See the License for the specific language governing
13 : : * permissions and limitations under the License.
14 : : */
15 : :
16 : : #include <ctype.h>
17 : :
18 : : #include "crypto/s2n_hash.h"
19 : : #include "stuffer/s2n_stuffer.h"
20 : : #include "tls/extensions/s2n_client_supported_versions.h"
21 : : #include "tls/extensions/s2n_extension_list.h"
22 : : #include "tls/s2n_client_hello.h"
23 : : #include "tls/s2n_fingerprint.h"
24 : : #include "tls/s2n_protocol_preferences.h"
25 : : #include "utils/s2n_blob.h"
26 : : #include "utils/s2n_safety.h"
27 : :
28 : : #define S2N_JA4_LIST_DIV ','
29 : : #define S2N_JA4_PART_DIV '_'
30 : :
31 : : /**
32 : : *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-ciphers
33 : : *# 2 character number of cipher suites
34 : : *
35 : : *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-extensions
36 : : *# Same as counting ciphers.
37 : : */
38 : 142 : #define S2N_JA4_COUNT_SIZE 2
39 : :
40 : 17130 : #define S2N_HEX_PER_BYTE 2
41 : 81 : #define S2N_JA4_DIGEST_HEX_CHAR_LIMIT 12
42 : 81 : #define S2N_JA4_DIGEST_BYTE_LIMIT (S2N_JA4_DIGEST_HEX_CHAR_LIMIT / S2N_HEX_PER_BYTE)
43 : :
44 : 62 : #define S2N_JA4_A_SIZE 10
45 : : #define S2N_JA4_B_SIZE S2N_JA4_DIGEST_HEX_CHAR_LIMIT
46 : : #define S2N_JA4_C_SIZE S2N_JA4_DIGEST_HEX_CHAR_LIMIT
47 : : #define S2N_JA4_SIZE (S2N_JA4_A_SIZE + 1 + S2N_JA4_B_SIZE + 1 + S2N_JA4_C_SIZE)
48 : :
49 : : #define S2N_JA4_LIST_LIMIT 99
50 : 17049 : #define S2N_JA4_IANA_HEX_SIZE (S2N_HEX_PER_BYTE * sizeof(uint16_t))
51 : 236 : #define S2N_JA4_IANA_ENTRY_SIZE (S2N_JA4_IANA_HEX_SIZE + 1)
52 : : #define S2N_JA4_WORKSPACE_SIZE ((S2N_JA4_LIST_LIMIT * (S2N_JA4_IANA_ENTRY_SIZE)))
53 : :
54 : : const char *s2n_ja4_version_strings[] = {
55 : : /**
56 : : *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version
57 : : *# 0x0304 = TLS 1.3 = “13”
58 : : *# 0x0303 = TLS 1.2 = “12”
59 : : *# 0x0302 = TLS 1.1 = “11”
60 : : *# 0x0301 = TLS 1.0 = “10”
61 : : */
62 : : [0x0304] = "13",
63 : : [0x0303] = "12",
64 : : [0x0302] = "11",
65 : : [0x0301] = "10",
66 : : /**
67 : : *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version
68 : : *# 0x0300 = SSL 3.0 = “s3”
69 : : *# 0x0002 = SSL 2.0 = “s2”
70 : : */
71 : : [0x0300] = "s3",
72 : : [0x0002] = "s2",
73 : : };
74 : :
75 : : /**
76 : : *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version
77 : : *# Unknown = “00”
78 : : */
79 : 57 : #define S2N_JA4_UNKNOWN_STR "00"
80 : :
81 : : DEFINE_POINTER_CLEANUP_FUNC(struct s2n_stuffer *, s2n_stuffer_wipe);
82 : :
83 : : static int s2n_fingerprint_ja4_iana_compare(const void *a, const void *b)
84 : 4074 : {
85 : 4074 : const uint8_t *iana_a = (const uint8_t *) a;
86 : 4074 : const uint8_t *iana_b = (const uint8_t *) b;
87 [ + + ]: 16813 : for (size_t i = 0; i < S2N_JA4_IANA_HEX_SIZE; i++) {
88 [ + + ]: 15009 : if (iana_a[i] != iana_b[i]) {
89 : 2270 : return iana_a[i] - iana_b[i];
90 : 2270 : }
91 : 15009 : }
92 : 1804 : return 0;
93 : 4074 : }
94 : :
95 : : static S2N_RESULT s2n_fingerprint_ja4_digest(struct s2n_fingerprint_hash *hash,
96 : : struct s2n_stuffer *out)
97 : 142 : {
98 [ - + ][ # # ]: 142 : RESULT_ENSURE_REF(hash);
99 [ + + ]: 142 : if (!s2n_fingerprint_hash_do_digest(hash)) {
100 : 18 : return S2N_RESULT_OK;
101 : 18 : }
102 : :
103 : : /* Instead of hashing empty inputs, JA4 sets the output to a string of all zeroes.
104 : : * (Actually hashing an empty input doesn't produce a digest of all zeroes)
105 : : *
106 : : *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#cipher-hash
107 : : *# If there are no ciphers in the sorted cipher list, then the value of
108 : : *# JA4_b is set to `000000000000`
109 : : *
110 : : *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash
111 : : *# If there are no extensions in the sorted extensions list, then the value of
112 : : *# JA4_c is set to `000000000000`
113 : : */
114 : 124 : uint64_t bytes = 0;
115 [ - + ]: 124 : RESULT_GUARD_POSIX(s2n_hash_get_currently_in_hash_total(hash->hash, &bytes));
116 [ + + ]: 124 : if (bytes == 0) {
117 [ - + ]: 43 : RESULT_GUARD_POSIX(s2n_stuffer_write_str(out, "000000000000"));
118 : 43 : return S2N_RESULT_OK;
119 : 43 : }
120 : :
121 : 81 : uint8_t digest_bytes[SHA256_DIGEST_LENGTH] = { 0 };
122 : 81 : struct s2n_blob digest = { 0 };
123 [ - + ]: 81 : RESULT_GUARD_POSIX(s2n_blob_init(&digest, digest_bytes, sizeof(digest_bytes)));
124 [ - + ]: 81 : RESULT_GUARD(s2n_fingerprint_hash_digest(hash, &digest));
125 : :
126 : : /* JA4 digests are truncated */
127 [ - + ][ # # ]: 81 : RESULT_ENSURE_LTE(S2N_JA4_DIGEST_BYTE_LIMIT, digest.size);
128 : 81 : digest.size = S2N_JA4_DIGEST_BYTE_LIMIT;
129 [ - + ]: 81 : RESULT_GUARD(s2n_stuffer_write_hex(out, &digest));
130 : 81 : return S2N_RESULT_OK;
131 : 81 : }
132 : :
133 : : /**
134 : : *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-ciphers
135 : : *# 2 character number of cipher suites, so if there’s 6 cipher suites
136 : : *# in the hello packet, then the value should be “06”.
137 : : *
138 : : *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-extensions
139 : : *# Same as counting ciphers.
140 : : */
141 : : static S2N_RESULT s2n_fingerprint_ja4_count(struct s2n_blob *output, uint16_t count)
142 : 142 : {
143 [ - + ][ # # ]: 142 : RESULT_ENSURE_REF(output);
144 : :
145 : : /**
146 : : *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-ciphers
147 : : *# If there’s > 99, which there should never be, then output “99”.
148 : : *
149 : : *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-extensions
150 : : *# Same as counting ciphers.
151 : : */
152 : 142 : count = MIN(count, 99);
153 : :
154 [ - + ][ # # ]: 142 : RESULT_ENSURE_EQ(output->size, 2);
155 : 142 : output->data[0] = (count / 10) + '0';
156 : 142 : output->data[1] = (count % 10) + '0';
157 : 142 : return S2N_RESULT_OK;
158 : 142 : }
159 : :
160 : : static S2N_RESULT s2n_fingerprint_get_extension_version(struct s2n_client_hello *ch,
161 : : uint16_t *client_version)
162 : 71 : {
163 [ - + ][ # # ]: 71 : RESULT_ENSURE_REF(ch);
164 [ # # ][ - + ]: 71 : RESULT_ENSURE_REF(client_version);
165 : :
166 : 71 : s2n_parsed_extension *extension = NULL;
167 [ + + ]: 71 : RESULT_GUARD_POSIX(s2n_client_hello_get_parsed_extension(
168 : 16 : S2N_EXTENSION_SUPPORTED_VERSIONS, &ch->extensions, &extension));
169 [ - + ][ # # ]: 16 : RESULT_ENSURE_REF(extension);
170 : :
171 : 16 : struct s2n_stuffer supported_versions = { 0 };
172 [ - + ]: 16 : RESULT_GUARD_POSIX(s2n_stuffer_init_written(&supported_versions, &extension->extension));
173 : :
174 [ + + ]: 16 : RESULT_GUARD_POSIX(s2n_stuffer_skip_read(&supported_versions, sizeof(uint8_t)));
175 [ + + ]: 28 : while (s2n_stuffer_data_available(&supported_versions)) {
176 : 16 : uint16_t version = 0;
177 [ - + ]: 16 : RESULT_GUARD_POSIX(s2n_stuffer_read_uint16(&supported_versions, &version));
178 : : /**
179 : : *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version
180 : : *# Remember to ignore GREASE values.
181 : : */
182 [ + + ]: 16 : if (s2n_fingerprint_is_grease_value(version)) {
183 : 3 : continue;
184 : 3 : }
185 : : /**
186 : : *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version
187 : : *# If extension 0x002b exists (supported_versions), then the version is
188 : : *# the highest value in the extension.
189 : : */
190 : 13 : *client_version = MAX(*client_version, version);
191 : 13 : }
192 : 12 : return S2N_RESULT_OK;
193 : 12 : }
194 : :
195 : : static S2N_RESULT s2n_fingerprint_ja4_version(struct s2n_stuffer *output,
196 : : struct s2n_client_hello *ch)
197 : 71 : {
198 : 71 : uint16_t client_version = 0;
199 [ + + ]: 71 : if (s2n_result_is_error(s2n_fingerprint_get_extension_version(ch, &client_version))) {
200 : : /**
201 : : *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version
202 : : *# If the extension doesn’t exist, then the TLS version is the value of
203 : : *# the Protocol Version.
204 : : */
205 [ - + ]: 59 : RESULT_GUARD(s2n_fingerprint_get_legacy_version(ch, &client_version));
206 : 59 : }
207 : :
208 : : /**
209 : : *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version
210 : : *# Handshake version (located at the top of the packet) should be ignored.
211 : : */
212 : :
213 : 71 : const char *version_str = NULL;
214 [ + + ]: 71 : if (client_version < s2n_array_len(s2n_ja4_version_strings)) {
215 : 68 : version_str = s2n_ja4_version_strings[client_version];
216 : 68 : }
217 [ + + ]: 71 : if (version_str == NULL) {
218 : 57 : version_str = S2N_JA4_UNKNOWN_STR;
219 : 57 : }
220 [ - + ]: 71 : RESULT_GUARD_POSIX(s2n_stuffer_write_str(output, version_str));
221 : :
222 : 71 : return S2N_RESULT_OK;
223 : 71 : }
224 : :
225 : : static S2N_RESULT s2n_client_hello_get_first_alpn(struct s2n_client_hello *ch, struct s2n_blob *first)
226 : 71 : {
227 [ # # ][ - + ]: 71 : RESULT_ENSURE_REF(ch);
228 : :
229 : 71 : s2n_parsed_extension *extension = NULL;
230 [ + + ]: 71 : RESULT_GUARD_POSIX(s2n_client_hello_get_parsed_extension(S2N_EXTENSION_ALPN,
231 : 16 : &ch->extensions, &extension));
232 [ - + ][ # # ]: 16 : RESULT_ENSURE_REF(extension);
233 : :
234 : 16 : struct s2n_stuffer protocols = { 0 };
235 [ - + ]: 16 : RESULT_GUARD_POSIX(s2n_stuffer_init_written(&protocols, &extension->extension));
236 : :
237 : 16 : uint16_t list_size = 0;
238 [ + + ]: 16 : RESULT_GUARD_POSIX(s2n_stuffer_read_uint16(&protocols, &list_size));
239 : :
240 [ + + ]: 11 : RESULT_GUARD(s2n_protocol_preferences_read(&protocols, first));
241 : 9 : return S2N_RESULT_OK;
242 : 11 : }
243 : :
244 : : /**
245 : : *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#alpn-extension-value
246 : : *# The first and last alphanumeric characters of the ALPN (Application-Layer
247 : : *# Protocol Negotiation) first value.
248 : : */
249 : : static S2N_RESULT s2n_fingerprint_ja4_alpn(struct s2n_stuffer *output,
250 : : struct s2n_client_hello *ch)
251 : 71 : {
252 : 71 : struct s2n_blob protocol = { 0 };
253 [ + + ]: 71 : if (s2n_result_is_error(s2n_client_hello_get_first_alpn(ch, &protocol))) {
254 : 62 : protocol.size = 0;
255 : 62 : }
256 : :
257 : : /**
258 : : *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#alpn-extension-value
259 : : *# If there is no ALPN extension, no ALPN values, or the first ALPN value
260 : : *# is empty, then we print "00" as the value in the fingerprint.
261 : : *
262 : : *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#alpn-extension-value
263 : : *# If the first ALPN value is only a single character, then that character
264 : : *# is treated as both the first and last character.
265 : : */
266 : 71 : uint8_t first_char = '0', last_char = '0';
267 [ + + ]: 71 : if (protocol.size > 0) {
268 : 9 : first_char = protocol.data[0];
269 : 9 : last_char = protocol.data[protocol.size - 1];
270 : 9 : }
271 : :
272 : : /**
273 : : *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#alpn-extension-value
274 : : *# If the first or last byte of the first ALPN is non-alphanumeric (meaning
275 : : *# not `0x30-0x39`, `0x41-0x5A`, or `0x61-0x7A`), then we print the first and
276 : : *# last characters of the hex representation of the first ALPN instead.
277 : : */
278 [ + + ][ + + ]: 71 : if (!isalnum(first_char) || !isalnum(last_char)) {
279 [ - + ]: 4 : RESULT_GUARD(s2n_hex_digit((first_char >> 4), &first_char));
280 [ - + ]: 4 : RESULT_GUARD(s2n_hex_digit((last_char & 0x0F), &last_char));
281 : 4 : }
282 : :
283 [ - + ]: 71 : RESULT_GUARD_POSIX(s2n_stuffer_write_char(output, first_char));
284 [ - + ]: 71 : RESULT_GUARD_POSIX(s2n_stuffer_write_char(output, last_char));
285 : 71 : return S2N_RESULT_OK;
286 : 71 : }
287 : :
288 : : /* Part "a" of the fingerprint is a descriptive prefix.
289 : : *
290 : : * https://github.com/FoxIO-LLC/ja4/main/technical_details/JA4.md
291 : : *# (QUIC=”q”, DTLS="d", or Normal TLS=”t”)
292 : : *# (2 character TLS version)
293 : : *# (SNI=”d” or no SNI=”i”)
294 : : *# (2 character count of ciphers)
295 : : *# (2 character count of extensions)
296 : : *# (first and last characters of first ALPN extension value)
297 : : */
298 : : static S2N_RESULT s2n_fingerprint_ja4_a(struct s2n_fingerprint *fingerprint,
299 : : struct s2n_stuffer *output, struct s2n_blob *ciphers_count, struct s2n_blob *extensions_count)
300 : 71 : {
301 [ # # ][ - + ]: 71 : RESULT_ENSURE_REF(fingerprint);
302 : :
303 : : /**
304 : : *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#quic-and-dtls
305 : : *# If the protocol is QUIC then the first character of the fingerprint is “q”,
306 : : *# if DTLS it is "d", else it is “t”.
307 : : *
308 : : * s2n-tls only supports TLS and QUIC. DTLS is not supported.
309 : : */
310 : 71 : bool is_quic = false;
311 [ - + ]: 71 : RESULT_GUARD_POSIX(s2n_client_hello_has_extension(fingerprint->client_hello,
312 : 71 : TLS_EXTENSION_QUIC_TRANSPORT_PARAMETERS, &is_quic));
313 [ + + ]: 71 : char protocol_char = (is_quic) ? 'q' : 't';
314 [ - + ]: 71 : RESULT_GUARD_POSIX(s2n_stuffer_write_char(output, protocol_char));
315 : :
316 [ - + ]: 71 : RESULT_GUARD(s2n_fingerprint_ja4_version(output, fingerprint->client_hello));
317 : :
318 : : /**
319 : : *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#sni
320 : : *# If the SNI extension (0x0000) exists, then the destination of the connection
321 : : *# is a domain, or “d” in the fingerprint.
322 : : *# If the SNI does not exist, then the destination is an IP address, or “i”.
323 : : */
324 : 71 : bool has_sni = false;
325 [ - + ]: 71 : RESULT_GUARD_POSIX(s2n_client_hello_has_extension(fingerprint->client_hello,
326 : 71 : TLS_EXTENSION_SERVER_NAME, &has_sni));
327 [ + + ]: 71 : char sni_char = (has_sni) ? 'd' : 'i';
328 [ - + ]: 71 : RESULT_GUARD_POSIX(s2n_stuffer_write_char(output, sni_char));
329 : :
330 : : /* Reserve two characters for the "count of ciphers".
331 : : * We'll calculate it later when we handle the cipher suite list for JA4_b.
332 : : */
333 : 71 : uint8_t *ciphers_count_mem = s2n_stuffer_raw_write(output, S2N_JA4_COUNT_SIZE);
334 [ - + ]: 71 : RESULT_GUARD_PTR(ciphers_count_mem);
335 [ - + ]: 71 : RESULT_GUARD_POSIX(s2n_blob_init(ciphers_count, ciphers_count_mem, S2N_JA4_COUNT_SIZE));
336 : :
337 : : /* Reserve two characters for the "count of extensions".
338 : : * We'll calculate it later when we handle the extensions list for JA4_c.
339 : : */
340 : 71 : uint8_t *extensions_count_mem = s2n_stuffer_raw_write(output, S2N_JA4_COUNT_SIZE);
341 [ - + ]: 71 : RESULT_GUARD_PTR(extensions_count_mem);
342 [ - + ]: 71 : RESULT_GUARD_POSIX(s2n_blob_init(extensions_count, extensions_count_mem, S2N_JA4_COUNT_SIZE));
343 : :
344 [ - + ]: 71 : RESULT_GUARD(s2n_fingerprint_ja4_alpn(output, fingerprint->client_hello));
345 : :
346 : 71 : return S2N_RESULT_OK;
347 : 71 : }
348 : :
349 : : /**
350 : : *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#cipher-hash
351 : : *# The list is created using the 4 character hex values of the ciphers,
352 : : *# lower case, comma delimited, ignoring GREASE.
353 : : */
354 : : static S2N_RESULT s2n_fingerprint_ja4_ciphers(struct s2n_fingerprint_hash *hash,
355 : : struct s2n_client_hello *ch, struct s2n_stuffer *sort_space, uint16_t *ciphers_count)
356 : 71 : {
357 [ # # ][ - + ]: 71 : RESULT_ENSURE_REF(ch);
358 [ # # ][ - + ]: 71 : RESULT_ENSURE_REF(sort_space);
359 [ # # ][ - + ]: 71 : RESULT_ENSURE_REF(ciphers_count);
360 : :
361 : 71 : struct s2n_stuffer cipher_suites = { 0 };
362 [ - + ]: 71 : RESULT_GUARD_POSIX(s2n_stuffer_init_written(&cipher_suites, &ch->cipher_suites));
363 : :
364 : 71 : DEFER_CLEANUP(struct s2n_stuffer *iana_list = sort_space, s2n_stuffer_wipe_pointer);
365 [ + + ]: 737 : while (s2n_stuffer_data_available(&cipher_suites)) {
366 : 666 : uint16_t iana = 0;
367 [ - + ]: 666 : RESULT_GUARD_POSIX(s2n_stuffer_read_uint16(&cipher_suites, &iana));
368 : : /**
369 : : *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-ciphers
370 : : *# Remember, ignore GREASE values. They don’t count.
371 : : */
372 [ + + ]: 666 : if (s2n_fingerprint_is_grease_value(iana)) {
373 : 3 : continue;
374 : 3 : }
375 [ - + ]: 663 : RESULT_GUARD(s2n_stuffer_write_uint16_hex(iana_list, iana));
376 [ - + ]: 663 : RESULT_GUARD_POSIX(s2n_stuffer_write_char(iana_list, S2N_JA4_LIST_DIV));
377 : 663 : }
378 : :
379 : 71 : size_t iana_list_size = s2n_stuffer_data_available(iana_list);
380 : 71 : size_t iana_count = iana_list_size / S2N_JA4_IANA_ENTRY_SIZE;
381 : 71 : *ciphers_count = iana_count;
382 [ + + ]: 71 : if (iana_count == 0) {
383 : 1 : return S2N_RESULT_OK;
384 : 1 : }
385 : :
386 : 70 : uint8_t *ianas = s2n_stuffer_raw_read(iana_list, iana_list_size);
387 [ - + ][ # # ]: 70 : RESULT_ENSURE_REF(ianas);
388 : 70 : qsort(ianas, iana_count, S2N_JA4_IANA_ENTRY_SIZE, s2n_fingerprint_ja4_iana_compare);
389 [ - + ]: 70 : RESULT_GUARD(s2n_fingerprint_hash_add_bytes(hash, ianas, iana_list_size - 1));
390 : 70 : return S2N_RESULT_OK;
391 : 70 : }
392 : :
393 : : /**
394 : : *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#cipher-hash
395 : : *# A 12 character truncated sha256 hash of the list of ciphers sorted in hex order,
396 : : *# first 12 characters.
397 : : */
398 : : static S2N_RESULT s2n_fingerprint_ja4_b(struct s2n_fingerprint *fingerprint,
399 : : struct s2n_fingerprint_hash *hash, struct s2n_blob *ciphers_count,
400 : : struct s2n_stuffer *output)
401 : 71 : {
402 [ - + ][ # # ]: 71 : RESULT_ENSURE_REF(fingerprint);
403 : :
404 : 71 : uint16_t ciphers_count_value = 0;
405 [ - + ]: 71 : RESULT_GUARD(s2n_fingerprint_ja4_ciphers(hash, fingerprint->client_hello,
406 : 71 : &fingerprint->workspace, &ciphers_count_value));
407 : :
408 [ - + ]: 71 : RESULT_GUARD(s2n_fingerprint_ja4_digest(hash, output));
409 [ - + ]: 71 : RESULT_GUARD(s2n_fingerprint_ja4_count(ciphers_count, ciphers_count_value));
410 : 71 : return S2N_RESULT_OK;
411 : 71 : }
412 : :
413 : : /**
414 : : *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash
415 : : *# The extension list is created using the 4 character hex values of the extensions,
416 : : *# lower case, comma delimited, sorted (not in the order they appear).
417 : : */
418 : : static S2N_RESULT s2n_fingerprint_ja4_extensions(struct s2n_fingerprint_hash *hash,
419 : : struct s2n_client_hello *ch, struct s2n_stuffer *sort_space, uint16_t *extensions_count)
420 : 71 : {
421 [ # # ][ - + ]: 71 : RESULT_ENSURE_REF(ch);
422 [ # # ][ - + ]: 71 : RESULT_ENSURE_REF(sort_space);
423 [ # # ][ - + ]: 71 : RESULT_ENSURE_REF(extensions_count);
424 : :
425 : 71 : struct s2n_stuffer extensions = { 0 };
426 [ - + ]: 71 : RESULT_GUARD_POSIX(s2n_stuffer_init_written(&extensions, &ch->extensions.raw));
427 : :
428 : 71 : DEFER_CLEANUP(struct s2n_stuffer *iana_list = sort_space, s2n_stuffer_wipe_pointer);
429 [ + + ]: 689 : while (s2n_stuffer_data_available(&extensions)) {
430 : 618 : uint16_t iana = 0;
431 [ - + ]: 618 : RESULT_GUARD(s2n_fingerprint_parse_extension(&extensions, &iana));
432 : :
433 : : /**
434 : : *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-extensions
435 : : *# Ignore GREASE.
436 : : */
437 [ + + ]: 618 : if (s2n_fingerprint_is_grease_value(iana)) {
438 : 3 : continue;
439 : 3 : }
440 : :
441 : : /* SNI and ALPN are included in the extension count, but not in the extension list.
442 : : *
443 : : *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash
444 : : *# Ignore the SNI extension (0000) and the ALPN extension (0010)
445 : : *# as we’ve already captured them in the _a_ section of the fingerprint.
446 : : *
447 : : *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-extensions
448 : : *# Include SNI and ALPN.
449 : : */
450 : 615 : (*extensions_count)++;
451 [ + + ][ + + ]: 615 : if (iana == TLS_EXTENSION_SERVER_NAME || iana == S2N_EXTENSION_ALPN) {
452 : 23 : continue;
453 : 23 : }
454 [ - + ]: 592 : RESULT_GUARD(s2n_stuffer_write_uint16_hex(iana_list, iana));
455 [ - + ]: 592 : RESULT_GUARD_POSIX(s2n_stuffer_write_char(iana_list, S2N_JA4_LIST_DIV));
456 : 592 : }
457 : :
458 : 71 : size_t iana_list_size = s2n_stuffer_data_available(iana_list);
459 : 71 : size_t iana_count = iana_list_size / S2N_JA4_IANA_ENTRY_SIZE;
460 [ + + ]: 71 : if (iana_count == 0) {
461 : 47 : return S2N_RESULT_OK;
462 : 47 : }
463 : :
464 : 24 : uint8_t *ianas = s2n_stuffer_raw_read(iana_list, iana_list_size);
465 [ - + ][ # # ]: 24 : RESULT_ENSURE_REF(ianas);
466 : 24 : qsort(ianas, iana_count, S2N_JA4_IANA_ENTRY_SIZE, s2n_fingerprint_ja4_iana_compare);
467 [ - + ]: 24 : RESULT_GUARD(s2n_fingerprint_hash_add_bytes(hash, ianas, iana_list_size - 1));
468 : 24 : return S2N_RESULT_OK;
469 : 24 : }
470 : :
471 : : static S2N_RESULT s2n_fingerprint_ja4_sig_algs(struct s2n_fingerprint_hash *hash,
472 : : struct s2n_client_hello *ch)
473 : 71 : {
474 [ # # ][ - + ]: 71 : RESULT_ENSURE_REF(ch);
475 : :
476 : 71 : s2n_parsed_extension *extension = NULL;
477 : 71 : int result = s2n_client_hello_get_parsed_extension(S2N_EXTENSION_SIGNATURE_ALGORITHMS,
478 : 71 : &ch->extensions, &extension);
479 [ + + ]: 71 : if (result != S2N_SUCCESS) {
480 : 66 : return S2N_RESULT_OK;
481 : 66 : }
482 [ - + ][ # # ]: 5 : RESULT_ENSURE_REF(extension);
483 : :
484 : 5 : struct s2n_stuffer sig_algs = { 0 };
485 [ - + ]: 5 : RESULT_GUARD_POSIX(s2n_stuffer_init_written(&sig_algs, &extension->extension));
486 : :
487 : 5 : uint8_t entry_bytes[S2N_JA4_IANA_ENTRY_SIZE] = { 0 };
488 : 5 : struct s2n_stuffer entry = { 0 };
489 [ - + ]: 5 : RESULT_GUARD_POSIX(s2n_blob_init(&entry.blob, entry_bytes, sizeof(entry_bytes)));
490 : :
491 : 5 : bool is_first = true;
492 [ + + ]: 5 : if (s2n_stuffer_skip_read(&sig_algs, sizeof(uint16_t)) != S2N_SUCCESS) {
493 : 1 : return S2N_RESULT_OK;
494 : 1 : }
495 [ + + ]: 39 : while (s2n_stuffer_data_available(&sig_algs)) {
496 : 35 : uint16_t iana = 0;
497 [ - + ]: 35 : RESULT_GUARD_POSIX(s2n_stuffer_read_uint16(&sig_algs, &iana));
498 [ - + ]: 35 : if (s2n_fingerprint_is_grease_value(iana)) {
499 : 0 : continue;
500 : 0 : }
501 [ + + ]: 35 : if (is_first) {
502 [ - + ]: 4 : RESULT_GUARD(s2n_fingerprint_hash_add_char(hash, S2N_JA4_PART_DIV));
503 : 31 : } else {
504 [ - + ]: 31 : RESULT_GUARD_POSIX(s2n_stuffer_write_char(&entry, S2N_JA4_LIST_DIV));
505 : 31 : }
506 [ - + ]: 35 : RESULT_GUARD(s2n_stuffer_write_uint16_hex(&entry, iana));
507 [ - + ]: 35 : RESULT_GUARD(s2n_fingerprint_hash_add_bytes(hash, entry_bytes,
508 : 35 : s2n_stuffer_data_available(&entry)));
509 [ - + ]: 35 : RESULT_GUARD_POSIX(s2n_stuffer_rewrite(&entry));
510 : 35 : is_first = false;
511 : 35 : }
512 : 4 : return S2N_RESULT_OK;
513 : 4 : }
514 : :
515 : : /**
516 : : *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash
517 : : *# A 12 character truncated sha256 hash of the list of extensions, sorted by
518 : : *# hex value, followed by the list of signature algorithms, in the order that
519 : : *# they appear (not sorted).
520 : : */
521 : : static S2N_RESULT s2n_fingerprint_ja4_c(struct s2n_fingerprint *fingerprint,
522 : : struct s2n_fingerprint_hash *hash, struct s2n_blob *extensions_count,
523 : : struct s2n_stuffer *output)
524 : 71 : {
525 [ - + ][ # # ]: 71 : RESULT_ENSURE_REF(fingerprint);
526 : :
527 : 71 : uint16_t extensions_count_value = 0;
528 [ - + ]: 71 : RESULT_GUARD(s2n_fingerprint_ja4_extensions(hash, fingerprint->client_hello,
529 : 71 : &fingerprint->workspace, &extensions_count_value));
530 : :
531 : : /**
532 : : *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash
533 : : *# The signature algorithm hex values are then added to the end of the list
534 : : *# in the order that they appear (not sorted) with an underscore delimiting
535 : : *# the two lists.
536 : : *
537 : : *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash
538 : : *# If there are no signature algorithms in the hello packet,
539 : : *# then the string ends without an underscore and is hashed.
540 : : *
541 : : * s2n_fingerprint_ja4_sig_algs handles writing the underscore because we
542 : : * need to skip writing it if there are no signature algorithms.
543 : : */
544 [ - + ]: 71 : RESULT_GUARD(s2n_fingerprint_ja4_sig_algs(hash, fingerprint->client_hello));
545 : :
546 [ - + ]: 71 : RESULT_GUARD(s2n_fingerprint_ja4_digest(hash, output));
547 [ - + ]: 71 : RESULT_GUARD(s2n_fingerprint_ja4_count(extensions_count, extensions_count_value));
548 : 71 : return S2N_RESULT_OK;
549 : 71 : }
550 : :
551 : : /* JA4 fingerprints are basically of the form a_b_c:
552 : : *
553 : : *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#ja4-algorithm
554 : : *# (QUIC=”q”, DTLS="d", or Normal TLS=”t”)
555 : : *# (2 character TLS version)
556 : : *# (SNI=”d” or no SNI=”i”)
557 : : *# (2 character count of ciphers)
558 : : *# (2 character count of extensions)
559 : : *# (first and last characters of first ALPN extension value)
560 : : *# _
561 : : *# (sha256 hash of the list of cipher hex codes sorted in hex order, truncated to 12 characters)
562 : : *# _
563 : : *# (sha256 hash of (the list of extension hex codes sorted in hex order)_(the list of signature algorithms), truncated to 12 characters)
564 : : *#
565 : : *# The end result is a fingerprint that looks like:
566 : : *# t13d1516h2_8daaf6152771_b186095e22b6
567 : : */
568 : : static S2N_RESULT s2n_fingerprint_ja4(struct s2n_fingerprint *fingerprint,
569 : : struct s2n_fingerprint_hash *hash, struct s2n_stuffer *output)
570 : 71 : {
571 [ - + ][ # # ]: 71 : RESULT_ENSURE_REF(fingerprint);
572 [ - + ][ # # ]: 71 : RESULT_ENSURE_REF(hash);
573 [ - + ][ # # ]: 71 : RESULT_ENSURE_REF(output);
574 : :
575 [ + - ]: 71 : if (s2n_stuffer_is_freed(&fingerprint->workspace)) {
576 [ - + ]: 71 : RESULT_GUARD_POSIX(s2n_stuffer_growable_alloc(&fingerprint->workspace, S2N_JA4_WORKSPACE_SIZE));
577 : 71 : }
578 : :
579 : 71 : struct s2n_blob ciphers_count = { 0 };
580 : 71 : struct s2n_blob extensions_count = { 0 };
581 [ - + ]: 71 : RESULT_GUARD(s2n_fingerprint_ja4_a(fingerprint, output, &ciphers_count, &extensions_count));
582 [ - + ]: 71 : RESULT_GUARD_POSIX(s2n_stuffer_write_char(output, S2N_JA4_PART_DIV));
583 [ - + ]: 71 : RESULT_GUARD(s2n_fingerprint_ja4_b(fingerprint, hash, &ciphers_count, output));
584 [ - + ]: 71 : RESULT_GUARD_POSIX(s2n_stuffer_write_char(output, S2N_JA4_PART_DIV));
585 [ - + ]: 71 : RESULT_GUARD(s2n_fingerprint_ja4_c(fingerprint, hash, &extensions_count, output));
586 : :
587 [ + + ]: 71 : if (s2n_fingerprint_hash_do_digest(hash)) {
588 : : /* The extra two bytes are for the characters separating the parts */
589 : 62 : fingerprint->raw_size = hash->bytes_digested + S2N_JA4_A_SIZE + 2;
590 : 62 : } else {
591 : 9 : fingerprint->raw_size = s2n_stuffer_data_available(output);
592 : 9 : }
593 : :
594 : 71 : return S2N_RESULT_OK;
595 : 71 : }
596 : :
597 : : struct s2n_fingerprint_method ja4_fingerprint = {
598 : : .hash = S2N_HASH_SHA256,
599 : : .hash_str_size = S2N_JA4_SIZE,
600 : : .fingerprint = s2n_fingerprint_ja4,
601 : : };
|