Skip to content

Commit e3da2ec

Browse files
Zainullin DamirZainullin Damir
authored andcommitted
Process plugibs - Introduce TLS process plugin
1 parent e0d85f5 commit e3da2ec

File tree

12 files changed

+810
-529
lines changed

12 files changed

+810
-529
lines changed

src/plugins/process/tls/CMakeLists.txt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,26 @@ project(ipfixprobe-process-tls VERSION 1.0.0 DESCRIPTION "ipfixprobe-process-tls
33
add_library(ipfixprobe-process-tls MODULE
44
src/tls.cpp
55
src/tls.hpp
6+
src/tlsContext.hpp
7+
src/tlsFields.hpp
68
src/md5.cpp
79
src/md5.hpp
810
src/sha256.hpp
11+
src/ja3.hpp
12+
src/ja4.hpp
913
)
1014

1115
set_target_properties(ipfixprobe-process-tls PROPERTIES
1216
CXX_VISIBILITY_PRESET hidden
1317
VISIBILITY_INLINES_HIDDEN YES
1418
)
1519

16-
target_include_directories(ipfixprobe-process-tls PRIVATE
20+
target_include_directories(ipfixprobe-process-tls PRIVATE
1721
${CMAKE_SOURCE_DIR}/include/
22+
${CMAKE_SOURCE_DIR}/include/ipfixprobe/processPlugin
23+
${CMAKE_SOURCE_DIR}/include/ipfixprobe/pluginFactory
1824
${CMAKE_SOURCE_DIR}/src/plugins/process/common
25+
${adaptmon_SOURCE_DIR}/lib/include/public/
1926
)
2027

2128
target_link_libraries(ipfixprobe-process-tls PRIVATE

src/plugins/process/tls/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# QUIC Plugin
2+
3+
The **QUIC Plugin** parses QUIC packets and exports extracted values.
4+
5+
## Output Fields
6+
7+
| Field Name | Data Type | Description |
8+
| -------------- | ------------------- | -------------------------------------------------------------------- |
9+
| `TLS_SNI` | `string` | Subject Name Indentifier (SNI) from the TLS handshake |
10+
| `TLS_JA3` | `string` | JA3 fingerprint of the TLS Client Hello |
11+
| `TLS_JA4` | `string` | JA4 fingerprint of the TLS Client Hello |
12+
| `TLS_ALPN` | `string` | Application-Layer Protocol Negotiation (ALPN) from the TLS handshake |
13+
| `TLS_VERSION` | `uint16_t` | TLS version used in the connection |
14+
| `TLS_EXT_TYPE` | `array of uint16_t` | Types of TLS extensions in the Client Hello |
15+
| `TLS_EXT_LEN` | `array of uint16_t` | Lengths of TLS extensions in the Client Hello |
16+
| `TLS_EXT` | `array of bytes` | Payload of TLS extensions in the Client Hello |
17+
18+
## Usage
19+
20+
### YAML Configuration
21+
22+
Add the plugin to your ipfixprobe YAML configuration:
23+
24+
```yaml
25+
process_plugins:
26+
- tls
27+
```
28+
29+
### CLI Usage
30+
31+
You can also enable the plugin directly from the command line:
32+
33+
`ipfixprobe -p tls ...`
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* @file
3+
* @brief JA3 fingerprint generation for TLS ClientHello messages.
4+
* @author Damir Zainullin <[email protected]>
5+
* @date 2025
6+
*
7+
* Provides a class that generates JA3 fingerprints for TLS ClientHello messages.
8+
*
9+
* @copyright Copyright (c) 2025 CESNET, z.s.p.o.
10+
*/
11+
#pragma once
12+
13+
#include "md5.hpp"
14+
#include "tlsContext.hpp"
15+
16+
#include <charconv>
17+
18+
#include <boost/static_string.hpp>
19+
#include <tlsParser/tlsParser.hpp>
20+
#include <utils/stringUtils.hpp>
21+
22+
namespace ipxp::process::tls {
23+
24+
/*
25+
constexpr static
26+
std::string concatenateJA3(auto&& inputRange, auto&& buffer) noexcept
27+
{
28+
std::array<char, 20> tmp;
29+
concatenateRangeTo(inputRange | std::views::transform([](const auto& value) {
30+
return std::to_string(value);
31+
}), buffer, '-');
32+
if (vector.empty()) {
33+
return "";
34+
}
35+
return std::accumulate(
36+
std::next(vector.begin()),
37+
vector.end(),
38+
std::to_string(vector[0]),
39+
[](const std::string& a, uint16_t b) { return a + "-" + std::to_string(b); });
40+
}*/
41+
42+
/**
43+
* @class JA3
44+
* @brief Generates JA3 fingerprint for TLS ClientHello messages.
45+
*
46+
* The JA3 class constructs a JA3 fingerprint string based on the provided
47+
* TLS ClientHello parameters, including protocol type, version, server names,
48+
* ALPNs, cipher suites, extension types, signature algorithms, and supported versions.
49+
*/
50+
class JA3 {
51+
public:
52+
JA3(const uint16_t version,
53+
std::span<const uint16_t> cipherSuites,
54+
std::span<const uint16_t> extensionsTypes,
55+
std::span<const uint16_t> supportedGroups,
56+
std::span<const uint8_t> pointFormats)
57+
{
58+
constexpr std::size_t bufferSize = 512;
59+
boost::static_string<bufferSize> result;
60+
61+
pushBackWithDelimiter(std::to_string(version), result, ',');
62+
63+
auto cipherSuitesRange = cipherSuites | integerToCharPtrView;
64+
concatenateRangeTo(cipherSuitesRange, result, '-', ',');
65+
66+
auto extensionsTypesRange = extensionsTypes
67+
| std::views::filter(std::not_fn(TLSParser::isGreaseValue)) | integerToCharPtrView;
68+
concatenateRangeTo(extensionsTypesRange, result, '-', ',');
69+
70+
auto supportedGroupsRange = supportedGroups
71+
| std::views::filter(std::not_fn(TLSParser::isGreaseValue)) | integerToCharPtrView;
72+
concatenateRangeTo(supportedGroupsRange, result, '-', ',');
73+
74+
auto pointFormatsRange = pointFormats | integerToCharPtrView;
75+
concatenateRangeTo(pointFormatsRange, result, '-');
76+
77+
md5_get_bin(std::string_view(result.data(), result.size()), hash.data());
78+
}
79+
80+
std::string_view getHash() const noexcept { return std::string_view(hash.data(), hash.size()); }
81+
82+
private:
83+
constexpr static std::size_t JA3_SIZE = 16;
84+
std::array<char, JA3_SIZE> hash;
85+
};
86+
87+
} // namespace ipxp::process::tls
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
/**
2+
* @file
3+
* @brief JA4 fingerprint generation for TLS ClientHello messages.
4+
* @author Damir Zainullin <[email protected]>
5+
* @date 2025
6+
*
7+
* Provides a class that generates JA4 fingerprints for TLS ClientHello messages.
8+
*
9+
* @copyright Copyright (c) 2025 CESNET, z.s.p.o.
10+
*/
11+
12+
#pragma once
13+
14+
#include "sha256.hpp"
15+
#include "tlsContext.hpp"
16+
17+
#include <bit>
18+
#include <charconv>
19+
#include <format>
20+
21+
#include <boost/static_string.hpp>
22+
#include <tlsParser/tlsParser.hpp>
23+
#include <utils/stringUtils.hpp>
24+
#include <utils/stringViewUtils.hpp>
25+
26+
namespace ipxp::process::tls {
27+
28+
constexpr std::size_t TRUNC_SIZE = 12;
29+
30+
constexpr static std::string_view toLabel(const uint16_t version) noexcept
31+
{
32+
switch (version) {
33+
case 0x0304:
34+
return "13";
35+
case 0x0303:
36+
return "12";
37+
case 0x0302:
38+
return "11";
39+
case 0x0301:
40+
return "10";
41+
case 0x0300:
42+
return "s3";
43+
case 0x0002:
44+
return "s2";
45+
case 0xfeff:
46+
return "d1";
47+
case 0xfefd:
48+
return "d2";
49+
case 0xfefc:
50+
return "d3";
51+
default:
52+
return "00";
53+
}
54+
}
55+
56+
constexpr static std::string_view
57+
getVersionLabel(std::span<const uint16_t> supportedVersions, const TLSHandshake& handshake) noexcept
58+
{
59+
if (supportedVersions.empty()) {
60+
return toLabel(std::bit_cast<uint16_t>(handshake.version));
61+
}
62+
63+
return toLabel(*std::ranges::max_element(supportedVersions));
64+
}
65+
66+
constexpr static char alpnByteToLabel(char byte, bool isHighNibble)
67+
{
68+
if (std::isalnum(byte)) {
69+
return byte;
70+
}
71+
72+
const uint8_t nibble = isHighNibble ? byte >> 4 : byte & 0x0F;
73+
return nibble < 0xA ? ('0' + nibble) : ('A' + nibble - 0xA);
74+
}
75+
76+
static std::string_view getALPNLabel(std::span<const std::string_view> alpns)
77+
{
78+
std::string alpn_label;
79+
if (alpns.empty() || alpns[0].empty()) {
80+
return "00";
81+
}
82+
83+
static std::array<char, 2> buffer;
84+
std::string_view alpn = alpns[0];
85+
buffer[0] = alpnByteToLabel(alpn[0], true);
86+
buffer[1] = alpnByteToLabel(alpn.back(), false);
87+
88+
return std::string_view(buffer.data(), buffer.size());
89+
}
90+
91+
constexpr static inline auto rangeToHexString
92+
= std::views::transform([](const auto& value) mutable {
93+
static std::array<char, 6> buffer;
94+
auto end = std::format_to(buffer.begin(), "{:04x},", value);
95+
return std::string_view(buffer.begin(), end);
96+
});
97+
98+
static std::string_view getTruncatedHashHex(std::string_view input)
99+
{
100+
static boost::static_string<TRUNC_SIZE> buffer;
101+
102+
constexpr std::size_t sha256HashSize = 32;
103+
std::array<uint8_t, sha256HashSize> hash {};
104+
hash_it(reinterpret_cast<const uint8_t*>(input.data()), input.length(), hash.data());
105+
106+
std::ranges::copy(
107+
hash | std::views::take(buffer.size() / 2) | std::views::transform([](const uint8_t byte) {
108+
return std::format("{:02x}", byte);
109+
}) | std::views::join,
110+
std::back_inserter(buffer));
111+
return std::string_view(buffer.data(), buffer.size());
112+
}
113+
114+
static std::string_view getTruncatedCipherHash(std::span<const uint16_t> cipherSuites)
115+
{
116+
if (cipherSuites.empty()) {
117+
static const std::array<char, TRUNC_SIZE> emptyCiphers {0};
118+
return std::string_view(emptyCiphers.data(), emptyCiphers.size());
119+
}
120+
121+
std::vector<uint16_t> sortedCipherSuites(cipherSuites.begin(), cipherSuites.end());
122+
std::ranges::sort(sortedCipherSuites);
123+
124+
std::string cipherHexString;
125+
std::ranges::copy(
126+
sortedCipherSuites | rangeToHexString | std::views::join,
127+
std::back_inserter(cipherHexString));
128+
return getTruncatedHashHex(cipherHexString);
129+
}
130+
131+
static std::string_view getTruncatedExtensionsHash(
132+
std::span<const uint16_t> extensionTypes,
133+
std::span<const uint16_t> signatureAlgorithms)
134+
{
135+
constexpr std::size_t MAX_EXTENSIONS = 100;
136+
boost::container::static_vector<uint16_t, MAX_EXTENSIONS> sortedExtensions;
137+
std::ranges::copy(
138+
extensionTypes | std::views::filter([](const uint16_t type) {
139+
return type != static_cast<uint16_t>(TLSExtensionType::ALPN)
140+
&& type != static_cast<uint16_t>(TLSExtensionType::SERVER_NAME)
141+
&& !TLSParser::isGreaseValue(type);
142+
}) | std::views::take(sortedExtensions.capacity()),
143+
std::back_inserter(sortedExtensions));
144+
std::ranges::sort(sortedExtensions);
145+
146+
constexpr std::size_t MAX_STRING_LENGTH = 2 * MAX_EXTENSIONS * sizeof(uint16_t) + 1;
147+
boost::static_string<MAX_STRING_LENGTH> finalString;
148+
concatenateRangeTo(sortedExtensions | rangeToHexString, finalString, '-', '_');
149+
concatenateRangeTo(
150+
signatureAlgorithms | std::views::drop(1) | rangeToHexString,
151+
finalString,
152+
'-');
153+
154+
return getTruncatedHashHex(toStringView(finalString));
155+
}
156+
157+
/**
158+
* @class JA4
159+
* @brief Generates JA4 fingerprint for TLS ClientHello messages.
160+
*
161+
* The JA4 class constructs a JA4 fingerprint string based on the provided
162+
* TLS ClientHello parameters, including protocol type, version, server names,
163+
* ALPNs, cipher suites, extension types, signature algorithms, and supported versions.
164+
*/
165+
class JA4 {
166+
public:
167+
JA4(const uint8_t l4Protocol,
168+
const TLSHandshake& handshake,
169+
std::span<const std::string_view> serverNames,
170+
std::span<const std::string_view> alpns,
171+
std::span<const uint16_t> cipherSuites,
172+
std::span<const uint16_t> extensionTypes,
173+
std::span<const uint16_t> signatureAlgorithms,
174+
std::span<const uint16_t> supportedVersions) noexcept
175+
{
176+
// TODO USE VALUES FROM DISSECTOR
177+
constexpr uint8_t UDP_ID = 17;
178+
value.push_back(l4Protocol == UDP_ID ? 'q' : 't');
179+
180+
std::string_view versionLabel = getVersionLabel(supportedVersions, handshake);
181+
value.append(versionLabel.begin(), versionLabel.end());
182+
183+
value.push_back(serverNames.empty() ? 'i' : 'd');
184+
185+
value.push_back(std::min(cipherSuites.size(), 99UL));
186+
187+
value.push_back(std::min(extensionTypes.size(), 99UL));
188+
189+
std::string_view alpnLabel = getALPNLabel(alpns);
190+
value.append(alpnLabel.begin(), alpnLabel.end());
191+
192+
std::string_view cipherHash = getTruncatedCipherHash(cipherSuites);
193+
value.append(cipherHash.begin(), cipherHash.end());
194+
195+
std::string_view extensionsHash
196+
= getTruncatedExtensionsHash(extensionTypes, signatureAlgorithms);
197+
value.append(extensionsHash.begin(), extensionsHash.end());
198+
}
199+
200+
std::string_view getView() const noexcept
201+
{
202+
return std::string_view(value.data(), value.size());
203+
}
204+
205+
private:
206+
boost::static_string<TLSContext::JA4_SIZE> value;
207+
};
208+
209+
} // namespace ipxp::process::tls

src/plugins/process/tls/src/md5.cpp

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ documentation and/or software.
3636
/* system implementation headers */
3737
#include <cstdio>
3838

39-
namespace ipxp {
39+
namespace ipxp::process::tls {
4040

4141
// Constants for MD5Transform routine.
4242
#define S11 7
@@ -125,6 +125,13 @@ MD5::MD5(const std::string& text)
125125
finalize();
126126
}
127127

128+
MD5::MD5(std::string_view text)
129+
{
130+
init();
131+
update(text.data(), text.size());
132+
finalize();
133+
}
134+
128135
//////////////////////////////
129136

130137
void MD5::init()
@@ -383,4 +390,10 @@ void md5_get_bin(const std::string str, void* dest)
383390
memcpy(dest, md5.binary_digest(), 16);
384391
}
385392

386-
} // namespace ipxp
393+
void md5_get_bin(std::string_view str, void* dest)
394+
{
395+
MD5 md5 = MD5(str);
396+
memcpy(dest, md5.binary_digest(), 16);
397+
}
398+
399+
} // namespace ipxp::process::tls

0 commit comments

Comments
 (0)