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 "tls/extensions/s2n_extension_list.h"
17 : : #include "tls/s2n_fingerprint.h"
18 : : #include "utils/s2n_blob.h"
19 : : #include "utils/s2n_safety.h"
20 : :
21 : : #define S2N_JA3_FIELD_DIV ','
22 : : #define S2N_JA3_LIST_DIV '-'
23 : :
24 : : /* UINT16_MAX == 65535 */
25 : : #define S2N_UINT16_STR_MAX_SIZE 5
26 : :
27 : : static S2N_RESULT s2n_fingerprint_ja3_digest(struct s2n_fingerprint_hash *hash,
28 : : struct s2n_stuffer *out)
29 : 67 : {
30 [ + + ]: 67 : if (!s2n_fingerprint_hash_do_digest(hash)) {
31 : 40 : return S2N_RESULT_OK;
32 : 40 : }
33 : :
34 : 27 : uint8_t digest_bytes[MD5_DIGEST_LENGTH] = { 0 };
35 : 27 : struct s2n_blob digest = { 0 };
36 [ - + ]: 27 : RESULT_GUARD_POSIX(s2n_blob_init(&digest, digest_bytes, sizeof(digest_bytes)));
37 [ - + ]: 27 : RESULT_GUARD(s2n_fingerprint_hash_digest(hash, &digest));
38 [ - + ]: 27 : RESULT_GUARD(s2n_stuffer_write_hex(out, &digest));
39 : :
40 : 27 : return S2N_RESULT_OK;
41 : 27 : }
42 : :
43 : : static S2N_RESULT s2n_fingerprint_ja3_iana(struct s2n_fingerprint_hash *hash,
44 : : bool *is_list, uint16_t iana)
45 : 1577 : {
46 [ + + ]: 1577 : if (s2n_fingerprint_is_grease_value(iana)) {
47 : 22 : return S2N_RESULT_OK;
48 : 22 : }
49 : :
50 : : /* If we have already written at least one value for this field,
51 : : * then we are writing a list and need to prepend a list divider before
52 : : * writing the next value.
53 : : */
54 [ + + ]: 1555 : if (*is_list) {
55 [ + + ]: 1313 : RESULT_GUARD(s2n_fingerprint_hash_add_char(hash, S2N_JA3_LIST_DIV));
56 : 1313 : } else {
57 : 242 : *is_list = true;
58 : 242 : }
59 : :
60 : : /* snprintf always appends a '\0' to the output,
61 : : * but that extra '\0' is not included in the return value */
62 : 1554 : char str[S2N_UINT16_STR_MAX_SIZE + 1] = { 0 };
63 : 1554 : int written = snprintf(str, sizeof(str), "%u", iana);
64 [ # # ][ - + ]: 1554 : RESULT_ENSURE_GT(written, 0);
65 [ # # ][ - + ]: 1554 : RESULT_ENSURE_LTE(written, S2N_UINT16_STR_MAX_SIZE);
66 : :
67 [ + + ]: 1554 : RESULT_GUARD(s2n_fingerprint_hash_add_str(hash, str, written));
68 : 1546 : return S2N_RESULT_OK;
69 : 1554 : }
70 : :
71 : : static S2N_RESULT s2n_fingerprint_ja3_version(struct s2n_fingerprint_hash *hash,
72 : : struct s2n_client_hello *ch)
73 : 78 : {
74 : 78 : uint16_t version = 0;
75 [ - + ]: 78 : RESULT_GUARD(s2n_fingerprint_get_legacy_version(ch, &version));
76 : :
77 : 78 : bool is_list = false;
78 [ + + ]: 78 : RESULT_GUARD(s2n_fingerprint_ja3_iana(hash, &is_list, version));
79 : 76 : return S2N_RESULT_OK;
80 : 78 : }
81 : :
82 : : static S2N_RESULT s2n_fingerprint_ja3_cipher_suites(struct s2n_fingerprint_hash *hash,
83 : : struct s2n_client_hello *ch)
84 : 75 : {
85 [ - + ][ # # ]: 75 : RESULT_ENSURE_REF(ch);
86 : :
87 : 75 : struct s2n_stuffer ciphers = { 0 };
88 [ - + ]: 75 : RESULT_GUARD_POSIX(s2n_stuffer_init_written(&ciphers, &ch->cipher_suites));
89 : :
90 : 75 : bool found = false;
91 [ + + ]: 1151 : while (s2n_stuffer_data_available(&ciphers)) {
92 : 1082 : uint16_t iana = 0;
93 [ - + ]: 1082 : RESULT_GUARD_POSIX(s2n_stuffer_read_uint16(&ciphers, &iana));
94 [ + + ]: 1082 : RESULT_GUARD(s2n_fingerprint_ja3_iana(hash, &found, iana));
95 : 1082 : }
96 : 69 : return S2N_RESULT_OK;
97 : 75 : }
98 : :
99 : : static S2N_RESULT s2n_fingerprint_ja3_extensions(struct s2n_fingerprint_hash *hash,
100 : : struct s2n_client_hello *ch)
101 : 69 : {
102 [ # # ][ - + ]: 69 : RESULT_ENSURE_REF(ch);
103 : :
104 : : /* We have to use the raw extensions instead of the parsed extensions
105 : : * because s2n-tls both intentionally ignores any unknown extensions
106 : : * and reorders the extensions when parsing the list.
107 : : */
108 : 69 : struct s2n_stuffer extensions = { 0 };
109 [ - + ]: 69 : RESULT_GUARD_POSIX(s2n_stuffer_init_written(&extensions, &ch->extensions.raw));
110 : :
111 : 69 : bool found = false;
112 [ + + ]: 321 : while (s2n_stuffer_data_available(&extensions)) {
113 : 252 : uint16_t iana = 0;
114 [ - + ]: 252 : RESULT_GUARD(s2n_fingerprint_parse_extension(&extensions, &iana));
115 [ - + ]: 252 : RESULT_GUARD(s2n_fingerprint_ja3_iana(hash, &found, iana));
116 : 252 : }
117 : 69 : return S2N_RESULT_OK;
118 : 69 : }
119 : :
120 : : static S2N_RESULT s2n_fingerprint_ja3_elliptic_curves(struct s2n_fingerprint_hash *hash,
121 : : struct s2n_client_hello *ch)
122 : 69 : {
123 [ # # ][ - + ]: 69 : RESULT_ENSURE_REF(ch);
124 : :
125 : 69 : s2n_parsed_extension *extension = NULL;
126 : 69 : int result = s2n_client_hello_get_parsed_extension(S2N_EXTENSION_SUPPORTED_GROUPS,
127 : 69 : &ch->extensions, &extension);
128 [ + + ]: 69 : if (result != S2N_SUCCESS) {
129 : 38 : return S2N_RESULT_OK;
130 : 38 : }
131 : :
132 : 31 : struct s2n_stuffer elliptic_curves = { 0 };
133 [ - + ]: 31 : RESULT_GUARD_POSIX(s2n_stuffer_init_written(&elliptic_curves, &extension->extension));
134 [ - + ]: 31 : RESULT_GUARD_POSIX(s2n_stuffer_skip_read(&elliptic_curves, sizeof(uint16_t)));
135 : :
136 : 31 : bool found = false;
137 [ + + ]: 141 : while (s2n_stuffer_data_available(&elliptic_curves)) {
138 : 110 : uint16_t iana = 0;
139 [ - + ]: 110 : RESULT_GUARD_POSIX(s2n_stuffer_read_uint16(&elliptic_curves, &iana));
140 [ - + ]: 110 : RESULT_GUARD(s2n_fingerprint_ja3_iana(hash, &found, iana));
141 : 110 : }
142 : 31 : return S2N_RESULT_OK;
143 : 31 : }
144 : :
145 : : static S2N_RESULT s2n_fingerprint_ja3_point_formats(struct s2n_fingerprint_hash *hash,
146 : : struct s2n_client_hello *ch)
147 : 68 : {
148 [ # # ][ - + ]: 68 : RESULT_ENSURE_REF(ch);
149 : :
150 : 68 : s2n_parsed_extension *extension = NULL;
151 : 68 : int result = s2n_client_hello_get_parsed_extension(S2N_EXTENSION_EC_POINT_FORMATS,
152 : 68 : &ch->extensions, &extension);
153 [ + + ]: 68 : if (result != S2N_SUCCESS) {
154 : 41 : return S2N_RESULT_OK;
155 : 41 : }
156 : :
157 : 27 : struct s2n_stuffer point_formats = { 0 };
158 [ - + ]: 27 : RESULT_GUARD_POSIX(s2n_stuffer_init_written(&point_formats, &extension->extension));
159 [ - + ]: 27 : RESULT_GUARD_POSIX(s2n_stuffer_skip_read(&point_formats, sizeof(uint8_t)));
160 : :
161 : 27 : bool found = false;
162 [ + + ]: 81 : while (s2n_stuffer_data_available(&point_formats)) {
163 : 55 : uint8_t iana = 0;
164 [ - + ]: 55 : RESULT_GUARD_POSIX(s2n_stuffer_read_uint8(&point_formats, &iana));
165 [ + + ]: 55 : RESULT_GUARD(s2n_fingerprint_ja3_iana(hash, &found, iana));
166 : 55 : }
167 : 26 : return S2N_RESULT_OK;
168 : 27 : }
169 : :
170 : : /* JA3 involves concatenating a set of fields from the ClientHello:
171 : : * SSLVersion,Cipher,SSLExtension,EllipticCurve,EllipticCurvePointFormat
172 : : * For example:
173 : : * "769,47-53-5-10-49161-49162-49171-49172-50-56-19-4,0-10-11,23-24-25,0"
174 : : * See https://github.com/salesforce/ja3
175 : : */
176 : : static S2N_RESULT s2n_fingerprint_ja3_write(struct s2n_fingerprint_hash *hash,
177 : : struct s2n_client_hello *ch)
178 : 78 : {
179 [ + + ]: 78 : RESULT_GUARD(s2n_fingerprint_ja3_version(hash, ch));
180 [ + + ]: 76 : RESULT_GUARD(s2n_fingerprint_hash_add_char(hash, S2N_JA3_FIELD_DIV));
181 [ + + ]: 75 : RESULT_GUARD(s2n_fingerprint_ja3_cipher_suites(hash, ch));
182 [ - + ]: 69 : RESULT_GUARD(s2n_fingerprint_hash_add_char(hash, S2N_JA3_FIELD_DIV));
183 [ - + ]: 69 : RESULT_GUARD(s2n_fingerprint_ja3_extensions(hash, ch));
184 [ - + ]: 69 : RESULT_GUARD(s2n_fingerprint_hash_add_char(hash, S2N_JA3_FIELD_DIV));
185 [ - + ]: 69 : RESULT_GUARD(s2n_fingerprint_ja3_elliptic_curves(hash, ch));
186 [ + + ]: 69 : RESULT_GUARD(s2n_fingerprint_hash_add_char(hash, S2N_JA3_FIELD_DIV));
187 [ + + ]: 68 : RESULT_GUARD(s2n_fingerprint_ja3_point_formats(hash, ch));
188 : 67 : return S2N_RESULT_OK;
189 : 68 : }
190 : :
191 : : S2N_RESULT s2n_fingerprint_ja3(struct s2n_fingerprint *fingerprint,
192 : : struct s2n_fingerprint_hash *hash, struct s2n_stuffer *output)
193 : 78 : {
194 [ # # ][ - + ]: 78 : RESULT_ENSURE_REF(fingerprint);
195 [ + + ]: 78 : RESULT_GUARD(s2n_fingerprint_ja3_write(hash, fingerprint->client_hello));
196 [ - + ]: 67 : RESULT_GUARD(s2n_fingerprint_ja3_digest(hash, output));
197 : :
198 [ + + ]: 67 : if (s2n_fingerprint_hash_do_digest(hash)) {
199 : 27 : fingerprint->raw_size = hash->bytes_digested;
200 : 40 : } else {
201 : 40 : fingerprint->raw_size = s2n_stuffer_data_available(output);
202 : 40 : }
203 : 67 : return S2N_RESULT_OK;
204 : 67 : }
205 : :
206 : : struct s2n_fingerprint_method ja3_fingerprint = {
207 : : /* The hash doesn't have to be cryptographically secure,
208 : : * so the weakness of MD5 shouldn't be a problem. */
209 : : .hash = S2N_HASH_MD5,
210 : : /* The hash string is a single MD5 digest represented as hex */
211 : : .hash_str_size = S2N_JA3_HASH_STR_SIZE,
212 : : .fingerprint = s2n_fingerprint_ja3,
213 : : };
|