Skip to content

Commit b875e19

Browse files
committed
add PyRunner
1 parent b2be903 commit b875e19

File tree

10 files changed

+336
-168
lines changed

10 files changed

+336
-168
lines changed

CMakeLists.txt

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ set(LIEF_MACHO OFF)
5959
set(LIEF_DEX OFF)
6060
set(LIEF_ART OFF)
6161

62-
if(NOT EXISTS "${CMAKE_SOURCE_DIR}/libs/LIEF/CMakeLists.txt" AND NOT NO_DATABASE)
62+
if(NOT EXISTS "${CMAKE_SOURCE_DIR}/libs/LIEF/CMakeLists.txt"
63+
AND NOT NO_DATABASE
64+
)
6365
message(STATUS "Submodule 'libs/LIEF' not found. Updating submodules...")
6466
execute_process(
6567
COMMAND git submodule update --init --recursive
@@ -91,6 +93,8 @@ set(SOURCES_MC
9193
src/Model.cpp
9294
src/Network.hpp
9395
src/Network.cpp
96+
src/PyRunner.cpp
97+
src/PyRunner.hpp
9498
src/Runner.cpp
9599
src/Runner.hpp
96100
src/binary.hpp
@@ -147,6 +151,19 @@ if(TEST)
147151
CXX_STANDARD 17
148152
CXX_STANDARD_REQUIRED ON
149153
)
154+
message(STATUS "Setup test files")
155+
execute_process(
156+
COMMAND bash set_up_files.sh "${CMAKE_SOURCE_DIR}/build"
157+
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/test"
158+
RESULT_VARIABLE SETUP_RESULT
159+
)
160+
161+
if(NOT SETUP_RESULT EQUAL 0)
162+
message(
163+
FATAL_ERROR
164+
"set_up_files.sh failed with exit code ${SETUP_RESULT}"
165+
)
166+
endif()
150167
else()
151168
set(SOURCES ${SOURCES_MC} ${SOURCES_VIEW})
152169
add_executable(${PROJECT_NAME} ${SOURCES})

database/get_leaf_cert.py

Lines changed: 70 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
"""
2-
This module retrieves only the leaf certificate from servers that don't
3-
follow the standard TLS handshake procedure and fail to provide the complete
4-
certificate chain. While `curl` can still connect to these servers by
5-
specifying only the leaf certificate (and defaults to finding the chain in
6-
/etc/ssl/certs), the `requests` module requires a full certificate bundle.
7-
This module is intended for use cases where only the leaf certificate
8-
is needed.
2+
Leaf Certificate Fetcher and Inspector for Misconfigured HTTPS Servers.
3+
4+
This module retrieves **only the leaf certificate** (the server's own certificate)
5+
from a given hostname over TLS, without verifying the chain.
6+
7+
Some servers are misconfigured and fail to send the full certificate chain
8+
during the TLS handshake. While `curl` can still connect to these by relying
9+
on system root certificates, higher-level libraries like `requests` may reject
10+
the connection unless a full CA bundle is provided.
911
"""
1012

1113
import sys
@@ -18,40 +20,73 @@
1820

1921

2022
def get_certificate(hostname):
21-
"""OpenSSL with TCP get the certificate"""
23+
"""
24+
Perform a raw TLS connection to fetch the server's leaf certificate.
25+
26+
This bypasses certificate verification to ensure we can still retrieve
27+
certificates from misconfigured servers.
28+
29+
Args:
30+
hostname (str): The domain to connect to (without scheme or port).
31+
32+
Returns:
33+
x509.Certificate: Parsed certificate object, or None on failure.
34+
"""
2235
context = ssl.create_default_context()
23-
# Disable certificate verification for the first connection
24-
context.check_hostname = False
25-
context.verify_mode = ssl.CERT_NONE
36+
context.check_hostname = False # Don't verify host name
37+
context.verify_mode = ssl.CERT_NONE # Don't verify certificate chain
2638

2739
with socket.create_connection((hostname, 443)) as sock:
2840
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
29-
# Get certificate info
30-
cert_der = ssock.getpeercert(True)
41+
cert_der = ssock.getpeercert(True) # Get DER-encoded certificate
3142
if cert_der:
3243
return x509.load_der_x509_certificate(cert_der, default_backend())
3344
return None
3445

3546

3647
def get_sha256_fingerprint(cert):
37-
"""Identify the sum, we might want verify the certificate"""
48+
"""
49+
Compute SHA-256 fingerprint of a certificate.
50+
51+
Args:
52+
cert (x509.Certificate): The certificate to hash.
53+
54+
Returns:
55+
bytes: Raw SHA-256 digest.
56+
"""
3857
cert_der = cert.public_bytes(serialization.Encoding.DER)
3958
digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
4059
digest.update(cert_der)
4160
return digest.finalize()
4261

4362

4463
def get_serial_number_hex(cert):
45-
"""Identify the serial, this can be use to look for the certificate"""
46-
# Get the serial number in a byte format
47-
serial_number_bytes = cert.serial_number \
48-
.to_bytes((cert.serial_number.bit_length() + 7) // 8, 'big')
49-
# Format it as a hex string
64+
"""
65+
Return the certificate's serial number formatted as hexadecimal.
66+
67+
Useful for identifying and verifying specific certs manually.
68+
69+
Args:
70+
cert (x509.Certificate): The certificate to inspect.
71+
72+
Returns:
73+
str: Colon-separated hex string (e.g., "01:AB:CD:EF").
74+
"""
75+
serial_number_bytes = cert.serial_number.to_bytes(
76+
(cert.serial_number.bit_length() + 7) // 8, 'big'
77+
)
5078
return ':'.join(f'{b:02X}' for b in serial_number_bytes)
5179

5280

5381
def print_certificate_details(cert):
54-
"""Log basic certificate information"""
82+
"""
83+
Print human-readable information about the certificate.
84+
85+
Includes fingerprint, serial number, subject and issuer details.
86+
87+
Args:
88+
cert (x509.Certificate): The certificate to print.
89+
"""
5590
fingerprint = get_sha256_fingerprint(cert)
5691
fingerprint_hex = ':'.join(f'{b:02X}' for b in fingerprint)
5792
serial_number_hex = get_serial_number_hex(cert)
@@ -64,80 +99,32 @@ def print_certificate_details(cert):
6499

65100

66101
def run(url):
67-
"""Retrieve and return the certificate's public key in PEM byte format."""
102+
"""
103+
Determine the target host from the URL, fetch and print its leaf certificate.
104+
105+
Args:
106+
url (str): The full URL (must start with one of the known domains).
107+
108+
Returns:
109+
bytes: The PEM-encoded certificate (as bytes), or exits on failure.
110+
"""
111+
# Map URL to known hosts. These are the only ones we allow.
68112
if url.startswith("https://www.trle.net"):
69113
host = 'trle.net'
70114
elif url.startswith("https://trcustoms.org"):
71115
host = 'trcustoms.org'
72116
else:
73-
sys.exit(1)
117+
sys.exit(1) # Reject unknown domains for safety
118+
74119
certificate = get_certificate(host)
75120
if not certificate:
121+
print("Failed to retrieve certificate.")
76122
sys.exit(1)
77123

78124
print_certificate_details(certificate)
125+
79126
if not isinstance(certificate, Certificate):
127+
print("Invalid certificate object.")
80128
sys.exit(1)
81129

82130
return certificate.public_bytes(encoding=serialization.Encoding.PEM)
83-
84-
85-
'''
86-
def validate_downloaded_key(id_number, expected_serial):
87-
"""Validate the certificate in binary form with the cryptography module"""
88-
pem_key = get_response(f"https://crt.sh/?d={id_number}", 'application/pkix-cert')
89-
90-
if not isinstance(pem_key, bytes):
91-
logging.error("Data type error, expected bytes got %s", type(pem_key))
92-
sys.exit(1)
93-
94-
# Load the certificate
95-
certificate = x509.load_pem_x509_certificate(pem_key, default_backend())
96-
97-
# Extract the serial number and convert it to hex (without leading '0x')
98-
hex_serial = f'{certificate.serial_number:x}'
99-
100-
# Compare the serial numbers
101-
if hex_serial == expected_serial:
102-
print("The downloaded PEM key matches the expected serial number.")
103-
else:
104-
logging.error("Serial mismatch! Expected: %s, but got: %s", expected_serial, hex_serial)
105-
sys.exit(1)
106-
107-
# Extract and validate the domain (Common Name)
108-
valid_domains = ["trle.net", "trcustoms.org", "data.trcustoms.org", "staging.trcustoms.org"]
109-
110-
# Check the Common Name (CN) in the certificate subject
111-
comon_name = certificate.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)[0].value
112-
if comon_name in valid_domains:
113-
print(f"Valid domain found in CN: {comon_name}")
114-
else:
115-
logging.error("Invalid domain in CN: %s", comon_name)
116-
sys.exit(1)
117-
118-
# Extract the Subject Alternative Name (SAN) extension
119-
try:
120-
san_extension = certificate.extensions \
121-
.get_extension_for_oid(ExtensionOID.SUBJECT_ALTERNATIVE_NAME)
122-
123-
# Extract all DNS names listed in the SAN extension
124-
dns_names = san_extension.value.get_values_for_type(x509.DNSName) # type: ignore
125-
126-
print(f"DNS Names in SAN: {dns_names}")
127-
128-
# Check if any of the DNS names match the valid domain list
129-
valid_domains = ["trle.net", "www.trle.net", "trcustoms.org", "*.trcustoms.org",
130-
"data.trcustoms.org", 'staging.trcustoms.org']
131-
132-
if all(domain in valid_domains for domain in dns_names):
133-
print(f"Valid domain found in SAN: {dns_names}")
134-
else:
135-
print(f"Invalid domain in SAN: {dns_names}")
136-
sys.exit(1)
137-
138-
except x509.ExtensionNotFound:
139-
print("No Subject Alternative Name (SAN) extension found in the certificate.")
140-
141-
pem_data = certificate.public_bytes(encoding=serialization.Encoding.PEM)
142-
return pem_data.decode('utf-8')
143-
'''

database/https.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,10 @@ def get_response(self, url, content_type):
206206
if not self.leaf_cert:
207207
sys.exit(1)
208208
temp_cert_path = self.set_leaf(curl)
209+
curl.setopt(pycurl.SSL_VERIFYPEER, 1)
210+
curl.setopt(pycurl.SSL_VERIFYHOST, 2)
211+
pinned_key = "sha256//7WRPcNY2QpOjWiQSLbiBu/9Og69JmzccPAdfj2RT5Vw="
212+
curl.setopt(pycurl.PINNEDPUBLICKEY, pinned_key)
209213

210214
headers_list = [
211215
'User-Agent: Wget/1.21.1 (linux-gnu)',

0 commit comments

Comments
 (0)