Skip to content

Commit c883467

Browse files
authored
Add X509Toolkit - X509 example (#1917)
1 parent aa68b62 commit c883467

21 files changed

+1203
-7
lines changed

.github/workflows/build_and_test.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -696,7 +696,7 @@ jobs:
696696
release: ${{ matrix.version }}
697697
usesh: true
698698
prepare: |
699-
pkg install -y python3 bash cmake git gmake gsed libpcap tcpreplay
699+
pkg install -y python bash cmake git gmake gsed libpcap tcpreplay libffi openssl py311-cryptography
700700
701701
run: |
702702
echo "Building PcapPlusPlus"
@@ -710,14 +710,14 @@ jobs:
710710
ifconfig "$interface_name" promisc
711711
712712
echo "Testing PcapPlusPlus"
713-
python3 -m ensurepip
714-
python3 -m pip install -r ci/run_tests/requirements.txt
715-
python3 ci/run_tests/run_tests.py --interface "$interface_name"
713+
python -m ensurepip
714+
python -m pip install -r ci/run_tests/requirements.txt
715+
python ci/run_tests/run_tests.py --interface "$interface_name"
716716
717717
echo "Testing PcapPlusPlus examples"
718718
cd Tests/ExamplesTest
719-
python3 -m pip install -r requirements.txt
720-
python3 -m pytest --interface "$interface_name" --root-path=../../Dist/examples_bin
719+
python -m pip install -r requirements.txt
720+
python -m pytest --interface "$interface_name" --root-path=../../Dist/examples_bin
721721
722722
android:
723723
strategy:

Examples/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ add_subdirectory(PcapSplitter)
3535
add_subdirectory(SSLAnalyzer)
3636
add_subdirectory(TcpReassembly)
3737
add_subdirectory(TLSFingerprinting)
38+
add_subdirectory(X509Toolkit)
3839

3940
if(PCAPPP_BUILD_TUTORIALS)
4041
set(PCAPPP_BINARY_TUTORIAL_DIR ${CMAKE_BINARY_DIR}/tutorials_bin)

Examples/X509Toolkit/CMakeLists.txt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
add_executable(X509Toolkit main.cpp)
2+
3+
target_link_libraries(X509Toolkit PUBLIC PcapPlusPlus::Pcap++)
4+
5+
if(MSVC)
6+
# This executable requires getopt.h not available on VStudio
7+
target_link_libraries(X509Toolkit PRIVATE Getopt-for-Visual-Studio)
8+
endif()
9+
10+
set_target_properties(
11+
X509Toolkit
12+
PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${PCAPPP_BINARY_EXAMPLES_DIR}" RUNTIME_OUTPUT_NAME "X509Toolkit"
13+
)
14+
15+
if(PCAPPP_INSTALL)
16+
install(TARGETS X509Toolkit EXPORT PcapPlusPlusTargets RUNTIME DESTINATION ${PCAPPP_INSTALL_BINDIR})
17+
endif()

Examples/X509Toolkit/PcapExtract.h

Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
#pragma once
2+
3+
#include "PcapFileDevice.h"
4+
#include "SSLLayer.h"
5+
#include "TcpReassembly.h"
6+
#include "GeneralUtils.h"
7+
#include "SystemUtils.h"
8+
#include "Logger.h"
9+
#include <iostream>
10+
#include <vector>
11+
#include <unordered_map>
12+
13+
constexpr char getOsPathSeparator()
14+
{
15+
#ifdef _WIN32
16+
return '\\';
17+
#else
18+
return '/';
19+
#endif
20+
}
21+
22+
/// Extracts X.509 certificates from PCAP/PCAPNG files by analyzing SSL/TLS traffic.
23+
class PcapExtract
24+
{
25+
public:
26+
/// Creates a new extractor for the given PCAP file and output configuration.
27+
PcapExtract(const std::string& pcapFileName, const std::string& outputDirectory, const std::string& format,
28+
bool showStats)
29+
: m_PcapFileName(pcapFileName), m_OutputDirectory(outputDirectory), m_Format(format), m_ShowStats(showStats)
30+
{
31+
if (format != "PEM" && format != "DER")
32+
{
33+
throw std::invalid_argument("Unsupported format: " + format);
34+
}
35+
36+
if (!outputDirectory.empty() && !pcpp::directoryExists(outputDirectory))
37+
{
38+
throw std::invalid_argument("Output directory '" + outputDirectory + "' does not exist");
39+
}
40+
}
41+
42+
/// Processes the PCAP file, extracts certificates, and writes them to the output.
43+
void run()
44+
{
45+
if (m_RanOnce)
46+
{
47+
throw std::runtime_error("Task already ran");
48+
}
49+
50+
std::unique_ptr<pcpp::IFileReaderDevice> reader(pcpp::IFileReaderDevice::getReader(m_PcapFileName));
51+
52+
if (!reader->open())
53+
{
54+
throw std::runtime_error("Error opening pcap file");
55+
}
56+
57+
pcpp::TcpReassembly tcpReassembly(onMessageReady, this, nullptr, onConnectionEnd);
58+
59+
m_RanOnce = true;
60+
pcpp::RawPacketVector rawPackets;
61+
pcpp::Logger::getInstance().suppressLogs();
62+
while (reader->getNextPackets(rawPackets, 20) > 0)
63+
{
64+
m_Stats.packets += rawPackets.size();
65+
for (auto& rawPacket : rawPackets)
66+
{
67+
tcpReassembly.reassemblePacket(rawPacket);
68+
}
69+
rawPackets.clear();
70+
}
71+
pcpp::Logger::getInstance().enableLogs();
72+
73+
if (m_ShowStats)
74+
{
75+
std::cout << "Packet count: " << m_Stats.packets << std::endl
76+
<< "TLS messages: " << m_Stats.sslMessages << std::endl
77+
<< "TLS Flows: " << m_Stats.sslFlows << std::endl
78+
<< "TLS handshake messages: " << m_Stats.sslHandshakeMessages << std::endl
79+
<< "Certificates parsed: " << m_Stats.parsedCertificates << std::endl
80+
<< "Certificates failed parsing: " << m_Stats.failedParsingCertificates << std::endl
81+
<< "Incomplete Certificates: " << m_Stats.failedIncompleteCertificates << std::endl;
82+
}
83+
}
84+
85+
private:
86+
/// Data associated with a single SSL/TLS connection
87+
struct SSLConnectionData
88+
{
89+
int8_t curSide = -1;
90+
std::vector<uint8_t> data;
91+
bool canIgnoreFlow = false;
92+
93+
explicit SSLConnectionData(int8_t side) : curSide(side)
94+
{}
95+
};
96+
97+
using SSLConnectionManager = std::unordered_map<uint32_t, SSLConnectionData>;
98+
99+
/// Tracks statistics about the PCAP processing
100+
struct SSLPcapStats
101+
{
102+
uint64_t packets = 0;
103+
uint64_t sslMessages = 0;
104+
uint64_t sslHandshakeMessages = 0;
105+
uint64_t sslFlows = 0;
106+
uint64_t parsedCertificates = 0;
107+
uint64_t failedIncompleteCertificates = 0;
108+
uint64_t failedParsingCertificates = 0;
109+
};
110+
111+
std::string m_PcapFileName;
112+
std::string m_OutputDirectory;
113+
std::string m_Format;
114+
bool m_ShowStats;
115+
SSLConnectionManager m_ConnectionManager;
116+
SSLPcapStats m_Stats;
117+
bool m_RanOnce = false;
118+
119+
/// Callback for when a complete TCP message is ready for processing
120+
static void onMessageReady(int8_t side, const pcpp::TcpStreamData& tcpData, void* userCookie)
121+
{
122+
auto* thisInstance = static_cast<PcapExtract*>(userCookie);
123+
124+
// Skip non-SSL traffic
125+
if (!(pcpp::SSLLayer::isSSLPort(tcpData.getConnectionData().srcPort) ||
126+
pcpp::SSLLayer::isSSLPort(tcpData.getConnectionData().dstPort)))
127+
{
128+
return;
129+
}
130+
131+
thisInstance->m_Stats.sslMessages++;
132+
133+
// Find or create connection state for this flow
134+
auto flow = thisInstance->m_ConnectionManager.find(tcpData.getConnectionData().flowKey);
135+
if (flow == thisInstance->m_ConnectionManager.end())
136+
{
137+
thisInstance->m_Stats.sslFlows++;
138+
thisInstance->m_ConnectionManager.insert(
139+
std::make_pair(tcpData.getConnectionData().flowKey, SSLConnectionData(side)));
140+
flow = thisInstance->m_ConnectionManager.find(tcpData.getConnectionData().flowKey);
141+
}
142+
143+
// Skip if we're already ignoring this flow
144+
if (flow->second.canIgnoreFlow)
145+
{
146+
return;
147+
}
148+
149+
// If this is a continuation from the same side, accumulate the data
150+
if (flow->second.curSide == side && !tcpData.isBytesMissing())
151+
{
152+
flow->second.data.insert(flow->second.data.end(), tcpData.getData(),
153+
tcpData.getData() + tcpData.getDataLength());
154+
return;
155+
}
156+
157+
// If we have no accumulated data, return
158+
if (flow->second.data.empty())
159+
{
160+
return;
161+
}
162+
163+
// Process the accumulated data as a complete SSL message
164+
size_t const dataSize = flow->second.data.size();
165+
auto* data = new uint8_t[dataSize];
166+
std::memcpy(data, flow->second.data.data(), dataSize);
167+
168+
// Update connection state with new data
169+
flow->second.curSide = side;
170+
flow->second.data.clear();
171+
flow->second.data.insert(flow->second.data.end(), tcpData.getData(),
172+
tcpData.getData() + tcpData.getDataLength());
173+
174+
// We have all the data for this SSL message. Parse and handle the message
175+
auto sslMessage =
176+
std::unique_ptr<pcpp::SSLLayer>(pcpp::SSLLayer::createSSLMessage(data, dataSize, nullptr, nullptr));
177+
if (sslMessage != nullptr)
178+
{
179+
thisInstance->handleSSLMessage(sslMessage.get(), &flow->second);
180+
}
181+
};
182+
183+
/// Callback for when a TCP connection ends
184+
static void onConnectionEnd(const pcpp::ConnectionData& connectionData,
185+
pcpp::TcpReassembly::ConnectionEndReason reason, void* userCookie)
186+
{
187+
auto* thisInstance = static_cast<PcapExtract*>(userCookie);
188+
auto flow = thisInstance->m_ConnectionManager.find(connectionData.flowKey);
189+
if (flow != thisInstance->m_ConnectionManager.end())
190+
{
191+
thisInstance->m_ConnectionManager.erase(flow);
192+
}
193+
}
194+
195+
/// Handles an SSL/TLS message and extracts certificates if present
196+
void handleSSLMessage(pcpp::SSLLayer* sslMessage, SSLConnectionData* sslConnectionData)
197+
{
198+
// Iterate over all the SSL/TLS layers in the message
199+
while (sslMessage != nullptr)
200+
{
201+
auto* applicationDataLayer = dynamic_cast<pcpp::SSLApplicationDataLayer*>(sslMessage);
202+
if (applicationDataLayer != nullptr)
203+
{
204+
sslConnectionData->data.clear();
205+
sslConnectionData->canIgnoreFlow = true;
206+
return;
207+
}
208+
209+
// Ignore non-handshake messages
210+
auto* handshakeLayer = dynamic_cast<pcpp::SSLHandshakeLayer*>(sslMessage);
211+
if (handshakeLayer == nullptr)
212+
{
213+
sslMessage->parseNextLayer();
214+
sslMessage = dynamic_cast<pcpp::SSLLayer*>(sslMessage->getNextLayer());
215+
continue;
216+
}
217+
218+
m_Stats.sslHandshakeMessages++;
219+
220+
// Iterate over all certificate messages
221+
auto* certMessage = handshakeLayer->getHandshakeMessageOfType<pcpp::SSLCertificateMessage>();
222+
while (certMessage != nullptr)
223+
{
224+
handleSSLCertificateMessage(certMessage);
225+
certMessage = handshakeLayer->getNextHandshakeMessageOfType<pcpp::SSLCertificateMessage>(certMessage);
226+
}
227+
228+
sslMessage->parseNextLayer();
229+
sslMessage = dynamic_cast<pcpp::SSLLayer*>(sslMessage->getNextLayer());
230+
}
231+
}
232+
233+
/// Handles an SSL/TLS certificate message
234+
void handleSSLCertificateMessage(const pcpp::SSLCertificateMessage* sslCertificateMessage)
235+
{
236+
// Iterate over all certificates in this message
237+
for (int i = 0; i < sslCertificateMessage->getNumOfCertificates(); i++)
238+
{
239+
try
240+
{
241+
// Parse the certificate
242+
auto x509Cert = sslCertificateMessage->getCertificate(i)->getX509Certificate();
243+
if (x509Cert != nullptr)
244+
{
245+
handleX509Certificate(x509Cert);
246+
}
247+
else
248+
{
249+
m_Stats.failedIncompleteCertificates++;
250+
}
251+
}
252+
catch (...)
253+
{
254+
m_Stats.failedParsingCertificates++;
255+
}
256+
}
257+
}
258+
259+
/// Handles an X.509 certificate
260+
void handleX509Certificate(const std::unique_ptr<pcpp::X509Certificate>& x509Cert)
261+
{
262+
if (m_Format == "PEM")
263+
{
264+
handlePEM(x509Cert);
265+
}
266+
else if (m_Format == "DER")
267+
{
268+
handleDER(x509Cert);
269+
}
270+
else
271+
{
272+
throw std::invalid_argument("Unsupported format: " + m_Format);
273+
}
274+
275+
m_Stats.parsedCertificates++;
276+
}
277+
278+
/// Stores the certificate in PEM format, either to stdout or to a file
279+
void handlePEM(const std::unique_ptr<pcpp::X509Certificate>& x509Cert)
280+
{
281+
auto pem = x509Cert->toPEM();
282+
if (m_OutputDirectory.empty())
283+
{
284+
std::cout << pem << std::endl;
285+
}
286+
else
287+
{
288+
std::string const outputFileName =
289+
m_OutputDirectory + getOsPathSeparator() + std::to_string(m_Stats.parsedCertificates + 1) + ".pem";
290+
std::ofstream pemFile(outputFileName);
291+
if (!pemFile.is_open())
292+
{
293+
throw std::runtime_error("Unable to open file " + outputFileName);
294+
}
295+
pemFile << pem;
296+
pemFile.close();
297+
}
298+
}
299+
300+
/// Stores the certificate in DER format, either to stdout or to a file
301+
void handleDER(const std::unique_ptr<pcpp::X509Certificate>& x509Cert)
302+
{
303+
auto der = x509Cert->toDER();
304+
if (m_OutputDirectory.empty())
305+
{
306+
std::cout << pcpp::Base64::encode(der) << std::endl << "==============" << std::endl;
307+
}
308+
else
309+
{
310+
std::string const outputFileName =
311+
m_OutputDirectory + getOsPathSeparator() + std::to_string(m_Stats.parsedCertificates + 1) + ".der";
312+
std::ofstream derFile(outputFileName, std::ios::binary);
313+
if (!derFile.is_open())
314+
{
315+
throw std::runtime_error("Unable to open file " + outputFileName);
316+
}
317+
derFile.write(reinterpret_cast<const char*>(der.data()), der.size());
318+
derFile.close();
319+
}
320+
}
321+
};

0 commit comments

Comments
 (0)