From a841fca8fdabbbb4b5c68e19b4dea9f3fcabbf51 Mon Sep 17 00:00:00 2001 From: Lindsay Stewart Date: Thu, 19 Sep 2024 21:08:21 -0700 Subject: [PATCH] fix: update ja4 compliance (#4773) --- api/unstable/fingerprint.h | 4 +- .../technical_details/JA4.txt | 46 ++-- .../JA4/alpn-extension-value.toml | 44 ++++ .../technical_details/JA4/cipher-hash.toml | 11 +- .../technical_details/JA4/details.toml | 2 +- .../technical_details/JA4/example.toml | 2 +- .../technical_details/JA4/extension-hash.toml | 13 +- .../technical_details/JA4/ja4-algorithm.toml | 4 +- .../JA4/ja4-tls-client-fingerprinting.toml | 2 +- .../JA4/number-of-ciphers.toml | 2 +- .../JA4/number-of-extensions.toml | 2 +- .../technical_details/JA4/quic-and-dtls.toml | 19 ++ .../technical_details/JA4/raw-output.toml | 2 +- .../technical_details/JA4/sni.toml | 2 +- .../JA4/tls-and-dtls-version.toml} | 13 +- .../JA4/alpn-extension-value.toml | 26 --- .../v0.18.2/technical_details/JA4/quic.toml | 15 -- tests/pcap/data/no_extensions.pcap | Bin 0 -> 3089 bytes tests/pcap/tests/s2n_client_hellos.rs | 13 +- tests/unit/s2n_fingerprint_ja4_test.c | 207 +++++++++++++----- tls/s2n_fingerprint.c | 2 +- tls/s2n_fingerprint_ja4.c | 107 +++++---- 22 files changed, 362 insertions(+), 176 deletions(-) rename compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/{v0.18.2 => df3c067}/technical_details/JA4.txt (69%) create mode 100644 compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/alpn-extension-value.toml rename compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/{v0.18.2 => df3c067}/technical_details/JA4/cipher-hash.toml (62%) rename compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/{v0.18.2 => df3c067}/technical_details/JA4/details.toml (72%) rename compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/{v0.18.2 => df3c067}/technical_details/JA4/example.toml (84%) rename compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/{v0.18.2 => df3c067}/technical_details/JA4/extension-hash.toml (82%) rename compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/{v0.18.2 => df3c067}/technical_details/JA4/ja4-algorithm.toml (78%) rename compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/{v0.18.2 => df3c067}/technical_details/JA4/ja4-tls-client-fingerprinting.toml (66%) rename compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/{v0.18.2 => df3c067}/technical_details/JA4/number-of-ciphers.toml (85%) rename compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/{v0.18.2 => df3c067}/technical_details/JA4/number-of-extensions.toml (71%) create mode 100644 compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/quic-and-dtls.toml rename compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/{v0.18.2 => df3c067}/technical_details/JA4/raw-output.toml (90%) rename compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/{v0.18.2 => df3c067}/technical_details/JA4/sni.toml (83%) rename compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/{v0.18.2/technical_details/JA4/tls-version.toml => df3c067/technical_details/JA4/tls-and-dtls-version.toml} (52%) delete mode 100644 compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/alpn-extension-value.toml delete mode 100644 compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/quic.toml create mode 100644 tests/pcap/data/no_extensions.pcap diff --git a/api/unstable/fingerprint.h b/api/unstable/fingerprint.h index 16ec3f12f34..3f8e1351f7c 100644 --- a/api/unstable/fingerprint.h +++ b/api/unstable/fingerprint.h @@ -106,7 +106,7 @@ S2N_API int s2n_fingerprint_get_hash_size(const struct s2n_fingerprint *fingerpr * * JA4: A string consisting of three parts, separated by underscores: the prefix, * and the hex-encoded truncated SHA256 hashes of the other two parts of the raw string. - * - See https://github.com/FoxIO-LLC/ja4/blob/v0.18.2/technical_details/JA4.md + * - See https://github.com/FoxIO-LLC/ja4/blob/df3c067/technical_details/JA4.md * - Example: "t13i310900_e8f1e7e78f70_1f22a2ca17c4" * * @param fingerprint The s2n_fingerprint to be used for the hash @@ -145,7 +145,7 @@ S2N_API int s2n_fingerprint_get_raw_size(const struct s2n_fingerprint *fingerpri * 156-61-60-53-47-255,11-10-35-22-23-13-43-45-51,29-23-30-25-24,0-1-2" * * JA4: A string consisting of three parts: a prefix, and two lists of hex values. - * - See https://github.com/FoxIO-LLC/ja4/blob/v0.18.2/technical_details/JA4.md + * - See https://github.com/FoxIO-LLC/ja4/blob/df3c067/technical_details/JA4.md * - Example: "t13i310900_002f,0033,0035,0039,003c,003d,0067,006b,009c,009d,009e, * 009f,00ff,1301,1302,1303,c009,c00a,c013,c014,c023,c024,c027,c028, * c02b,c02c,c02f,c030,cca8,cca9,ccaa_000a,000b,000d,0016,0017,0023, diff --git a/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.txt b/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.txt similarity index 69% rename from compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.txt rename to compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.txt index 02c493ad032..771609bf3e1 100644 --- a/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.txt +++ b/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.txt @@ -5,7 +5,7 @@ JA4 looks at the TLS Client Hello packet and builds a fingerprint of the client based on attributes within the packet. ### JA4 Algorithm: -(QUIC=”q” or TCP=”t”) +(QUIC=”q”, DTLS="d", or Normal TLS=”t”) (2 character TLS version) (SNI=”d” or no SNI=”i”) (2 character count of ciphers) @@ -22,22 +22,29 @@ t13d1516h2_8daaf6152771_b186095e22b6 ## Details: The program needs to ignore GREASE values anywhere it sees them: (https://datatracker.ietf.org/doc/html/draft-davidben-tls-grease-01#page-5) -### QUIC: +### QUIC and DTLS: +“q”, "d" or “t”, denotes whether the hello packet is for QUIC, DTLS, or normal TLS. + https://en.wikipedia.org/wiki/QUIC -“q” or “t”, which denotes whether the hello packet is for QUIC or TCP. QUIC is the protocol which the new HTTP/3 standard utilizes, encapsulating TLS 1.3 into UDP packets. As QUIC was developed by Google, if an organization heavily utilizes Google products, QUIC could make up half of their network traffic, so this is important to capture. +QUIC is the protocol which the new HTTP/3 standard utilizes, encapsulating TLS 1.3 into UDP packets. As QUIC was developed by Google, if an organization heavily utilizes Google products, QUIC could make up half of their network traffic, so this is important to capture. + +https://en.wikipedia.org/wiki/Datagram_Transport_Layer_Security +DTLS is a version of TLS that can operate over UDP or SCTP. -If the protocol is QUIC then the first character of the fingerprint is “q” if not, it’s “t”. +If the protocol is QUIC then the first character of the fingerprint is “q”, if DTLS it is "d", else it is “t”. -### TLS Version: -TLS version is shown in 3 different places. If extension 0x002b exists (supported_versions), then the version is the highest value in the extension. Remember to ignore GREASE values. If the extension doesn’t exist, then the TLS version is the value of the Protocol Version. Handshake version (located at the top of the packet) should be ignored. +### TLS and DTLS Version: +The TLS version is shown in 3 different places. If extension 0x002b exists (supported_versions), then the version is the highest value in the extension. Remember to ignore GREASE values. If the extension doesn’t exist, then the TLS version is the value of the Protocol Version. Handshake version (located at the top of the packet) should be ignored. 0x0304 = TLS 1.3 = “13” 0x0303 = TLS 1.2 = “12” 0x0302 = TLS 1.1 = “11” 0x0301 = TLS 1.0 = “10” 0x0300 = SSL 3.0 = “s3” -0x0200 = SSL 2.0 = “s2” -0x0100 = SSL 1.0 = “s1” +0x0002 = SSL 2.0 = “s2” +0xfeff = DTLS 1.0 = "d1" +0xfefd = DTLS 1.2 = "d2" +0xfefc = DTLS 1.3 = "d3" Unknown = “00” @@ -51,16 +58,21 @@ If the SNI extension (0x0000) exists, then the destination of the connection is Same as counting ciphers. Ignore GREASE. Include SNI and ALPN. ### ALPN Extension Value: -The first and last characters of the ALPN (Application-Layer Protocol Negotiation) first value. +The first and last alphanumeric characters of the ALPN (Application-Layer Protocol Negotiation) first value. List of possible ALPN Values (scroll down): https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml - - -In the above example, the first ALPN value is h2 so the first and last characters to use in the fingerprint are “h2”. IF the first ALPN listed was http/1.1 then the first and last characters to use in the fingerprint would be “h1”. +In the above example, the first ALPN value is h2 so the first and last characters to use in the fingerprint are “h2”. If the first ALPN listed was http/1.1 then the first and last characters to use in the fingerprint would be “h1”. In Wireshark this field is located under tls.handshake.extensions_alpn_str -If there are no ALPN values or no ALPN extension then we print “00” as the value in the fingerprint. +If there is no ALPN extension, no ALPN values, or the first ALPN value is empty, then we print "00" as the value in the fingerprint. If the first ALPN value is only a single character, then that character is treated as both the first and last character. + +If the first or last byte of the first ALPN is non-alphanumeric (meaning not `0x30-0x39`, `0x41-0x5A`, or `0x61-0x7A`), then we print the first and last characters of the hex representation of the first ALPN instead. For example: +* `0xAB` would be printed as "ab" +* `0xAB 0xCD` would be printed as "ad" +* `0x30 0xAB` would be printed as "3b" +* `0x30 0x31 0xAB 0xCD` would be printed as "3d" +* `0x30 0xAB 0xCD 0x31` would be printed as "01" ### Cipher hash: A 12 character truncated sha256 hash of the list of ciphers sorted in hex order, first 12 characters. The list is created using the 4 character hex values of the ciphers, lower case, comma delimited, ignoring GREASE. @@ -73,10 +85,13 @@ Is sorted to: 002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9 = 8daaf6152771 ``` +If there are no ciphers in the sorted cipher list, then the value of JA4_b is set to `000000000000` +We do this rather than running a sha256 hash of nothing as this makes it clear to the user when a field has no values. + ### Extension hash: A 12 character truncated sha256 hash of the list of extensions, sorted by hex value, followed by the list of signature algorithms, in the order that they appear (not sorted). -The extension list is created using the 4 character hex values of the extensions, lower case, comma delimited, sorted (not in the order they appear). Ignore the SNI extension (0000) and the ALPN extension (0010) as we’ve already captured them in the _a_ section of the fingerprint. These values are omitted so that the same application would have the same _b_ section of the fingerprint regardless of if it were going to a domain, IP, or changing ALPNs. +The extension list is created using the 4 character hex values of the extensions, lower case, comma delimited, sorted (not in the order they appear). Ignore the SNI extension (0000) and the ALPN extension (0010) as we’ve already captured them in the _a_ section of the fingerprint. These values are omitted so that the same application would have the same _c_ section of the fingerprint regardless of if it were going to a domain, IP, or changing ALPNs. For example: ``` @@ -112,6 +127,9 @@ For example: 0005,000a,000b,000d,0012,0015,0017,001b,0023,002b,002d,0033,4469,ff01 = 6d807ffa2a79 ``` +If there are no extensions in the sorted extensions list, then the value of JA4_c is set to `000000000000` +We do this rather than running a sha256 hash of nothing as this makes it clear to the user when a field has no values. + ### Example JA4 fingerprint: diff --git a/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/alpn-extension-value.toml b/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/alpn-extension-value.toml new file mode 100644 index 00000000000..a34b8859156 --- /dev/null +++ b/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/alpn-extension-value.toml @@ -0,0 +1,44 @@ +target = "https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#alpn-extension-value" + +# ### ALPN Extension Value: +# +# The first and last alphanumeric characters of the ALPN (Application-Layer Protocol Negotiation) first value. +# List of possible ALPN Values (scroll down): https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml +# +# In the above example, the first ALPN value is h2 so the first and last characters to use in the fingerprint are “h2”. If the first ALPN listed was http/1.1 then the first and last characters to use in the fingerprint would be “h1”. +# +# In Wireshark this field is located under tls.handshake.extensions_alpn_str +# +# If there is no ALPN extension, no ALPN values, or the first ALPN value is empty, then we print "00" as the value in the fingerprint. If the first ALPN value is only a single character, then that character is treated as both the first and last character. +# +# If the first or last byte of the first ALPN is non-alphanumeric (meaning not `0x30-0x39`, `0x41-0x5A`, or `0x61-0x7A`), then we print the first and last characters of the hex representation of the first ALPN instead. For example: +# * `0xAB` would be printed as "ab" +# * `0xAB 0xCD` would be printed as "ad" +# * `0x30 0xAB` would be printed as "3b" +# * `0x30 0x31 0xAB 0xCD` would be printed as "3d" +# * `0x30 0xAB 0xCD 0x31` would be printed as "01" +# + +[[spec]] +level = "MUST" +quote = ''' +The first and last alphanumeric characters of the ALPN (Application-Layer Protocol Negotiation) first value. +''' + +[[spec]] +level = "MUST" +quote = ''' +If there is no ALPN extension, no ALPN values, or the first ALPN value is empty, then we print "00" as the value in the fingerprint. +''' + +[[spec]] +level = "MUST" +quote = ''' +If the first ALPN value is only a single character, then that character is treated as both the first and last character. +''' + +[[spec]] +level = "MUST" +quote = ''' +If the first or last byte of the first ALPN is non-alphanumeric (meaning not `0x30-0x39`, `0x41-0x5A`, or `0x61-0x7A`), then we print the first and last characters of the hex representation of the first ALPN instead. +''' diff --git a/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/cipher-hash.toml b/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/cipher-hash.toml similarity index 62% rename from compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/cipher-hash.toml rename to compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/cipher-hash.toml index 4d853664d10..eed81c0f030 100644 --- a/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/cipher-hash.toml +++ b/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/cipher-hash.toml @@ -1,7 +1,6 @@ -target = "https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#cipher-hash" +target = "https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#cipher-hash" # ### Cipher hash: -# # A 12 character truncated sha256 hash of the list of ciphers sorted in hex order, first 12 characters. The list is created using the 4 character hex values of the ciphers, lower case, comma delimited, ignoring GREASE. # Example: # ``` @@ -12,6 +11,9 @@ target = "https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_deta # 002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9 = 8daaf6152771 # ``` # +# If there are no ciphers in the sorted cipher list, then the value of JA4_b is set to `000000000000` +# We do this rather than running a sha256 hash of nothing as this makes it clear to the user when a field has no values. +# [[spec]] level = "MUST" @@ -25,3 +27,8 @@ quote = ''' The list is created using the 4 character hex values of the ciphers, lower case, comma delimited, ignoring GREASE. ''' +[[spec]] +level = "MUST" +quote = ''' +If there are no ciphers in the sorted cipher list, then the value of JA4_b is set to `000000000000` +''' diff --git a/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/details.toml b/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/details.toml similarity index 72% rename from compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/details.toml rename to compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/details.toml index d637e305aec..16510da898c 100644 --- a/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/details.toml +++ b/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/details.toml @@ -1,4 +1,4 @@ -target = "https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#details" +target = "https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#details" # ## Details: # diff --git a/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/example.toml b/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/example.toml similarity index 84% rename from compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/example.toml rename to compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/example.toml index 3fd0f6fb383..69e3305c50b 100644 --- a/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/example.toml +++ b/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/example.toml @@ -1,4 +1,4 @@ -target = "https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#example" +target = "https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#example" # ### Example # diff --git a/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/extension-hash.toml b/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/extension-hash.toml similarity index 82% rename from compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/extension-hash.toml rename to compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/extension-hash.toml index 4413cef9521..f39862a4032 100644 --- a/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/extension-hash.toml +++ b/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/extension-hash.toml @@ -1,10 +1,10 @@ -target = "https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#extension-hash" +target = "https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash" # ### Extension hash: # # A 12 character truncated sha256 hash of the list of extensions, sorted by hex value, followed by the list of signature algorithms, in the order that they appear (not sorted). # -# The extension list is created using the 4 character hex values of the extensions, lower case, comma delimited, sorted (not in the order they appear). Ignore the SNI extension (0000) and the ALPN extension (0010) as we’ve already captured them in the _a_ section of the fingerprint. These values are omitted so that the same application would have the same _b_ section of the fingerprint regardless of if it were going to a domain, IP, or changing ALPNs. +# The extension list is created using the 4 character hex values of the extensions, lower case, comma delimited, sorted (not in the order they appear). Ignore the SNI extension (0000) and the ALPN extension (0010) as we’ve already captured them in the _a_ section of the fingerprint. These values are omitted so that the same application would have the same _c_ section of the fingerprint regardless of if it were going to a domain, IP, or changing ALPNs. # # For example: # ``` @@ -40,6 +40,9 @@ target = "https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_deta # 0005,000a,000b,000d,0012,0015,0017,001b,0023,002b,002d,0033,4469,ff01 = 6d807ffa2a79 # ``` # +# If there are no extensions in the sorted extensions list, then the value of JA4_c is set to `000000000000` +# We do this rather than running a sha256 hash of nothing as this makes it clear to the user when a field has no values. +# [[spec]] level = "MUST" @@ -70,3 +73,9 @@ level = "MUST" quote = ''' If there are no signature algorithms in the hello packet, then the string ends without an underscore and is hashed. ''' + +[[spec]] +level = "MUST" +quote = ''' +If there are no extensions in the sorted extensions list, then the value of JA4_c is set to `000000000000` +''' diff --git a/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/ja4-algorithm.toml b/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/ja4-algorithm.toml similarity index 78% rename from compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/ja4-algorithm.toml rename to compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/ja4-algorithm.toml index ac4a36b24cf..2e25be51b61 100644 --- a/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/ja4-algorithm.toml +++ b/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/ja4-algorithm.toml @@ -1,8 +1,8 @@ -target = "https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#ja4-algorithm" +target = "https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#ja4-algorithm" # ### JA4 Algorithm: # -# (QUIC=”q” or TCP=”t”) +# (QUIC=”q”, DTLS="d", or Normal TLS=”t”) # (2 character TLS version) # (SNI=”d” or no SNI=”i”) # (2 character count of ciphers) diff --git a/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/ja4-tls-client-fingerprinting.toml b/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/ja4-tls-client-fingerprinting.toml similarity index 66% rename from compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/ja4-tls-client-fingerprinting.toml rename to compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/ja4-tls-client-fingerprinting.toml index 4166fcf4992..6ab5135c40d 100644 --- a/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/ja4-tls-client-fingerprinting.toml +++ b/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/ja4-tls-client-fingerprinting.toml @@ -1,4 +1,4 @@ -target = "https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#ja4-tls-client-fingerprinting" +target = "https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#ja4-tls-client-fingerprinting" # # JA4: TLS Client Fingerprinting # diff --git a/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/number-of-ciphers.toml b/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/number-of-ciphers.toml similarity index 85% rename from compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/number-of-ciphers.toml rename to compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/number-of-ciphers.toml index c48c1f5de1c..61fe0f49be5 100644 --- a/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/number-of-ciphers.toml +++ b/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/number-of-ciphers.toml @@ -1,4 +1,4 @@ -target = "https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#number-of-ciphers" +target = "https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-ciphers" # ### Number of Ciphers: # diff --git a/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/number-of-extensions.toml b/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/number-of-extensions.toml similarity index 71% rename from compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/number-of-extensions.toml rename to compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/number-of-extensions.toml index 5523938de3a..e855057e5c7 100644 --- a/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/number-of-extensions.toml +++ b/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/number-of-extensions.toml @@ -1,4 +1,4 @@ -target = "https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#number-of-extensions" +target = "https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-extensions" # ### Number of Extensions: # diff --git a/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/quic-and-dtls.toml b/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/quic-and-dtls.toml new file mode 100644 index 00000000000..7337a87251d --- /dev/null +++ b/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/quic-and-dtls.toml @@ -0,0 +1,19 @@ +target = "https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#quic-and-dtls" + +# ### QUIC and DTLS: +# “q”, "d" or “t”, denotes whether the hello packet is for QUIC, DTLS, or normal TLS. +# +# https://en.wikipedia.org/wiki/QUIC +# QUIC is the protocol which the new HTTP/3 standard utilizes, encapsulating TLS 1.3 into UDP packets. As QUIC was developed by Google, if an organization heavily utilizes Google products, QUIC could make up half of their network traffic, so this is important to capture. +# +# https://en.wikipedia.org/wiki/Datagram_Transport_Layer_Security +# DTLS is a version of TLS that can operate over UDP or SCTP. +# +# If the protocol is QUIC then the first character of the fingerprint is “q”, if DTLS it is "d", else it is “t”. +# + +[[spec]] +level = "MUST" +quote = ''' +If the protocol is QUIC then the first character of the fingerprint is “q”, if DTLS it is "d", else it is “t”. +''' diff --git a/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/raw-output.toml b/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/raw-output.toml similarity index 90% rename from compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/raw-output.toml rename to compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/raw-output.toml index 3dc443bbbb0..3c8c0c2ceca 100644 --- a/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/raw-output.toml +++ b/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/raw-output.toml @@ -1,4 +1,4 @@ -target = "https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#raw-output" +target = "https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#raw-output" # ### Raw Output # diff --git a/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/sni.toml b/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/sni.toml similarity index 83% rename from compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/sni.toml rename to compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/sni.toml index 7b8b6689a9e..53c7d2ba13c 100644 --- a/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/sni.toml +++ b/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/sni.toml @@ -1,4 +1,4 @@ -target = "https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#sni" +target = "https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#sni" # ### SNI: # diff --git a/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/tls-version.toml b/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/tls-and-dtls-version.toml similarity index 52% rename from compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/tls-version.toml rename to compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/tls-and-dtls-version.toml index 1eaa4c93720..fc91125595c 100644 --- a/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/tls-version.toml +++ b/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4/tls-and-dtls-version.toml @@ -1,16 +1,17 @@ -target = "https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#tls-version" +target = "https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version" -# ### TLS Version: -# -# TLS version is shown in 3 different places. If extension 0x002b exists (supported_versions), then the version is the highest value in the extension. Remember to ignore GREASE values. If the extension doesn’t exist, then the TLS version is the value of the Protocol Version. Handshake version (located at the top of the packet) should be ignored. +# ### TLS and DTLS Version: +# The TLS version is shown in 3 different places. If extension 0x002b exists (supported_versions), then the version is the highest value in the extension. Remember to ignore GREASE values. If the extension doesn’t exist, then the TLS version is the value of the Protocol Version. Handshake version (located at the top of the packet) should be ignored. # # 0x0304 = TLS 1.3 = “13” # 0x0303 = TLS 1.2 = “12” # 0x0302 = TLS 1.1 = “11” # 0x0301 = TLS 1.0 = “10” # 0x0300 = SSL 3.0 = “s3” -# 0x0200 = SSL 2.0 = “s2” -# 0x0100 = SSL 1.0 = “s1” +# 0x0002 = SSL 2.0 = “s2” +# 0xfeff = DTLS 1.0 = "d1" +# 0xfefd = DTLS 1.2 = "d2" +# 0xfefc = DTLS 1.3 = "d3" # # Unknown = “00” # diff --git a/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/alpn-extension-value.toml b/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/alpn-extension-value.toml deleted file mode 100644 index 2f716e6d596..00000000000 --- a/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/alpn-extension-value.toml +++ /dev/null @@ -1,26 +0,0 @@ -target = "https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#alpn-extension-value" - -# ### ALPN Extension Value: -# -# The first and last characters of the ALPN (Application-Layer Protocol Negotiation) first value. -# List of possible ALPN Values (scroll down): https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml -# -# -# -# In the above example, the first ALPN value is h2 so the first and last characters to use in the fingerprint are “h2”. IF the first ALPN listed was http/1.1 then the first and last characters to use in the fingerprint would be “h1”. -# -# In Wireshark this field is located under tls.handshake.extensions_alpn_str -# -# If there are no ALPN values or no ALPN extension then we print “00” as the value in the fingerprint. -# -[[spec]] -level = "MUST" -quote = ''' -The first and last characters of the ALPN (Application-Layer Protocol Negotiation) first value. -''' - -[[spec]] -level = "MUST" -quote = ''' -If there are no ALPN values or no ALPN extension then we print “00” as the value in the fingerprint. -''' diff --git a/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/quic.toml b/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/quic.toml deleted file mode 100644 index 7509e2e7019..00000000000 --- a/compliance/specs/raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4/quic.toml +++ /dev/null @@ -1,15 +0,0 @@ -target = "https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#quic" - -# ### QUIC: -# -# https://en.wikipedia.org/wiki/QUIC -# “q” or “t”, which denotes whether the hello packet is for QUIC or TCP. QUIC is the protocol which the new HTTP/3 standard utilizes, encapsulating TLS 1.3 into UDP packets. As QUIC was developed by Google, if an organization heavily utilizes Google products, QUIC could make up half of their network traffic, so this is important to capture. -# -# If the protocol is QUIC then the first character of the fingerprint is “q” if not, it’s “t”. -# - -[[spec]] -level = "MUST" -quote = ''' -If the protocol is QUIC then the first character of the fingerprint is “q” if not, it’s “t”. -''' diff --git a/tests/pcap/data/no_extensions.pcap b/tests/pcap/data/no_extensions.pcap new file mode 100644 index 0000000000000000000000000000000000000000..58a5ce0bc8c0413fec0b0c7f6a26c22cf04e97fc GIT binary patch literal 3089 zcmaKu3p7;w9>@QC_82pRG+ud+XAhLkjH{TKgyb|Uc@+*W<5fr=v9c&Rm-KLYD0PZP zNgcu|+$oP3r+Yn*8hLbxlpK@WIg>~!#oc=|=ct4KTK~0X|M#rV{{Ft-|Npml=l!xP z1V99T4I+Th3qhnPq{5O4oYCLdoIl9G0RR?xI~%Y`%sH>9fDzLj9kAY832sTs*;fD>>j@$)yW)`9+syt)A}_Tqf5wb zDA^8u5;o@#Tr-pGf@_YEkmodqq4eC8cyzI3szLngrcxHf8fL_@(zs1 zc(ou=6(yGbNgTn5Dlh~NqXs9#FvI48SlP9@>6BhcjBtE9Gx9Av^tV>E;oZD<_GebP zYw3XTLA_1kTW#MIzg8~emb+)YN&4AyUqlz>{w>iOv-m8)6i@_;0%L)xz(jxu^aZN~ zRDqJfK(HE^f*gcn1dtW#;q5{KU2LS*M43mD}Q(f|C zkI3PN^1#A_b`Bk0J8m2KHGCXe1nFJM_xU`)VXZWyAo zLe89tn3-E3O!E!M25(h)b901&SR&?g3y9{I1UW=zmOzXM4hTU~A-(yJgroqR3dsX> zok~cBAW)Y;n^tV%D(p&6mTPw+M~YAB=4&LG4Rq{m_WaKNsQHkA7I&1huPD+wxx&AU zsT)vI-u?Mra-7|;0WTt>=SGmTenyhWs8ifxGZ^1oJ(MK=L-DTAxWRBZv4Hz2w7Dp6 z*@-gJnA@%x$Eez#?v+MPpwpIyF5XWxSLk2EkAZ%q|If8 z+?&1>ztz>#Oz3rbEU7<-A$}?!?vW!?7x%~WcG^fQy?tCw`0+o(DQ^QxHGQw;-y9xm z%6{qa`AuC|e)8e>A?fjk8>MQtWu48mEljhh+&&W4Xyg#*ruJ)QqK;zbNp}rq^=oFD zw`P_16ahU!Q2s8&6H!8K)1nbk|57OtQQUxtu&CHLLW7iqcQg;lVL=_N(50KI>fa>7olWr|Xyb-lK7&-a}wRtmpf8 zO(FDy?duGVEA3Bk-+v_c)$~J1Bl|+udi|N}w@>L9%Uh{ASV$i!S?05|*6v?SZ9gt_ zjQX_eLY<5al$EZkBM7?MT5~hb?y6n~O~du1vi;FhxrO^33Mr?aHS_&FNDfks8M|bE zjP39HzW>}eTYgeMKV53Ypmm(<*zIghQmo(!12nfxrj>1MIKul{oF0w z_a6l-4&2CnbbCb+Wb9bY8S)o8I+_2P@-Z=H{NvU$qaoQ<%PqPB2i+}C7ty$WE$fs` z<3YIjDu3%&DlU}_-=vxl-K;VSF%wRv*omX>a9^Z;Ok15S+NSmmw6Gp2)8HP7}f#RN% zOy}~G4!7(3HO>mHyZqI$xLCbQ#l>&M+x$C;xwCEllUXUx`ootm#Ptf2Ht&KGGtr!| zIe*|U#6x6vd;{o9h_f5O`UAcJuoq&iBrc4&UO@@nTM&fpFm~f5`_4qis23BDl=)3* zwCen$X+!^ZzR1ZAzjN`bu5QbY8#Pg)`rB{!9xz)SzaGWlOizq-;1KAT~eZ9b-Kx(XMWLlQ>^r|;}Y`Yif=A^p`{^=2*tz| zqvDo0j2?IvkrHvUS+MNUERq&0*Q<{T63|$&Ie*~k?k0QUM9T$DmrmKwRifp#5 zZPaF8BS+mT@rqls_n(3AxOT5_#?VFVe7vCVMBDD(&CfoFKKCRfv$wc*0uEMK;gT}= z4{+COB{Jr6Oe*EE4x}t5!*;y*;h-ovkx1xNJ9#U9;9J@z-(j1gd&Ja?T87pLr!ioZ z!}~^bYi+FsA?(AYLzs*wsEoz^Q>KVe|?0ssI2 literal 0 HcmV?d00001 diff --git a/tests/pcap/tests/s2n_client_hellos.rs b/tests/pcap/tests/s2n_client_hellos.rs index d814ffb4ba0..ae913662192 100644 --- a/tests/pcap/tests/s2n_client_hellos.rs +++ b/tests/pcap/tests/s2n_client_hellos.rs @@ -87,10 +87,15 @@ fn ja4_fingerprints() -> Result<()> { .ja4_string() .expect("pcap did not contain ja4 string"); - // Handle known issues: - // tshark currently doesn't handle special alpn characters correctly - // TODO: remove this when tshark updates - let exceptions = [("-+", "2b"), ("__", "5f")]; + // Handle known tshark issues. + // TODO: remove these when tshark updates + let exceptions = [ + // tshark currently doesn't handle special alpn characters correctly + ("-+", "2b"), + ("__", "5f"), + // tshark currently doesn't handle empty hashes correctly + ("e3b0c44298fc", "000000000000"), + ]; for (a, b) in exceptions { tshark_hash = tshark_hash.replace(a, b); tshark_str = tshark_str.replace(a, b); diff --git a/tests/unit/s2n_fingerprint_ja4_test.c b/tests/unit/s2n_fingerprint_ja4_test.c index cdac483c9bf..df533fc3b13 100644 --- a/tests/unit/s2n_fingerprint_ja4_test.c +++ b/tests/unit/s2n_fingerprint_ja4_test.c @@ -242,10 +242,10 @@ int main(int argc, char **argv) { /* Test protocol * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#quic + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#quic-and-dtls *= type=test - *# If the protocol is QUIC then the first character of the fingerprint is “q” - *# if not, it’s “t”. + *# If the protocol is QUIC then the first character of the fingerprint + *# is “q”, if DTLS it is "d", else it is “t”. */ { /* Test QUIC */ @@ -278,7 +278,7 @@ int main(int argc, char **argv) /* Test destination * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#sni + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#sni *= type=test *# If the SNI extension (0x0000) exists, then the destination of the connection *# is a domain, or “d” in the fingerprint. @@ -321,7 +321,7 @@ int main(int argc, char **argv) const char *str; } test_cases[] = { /** - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#tls-version + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version *= type=test *# 0x0304 = TLS 1.3 = “13” *# 0x0303 = TLS 1.2 = “12” @@ -333,16 +333,15 @@ int main(int argc, char **argv) { .bytes = { 0x03, 0x02 }, .version = S2N_TLS11, .str = "11" }, { .bytes = { 0x03, 0x01 }, .version = S2N_TLS10, .str = "10" }, /** - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#tls-version + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version *= type=test *# 0x0300 = SSL 3.0 = “s3” - *# 0x0200 = SSL 2.0 = “s2” - *# 0x0100 = SSL 1.0 = “s1” + *# 0x0002 = SSL 2.0 = “s2” */ { .bytes = { 0x03, 0x00 }, .version = S2N_SSLv3, .str = "s3" }, - { .bytes = { 0x02, 0x00 }, .version = S2N_SSLv2, .str = "s2" }, - { .bytes = { 0x01, 0x00 }, .version = 10, .str = "s1" }, + { .bytes = { 0x00, 0x02 }, .version = S2N_SSLv2, .str = "s2" }, /* Bad values */ + { .bytes = { 0x01, 0x00 }, .version = 10, .str = "00" }, { .bytes = { 0x00, 0x00 }, .version = 0, .str = "00" }, { .bytes = { 0x00, 0xFF }, .version = UINT8_MAX, .str = "00" }, { .bytes = { 0xFF, 0xFF }, .version = UINT8_MAX, .str = "00" }, @@ -351,7 +350,7 @@ int main(int argc, char **argv) for (size_t i = 0; i < s2n_array_len(test_cases); i++) { /* Test record version not used. * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#tls-version + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version *= type=test *# Handshake version (located at the top of the packet) should be ignored. */ @@ -379,7 +378,7 @@ int main(int argc, char **argv) /* Test version from extension * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#tls-version + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version *= type=test *# If extension 0x002b exists (supported_versions), then the version *# is the highest value in the extension. @@ -409,7 +408,7 @@ int main(int argc, char **argv) /* Test version from legacy field * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#tls-version + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version *= type=test *# If the extension doesn’t exist, then the TLS version is the value *# of the Protocol Version. @@ -437,7 +436,7 @@ int main(int argc, char **argv) /* Test with grease values * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#tls-version + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version *= type=test *# Remember to ignore GREASE values. */ @@ -480,7 +479,7 @@ int main(int argc, char **argv) const char *str; } count_test_cases[] = { /** - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#number-of-ciphers + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-ciphers *= type=test *# if there’s 6 cipher suites in the hello packet, then the *# value should be “06”. @@ -490,7 +489,7 @@ int main(int argc, char **argv) { .size = 50, .str = "50" }, { .size = 99, .str = "99" }, /** - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#number-of-ciphers + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-ciphers *= type=test *# If there’s > 99, which there should never be, then output “99”. */ @@ -502,7 +501,7 @@ int main(int argc, char **argv) { /* Test basic cipher suite list * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#number-of-ciphers + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-ciphers *= type=test *# 2 character number of cipher suites, so if there’s 6 cipher suites *# in the hello packet, then the value should be “06”. @@ -520,7 +519,7 @@ int main(int argc, char **argv) /* Test with grease values * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#number-of-ciphers + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-ciphers *= type=test *# Remember, ignore GREASE values. They don’t count. */ @@ -554,7 +553,7 @@ int main(int argc, char **argv) /* Test number of extensions * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#number-of-extensions + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-extensions *= type=test *# Same as counting ciphers. */ @@ -573,7 +572,7 @@ int main(int argc, char **argv) /* Test with grease values * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#number-of-extensions + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-extensions *= type=test *# Ignore GREASE. */ @@ -606,7 +605,7 @@ int main(int argc, char **argv) /* Ensure SNI and ALPN are included * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#number-of-extensions + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-extensions *= type=test *# Include SNI and ALPN. */ @@ -638,15 +637,15 @@ int main(int argc, char **argv) { /* Test basic ALPN * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#alpn-extension-value + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#alpn-extension-value *= type=test - *# The first and last characters of the ALPN (Application-Layer - *# Protocol Negotiation) first value. + *# The first and last alphanumeric characters of the ALPN + *# (Application-Layer Protocol Negotiation) first value. */ { /* 2 characters: h2 * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#alpn-extension-value + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#alpn-extension-value *= type=test *# In the above example, the first ALPN value is h2 so the first *# and last characters to use in the fingerprint are “h2”. @@ -678,9 +677,9 @@ int main(int argc, char **argv) /* More than 2 characters: "http/1.1" * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#alpn-extension-value + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#alpn-extension-value *= type=test - *# IF the first ALPN listed was http/1.1 then the first and last + *# If the first ALPN listed was http/1.1 then the first and last *# characters to use in the fingerprint would be “h1”. */ { @@ -709,7 +708,13 @@ int main(int argc, char **argv) }; }; - /* Test 1-byte alpn value */ + /* Test 1-byte alpn value + * + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#alpn-extension-value + *= type=test + *# If the first ALPN value is only a single character, then that character + *# is treated as both the first and last character. + */ { S2N_INIT_CLIENT_HELLO(client_hello_bytes, S2N_TEST_CLIENT_HELLO_VERSION, @@ -736,8 +741,12 @@ int main(int argc, char **argv) /* Test non-ascii alpn values * - * The spec does not currently define this case, but will be updated in the - * future according to https://github.com/FoxIO-LLC/ja4/issues/148 + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#alpn-extension-value + *= type=test + *# If the first or last byte of the first ALPN is non-alphanumeric + *# (meaning not `0x30-0x39`, `0x41-0x5A`, or `0x61-0x7A`), then we + *# print the first and last characters of the hex representation of + *# the first ALPN instead. */ { struct { @@ -745,10 +754,29 @@ int main(int argc, char **argv) const size_t bytes_size; const char *str; } test_cases[] = { + /** + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#alpn-extension-value + *= type=test + *# For example: + *# * `0xAB` would be printed as "ab" + *# * `0xAB 0xCD` would be printed as "ad" + *# * `0x30 0xAB` would be printed as "3b" + *# * `0x30 0x31 0xAB 0xCD` would be printed as "3d" + */ { .bytes = { 0xAB }, .str = "ab", .bytes_size = 1 }, { .bytes = { 0xAB, 0xCD }, .str = "ad", .bytes_size = 2 }, { .bytes = { 0x30, 0xAB }, .str = "3b", .bytes_size = 2 }, { .bytes = { 0x30, 0x31, 0xAB, 0xCD }, .str = "3d", .bytes_size = 4 }, + /** + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#alpn-extension-value + *= type=test + *# * `0x30 0xAB 0xCD 0x31` would be printed as "01" + * + * Why is this value "01" instead of "31"? + * Because 0x30 and 0x31 are both alphanumeric, so we use their + * ascii values ('0' and '1') rather than their hex values. + * It doesn't matter than 0xAB and 0xCD aren't alphanumeric. + */ { .bytes = { 0x30, 0xAB, 0xCD, 0x31 }, .str = "01", .bytes_size = 4 }, }; @@ -791,10 +819,10 @@ int main(int argc, char **argv) /* Test no ALPN * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#alpn-extension-value + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#alpn-extension-value *= type=test - *# If there are no ALPN values or no ALPN extension then we print - *# “00” as the value in the fingerprint. + *# If there is no ALPN extension, no ALPN values, or the first ALPN + *# value is empty, then we print "00" as the value in the fingerprint. */ { /* Test no ALPN extension */ @@ -834,7 +862,7 @@ int main(int argc, char **argv) EXPECT_EQUAL(output[S2N_JA4_A_ALPN_LAST], '0'); }; - /* Test empty / invalid alpn value */ + /* Test empty first alpn value */ { S2N_INIT_CLIENT_HELLO(client_hello_bytes, S2N_TEST_CLIENT_HELLO_VERSION, @@ -872,7 +900,7 @@ int main(int argc, char **argv) 0x00, 30, /* cipher suites * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#cipher-hash + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#cipher-hash *= type=test *# Example: *# ``` @@ -880,13 +908,13 @@ int main(int argc, char **argv) */ 0x13, 0x01, 0x13, 0x02, 0x13, 0x03, 0xc0, 0x2b, 0xc0, 0x2f, /* - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#cipher-hash + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#cipher-hash *= type=test *# c02c,c030,cca9,cca8,c013, */ 0xc0, 0x2c, 0xc0, 0x30, 0xcc, 0xa9, 0xcc, 0xa8, 0xc0, 0x13, /* - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#cipher-hash + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#cipher-hash *= type=test *# c014,009c,009d,002f,0035 *# ``` @@ -899,7 +927,7 @@ int main(int argc, char **argv) /* Test raw list * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#cipher-hash + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#cipher-hash *= type=test *# The list is created using the 4 character hex values of the ciphers, *# lower case, comma delimited, ignoring GREASE. @@ -907,7 +935,7 @@ int main(int argc, char **argv) { /* Expected result from docs * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#cipher-hash + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#cipher-hash *= type=test *# 002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9 */ @@ -928,7 +956,7 @@ int main(int argc, char **argv) /* Test hashed list * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#cipher-hash + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#cipher-hash *= type=test *# A 12 character truncated sha256 hash of the list of ciphers sorted *# in hex order, first 12 characters. @@ -936,7 +964,7 @@ int main(int argc, char **argv) { /* Expected result from docs * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#cipher-hash + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#cipher-hash *= type=test *# = 8daaf6152771 */ @@ -954,9 +982,56 @@ int main(int argc, char **argv) EXPECT_BYTEARRAY_EQUAL(output_b, expected, strlen(expected)); }; + /* Test empty list + * + * s2n-tls treats an empty cipher suite list as a parsing error, since + * the RFC requires it to contain at least one cipher suite. The definition + * of the field is: + * CipherSuite cipher_suites<2..2^16-2>; + * Meaning that the list must contain at least two bytes, which means + * at least one CipherSuite. + * + * However, s2n-tls could later decide to error on empty cipher suite lists + * when processing the ClientHello instead of when parsing it, so we + * should still test this case by manipulating the parsed ClientHello + * before fingerprinting it. + * + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#cipher-hash + *= type=test + *# If there are no ciphers in the sorted cipher list, then the value of + *# JA4_b is set to `000000000000` + */ + { + const char expected[] = "000000000000"; + + uint8_t output[S2N_TEST_OUTPUT_SIZE] = { 0 }; + uint32_t output_size = 0; + + DEFER_CLEANUP(struct s2n_client_hello *client_hello = NULL, s2n_client_hello_free); + client_hello = s2n_client_hello_parse_message(client_hello_bytes, sizeof(client_hello_bytes)); + EXPECT_NOT_NULL(client_hello); + + /* Override the size of the parsed cipher_suites to imitate having + * read an empty list. + */ + client_hello->cipher_suites.size = 0; + + DEFER_CLEANUP(struct s2n_fingerprint *fingerprint = NULL, s2n_fingerprint_free); + fingerprint = s2n_fingerprint_new(S2N_FINGERPRINT_JA4); + EXPECT_NOT_NULL(fingerprint); + EXPECT_SUCCESS(s2n_fingerprint_set_client_hello(fingerprint, client_hello)); + EXPECT_SUCCESS(s2n_fingerprint_get_hash(fingerprint, + sizeof(output), output, &output_size)); + + EXPECT_TRUE(output_size > S2N_JA4_B_START); + uint8_t *output_b = &output[S2N_JA4_B_START]; + EXPECT_TRUE(output_size >= S2N_JA4_B_START + strlen(expected)); + EXPECT_BYTEARRAY_EQUAL(output_b, expected, strlen(expected)); + }; + /* Test sorting * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#cipher-hash + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#cipher-hash *= type=test *# list of ciphers sorted in hex order */ @@ -1061,7 +1136,7 @@ int main(int argc, char **argv) 0x00, (16 * 4) + (9 * 2), /* extensions * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#extension-hash + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash *= type=test *# For example: *# ``` @@ -1078,7 +1153,7 @@ int main(int argc, char **argv) /* signature algorithms size */ 0x00, 8 * 2, /* signature algorithms - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#extension-hash + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash *= type=test *# For example the signature algorithms: *# ``` @@ -1089,7 +1164,7 @@ int main(int argc, char **argv) 0x08, 0x05, 0x05, 0x01, 0x08, 0x06, 0x06, 0x01, /* more extensions * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#extension-hash + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash *= type=test *# 0005,0023,0012,002b,ff01,000b,000a,0015 *# ``` @@ -1107,7 +1182,7 @@ int main(int argc, char **argv) /* Expected raw extensions string from docs * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#extension-hash + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash *= type=test *# 0005,000a,000b,000d,0012,0015,0017,001b,0023,002b,002d,0033,4469,ff01 */ @@ -1119,7 +1194,7 @@ int main(int argc, char **argv) * Instead of actually testing this, we just test that the known value * will test that SNI and ALPN are ignored. * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#extension-hash + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash *= type=test *# Ignore the SNI extension (0000) and the ALPN extension (0010) *# as we’ve already captured them in the _a_ section of the fingerprint. @@ -1130,7 +1205,7 @@ int main(int argc, char **argv) /* Known value from docs, in string form * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#extension-hash + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash *= type=test *# For example: *# ``` @@ -1144,7 +1219,7 @@ int main(int argc, char **argv) /* Expected result from docs * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#extension-hash + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash *= type=test *# 0005,000a,000b,000d,0012,0015,0017,001b,0023,002b,002d,0033,4469,ff01 */ @@ -1154,7 +1229,7 @@ int main(int argc, char **argv) /* Test raw extension list * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#extension-hash + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash *= type=test *# The extension list is created using the 4 character hex values of the extensions, *# lower case, comma delimited, sorted (not in the order they appear). @@ -1176,7 +1251,7 @@ int main(int argc, char **argv) /* Test raw signature algorithms list * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#extension-hash + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash *= type=test *# The signature algorithm hex values are then added to the end of the *# list in the order that they appear (not sorted) with an underscore @@ -1185,7 +1260,7 @@ int main(int argc, char **argv) { /* Expected result from docs * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#extension-hash + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash *= type=test *# Are added to the end of the previous string to create: *# ``` @@ -1210,7 +1285,7 @@ int main(int argc, char **argv) /* Test hashed lists * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#extension-hash + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash *= type=test *# A 12 character truncated sha256 hash of the list of extensions, *# sorted by hex value, followed by the list of signature algorithms, @@ -1219,7 +1294,7 @@ int main(int argc, char **argv) { /* Expected result from docs * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#extension-hash + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash *= type=test *# Hashed to: *# ``` @@ -1246,7 +1321,7 @@ int main(int argc, char **argv) /* Test with no signature schemes * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#extension-hash + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash *= type=test *# If there are no signature algorithms in the hello packet, *# then the string ends without an underscore and is hashed. @@ -1304,6 +1379,28 @@ int main(int argc, char **argv) EXPECT_EQUAL(underscore_count, 2); }; }; + + /* Test empty extension list + * + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash + *= type=test + *# If there are no extensions in the sorted extensions list, then the + *# value of JA4_c is set to `000000000000` + */ + { + const char expected[] = "000000000000"; + + uint8_t output[S2N_TEST_OUTPUT_SIZE] = { 0 }; + uint32_t output_size = 0; + EXPECT_OK(s2n_test_ja4_hash_from_bytes( + minimal_client_hello_bytes, sizeof(minimal_client_hello_bytes), + sizeof(output), output, &output_size)); + + EXPECT_TRUE(output_size > S2N_JA4_C_HASH_START); + uint8_t *output_c = &output[S2N_JA4_C_HASH_START]; + EXPECT_TRUE(output_size >= S2N_JA4_C_HASH_START + strlen(expected)); + EXPECT_BYTEARRAY_EQUAL(output_c, expected, strlen(expected)); + }; }; /* Test with malformed extensions diff --git a/tls/s2n_fingerprint.c b/tls/s2n_fingerprint.c index 69351e07d77..02c585969db 100644 --- a/tls/s2n_fingerprint.c +++ b/tls/s2n_fingerprint.c @@ -184,7 +184,7 @@ static S2N_RESULT s2n_assert_grease_value(uint16_t val) } /** - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#details + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#details *# The program needs to ignore GREASE values anywhere it sees them */ bool s2n_fingerprint_is_grease_value(uint16_t val) diff --git a/tls/s2n_fingerprint_ja4.c b/tls/s2n_fingerprint_ja4.c index 38bc03cc2c5..171e514a7d6 100644 --- a/tls/s2n_fingerprint_ja4.c +++ b/tls/s2n_fingerprint_ja4.c @@ -29,10 +29,10 @@ #define S2N_JA4_PART_DIV '_' /** - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#number-of-ciphers + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-ciphers *# 2 character number of cipher suites * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#number-of-extensions + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-extensions *# Same as counting ciphers. */ #define S2N_JA4_COUNT_SIZE 2 @@ -53,7 +53,7 @@ const char *s2n_ja4_version_strings[] = { /** - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#tls-version + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version *# 0x0304 = TLS 1.3 = “13” *# 0x0303 = TLS 1.2 = “12” *# 0x0302 = TLS 1.1 = “11” @@ -64,18 +64,16 @@ const char *s2n_ja4_version_strings[] = { [0x0302] = "11", [0x0301] = "10", /** - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#tls-version + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version *# 0x0300 = SSL 3.0 = “s3” - *# 0x0200 = SSL 2.0 = “s2” - *# 0x0100 = SSL 1.0 = “s1” + *# 0x0002 = SSL 2.0 = “s2” */ [0x0300] = "s3", - [0x0200] = "s2", - [0x0100] = "s1", + [0x0002] = "s2", }; /** - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#tls-version + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version *# Unknown = “00” */ #define S2N_JA4_UNKNOWN_STR "00" @@ -97,10 +95,29 @@ static int s2n_fingerprint_ja4_iana_compare(const void *a, const void *b) static S2N_RESULT s2n_fingerprint_ja4_digest(struct s2n_fingerprint_hash *hash, struct s2n_stuffer *out) { + RESULT_ENSURE_REF(hash); if (!s2n_fingerprint_hash_do_digest(hash)) { return S2N_RESULT_OK; } + /* Instead of hashing empty inputs, JA4 sets the output to a string of all zeroes. + * (Actually hashing an empty input doesn't produce a digest of all zeroes) + * + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#cipher-hash + *# If there are no ciphers in the sorted cipher list, then the value of + *# JA4_b is set to `000000000000` + * + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash + *# If there are no extensions in the sorted extensions list, then the value of + *# JA4_c is set to `000000000000` + */ + uint64_t bytes = 0; + RESULT_GUARD_POSIX(s2n_hash_get_currently_in_hash_total(hash->hash, &bytes)); + if (bytes == 0) { + RESULT_GUARD_POSIX(s2n_stuffer_write_str(out, "000000000000")); + return S2N_RESULT_OK; + } + uint8_t digest_bytes[SHA256_DIGEST_LENGTH] = { 0 }; struct s2n_blob digest = { 0 }; RESULT_GUARD_POSIX(s2n_blob_init(&digest, digest_bytes, sizeof(digest_bytes))); @@ -114,11 +131,11 @@ static S2N_RESULT s2n_fingerprint_ja4_digest(struct s2n_fingerprint_hash *hash, } /** - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#number-of-ciphers + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-ciphers *# 2 character number of cipher suites, so if there’s 6 cipher suites *# in the hello packet, then the value should be “06”. * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#number-of-extensions + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-extensions *# Same as counting ciphers. */ static S2N_RESULT s2n_fingerprint_ja4_count(struct s2n_blob *output, uint16_t count) @@ -126,10 +143,10 @@ static S2N_RESULT s2n_fingerprint_ja4_count(struct s2n_blob *output, uint16_t co RESULT_ENSURE_REF(output); /** - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#number-of-ciphers + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-ciphers *# If there’s > 99, which there should never be, then output “99”. * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#number-of-extensions + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-extensions *# Same as counting ciphers. */ count = MIN(count, 99); @@ -159,14 +176,14 @@ static S2N_RESULT s2n_fingerprint_get_extension_version(struct s2n_client_hello uint16_t version = 0; RESULT_GUARD_POSIX(s2n_stuffer_read_uint16(&supported_versions, &version)); /** - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#tls-version + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version *# Remember to ignore GREASE values. */ if (s2n_fingerprint_is_grease_value(version)) { continue; } /** - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#tls-version + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version *# If extension 0x002b exists (supported_versions), then the version is *# the highest value in the extension. */ @@ -181,7 +198,7 @@ static S2N_RESULT s2n_fingerprint_ja4_version(struct s2n_stuffer *output, uint16_t client_version = 0; if (s2n_result_is_error(s2n_fingerprint_get_extension_version(ch, &client_version))) { /** - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#tls-version + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version *# If the extension doesn’t exist, then the TLS version is the value of *# the Protocol Version. */ @@ -189,7 +206,7 @@ static S2N_RESULT s2n_fingerprint_ja4_version(struct s2n_stuffer *output, } /** - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#tls-version + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#tls-and-dtls-version *# Handshake version (located at the top of the packet) should be ignored. */ @@ -225,8 +242,9 @@ static S2N_RESULT s2n_client_hello_get_first_alpn(struct s2n_client_hello *ch, s } /** - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#alpn-extension-value - *# The first and last characters of the ALPN (Application-Layer Protocol Negotiation) first value. + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#alpn-extension-value + *# The first and last alphanumeric characters of the ALPN (Application-Layer + *# Protocol Negotiation) first value. */ static S2N_RESULT s2n_fingerprint_ja4_alpn(struct s2n_stuffer *output, struct s2n_client_hello *ch) @@ -237,9 +255,13 @@ static S2N_RESULT s2n_fingerprint_ja4_alpn(struct s2n_stuffer *output, } /** - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#alpn-extension-value - *# If there are no ALPN values or no ALPN extension then we print “00” - *# as the value in the fingerprint. + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#alpn-extension-value + *# If there is no ALPN extension, no ALPN values, or the first ALPN value + *# is empty, then we print "00" as the value in the fingerprint. + * + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#alpn-extension-value + *# If the first ALPN value is only a single character, then that character + *# is treated as both the first and last character. */ uint8_t first_char = '0', last_char = '0'; if (protocol.size > 0) { @@ -247,8 +269,11 @@ static S2N_RESULT s2n_fingerprint_ja4_alpn(struct s2n_stuffer *output, last_char = protocol.data[protocol.size - 1]; } - /* The spec does not currently define this case, but will be updated in the - * future according to https://github.com/FoxIO-LLC/ja4/issues/148 + /** + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#alpn-extension-value + *# If the first or last byte of the first ALPN is non-alphanumeric (meaning + *# not `0x30-0x39`, `0x41-0x5A`, or `0x61-0x7A`), then we print the first and + *# last characters of the hex representation of the first ALPN instead. */ if (!isalnum(first_char) || !isalnum(last_char)) { RESULT_GUARD(s2n_hex_digit((first_char >> 4), &first_char)); @@ -276,9 +301,11 @@ static S2N_RESULT s2n_fingerprint_ja4_a(struct s2n_fingerprint *fingerprint, RESULT_ENSURE_REF(fingerprint); /** - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#quic - *# If the protocol is QUIC then the first character of the fingerprint is “q” - *# if not, it’s “t”. + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#quic-and-dtls + *# If the protocol is QUIC then the first character of the fingerprint is “q”, + *# if DTLS it is "d", else it is “t”. + * + * s2n-tls only supports TLS and QUIC. DTLS is not supported. */ bool is_quic = false; RESULT_GUARD_POSIX(s2n_client_hello_has_extension(fingerprint->client_hello, @@ -289,7 +316,7 @@ static S2N_RESULT s2n_fingerprint_ja4_a(struct s2n_fingerprint *fingerprint, RESULT_GUARD(s2n_fingerprint_ja4_version(output, fingerprint->client_hello)); /** - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#sni + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#sni *# If the SNI extension (0x0000) exists, then the destination of the connection *# is a domain, or “d” in the fingerprint. *# If the SNI does not exist, then the destination is an IP address, or “i”. @@ -320,7 +347,7 @@ static S2N_RESULT s2n_fingerprint_ja4_a(struct s2n_fingerprint *fingerprint, } /** - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#cipher-hash + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#cipher-hash *# The list is created using the 4 character hex values of the ciphers, *# lower case, comma delimited, ignoring GREASE. */ @@ -339,7 +366,7 @@ static S2N_RESULT s2n_fingerprint_ja4_ciphers(struct s2n_fingerprint_hash *hash, uint16_t iana = 0; RESULT_GUARD_POSIX(s2n_stuffer_read_uint16(&cipher_suites, &iana)); /** - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#number-of-ciphers + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-ciphers *# Remember, ignore GREASE values. They don’t count. */ if (s2n_fingerprint_is_grease_value(iana)) { @@ -364,7 +391,7 @@ static S2N_RESULT s2n_fingerprint_ja4_ciphers(struct s2n_fingerprint_hash *hash, } /** - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#cipher-hash + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#cipher-hash *# A 12 character truncated sha256 hash of the list of ciphers sorted in hex order, *# first 12 characters. */ @@ -384,7 +411,7 @@ static S2N_RESULT s2n_fingerprint_ja4_b(struct s2n_fingerprint *fingerprint, } /** - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#extension-hash + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash *# The extension list is created using the 4 character hex values of the extensions, *# lower case, comma delimited, sorted (not in the order they appear). */ @@ -404,7 +431,7 @@ static S2N_RESULT s2n_fingerprint_ja4_extensions(struct s2n_fingerprint_hash *ha RESULT_GUARD(s2n_fingerprint_parse_extension(&extensions, &iana)); /** - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#number-of-extensions + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-extensions *# Ignore GREASE. */ if (s2n_fingerprint_is_grease_value(iana)) { @@ -413,11 +440,11 @@ static S2N_RESULT s2n_fingerprint_ja4_extensions(struct s2n_fingerprint_hash *ha /* SNI and ALPN are included in the extension count, but not in the extension list. * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#extension-hash + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash *# Ignore the SNI extension (0000) and the ALPN extension (0010) *# as we’ve already captured them in the _a_ section of the fingerprint. * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#number-of-extensions + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#number-of-extensions *# Include SNI and ALPN. */ (*extensions_count)++; @@ -486,7 +513,7 @@ static S2N_RESULT s2n_fingerprint_ja4_sig_algs(struct s2n_fingerprint_hash *hash } /** - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#extension-hash + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash *# A 12 character truncated sha256 hash of the list of extensions, sorted by *# hex value, followed by the list of signature algorithms, in the order that *# they appear (not sorted). @@ -502,12 +529,12 @@ static S2N_RESULT s2n_fingerprint_ja4_c(struct s2n_fingerprint *fingerprint, &fingerprint->workspace, &extensions_count_value)); /** - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#extension-hash + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash *# The signature algorithm hex values are then added to the end of the list *# in the order that they appear (not sorted) with an underscore delimiting *# the two lists. * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#extension-hash + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#extension-hash *# If there are no signature algorithms in the hello packet, *# then the string ends without an underscore and is hashed. * @@ -523,8 +550,8 @@ static S2N_RESULT s2n_fingerprint_ja4_c(struct s2n_fingerprint *fingerprint, /* JA4 fingerprints are basically of the form a_b_c: * - *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/v0.18.2/technical_details/JA4.md#ja4-algorithm - *# (QUIC=”q” or TCP=”t”) + *= https://raw.githubusercontent.com/FoxIO-LLC/ja4/df3c067/technical_details/JA4.md#ja4-algorithm + *# (QUIC=”q”, DTLS="d", or Normal TLS=”t”) *# (2 character TLS version) *# (SNI=”d” or no SNI=”i”) *# (2 character count of ciphers)