From d2830bed0ac620393c5648f022fb6bf5182461b7 Mon Sep 17 00:00:00 2001 From: wbi Date: Wed, 14 Jan 2026 17:47:02 +0100 Subject: [PATCH 01/29] Fix #1452 Normalize plugins Certificates output --- dissect/target/helpers/certificate.py | 21 +++++++-- dissect/target/plugins/os/windows/certlog.py | 48 +++++++++++++++----- dissect/target/plugins/os/windows/ual.py | 4 +- tests/plugins/apps/webserver/test_apache.py | 2 +- tests/plugins/os/windows/test_certlog.py | 8 ++-- 5 files changed, 61 insertions(+), 22 deletions(-) diff --git a/dissect/target/helpers/certificate.py b/dissect/target/helpers/certificate.py index a9b799a468..33dbd88c4b 100755 --- a/dissect/target/helpers/certificate.py +++ b/dissect/target/helpers/certificate.py @@ -15,10 +15,9 @@ except ImportError: HAS_ASN1 = False - COMMON_CERTIFICATE_FIELDS = [ ("digest", "fingerprint"), - ("varint", "serial_number"), + ("string", "serial_number"), ("datetime", "not_valid_before"), ("datetime", "not_valid_after"), ("string", "issuer_dn"), @@ -75,6 +74,22 @@ def compute_pem_fingerprints(pem: str | bytes) -> tuple[str, str, str]: return md5, sha1, sha256 +def format_serial_number_as_hex(serial_number: int | None) -> str | None: + """ + Format serial_number from integer to hex. Add a prefix 0 if output length is not pair, in order to be consistent + with usual serial_number representation (navigator, openssl etc...). + + :param serial_number: + :return: + """ + if serial_number is None: + return serial_number + serial_number_as_hex = f"{serial_number:x}" + if len(serial_number_as_hex) % 2 == 1: + serial_number_as_hex = f"0{serial_number_as_hex}" + return serial_number_as_hex + + def parse_x509(file: str | bytes | Path) -> CertificateRecord: """Parses a PEM file. Returns a CertificateREcord. Does not parse a public key embedded in a x509 certificate.""" @@ -111,6 +126,6 @@ def parse_x509(file: str | bytes | Path) -> CertificateRecord: issuer_dn=",".join(issuer), subject_dn=",".join(subject), fingerprint=(md5, crt.sha1.hex(), crt.sha256.hex()), - serial_number=crt.serial_number, + serial_number=format_serial_number_as_hex(crt.serial_number), pem=crt.dump(), ) diff --git a/dissect/target/plugins/os/windows/certlog.py b/dissect/target/plugins/os/windows/certlog.py index e871e870c5..5ebd9594fe 100644 --- a/dissect/target/plugins/os/windows/certlog.py +++ b/dissect/target/plugins/os/windows/certlog.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING, Any, Union from dissect.database.ese.tools import certlog from dissect.database.exception import Error @@ -10,7 +10,7 @@ from dissect.target.plugin import Plugin, export if TYPE_CHECKING: - from collections.abc import Iterator + from collections.abc import Callable, Iterator from pathlib import Path from dissect.target.target import Target @@ -43,7 +43,7 @@ CertificateRecord = TargetRecordDescriptor( "filesystem/windows/certlog/certificate", [ - ("string", "certificate_hash2"), + ("digest", "fingerprint"), ("string", "certificate_template"), ("string", "common_name"), ("string", "country"), @@ -57,7 +57,7 @@ ("string", "organization"), ("string", "organizational_unit"), ("string", "public_key_algorithm"), - ("string", "serial_number_hex"), + ("string", "serial_number"), ("string", "state_or_province"), ("string", "street_address"), ("string", "subject_key_identifier"), @@ -69,8 +69,8 @@ ("string", "enrollment_flags"), ("string", "general_flags"), ("string", "issuer_name_id"), - ("datetime", "not_after"), - ("datetime", "not_before"), + ("datetime", "not_valid_after"), + ("datetime", "not_valid_before"), ("string", "private_key_flags"), ("string", "public_key"), ("varint", "public_key_length"), @@ -98,7 +98,7 @@ ("string", "distinguished_name"), ("string", "domain_component"), ("string", "email"), - ("string", "endorsement_certificate_hash"), + ("string", "endorsement_fingerprint"), ("string", "endorsement_key_hash"), ("string", "given_name"), ("string", "initials"), @@ -172,7 +172,8 @@ "$AttributeValue": "common_name", "$CRLPublishError": "crl_publish_error", "$CallerName": "caller_name", - "$CertificateHash2": "certificate_hash2", + "$CertificateHash": "fingerprint", + "$CertificateHash2": "fingerprint", "$CertificateTemplate": "certificate_template", "$CommonName": "common_name", "$Country": "country", @@ -181,7 +182,7 @@ "$DistinguishedName": "distinguished_name", "$DomainComponent": "domain_component", "$EMail": "email", - "$EndorsementCertificateHash": "endorsement_certificate_hash", + "$EndorsementCertificateHash": "endorsement_fingerprint", "$EndorsementKeyHash": "endorsement_key_hash", "$ExtensionName": "extension_name", "$GivenName": "given_name", @@ -193,7 +194,7 @@ "$PublicKeyAlgorithm": "public_key_algorithm", "$RequestAttributes": "request_attributes", "$RequesterName": "requester_name", - "$SerialNumber": "serial_number_hex", + "$SerialNumber": "serial_number", "$SignerApplicationPolicies": "signer_application_policies", "$SignerPolicies": "signer_policies", "$StateOrProvince": "state_or_province", @@ -221,8 +222,8 @@ "NameId": "name_id", "NextPublish": "next_publish", "NextUpdate": "next_update", - "NotAfter": "not_after", - "NotBefore": "not_before", + "NotAfter": "not_valid_after", + "NotBefore": "not_valid_before", "Number": "number", "PrivateKeyFlags": "private_key_flags", "PropagationComplete": "propagation_complete", @@ -254,6 +255,27 @@ } +def format_fingerprint(input_hash: str | None) -> tuple[str | None, str | None, str | None]: + if input_hash: + input_hash = input_hash.replace(" ", "") + # older version use md5 + if len(input_hash) == 64: + return input_hash, None, None + return None, input_hash, None + + +def format_serial_number(serial_number_as_hex: str | None) -> str | None: + if not serial_number_as_hex: + return None + return serial_number_as_hex.replace(" ", "") + + +FORMATING_FUNC: dict[str, Callable[[Any], Any]] = { + "fingerprint": format_fingerprint, + "serial_number": format_serial_number, +} + + class CertLogPlugin(Plugin): """Return all available data stored in CertLog databases. @@ -302,6 +324,8 @@ def read_records(self, table_name: str, record_type: CertLogRecord) -> Iterator[ record_values = {} for column, value in column_values: new_column = FIELD_MAPPINGS.get(column) + if new_column in FORMATING_FUNC: + value = FORMATING_FUNC[new_column](value) if new_column: record_values[new_column] = value else: diff --git a/dissect/target/plugins/os/windows/ual.py b/dissect/target/plugins/os/windows/ual.py index c2eecd7663..f3b8a9e218 100644 --- a/dissect/target/plugins/os/windows/ual.py +++ b/dissect/target/plugins/os/windows/ual.py @@ -53,7 +53,7 @@ ("datetime", "last_seen_active_date"), ("string", "vm_guid"), ("string", "bios_guid"), - ("string", "serial"), + ("string", "serial_number"), ("string", "path"), ], ) @@ -129,7 +129,7 @@ "ProductName": "product_name", "RoleGuid": "role_guid", "RoleName": "role_name", - "SerialNumber": "serial", + "SerialNumber": "serial_number", "ServicePackMajor": "service_pack_major_version", "ServicePackMinor": "service_pack_minor_version", "SystemDNSHostName": "system_dns_hostname", diff --git a/tests/plugins/apps/webserver/test_apache.py b/tests/plugins/apps/webserver/test_apache.py index 0e2b918bba..7c3e5175c6 100644 --- a/tests/plugins/apps/webserver/test_apache.py +++ b/tests/plugins/apps/webserver/test_apache.py @@ -500,7 +500,7 @@ def test_apache_hosts_certificates(target_unix: Target, fs_unix: VirtualFilesyst assert records[0].fingerprint.md5 == "a218ac9b6dbdaa8b23658c4d18c1cfc1" assert records[0].fingerprint.sha1 == "6566d8ebea1feb4eb3d12d9486cddb69e4e9e827" assert records[0].fingerprint.sha256 == "7221d881743505f13b7bfe854bdf800d7f0cd22d34307ed7157808a295299471" - assert records[0].serial_number == 21067204948278457910649605551283467908287726794 + assert records[0].serial_number == "03b0afa702c33e37fffd40e0c402b2120c1284ca" assert records[0].not_valid_before == datetime(2025, 11, 27, 15, 31, 20, tzinfo=timezone.utc) assert records[0].not_valid_after == datetime(2026, 11, 27, 15, 31, 20, tzinfo=timezone.utc) assert records[0].issuer_dn == "C=AU,ST=Some-State,O=Internet Widgits Pty Ltd,CN=example.com" diff --git a/tests/plugins/os/windows/test_certlog.py b/tests/plugins/os/windows/test_certlog.py index 1ebacfbbc2..9936a79ab2 100644 --- a/tests/plugins/os/windows/test_certlog.py +++ b/tests/plugins/os/windows/test_certlog.py @@ -1,5 +1,6 @@ from __future__ import annotations +import hashlib from typing import TYPE_CHECKING from dissect.target.plugins.os.windows import certlog @@ -23,10 +24,9 @@ def test_certlog_plugin(target_win: Target, fs_win: VirtualFilesystem) -> None: certificates = list(target_win.certlog.certificates()) assert len(certificates) == 11 assert len(list(target_win.certlog.certificate_extensions())) == 92 - - assert certificates[0].serial_number_hex == "1169f0517d9b598e4ba7af46e4674066" - assert int(certificates[0].serial_number_hex, 16) == 23146941333149199441888068127529844838 - # TODO: Further certificate normalization, see https://github.com/fox-it/dissect.target/issues/1452 + assert certificates[0].serial_number == "1169f0517d9b598e4ba7af46e4674066" + assert certificates[0].fingerprint.sha1 == hashlib.sha1(certificates[0].raw_certificate).hexdigest() + assert certificates[0].fingerprint.sha1 == "061ec97dcef82d0e2d100b590e790a803d4573ae" def test_certlog_plugin_direct() -> None: From 7f5cb3236773651bb35f9eefaf34ce2bc6902e4f Mon Sep 17 00:00:00 2001 From: wbi Date: Wed, 14 Jan 2026 17:50:13 +0100 Subject: [PATCH 02/29] endorsement_fingerprint -> endorsement_certificate_hash --- dissect/target/plugins/os/windows/certlog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dissect/target/plugins/os/windows/certlog.py b/dissect/target/plugins/os/windows/certlog.py index 5ebd9594fe..8d437801b9 100644 --- a/dissect/target/plugins/os/windows/certlog.py +++ b/dissect/target/plugins/os/windows/certlog.py @@ -98,7 +98,7 @@ ("string", "distinguished_name"), ("string", "domain_component"), ("string", "email"), - ("string", "endorsement_fingerprint"), + ("string", "endorsement_certificate_hash"), ("string", "endorsement_key_hash"), ("string", "given_name"), ("string", "initials"), @@ -182,7 +182,7 @@ "$DistinguishedName": "distinguished_name", "$DomainComponent": "domain_component", "$EMail": "email", - "$EndorsementCertificateHash": "endorsement_fingerprint", + "$EndorsementCertificateHash": "endorsement_certificate_hash", "$EndorsementKeyHash": "endorsement_key_hash", "$ExtensionName": "extension_name", "$GivenName": "given_name", From c38f7334ed7d74a7917d83d22556b3f086f7eb6e Mon Sep 17 00:00:00 2001 From: wbi Date: Wed, 14 Jan 2026 17:51:04 +0100 Subject: [PATCH 03/29] distinguished_name -> subject_dn --- dissect/target/plugins/os/windows/certlog.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dissect/target/plugins/os/windows/certlog.py b/dissect/target/plugins/os/windows/certlog.py index 8d437801b9..11f779c1ee 100644 --- a/dissect/target/plugins/os/windows/certlog.py +++ b/dissect/target/plugins/os/windows/certlog.py @@ -48,7 +48,7 @@ ("string", "common_name"), ("string", "country"), ("string", "device_serial_number"), - ("string", "distinguished_name"), + ("string", "subject_dn"), ("string", "domain_component"), ("string", "email"), ("string", "given_name"), @@ -95,7 +95,7 @@ ("string", "device_serial_number"), ("string", "disposition"), ("string", "disposition_message"), - ("string", "distinguished_name"), + ("string", "subject_dn"), ("string", "domain_component"), ("string", "email"), ("string", "endorsement_certificate_hash"), @@ -179,7 +179,7 @@ "$Country": "country", "$DeviceSerialNumber": "device_serial_number", "$DispositionMessage": "disposition_message", - "$DistinguishedName": "distinguished_name", + "$DistinguishedName": "subject_dn", "$DomainComponent": "domain_component", "$EMail": "email", "$EndorsementCertificateHash": "endorsement_certificate_hash", From c308cbf85e9c7bd6c9c29817c5c654800ce7898d Mon Sep 17 00:00:00 2001 From: wbi Date: Wed, 14 Jan 2026 17:55:09 +0100 Subject: [PATCH 04/29] Fix tests --- tests/plugins/apps/webserver/test_nginx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/plugins/apps/webserver/test_nginx.py b/tests/plugins/apps/webserver/test_nginx.py index de5a77b37a..5905821c9f 100644 --- a/tests/plugins/apps/webserver/test_nginx.py +++ b/tests/plugins/apps/webserver/test_nginx.py @@ -273,5 +273,5 @@ def test_nginx_host_certificate(target_unix: Target, fs_unix: VirtualFilesystem) assert records[0].fingerprint.md5 == "a218ac9b6dbdaa8b23658c4d18c1cfc1" assert records[0].fingerprint.sha1 == "6566d8ebea1feb4eb3d12d9486cddb69e4e9e827" assert records[0].fingerprint.sha256 == "7221d881743505f13b7bfe854bdf800d7f0cd22d34307ed7157808a295299471" - assert records[0].serial_number == 21067204948278457910649605551283467908287726794 + assert records[0].serial_number == "03b0afa702c33e37fffd40e0c402b2120c1284ca" assert records[0].issuer_dn == "C=AU,ST=Some-State,O=Internet Widgits Pty Ltd,CN=example.com" From a76e2cac76c236f33986167abb7cc669217cb5f2 Mon Sep 17 00:00:00 2001 From: wbi Date: Thu, 15 Jan 2026 12:01:25 +0100 Subject: [PATCH 05/29] Add warning if column name would cause duplicate. Fix documentation related to format fingerprint --- dissect/target/plugins/os/windows/certlog.py | 40 +++++++++++++++----- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/dissect/target/plugins/os/windows/certlog.py b/dissect/target/plugins/os/windows/certlog.py index 11f779c1ee..b463c44003 100644 --- a/dissect/target/plugins/os/windows/certlog.py +++ b/dissect/target/plugins/os/windows/certlog.py @@ -255,22 +255,35 @@ } -def format_fingerprint(input_hash: str | None) -> tuple[str | None, str | None, str | None]: +def format_fingerprint(input_hash: str | None, target: Target) -> tuple[str | None, str | None, str | None]: if input_hash: input_hash = input_hash.replace(" ", "") - # older version use md5 - if len(input_hash) == 64: - return input_hash, None, None - return None, input_hash, None + # hash is expected to be a sha1, but as it not documented, we make this function more flexible if hash is + # in another standard format (md5/sha256), especially in the futur + match len(input_hash): + case 32: + return input_hash, None, None + case 40: + return None, input_hash, None + case 64: + return None, None, input_hash + case _: + target.log.warning( + "Unexpected hash size found while processing certlog " + "$CertificateHash/$CertificateHash2 column: len %d, content %s", + len(input_hash), + input_hash, + ) + return None, None, None -def format_serial_number(serial_number_as_hex: str | None) -> str | None: +def format_serial_number(serial_number_as_hex: str | None, target: Target) -> str | None: if not serial_number_as_hex: return None return serial_number_as_hex.replace(" ", "") -FORMATING_FUNC: dict[str, Callable[[Any], Any]] = { +FORMATING_FUNC: dict[str, Callable[[Any, Target], Any]] = { "fingerprint": format_fingerprint, "serial_number": format_serial_number, } @@ -325,9 +338,18 @@ def read_records(self, table_name: str, record_type: CertLogRecord) -> Iterator[ for column, value in column_values: new_column = FIELD_MAPPINGS.get(column) if new_column in FORMATING_FUNC: - value = FORMATING_FUNC[new_column](value) - if new_column: + value = FORMATING_FUNC[new_column](value, self.target) + if new_column and new_column not in record_values: record_values[new_column] = value + elif new_column: + self.target.log.debug( + "Unexpected element while processing %s entries : %s column already exists " + "(mapped from original column name %s). This may be cause by two column that were not" + " expected to be present in the same time.", + table_name, + new_column, + column, + ) else: self.target.log.debug( "Unexpected column for table %s in CA %s: %s", table_name, ca_name, column From c51bd475a971ff95eef65789314ee735f5c94973 Mon Sep 17 00:00:00 2001 From: wbi Date: Thu, 15 Jan 2026 13:54:46 +0100 Subject: [PATCH 06/29] Revert UAL change --- dissect/target/plugins/os/windows/ual.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dissect/target/plugins/os/windows/ual.py b/dissect/target/plugins/os/windows/ual.py index f3b8a9e218..c2eecd7663 100644 --- a/dissect/target/plugins/os/windows/ual.py +++ b/dissect/target/plugins/os/windows/ual.py @@ -53,7 +53,7 @@ ("datetime", "last_seen_active_date"), ("string", "vm_guid"), ("string", "bios_guid"), - ("string", "serial_number"), + ("string", "serial"), ("string", "path"), ], ) @@ -129,7 +129,7 @@ "ProductName": "product_name", "RoleGuid": "role_guid", "RoleName": "role_name", - "SerialNumber": "serial_number", + "SerialNumber": "serial", "ServicePackMajor": "service_pack_major_version", "ServicePackMinor": "service_pack_minor_version", "SystemDNSHostName": "system_dns_hostname", From fa824c52dfb291ab939630ac559a3f06790e82c3 Mon Sep 17 00:00:00 2001 From: wbi Date: Thu, 15 Jan 2026 14:46:28 +0100 Subject: [PATCH 07/29] Add serial_number and serial_number_hex Add tests related to negative serial number processing --- dissect/target/helpers/certificate.py | 16 ++-- dissect/target/plugins/os/windows/certlog.py | 15 +++- .../apps/webserver/negative_serial.crt | 3 + .../apps/webserver/negative_serial.key | 3 + tests/plugins/apps/webserver/test_apache.py | 90 ++++++++++++++++++- tests/plugins/apps/webserver/test_nginx.py | 3 +- tests/plugins/os/windows/test_certlog.py | 3 +- 7 files changed, 122 insertions(+), 11 deletions(-) create mode 100644 tests/_data/plugins/apps/webserver/negative_serial.crt create mode 100644 tests/_data/plugins/apps/webserver/negative_serial.key diff --git a/dissect/target/helpers/certificate.py b/dissect/target/helpers/certificate.py index 33dbd88c4b..210d07358d 100755 --- a/dissect/target/helpers/certificate.py +++ b/dissect/target/helpers/certificate.py @@ -17,7 +17,8 @@ COMMON_CERTIFICATE_FIELDS = [ ("digest", "fingerprint"), - ("string", "serial_number"), + ("varint", "serial_number"), + ("string", "serial_number_hex"), ("datetime", "not_valid_before"), ("datetime", "not_valid_after"), ("string", "issuer_dn"), @@ -84,10 +85,12 @@ def format_serial_number_as_hex(serial_number: int | None) -> str | None: """ if serial_number is None: return serial_number - serial_number_as_hex = f"{serial_number:x}" - if len(serial_number_as_hex) % 2 == 1: - serial_number_as_hex = f"0{serial_number_as_hex}" - return serial_number_as_hex + if serial_number > 0: + serial_number_as_hex = f"{serial_number:x}" + if len(serial_number_as_hex) % 2 == 1: + serial_number_as_hex = f"0{serial_number_as_hex}" + return serial_number_as_hex + return f"-{-serial_number:x}" def parse_x509(file: str | bytes | Path) -> CertificateRecord: @@ -126,6 +129,7 @@ def parse_x509(file: str | bytes | Path) -> CertificateRecord: issuer_dn=",".join(issuer), subject_dn=",".join(subject), fingerprint=(md5, crt.sha1.hex(), crt.sha256.hex()), - serial_number=format_serial_number_as_hex(crt.serial_number), + serial_number=crt.serial_number, + serial_number_hex=format_serial_number_as_hex(crt.serial_number), pem=crt.dump(), ) diff --git a/dissect/target/plugins/os/windows/certlog.py b/dissect/target/plugins/os/windows/certlog.py index b463c44003..e161f52df6 100644 --- a/dissect/target/plugins/os/windows/certlog.py +++ b/dissect/target/plugins/os/windows/certlog.py @@ -58,6 +58,8 @@ ("string", "organizational_unit"), ("string", "public_key_algorithm"), ("string", "serial_number"), + ("string", "serial_number_hex"), + ("varint", "serial_number"), ("string", "state_or_province"), ("string", "street_address"), ("string", "subject_key_identifier"), @@ -194,7 +196,7 @@ "$PublicKeyAlgorithm": "public_key_algorithm", "$RequestAttributes": "request_attributes", "$RequesterName": "requester_name", - "$SerialNumber": "serial_number", + "$SerialNumber": "serial_number_hex", "$SignerApplicationPolicies": "signer_application_policies", "$SignerPolicies": "signer_policies", "$StateOrProvince": "state_or_province", @@ -283,9 +285,15 @@ def format_serial_number(serial_number_as_hex: str | None, target: Target) -> st return serial_number_as_hex.replace(" ", "") +def serial_number_as_int(serial_number_as_hex: str | None) -> int | None: + if not serial_number_as_hex: + return None + return int(serial_number_as_hex, 16) + + FORMATING_FUNC: dict[str, Callable[[Any, Target], Any]] = { "fingerprint": format_fingerprint, - "serial_number": format_serial_number, + "serial_number_hex": format_serial_number, } @@ -341,6 +349,9 @@ def read_records(self, table_name: str, record_type: CertLogRecord) -> Iterator[ value = FORMATING_FUNC[new_column](value, self.target) if new_column and new_column not in record_values: record_values[new_column] = value + # Serial number is format as int and string, to ease search of a specific sn in both format + if new_column == "serial_number_hex": + record_values["serial_number"] = serial_number_as_int(value) elif new_column: self.target.log.debug( "Unexpected element while processing %s entries : %s column already exists " diff --git a/tests/_data/plugins/apps/webserver/negative_serial.crt b/tests/_data/plugins/apps/webserver/negative_serial.crt new file mode 100644 index 0000000000..f60e2b8dfd --- /dev/null +++ b/tests/_data/plugins/apps/webserver/negative_serial.crt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d6f36dde378f080a05358ae6f572d1ef6a42725e23caafa076a7a532ad576888 +size 1440 diff --git a/tests/_data/plugins/apps/webserver/negative_serial.key b/tests/_data/plugins/apps/webserver/negative_serial.key new file mode 100644 index 0000000000..40c5e7aa6b --- /dev/null +++ b/tests/_data/plugins/apps/webserver/negative_serial.key @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2950633419e7dcd5b2830d6a6a05ac8fa675c58e70f19cc1ae23c7fb739b6ea +size 1704 diff --git a/tests/plugins/apps/webserver/test_apache.py b/tests/plugins/apps/webserver/test_apache.py index 7c3e5175c6..f6af4c9033 100644 --- a/tests/plugins/apps/webserver/test_apache.py +++ b/tests/plugins/apps/webserver/test_apache.py @@ -500,9 +500,97 @@ def test_apache_hosts_certificates(target_unix: Target, fs_unix: VirtualFilesyst assert records[0].fingerprint.md5 == "a218ac9b6dbdaa8b23658c4d18c1cfc1" assert records[0].fingerprint.sha1 == "6566d8ebea1feb4eb3d12d9486cddb69e4e9e827" assert records[0].fingerprint.sha256 == "7221d881743505f13b7bfe854bdf800d7f0cd22d34307ed7157808a295299471" - assert records[0].serial_number == "03b0afa702c33e37fffd40e0c402b2120c1284ca" + assert records[0].serial_number == 21067204948278457910649605551283467908287726794 + assert records[0].serial_number_hex == "03b0afa702c33e37fffd40e0c402b2120c1284ca" assert records[0].not_valid_before == datetime(2025, 11, 27, 15, 31, 20, tzinfo=timezone.utc) assert records[0].not_valid_after == datetime(2026, 11, 27, 15, 31, 20, tzinfo=timezone.utc) assert records[0].issuer_dn == "C=AU,ST=Some-State,O=Internet Widgits Pty Ltd,CN=example.com" assert records[0].host == "example.com" assert records[0].source == "/etc/apache2/ssl/example/cert.crt" + + +def test_apache_hosts_certificates_negative_serial_number(target_unix: Target, fs_unix: VirtualFilesystem) -> None: + """Test if we can parse Apache ``VirtualHost`` certificates, with a certificate using a negative serial number + certificate generated using + ``` + openssl genrsa -out negative_serial.key 2048 + openssl req -new -x509 -key negative_serial.key \ + -out negative_serial.crt -days 365 -set_serial -1337 -config openssl.cnf + ``` + + Where openssl.cnf has the following content + + ``` + [ req ] + default_bits = 2048 + distinguished_name = req_distinguished_name + x509_extensions = v3_ca + prompt = no + + [ req_distinguished_name ] + C = FR + ST = RHONE + L = Lyon + O = Dissect + OU = Demo + CN = docs.dissect.tools + + [ v3_ca ] + subjectKeyIdentifier = hash + authorityKeyIdentifier = keyid:always,issuer + basicConstraints = critical,CA:TRUE + keyUsage = critical,digitalSignature,keyEncipherment + subjectAltName = @alt_names + + [ alt_names ] + DNS.1 = monserveur.example.com + DNS.2 = www.monserveur.example.com + IP.1 = 192.168.1.100 + ``` + + + """ + + fs_unix.map_file_fh("/etc/apache2/apache2.conf", BytesIO(b'ServerRoot "/etc/apache2"\n')) + + site = r""" + + ServerName example.com + ServerAlias www.example.com + DocumentRoot /var/www/html + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + SSLEngine on + SSLCertificateFile /path/to/cert.crt + SSLCertificateKeyFile /path/to/cert.key + + """ + fs_unix.map_file_fh("/etc/apache2/sites-available/example.conf", BytesIO(textwrap.dedent(site).encode())) + fs_unix.map_file("/path/to/cert.crt", absolute_path("_data/plugins/apps/webserver/negative_serial.crt")) + fs_unix.map_file("/path/to/cert.key", absolute_path("_data/plugins/apps/webserver/negative_serial.key")) + + # Map a default location too + fs_unix.map_file( + "/etc/apache2/ssl/example/cert.crt", absolute_path("_data/plugins/apps/webserver/negative_serial.crt") + ) + fs_unix.map_file( + "/etc/apache2/ssl/example/cert.key", absolute_path("_data/plugins/apps/webserver/negative_serial.key") + ) + + target_unix.add_plugin(ApachePlugin) + + records = sorted(target_unix.apache.certificates(), key=lambda r: r.source) + assert len(records) == 2 + + assert records[0].webserver == "apache" + assert records[0].fingerprint.md5 == "ca443d8103dea9606941eca59f91171a" + assert records[0].fingerprint.sha1 == "2611b6245659da68772c1b70830ec8d7b4b9c4af" + assert records[0].fingerprint.sha256 == "0d69e5c68a62353cc7a1dd9d088d60f1028184e8bb693a2ba81cedd05b8804c1" + assert records[0].serial_number == -1337 + # openssl display the following for negative numbers : Serial Number: -1337 (-0x539) + assert records[0].serial_number_hex == "-539" + assert records[0].not_valid_before == datetime(2026, 1, 15, 13, 32, 00, tzinfo=timezone.utc) + assert records[0].not_valid_after == datetime(2027, 1, 15, 13, 32, 00, tzinfo=timezone.utc) + assert records[0].issuer_dn == "C=FR,ST=RHONE,L=Lyon,O=Dissect,OU=Demo,CN=docs.dissect.tools" + assert records[0].host == "example.com" + assert records[0].source == "/etc/apache2/ssl/example/cert.crt" diff --git a/tests/plugins/apps/webserver/test_nginx.py b/tests/plugins/apps/webserver/test_nginx.py index 5905821c9f..e0c46afa8d 100644 --- a/tests/plugins/apps/webserver/test_nginx.py +++ b/tests/plugins/apps/webserver/test_nginx.py @@ -273,5 +273,6 @@ def test_nginx_host_certificate(target_unix: Target, fs_unix: VirtualFilesystem) assert records[0].fingerprint.md5 == "a218ac9b6dbdaa8b23658c4d18c1cfc1" assert records[0].fingerprint.sha1 == "6566d8ebea1feb4eb3d12d9486cddb69e4e9e827" assert records[0].fingerprint.sha256 == "7221d881743505f13b7bfe854bdf800d7f0cd22d34307ed7157808a295299471" - assert records[0].serial_number == "03b0afa702c33e37fffd40e0c402b2120c1284ca" + assert records[0].serial_number == 21067204948278457910649605551283467908287726794 + assert records[0].serial_number_hex == "03b0afa702c33e37fffd40e0c402b2120c1284ca" assert records[0].issuer_dn == "C=AU,ST=Some-State,O=Internet Widgits Pty Ltd,CN=example.com" diff --git a/tests/plugins/os/windows/test_certlog.py b/tests/plugins/os/windows/test_certlog.py index 9936a79ab2..193441bf54 100644 --- a/tests/plugins/os/windows/test_certlog.py +++ b/tests/plugins/os/windows/test_certlog.py @@ -24,7 +24,8 @@ def test_certlog_plugin(target_win: Target, fs_win: VirtualFilesystem) -> None: certificates = list(target_win.certlog.certificates()) assert len(certificates) == 11 assert len(list(target_win.certlog.certificate_extensions())) == 92 - assert certificates[0].serial_number == "1169f0517d9b598e4ba7af46e4674066" + assert certificates[0].serial_number == 21067204948278457910649605551283467908287726794 + assert certificates[0].serial_number_hex == "1169f0517d9b598e4ba7af46e4674066" assert certificates[0].fingerprint.sha1 == hashlib.sha1(certificates[0].raw_certificate).hexdigest() assert certificates[0].fingerprint.sha1 == "061ec97dcef82d0e2d100b590e790a803d4573ae" From d32e6e46191c309e5a65b3d45a0cf5fae9663547 Mon Sep 17 00:00:00 2001 From: wbi Date: Thu, 15 Jan 2026 14:55:58 +0100 Subject: [PATCH 08/29] Fix tests --- tests/plugins/os/windows/test_certlog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/plugins/os/windows/test_certlog.py b/tests/plugins/os/windows/test_certlog.py index 193441bf54..50b7a5a3cd 100644 --- a/tests/plugins/os/windows/test_certlog.py +++ b/tests/plugins/os/windows/test_certlog.py @@ -24,7 +24,7 @@ def test_certlog_plugin(target_win: Target, fs_win: VirtualFilesystem) -> None: certificates = list(target_win.certlog.certificates()) assert len(certificates) == 11 assert len(list(target_win.certlog.certificate_extensions())) == 92 - assert certificates[0].serial_number == 21067204948278457910649605551283467908287726794 + assert certificates[0].serial_number == 23146941333149199441888068127529844838 assert certificates[0].serial_number_hex == "1169f0517d9b598e4ba7af46e4674066" assert certificates[0].fingerprint.sha1 == hashlib.sha1(certificates[0].raw_certificate).hexdigest() assert certificates[0].fingerprint.sha1 == "061ec97dcef82d0e2d100b590e790a803d4573ae" From b16db6e2b695ed386bca3117caa6d2f0583f70a5 Mon Sep 17 00:00:00 2001 From: wbi Date: Thu, 15 Jan 2026 15:31:45 +0100 Subject: [PATCH 09/29] add 0x in prefix of hex string serial number --- dissect/target/helpers/certificate.py | 4 ++-- dissect/target/plugins/os/windows/certlog.py | 2 +- tests/plugins/apps/webserver/test_apache.py | 4 ++-- tests/plugins/apps/webserver/test_nginx.py | 2 +- tests/plugins/os/windows/test_certlog.py | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/dissect/target/helpers/certificate.py b/dissect/target/helpers/certificate.py index 210d07358d..bbbde3b25b 100755 --- a/dissect/target/helpers/certificate.py +++ b/dissect/target/helpers/certificate.py @@ -89,8 +89,8 @@ def format_serial_number_as_hex(serial_number: int | None) -> str | None: serial_number_as_hex = f"{serial_number:x}" if len(serial_number_as_hex) % 2 == 1: serial_number_as_hex = f"0{serial_number_as_hex}" - return serial_number_as_hex - return f"-{-serial_number:x}" + return f"0x{serial_number_as_hex}" + return f"-{-serial_number:#x}" def parse_x509(file: str | bytes | Path) -> CertificateRecord: diff --git a/dissect/target/plugins/os/windows/certlog.py b/dissect/target/plugins/os/windows/certlog.py index e161f52df6..ac2c662920 100644 --- a/dissect/target/plugins/os/windows/certlog.py +++ b/dissect/target/plugins/os/windows/certlog.py @@ -282,7 +282,7 @@ def format_fingerprint(input_hash: str | None, target: Target) -> tuple[str | No def format_serial_number(serial_number_as_hex: str | None, target: Target) -> str | None: if not serial_number_as_hex: return None - return serial_number_as_hex.replace(" ", "") + return f"0x{serial_number_as_hex.replace(' ', '')}" def serial_number_as_int(serial_number_as_hex: str | None) -> int | None: diff --git a/tests/plugins/apps/webserver/test_apache.py b/tests/plugins/apps/webserver/test_apache.py index f6af4c9033..4987243663 100644 --- a/tests/plugins/apps/webserver/test_apache.py +++ b/tests/plugins/apps/webserver/test_apache.py @@ -501,7 +501,7 @@ def test_apache_hosts_certificates(target_unix: Target, fs_unix: VirtualFilesyst assert records[0].fingerprint.sha1 == "6566d8ebea1feb4eb3d12d9486cddb69e4e9e827" assert records[0].fingerprint.sha256 == "7221d881743505f13b7bfe854bdf800d7f0cd22d34307ed7157808a295299471" assert records[0].serial_number == 21067204948278457910649605551283467908287726794 - assert records[0].serial_number_hex == "03b0afa702c33e37fffd40e0c402b2120c1284ca" + assert records[0].serial_number_hex == "0x03b0afa702c33e37fffd40e0c402b2120c1284ca" assert records[0].not_valid_before == datetime(2025, 11, 27, 15, 31, 20, tzinfo=timezone.utc) assert records[0].not_valid_after == datetime(2026, 11, 27, 15, 31, 20, tzinfo=timezone.utc) assert records[0].issuer_dn == "C=AU,ST=Some-State,O=Internet Widgits Pty Ltd,CN=example.com" @@ -588,7 +588,7 @@ def test_apache_hosts_certificates_negative_serial_number(target_unix: Target, f assert records[0].fingerprint.sha256 == "0d69e5c68a62353cc7a1dd9d088d60f1028184e8bb693a2ba81cedd05b8804c1" assert records[0].serial_number == -1337 # openssl display the following for negative numbers : Serial Number: -1337 (-0x539) - assert records[0].serial_number_hex == "-539" + assert records[0].serial_number_hex == "-0x539" assert records[0].not_valid_before == datetime(2026, 1, 15, 13, 32, 00, tzinfo=timezone.utc) assert records[0].not_valid_after == datetime(2027, 1, 15, 13, 32, 00, tzinfo=timezone.utc) assert records[0].issuer_dn == "C=FR,ST=RHONE,L=Lyon,O=Dissect,OU=Demo,CN=docs.dissect.tools" diff --git a/tests/plugins/apps/webserver/test_nginx.py b/tests/plugins/apps/webserver/test_nginx.py index e0c46afa8d..6b9bbd2b2e 100644 --- a/tests/plugins/apps/webserver/test_nginx.py +++ b/tests/plugins/apps/webserver/test_nginx.py @@ -274,5 +274,5 @@ def test_nginx_host_certificate(target_unix: Target, fs_unix: VirtualFilesystem) assert records[0].fingerprint.sha1 == "6566d8ebea1feb4eb3d12d9486cddb69e4e9e827" assert records[0].fingerprint.sha256 == "7221d881743505f13b7bfe854bdf800d7f0cd22d34307ed7157808a295299471" assert records[0].serial_number == 21067204948278457910649605551283467908287726794 - assert records[0].serial_number_hex == "03b0afa702c33e37fffd40e0c402b2120c1284ca" + assert records[0].serial_number_hex == "0x03b0afa702c33e37fffd40e0c402b2120c1284ca" assert records[0].issuer_dn == "C=AU,ST=Some-State,O=Internet Widgits Pty Ltd,CN=example.com" diff --git a/tests/plugins/os/windows/test_certlog.py b/tests/plugins/os/windows/test_certlog.py index 50b7a5a3cd..9adc0621e4 100644 --- a/tests/plugins/os/windows/test_certlog.py +++ b/tests/plugins/os/windows/test_certlog.py @@ -25,7 +25,7 @@ def test_certlog_plugin(target_win: Target, fs_win: VirtualFilesystem) -> None: assert len(certificates) == 11 assert len(list(target_win.certlog.certificate_extensions())) == 92 assert certificates[0].serial_number == 23146941333149199441888068127529844838 - assert certificates[0].serial_number_hex == "1169f0517d9b598e4ba7af46e4674066" + assert certificates[0].serial_number_hex == "0x1169f0517d9b598e4ba7af46e4674066" assert certificates[0].fingerprint.sha1 == hashlib.sha1(certificates[0].raw_certificate).hexdigest() assert certificates[0].fingerprint.sha1 == "061ec97dcef82d0e2d100b590e790a803d4573ae" From 5bb4d3066f2ba1f23479c4ebcaf81f2e4638beaa Mon Sep 17 00:00:00 2001 From: william billaud <23636016+william-billaud@users.noreply.github.com> Date: Fri, 16 Jan 2026 13:23:30 +0100 Subject: [PATCH 10/29] Update dissect/target/plugins/os/windows/certlog.py Co-authored-by: Erik Schamper <1254028+Schamper@users.noreply.github.com> --- dissect/target/plugins/os/windows/certlog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dissect/target/plugins/os/windows/certlog.py b/dissect/target/plugins/os/windows/certlog.py index ac2c662920..bd41bca601 100644 --- a/dissect/target/plugins/os/windows/certlog.py +++ b/dissect/target/plugins/os/windows/certlog.py @@ -261,7 +261,7 @@ def format_fingerprint(input_hash: str | None, target: Target) -> tuple[str | No if input_hash: input_hash = input_hash.replace(" ", "") # hash is expected to be a sha1, but as it not documented, we make this function more flexible if hash is - # in another standard format (md5/sha256), especially in the futur + # in another standard format (md5/sha256), especially in the future match len(input_hash): case 32: return input_hash, None, None From cc1662a62ac8eddff4f304ea45335de2d64dd1c0 Mon Sep 17 00:00:00 2001 From: william billaud <23636016+william-billaud@users.noreply.github.com> Date: Fri, 16 Jan 2026 13:57:34 +0100 Subject: [PATCH 11/29] Update dissect/target/helpers/certificate.py Co-authored-by: Erik Schamper <1254028+Schamper@users.noreply.github.com> --- dissect/target/helpers/certificate.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dissect/target/helpers/certificate.py b/dissect/target/helpers/certificate.py index bbbde3b25b..3898b2dce5 100755 --- a/dissect/target/helpers/certificate.py +++ b/dissect/target/helpers/certificate.py @@ -76,12 +76,12 @@ def compute_pem_fingerprints(pem: str | bytes) -> tuple[str, str, str]: def format_serial_number_as_hex(serial_number: int | None) -> str | None: - """ - Format serial_number from integer to hex. Add a prefix 0 if output length is not pair, in order to be consistent - with usual serial_number representation (navigator, openssl etc...). + """Format serial_number from integer to hex. + + Add a prefix 0 if output length is not pair, in order to be consistent with usual serial_number representation (navigator, openssl etc...). - :param serial_number: - :return: + Args: + serial_number: The serial number to format as hex. """ if serial_number is None: return serial_number From 31debe7f6123e81ffaaffff5474f5ae2dec5c88f Mon Sep 17 00:00:00 2001 From: wbi Date: Fri, 16 Jan 2026 13:58:40 +0100 Subject: [PATCH 12/29] Fix typo --- dissect/target/plugins/os/windows/certlog.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dissect/target/plugins/os/windows/certlog.py b/dissect/target/plugins/os/windows/certlog.py index bd41bca601..01372f74c3 100644 --- a/dissect/target/plugins/os/windows/certlog.py +++ b/dissect/target/plugins/os/windows/certlog.py @@ -57,7 +57,6 @@ ("string", "organization"), ("string", "organizational_unit"), ("string", "public_key_algorithm"), - ("string", "serial_number"), ("string", "serial_number_hex"), ("varint", "serial_number"), ("string", "state_or_province"), From a0d96920616a461ebbc3838387480d5f3d25dc0a Mon Sep 17 00:00:00 2001 From: wbi Date: Fri, 16 Jan 2026 14:48:29 +0100 Subject: [PATCH 13/29] Add tests related to certificate with negatives serial numbers --- dissect/target/helpers/certificate.py | 18 +++-- .../webserver/{ => certificates}/example.crt | 0 .../webserver/{ => certificates}/example.key | 0 .../{ => certificates}/negative_serial.crt | 0 .../{ => certificates}/negative_serial.key | 0 .../certificates/negative_serial_high.crt | 3 + .../certificates/negative_serial_high_2.crt | 3 + .../certificates/negative_serial_high_3.crt | 3 + .../certificates/negative_serial_high_4.crt | 3 + tests/plugins/apps/webserver/test_apache.py | 80 ++++++++++++++----- tests/plugins/apps/webserver/test_nginx.py | 4 +- 11 files changed, 88 insertions(+), 26 deletions(-) rename tests/_data/plugins/apps/webserver/{ => certificates}/example.crt (100%) rename tests/_data/plugins/apps/webserver/{ => certificates}/example.key (100%) rename tests/_data/plugins/apps/webserver/{ => certificates}/negative_serial.crt (100%) rename tests/_data/plugins/apps/webserver/{ => certificates}/negative_serial.key (100%) create mode 100644 tests/_data/plugins/apps/webserver/certificates/negative_serial_high.crt create mode 100644 tests/_data/plugins/apps/webserver/certificates/negative_serial_high_2.crt create mode 100644 tests/_data/plugins/apps/webserver/certificates/negative_serial_high_3.crt create mode 100644 tests/_data/plugins/apps/webserver/certificates/negative_serial_high_4.crt diff --git a/dissect/target/helpers/certificate.py b/dissect/target/helpers/certificate.py index 3898b2dce5..662e6e8c1d 100755 --- a/dissect/target/helpers/certificate.py +++ b/dissect/target/helpers/certificate.py @@ -76,21 +76,29 @@ def compute_pem_fingerprints(pem: str | bytes) -> tuple[str, str, str]: def format_serial_number_as_hex(serial_number: int | None) -> str | None: - """Format serial_number from integer to hex. - - Add a prefix 0 if output length is not pair, in order to be consistent with usual serial_number representation (navigator, openssl etc...). + """Format serial_number from integer to hex. + + Add a prefix 0 if output length is not pair, in order to be consistent with usual serial_number representation + (navigator, openssl etc...). + For negative number use the same representation as navigator, which differ from openssl + E.g for -1337 : + open ssl : Serial Number: -1337 (-0x539) + Navigator : FA C7 Args: serial_number: The serial number to format as hex. """ if serial_number is None: return serial_number + if serial_number > 0: serial_number_as_hex = f"{serial_number:x}" if len(serial_number_as_hex) % 2 == 1: serial_number_as_hex = f"0{serial_number_as_hex}" - return f"0x{serial_number_as_hex}" - return f"-{-serial_number:#x}" + return f"{serial_number_as_hex}" + # Representation is always a multiple of 8 bits, we need to compute this size + output_bin_len = (8 - (len(bin(serial_number)) - 3) % 8) + len(bin(serial_number)) - 3 + return f"{serial_number & ((1 << output_bin_len) - 1):x}" def parse_x509(file: str | bytes | Path) -> CertificateRecord: diff --git a/tests/_data/plugins/apps/webserver/example.crt b/tests/_data/plugins/apps/webserver/certificates/example.crt similarity index 100% rename from tests/_data/plugins/apps/webserver/example.crt rename to tests/_data/plugins/apps/webserver/certificates/example.crt diff --git a/tests/_data/plugins/apps/webserver/example.key b/tests/_data/plugins/apps/webserver/certificates/example.key similarity index 100% rename from tests/_data/plugins/apps/webserver/example.key rename to tests/_data/plugins/apps/webserver/certificates/example.key diff --git a/tests/_data/plugins/apps/webserver/negative_serial.crt b/tests/_data/plugins/apps/webserver/certificates/negative_serial.crt similarity index 100% rename from tests/_data/plugins/apps/webserver/negative_serial.crt rename to tests/_data/plugins/apps/webserver/certificates/negative_serial.crt diff --git a/tests/_data/plugins/apps/webserver/negative_serial.key b/tests/_data/plugins/apps/webserver/certificates/negative_serial.key similarity index 100% rename from tests/_data/plugins/apps/webserver/negative_serial.key rename to tests/_data/plugins/apps/webserver/certificates/negative_serial.key diff --git a/tests/_data/plugins/apps/webserver/certificates/negative_serial_high.crt b/tests/_data/plugins/apps/webserver/certificates/negative_serial_high.crt new file mode 100644 index 0000000000..f2a82b1734 --- /dev/null +++ b/tests/_data/plugins/apps/webserver/certificates/negative_serial_high.crt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bdf320de0e673176fe131dab252bda76a1e0a8ae1f46a382e2da0caf3cfa8db4 +size 1464 diff --git a/tests/_data/plugins/apps/webserver/certificates/negative_serial_high_2.crt b/tests/_data/plugins/apps/webserver/certificates/negative_serial_high_2.crt new file mode 100644 index 0000000000..fd00c78d6d --- /dev/null +++ b/tests/_data/plugins/apps/webserver/certificates/negative_serial_high_2.crt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:370814279fa55bb7968081924bf1e508495ae1d87079e651f4a9e32359ab2dc2 +size 1464 diff --git a/tests/_data/plugins/apps/webserver/certificates/negative_serial_high_3.crt b/tests/_data/plugins/apps/webserver/certificates/negative_serial_high_3.crt new file mode 100644 index 0000000000..01a0f0ac45 --- /dev/null +++ b/tests/_data/plugins/apps/webserver/certificates/negative_serial_high_3.crt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b5bff12a5eed82a63a815975e8e277b2400b993594e0d7a553ae5751e6b7805 +size 1464 diff --git a/tests/_data/plugins/apps/webserver/certificates/negative_serial_high_4.crt b/tests/_data/plugins/apps/webserver/certificates/negative_serial_high_4.crt new file mode 100644 index 0000000000..b3714ec8dc --- /dev/null +++ b/tests/_data/plugins/apps/webserver/certificates/negative_serial_high_4.crt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a24d908118cf2e6d4818250fabdbadc63aae32f9bf4f1abb2ebeba24f6948e5 +size 1464 diff --git a/tests/plugins/apps/webserver/test_apache.py b/tests/plugins/apps/webserver/test_apache.py index 4987243663..bbe8e7bc19 100644 --- a/tests/plugins/apps/webserver/test_apache.py +++ b/tests/plugins/apps/webserver/test_apache.py @@ -484,12 +484,16 @@ def test_apache_hosts_certificates(target_unix: Target, fs_unix: VirtualFilesyst """ fs_unix.map_file_fh("/etc/apache2/sites-available/example.conf", BytesIO(textwrap.dedent(site).encode())) - fs_unix.map_file("/path/to/cert.crt", absolute_path("_data/plugins/apps/webserver/example.crt")) - fs_unix.map_file("/path/to/cert.key", absolute_path("_data/plugins/apps/webserver/example.key")) + fs_unix.map_file("/path/to/cert.crt", absolute_path("_data/plugins/apps/webserver/certificates/example.crt")) + fs_unix.map_file("/path/to/cert.key", absolute_path("_data/plugins/apps/webserver/certificates/example.key")) # Map a default location too - fs_unix.map_file("/etc/apache2/ssl/example/cert.crt", absolute_path("_data/plugins/apps/webserver/example.crt")) - fs_unix.map_file("/etc/apache2/ssl/example/cert.key", absolute_path("_data/plugins/apps/webserver/example.key")) + fs_unix.map_file( + "/etc/apache2/ssl/example/cert.crt", absolute_path("_data/plugins/apps/webserver/certificates/example.crt") + ) + fs_unix.map_file( + "/etc/apache2/ssl/example/cert.key", absolute_path("_data/plugins/apps/webserver/certificates/example.key") + ) target_unix.add_plugin(ApachePlugin) @@ -501,7 +505,7 @@ def test_apache_hosts_certificates(target_unix: Target, fs_unix: VirtualFilesyst assert records[0].fingerprint.sha1 == "6566d8ebea1feb4eb3d12d9486cddb69e4e9e827" assert records[0].fingerprint.sha256 == "7221d881743505f13b7bfe854bdf800d7f0cd22d34307ed7157808a295299471" assert records[0].serial_number == 21067204948278457910649605551283467908287726794 - assert records[0].serial_number_hex == "0x03b0afa702c33e37fffd40e0c402b2120c1284ca" + assert records[0].serial_number_hex == "03b0afa702c33e37fffd40e0c402b2120c1284ca" assert records[0].not_valid_before == datetime(2025, 11, 27, 15, 31, 20, tzinfo=timezone.utc) assert records[0].not_valid_after == datetime(2026, 11, 27, 15, 31, 20, tzinfo=timezone.utc) assert records[0].issuer_dn == "C=AU,ST=Some-State,O=Internet Widgits Pty Ltd,CN=example.com" @@ -509,13 +513,52 @@ def test_apache_hosts_certificates(target_unix: Target, fs_unix: VirtualFilesyst assert records[0].source == "/etc/apache2/ssl/example/cert.crt" -def test_apache_hosts_certificates_negative_serial_number(target_unix: Target, fs_unix: VirtualFilesystem) -> None: +@pytest.mark.parametrize( + ("crt_name", "serial_number", "serial_number_hex"), + [ + pytest.param( + "negative_serial_high.crt", + -21067204948278457910649605551283467908287726794, + "fc4f5058fd3cc1c80002bf1f3bfd4dedf3ed7b36", + id="high_number", + ), + pytest.param( + "negative_serial_high_2.crt", + -210672049482784579106496055512834679082877267940, + "db192379e45f91d0001b773857e50b4b8746d01c", + id="high_number_158_bits", + ), + pytest.param( + "negative_serial_high_3.crt", + -421344098965569158212992111025669358165754535880, + "b63246f3c8bf23a00036ee70afca16970e8da038", + id="high_number_159_bits", + ), + pytest.param( + "negative_serial_high_4.crt", + -842688197931138316425984222051338716331509071760, + "ff6c648de7917e4740006ddce15f942d2e1d1b4070", + id="high_number_160_bits", + ), + pytest.param( + "negative_serial.crt", + -1337, + "fac7", + id="small_number", + ), + ], +) +def test_apache_hosts_certificates_negative_serial_number( + target_unix: Target, fs_unix: VirtualFilesystem, crt_name: str, serial_number: int, serial_number_hex: str +) -> None: """Test if we can parse Apache ``VirtualHost`` certificates, with a certificate using a negative serial number - certificate generated using + certificate generated using. + Each test use a nuber with a different length in binary format + ``` openssl genrsa -out negative_serial.key 2048 openssl req -new -x509 -key negative_serial.key \ - -out negative_serial.crt -days 365 -set_serial -1337 -config openssl.cnf + -out negative_serial.crt -days 365 -set_serial -config openssl.cnf ``` Where openssl.cnf has the following content @@ -566,15 +609,18 @@ def test_apache_hosts_certificates_negative_serial_number(target_unix: Target, f """ fs_unix.map_file_fh("/etc/apache2/sites-available/example.conf", BytesIO(textwrap.dedent(site).encode())) - fs_unix.map_file("/path/to/cert.crt", absolute_path("_data/plugins/apps/webserver/negative_serial.crt")) - fs_unix.map_file("/path/to/cert.key", absolute_path("_data/plugins/apps/webserver/negative_serial.key")) + fs_unix.map_file("/path/to/cert.crt", absolute_path(f"_data/plugins/apps/webserver/certificates/{crt_name}")) + fs_unix.map_file( + "/path/to/cert.key", absolute_path("_data/plugins/apps/webserver/certificates/negative_serial.key") + ) # Map a default location too fs_unix.map_file( - "/etc/apache2/ssl/example/cert.crt", absolute_path("_data/plugins/apps/webserver/negative_serial.crt") + "/etc/apache2/ssl/example/cert.crt", absolute_path(f"_data/plugins/apps/webserver/certificates/{crt_name}") ) fs_unix.map_file( - "/etc/apache2/ssl/example/cert.key", absolute_path("_data/plugins/apps/webserver/negative_serial.key") + "/etc/apache2/ssl/example/cert.key", + absolute_path("_data/plugins/apps/webserver/certificates/negative_serial.key"), ) target_unix.add_plugin(ApachePlugin) @@ -583,14 +629,10 @@ def test_apache_hosts_certificates_negative_serial_number(target_unix: Target, f assert len(records) == 2 assert records[0].webserver == "apache" - assert records[0].fingerprint.md5 == "ca443d8103dea9606941eca59f91171a" - assert records[0].fingerprint.sha1 == "2611b6245659da68772c1b70830ec8d7b4b9c4af" - assert records[0].fingerprint.sha256 == "0d69e5c68a62353cc7a1dd9d088d60f1028184e8bb693a2ba81cedd05b8804c1" - assert records[0].serial_number == -1337 + assert records[0].serial_number == serial_number # openssl display the following for negative numbers : Serial Number: -1337 (-0x539) - assert records[0].serial_number_hex == "-0x539" - assert records[0].not_valid_before == datetime(2026, 1, 15, 13, 32, 00, tzinfo=timezone.utc) - assert records[0].not_valid_after == datetime(2027, 1, 15, 13, 32, 00, tzinfo=timezone.utc) + # But navigators show FA:C7, we keep this representation + assert records[0].serial_number_hex == serial_number_hex assert records[0].issuer_dn == "C=FR,ST=RHONE,L=Lyon,O=Dissect,OU=Demo,CN=docs.dissect.tools" assert records[0].host == "example.com" assert records[0].source == "/etc/apache2/ssl/example/cert.crt" diff --git a/tests/plugins/apps/webserver/test_nginx.py b/tests/plugins/apps/webserver/test_nginx.py index 6b9bbd2b2e..2c60c92a0f 100644 --- a/tests/plugins/apps/webserver/test_nginx.py +++ b/tests/plugins/apps/webserver/test_nginx.py @@ -256,8 +256,8 @@ def test_nginx_host_certificate(target_unix: Target, fs_unix: VirtualFilesystem) } """ fs_unix.map_file_fh("/etc/nginx/sites-enabled/example.com.conf", BytesIO(textwrap.dedent(bar_conf).encode())) - fs_unix.map_file("/path/to/cert.pem", absolute_path("_data/plugins/apps/webserver/example.crt")) - fs_unix.map_file("/path/to/key.pem", absolute_path("_data/plugins/apps/webserver/example.key")) + fs_unix.map_file("/path/to/cert.pem", absolute_path("_data/plugins/apps/webserver/certificates/example.crt")) + fs_unix.map_file("/path/to/key.pem", absolute_path("_data/plugins/apps/webserver/certificates/example.key")) target_unix.add_plugin(NginxPlugin) records = list(target_unix.nginx.hosts()) From cc715529cdf9cedaec88f4ae669b02ae094a6b26 Mon Sep 17 00:00:00 2001 From: wbi Date: Fri, 16 Jan 2026 14:49:50 +0100 Subject: [PATCH 14/29] Fix nginx tests --- tests/plugins/apps/webserver/test_nginx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/plugins/apps/webserver/test_nginx.py b/tests/plugins/apps/webserver/test_nginx.py index 2c60c92a0f..136b174753 100644 --- a/tests/plugins/apps/webserver/test_nginx.py +++ b/tests/plugins/apps/webserver/test_nginx.py @@ -274,5 +274,5 @@ def test_nginx_host_certificate(target_unix: Target, fs_unix: VirtualFilesystem) assert records[0].fingerprint.sha1 == "6566d8ebea1feb4eb3d12d9486cddb69e4e9e827" assert records[0].fingerprint.sha256 == "7221d881743505f13b7bfe854bdf800d7f0cd22d34307ed7157808a295299471" assert records[0].serial_number == 21067204948278457910649605551283467908287726794 - assert records[0].serial_number_hex == "0x03b0afa702c33e37fffd40e0c402b2120c1284ca" + assert records[0].serial_number_hex == "03b0afa702c33e37fffd40e0c402b2120c1284ca" assert records[0].issuer_dn == "C=AU,ST=Some-State,O=Internet Widgits Pty Ltd,CN=example.com" From 9573af9ceb192c74bcb4964fa8fdff78f6d65ceb Mon Sep 17 00:00:00 2001 From: wbi Date: Fri, 16 Jan 2026 14:53:56 +0100 Subject: [PATCH 15/29] typo --- tests/plugins/apps/webserver/test_apache.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/plugins/apps/webserver/test_apache.py b/tests/plugins/apps/webserver/test_apache.py index bbe8e7bc19..967b875fb4 100644 --- a/tests/plugins/apps/webserver/test_apache.py +++ b/tests/plugins/apps/webserver/test_apache.py @@ -553,7 +553,8 @@ def test_apache_hosts_certificates_negative_serial_number( ) -> None: """Test if we can parse Apache ``VirtualHost`` certificates, with a certificate using a negative serial number certificate generated using. - Each test use a nuber with a different length in binary format + Each test use a number with a different length in binary format, + to ensure represenation match the one from navigator. ``` openssl genrsa -out negative_serial.key 2048 From ac04d610eb14d9a8f5013880914648a93c276b7e Mon Sep 17 00:00:00 2001 From: wbi Date: Fri, 16 Jan 2026 14:54:11 +0100 Subject: [PATCH 16/29] typo --- tests/plugins/apps/webserver/test_apache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/plugins/apps/webserver/test_apache.py b/tests/plugins/apps/webserver/test_apache.py index 967b875fb4..ee3cb4b09d 100644 --- a/tests/plugins/apps/webserver/test_apache.py +++ b/tests/plugins/apps/webserver/test_apache.py @@ -554,7 +554,7 @@ def test_apache_hosts_certificates_negative_serial_number( """Test if we can parse Apache ``VirtualHost`` certificates, with a certificate using a negative serial number certificate generated using. Each test use a number with a different length in binary format, - to ensure represenation match the one from navigator. + to ensure representation match the one from navigator. ``` openssl genrsa -out negative_serial.key 2048 From d2cc3275d6ccae39c78011a215b90386777aba41 Mon Sep 17 00:00:00 2001 From: william billaud <23636016+william-billaud@users.noreply.github.com> Date: Fri, 16 Jan 2026 16:33:42 +0100 Subject: [PATCH 17/29] Update dissect/target/helpers/certificate.py Co-authored-by: Erik Schamper <1254028+Schamper@users.noreply.github.com> --- dissect/target/helpers/certificate.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/dissect/target/helpers/certificate.py b/dissect/target/helpers/certificate.py index 662e6e8c1d..089689e81c 100755 --- a/dissect/target/helpers/certificate.py +++ b/dissect/target/helpers/certificate.py @@ -79,10 +79,12 @@ def format_serial_number_as_hex(serial_number: int | None) -> str | None: """Format serial_number from integer to hex. Add a prefix 0 if output length is not pair, in order to be consistent with usual serial_number representation - (navigator, openssl etc...). - For negative number use the same representation as navigator, which differ from openssl - E.g for -1337 : - open ssl : Serial Number: -1337 (-0x539) + (navigator, openssl etc...). + For negative number use the same representation as navigator, which differ from OpenSSL. + + For example for -1337:: + + OpenSSL : Serial Number: -1337 (-0x539) Navigator : FA C7 Args: From d4133119d0081e4abcc52d9caef2991f21355b14 Mon Sep 17 00:00:00 2001 From: william billaud <23636016+william-billaud@users.noreply.github.com> Date: Fri, 16 Jan 2026 16:33:53 +0100 Subject: [PATCH 18/29] Update dissect/target/helpers/certificate.py Co-authored-by: Erik Schamper <1254028+Schamper@users.noreply.github.com> --- dissect/target/helpers/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dissect/target/helpers/certificate.py b/dissect/target/helpers/certificate.py index 089689e81c..17664b5897 100755 --- a/dissect/target/helpers/certificate.py +++ b/dissect/target/helpers/certificate.py @@ -97,7 +97,7 @@ def format_serial_number_as_hex(serial_number: int | None) -> str | None: serial_number_as_hex = f"{serial_number:x}" if len(serial_number_as_hex) % 2 == 1: serial_number_as_hex = f"0{serial_number_as_hex}" - return f"{serial_number_as_hex}" + return serial_number_as_hex # Representation is always a multiple of 8 bits, we need to compute this size output_bin_len = (8 - (len(bin(serial_number)) - 3) % 8) + len(bin(serial_number)) - 3 return f"{serial_number & ((1 << output_bin_len) - 1):x}" From 6bc084fb47a31ccf1a52b21be0f9d2f2a10e7a5f Mon Sep 17 00:00:00 2001 From: william billaud <23636016+william-billaud@users.noreply.github.com> Date: Fri, 16 Jan 2026 16:35:35 +0100 Subject: [PATCH 19/29] Update tests/plugins/apps/webserver/test_apache.py Co-authored-by: Erik Schamper <1254028+Schamper@users.noreply.github.com> --- tests/plugins/apps/webserver/test_apache.py | 58 ++++++++++----------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/tests/plugins/apps/webserver/test_apache.py b/tests/plugins/apps/webserver/test_apache.py index ee3cb4b09d..88d235e721 100644 --- a/tests/plugins/apps/webserver/test_apache.py +++ b/tests/plugins/apps/webserver/test_apache.py @@ -562,37 +562,33 @@ def test_apache_hosts_certificates_negative_serial_number( -out negative_serial.crt -days 365 -set_serial -config openssl.cnf ``` - Where openssl.cnf has the following content - - ``` - [ req ] - default_bits = 2048 - distinguished_name = req_distinguished_name - x509_extensions = v3_ca - prompt = no - - [ req_distinguished_name ] - C = FR - ST = RHONE - L = Lyon - O = Dissect - OU = Demo - CN = docs.dissect.tools - - [ v3_ca ] - subjectKeyIdentifier = hash - authorityKeyIdentifier = keyid:always,issuer - basicConstraints = critical,CA:TRUE - keyUsage = critical,digitalSignature,keyEncipherment - subjectAltName = @alt_names - - [ alt_names ] - DNS.1 = monserveur.example.com - DNS.2 = www.monserveur.example.com - IP.1 = 192.168.1.100 - ``` - - + Where openssl.cnf has the following content:: + + [ req ] + default_bits = 2048 + distinguished_name = req_distinguished_name + x509_extensions = v3_ca + prompt = no + + [ req_distinguished_name ] + C = FR + ST = RHONE + L = Lyon + O = Dissect + OU = Demo + CN = docs.dissect.tools + + [ v3_ca ] + subjectKeyIdentifier = hash + authorityKeyIdentifier = keyid:always,issuer + basicConstraints = critical,CA:TRUE + keyUsage = critical,digitalSignature,keyEncipherment + subjectAltName = @alt_names + + [ alt_names ] + DNS.1 = monserveur.example.com + DNS.2 = www.monserveur.example.com + IP.1 = 192.168.1.100 """ fs_unix.map_file_fh("/etc/apache2/apache2.conf", BytesIO(b'ServerRoot "/etc/apache2"\n')) From 748f9e198726083389a877e95088ff3f01ab5221 Mon Sep 17 00:00:00 2001 From: william billaud <23636016+william-billaud@users.noreply.github.com> Date: Fri, 16 Jan 2026 16:36:06 +0100 Subject: [PATCH 20/29] Update dissect/target/plugins/os/windows/certlog.py Co-authored-by: Erik Schamper <1254028+Schamper@users.noreply.github.com> --- dissect/target/plugins/os/windows/certlog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dissect/target/plugins/os/windows/certlog.py b/dissect/target/plugins/os/windows/certlog.py index 01372f74c3..c11fb7bae2 100644 --- a/dissect/target/plugins/os/windows/certlog.py +++ b/dissect/target/plugins/os/windows/certlog.py @@ -281,7 +281,7 @@ def format_fingerprint(input_hash: str | None, target: Target) -> tuple[str | No def format_serial_number(serial_number_as_hex: str | None, target: Target) -> str | None: if not serial_number_as_hex: return None - return f"0x{serial_number_as_hex.replace(' ', '')}" + return serial_number_as_hex.replace(" ", "") def serial_number_as_int(serial_number_as_hex: str | None) -> int | None: From 2e1502eeacd478b933b4e7628868d6cf9f06a7d9 Mon Sep 17 00:00:00 2001 From: william billaud <23636016+william-billaud@users.noreply.github.com> Date: Fri, 16 Jan 2026 16:37:07 +0100 Subject: [PATCH 21/29] Update dissect/target/helpers/certificate.py Co-authored-by: Erik Schamper <1254028+Schamper@users.noreply.github.com> --- dissect/target/helpers/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dissect/target/helpers/certificate.py b/dissect/target/helpers/certificate.py index 17664b5897..4c79b98ec2 100755 --- a/dissect/target/helpers/certificate.py +++ b/dissect/target/helpers/certificate.py @@ -99,7 +99,7 @@ def format_serial_number_as_hex(serial_number: int | None) -> str | None: serial_number_as_hex = f"0{serial_number_as_hex}" return serial_number_as_hex # Representation is always a multiple of 8 bits, we need to compute this size - output_bin_len = (8 - (len(bin(serial_number)) - 3) % 8) + len(bin(serial_number)) - 3 + output_bin_len = (8 - (serial_number.bit_length() % 8) + serial_number.bit_length() return f"{serial_number & ((1 << output_bin_len) - 1):x}" From e7fdbb7c0f9035d67e3da3a63f800b6aa14d56ac Mon Sep 17 00:00:00 2001 From: wbi Date: Fri, 16 Jan 2026 16:37:13 +0100 Subject: [PATCH 22/29] Fix tests --- tests/plugins/os/windows/test_certlog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/plugins/os/windows/test_certlog.py b/tests/plugins/os/windows/test_certlog.py index 9adc0621e4..50b7a5a3cd 100644 --- a/tests/plugins/os/windows/test_certlog.py +++ b/tests/plugins/os/windows/test_certlog.py @@ -25,7 +25,7 @@ def test_certlog_plugin(target_win: Target, fs_win: VirtualFilesystem) -> None: assert len(certificates) == 11 assert len(list(target_win.certlog.certificate_extensions())) == 92 assert certificates[0].serial_number == 23146941333149199441888068127529844838 - assert certificates[0].serial_number_hex == "0x1169f0517d9b598e4ba7af46e4674066" + assert certificates[0].serial_number_hex == "1169f0517d9b598e4ba7af46e4674066" assert certificates[0].fingerprint.sha1 == hashlib.sha1(certificates[0].raw_certificate).hexdigest() assert certificates[0].fingerprint.sha1 == "061ec97dcef82d0e2d100b590e790a803d4573ae" From aa3355e2aa7d3f56f18bb53a8e433cf8f0200203 Mon Sep 17 00:00:00 2001 From: william billaud <23636016+william-billaud@users.noreply.github.com> Date: Fri, 16 Jan 2026 16:38:22 +0100 Subject: [PATCH 23/29] Update dissect/target/plugins/os/windows/certlog.py Co-authored-by: Erik Schamper <1254028+Schamper@users.noreply.github.com> --- dissect/target/plugins/os/windows/certlog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dissect/target/plugins/os/windows/certlog.py b/dissect/target/plugins/os/windows/certlog.py index c11fb7bae2..548c9b091a 100644 --- a/dissect/target/plugins/os/windows/certlog.py +++ b/dissect/target/plugins/os/windows/certlog.py @@ -278,7 +278,7 @@ def format_fingerprint(input_hash: str | None, target: Target) -> tuple[str | No return None, None, None -def format_serial_number(serial_number_as_hex: str | None, target: Target) -> str | None: +def format_serial_number(serial_number_as_hex: str | None) -> str | None: if not serial_number_as_hex: return None return serial_number_as_hex.replace(" ", "") From a02463c9880f04605d37b1cfe51b56c3a66c6e40 Mon Sep 17 00:00:00 2001 From: william billaud <23636016+william-billaud@users.noreply.github.com> Date: Fri, 16 Jan 2026 16:38:29 +0100 Subject: [PATCH 24/29] Update dissect/target/plugins/os/windows/certlog.py Co-authored-by: Erik Schamper <1254028+Schamper@users.noreply.github.com> --- dissect/target/plugins/os/windows/certlog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dissect/target/plugins/os/windows/certlog.py b/dissect/target/plugins/os/windows/certlog.py index 548c9b091a..d25b46dadb 100644 --- a/dissect/target/plugins/os/windows/certlog.py +++ b/dissect/target/plugins/os/windows/certlog.py @@ -256,7 +256,7 @@ } -def format_fingerprint(input_hash: str | None, target: Target) -> tuple[str | None, str | None, str | None]: +def format_fingerprint(input_hash: str | None) -> tuple[str | None, str | None, str | None]: if input_hash: input_hash = input_hash.replace(" ", "") # hash is expected to be a sha1, but as it not documented, we make this function more flexible if hash is From 649eb23e3be7b0405bd2715ec199157b8a66fef3 Mon Sep 17 00:00:00 2001 From: william billaud <23636016+william-billaud@users.noreply.github.com> Date: Fri, 16 Jan 2026 16:38:41 +0100 Subject: [PATCH 25/29] Update dissect/target/plugins/os/windows/certlog.py Co-authored-by: Erik Schamper <1254028+Schamper@users.noreply.github.com> --- dissect/target/plugins/os/windows/certlog.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/dissect/target/plugins/os/windows/certlog.py b/dissect/target/plugins/os/windows/certlog.py index d25b46dadb..3e960d5ed4 100644 --- a/dissect/target/plugins/os/windows/certlog.py +++ b/dissect/target/plugins/os/windows/certlog.py @@ -269,11 +269,9 @@ def format_fingerprint(input_hash: str | None) -> tuple[str | None, str | None, case 64: return None, None, input_hash case _: - target.log.warning( + raise ValueError( "Unexpected hash size found while processing certlog " - "$CertificateHash/$CertificateHash2 column: len %d, content %s", - len(input_hash), - input_hash, + f"$CertificateHash/$CertificateHash2 column: len {len(input_hash)}, content {input_hash}" ) return None, None, None From febfcdb41e043ca1be0be11245eea29adcc1e4d4 Mon Sep 17 00:00:00 2001 From: william billaud <23636016+william-billaud@users.noreply.github.com> Date: Fri, 16 Jan 2026 16:38:50 +0100 Subject: [PATCH 26/29] Update dissect/target/plugins/os/windows/certlog.py Co-authored-by: Erik Schamper <1254028+Schamper@users.noreply.github.com> --- dissect/target/plugins/os/windows/certlog.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/dissect/target/plugins/os/windows/certlog.py b/dissect/target/plugins/os/windows/certlog.py index 3e960d5ed4..0d0b41da51 100644 --- a/dissect/target/plugins/os/windows/certlog.py +++ b/dissect/target/plugins/os/windows/certlog.py @@ -343,7 +343,12 @@ def read_records(self, table_name: str, record_type: CertLogRecord) -> Iterator[ for column, value in column_values: new_column = FIELD_MAPPINGS.get(column) if new_column in FORMATING_FUNC: - value = FORMATING_FUNC[new_column](value, self.target) + try: + value = FORMATING_FUNC[new_column](value, self.target) + except Exception as e: + self.target.log.warning("Error formatting column %s (%s): %s", new_column, column, e) + self.target.log.debug("", exc_info=e) + value = None if new_column and new_column not in record_values: record_values[new_column] = value # Serial number is format as int and string, to ease search of a specific sn in both format From eaac0176caefb520b99c17f014819fadfb391fdc Mon Sep 17 00:00:00 2001 From: william billaud <23636016+william-billaud@users.noreply.github.com> Date: Fri, 16 Jan 2026 16:38:59 +0100 Subject: [PATCH 27/29] Update tests/plugins/apps/webserver/test_apache.py Co-authored-by: Erik Schamper <1254028+Schamper@users.noreply.github.com> --- tests/plugins/apps/webserver/test_apache.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/plugins/apps/webserver/test_apache.py b/tests/plugins/apps/webserver/test_apache.py index 88d235e721..9b3a6e7267 100644 --- a/tests/plugins/apps/webserver/test_apache.py +++ b/tests/plugins/apps/webserver/test_apache.py @@ -551,15 +551,15 @@ def test_apache_hosts_certificates(target_unix: Target, fs_unix: VirtualFilesyst def test_apache_hosts_certificates_negative_serial_number( target_unix: Target, fs_unix: VirtualFilesystem, crt_name: str, serial_number: int, serial_number_hex: str ) -> None: - """Test if we can parse Apache ``VirtualHost`` certificates, with a certificate using a negative serial number - certificate generated using. - Each test use a number with a different length in binary format, - to ensure representation match the one from navigator. - - ``` - openssl genrsa -out negative_serial.key 2048 - openssl req -new -x509 -key negative_serial.key \ - -out negative_serial.crt -days 365 -set_serial -config openssl.cnf + """Test if we can parse Apache ``VirtualHost`` certificates, with a certificate using a negative serial number. + + Each test uses a number with a different length in binary format, to ensure the representation matches the one from navigator. + + Generated using:: + + openssl genrsa -out negative_serial.key 2048 + openssl req -new -x509 -key negative_serial.key \ + -out negative_serial.crt -days 365 -set_serial -config openssl.cnf ``` Where openssl.cnf has the following content:: From 4e57748f828a65b8768e1fc85204a47fce555008 Mon Sep 17 00:00:00 2001 From: wbi Date: Fri, 16 Jan 2026 16:42:39 +0100 Subject: [PATCH 28/29] Linting --- dissect/target/helpers/certificate.py | 8 ++++---- tests/plugins/apps/webserver/test_apache.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/dissect/target/helpers/certificate.py b/dissect/target/helpers/certificate.py index 4c79b98ec2..6b591902a7 100755 --- a/dissect/target/helpers/certificate.py +++ b/dissect/target/helpers/certificate.py @@ -79,11 +79,11 @@ def format_serial_number_as_hex(serial_number: int | None) -> str | None: """Format serial_number from integer to hex. Add a prefix 0 if output length is not pair, in order to be consistent with usual serial_number representation - (navigator, openssl etc...). + (navigator, openssl etc...). For negative number use the same representation as navigator, which differ from OpenSSL. - + For example for -1337:: - + OpenSSL : Serial Number: -1337 (-0x539) Navigator : FA C7 @@ -99,7 +99,7 @@ def format_serial_number_as_hex(serial_number: int | None) -> str | None: serial_number_as_hex = f"0{serial_number_as_hex}" return serial_number_as_hex # Representation is always a multiple of 8 bits, we need to compute this size - output_bin_len = (8 - (serial_number.bit_length() % 8) + serial_number.bit_length() + output_bin_len = (8 - (serial_number.bit_length() % 8) + serial_number.bit_length()) return f"{serial_number & ((1 << output_bin_len) - 1):x}" diff --git a/tests/plugins/apps/webserver/test_apache.py b/tests/plugins/apps/webserver/test_apache.py index 9b3a6e7267..e6a2d47bcd 100644 --- a/tests/plugins/apps/webserver/test_apache.py +++ b/tests/plugins/apps/webserver/test_apache.py @@ -552,15 +552,15 @@ def test_apache_hosts_certificates_negative_serial_number( target_unix: Target, fs_unix: VirtualFilesystem, crt_name: str, serial_number: int, serial_number_hex: str ) -> None: """Test if we can parse Apache ``VirtualHost`` certificates, with a certificate using a negative serial number. - - Each test uses a number with a different length in binary format, to ensure the representation matches the one from navigator. + + Each test uses a number with a different length in binary format, to ensure the representation matches + the one from navigator. Generated using:: - + openssl genrsa -out negative_serial.key 2048 openssl req -new -x509 -key negative_serial.key \ -out negative_serial.crt -days 365 -set_serial -config openssl.cnf - ``` Where openssl.cnf has the following content:: From 2cb0603f1c23a4a59de7e5e39cfb1e991b4dec96 Mon Sep 17 00:00:00 2001 From: wbi Date: Fri, 16 Jan 2026 16:48:14 +0100 Subject: [PATCH 29/29] typo + typing --- dissect/target/helpers/certificate.py | 2 +- dissect/target/plugins/os/windows/certlog.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dissect/target/helpers/certificate.py b/dissect/target/helpers/certificate.py index 6b591902a7..c17dcae6c4 100755 --- a/dissect/target/helpers/certificate.py +++ b/dissect/target/helpers/certificate.py @@ -99,7 +99,7 @@ def format_serial_number_as_hex(serial_number: int | None) -> str | None: serial_number_as_hex = f"0{serial_number_as_hex}" return serial_number_as_hex # Representation is always a multiple of 8 bits, we need to compute this size - output_bin_len = (8 - (serial_number.bit_length() % 8) + serial_number.bit_length()) + output_bin_len = 8 - (serial_number.bit_length() % 8) + serial_number.bit_length() return f"{serial_number & ((1 << output_bin_len) - 1):x}" diff --git a/dissect/target/plugins/os/windows/certlog.py b/dissect/target/plugins/os/windows/certlog.py index 0d0b41da51..586c5239b4 100644 --- a/dissect/target/plugins/os/windows/certlog.py +++ b/dissect/target/plugins/os/windows/certlog.py @@ -288,7 +288,7 @@ def serial_number_as_int(serial_number_as_hex: str | None) -> int | None: return int(serial_number_as_hex, 16) -FORMATING_FUNC: dict[str, Callable[[Any, Target], Any]] = { +FORMATING_FUNC: dict[str, Callable[[Any], Any]] = { "fingerprint": format_fingerprint, "serial_number_hex": format_serial_number, } @@ -344,9 +344,9 @@ def read_records(self, table_name: str, record_type: CertLogRecord) -> Iterator[ new_column = FIELD_MAPPINGS.get(column) if new_column in FORMATING_FUNC: try: - value = FORMATING_FUNC[new_column](value, self.target) + value = FORMATING_FUNC[new_column](value) except Exception as e: - self.target.log.warning("Error formatting column %s (%s): %s", new_column, column, e) + self.target.log.warning("Error formatting column %s (%s): %s", new_column, column, value) self.target.log.debug("", exc_info=e) value = None if new_column and new_column not in record_values: