Skip to content

Commit 3c63ec2

Browse files
Normalize plugin certificates output (#1498)
1 parent a102b31 commit 3c63ec2

File tree

13 files changed

+257
-22
lines changed

13 files changed

+257
-22
lines changed

dissect/target/helpers/certificate.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@
1515
except ImportError:
1616
HAS_ASN1 = False
1717

18-
1918
COMMON_CERTIFICATE_FIELDS = [
2019
("digest", "fingerprint"),
2120
("varint", "serial_number"),
21+
("string", "serial_number_hex"),
2222
("datetime", "not_valid_before"),
2323
("datetime", "not_valid_after"),
2424
("string", "issuer_dn"),
@@ -75,6 +75,34 @@ def compute_pem_fingerprints(pem: str | bytes) -> tuple[str, str, str]:
7575
return md5, sha1, sha256
7676

7777

78+
def format_serial_number_as_hex(serial_number: int | None) -> str | None:
79+
"""Format serial_number from integer to hex.
80+
81+
Add a prefix 0 if output length is not pair, in order to be consistent with usual serial_number representation
82+
(navigator, openssl etc...).
83+
For negative number use the same representation as navigator, which differ from OpenSSL.
84+
85+
For example for -1337::
86+
87+
OpenSSL : Serial Number: -1337 (-0x539)
88+
Navigator : FA C7
89+
90+
Args:
91+
serial_number: The serial number to format as hex.
92+
"""
93+
if serial_number is None:
94+
return serial_number
95+
96+
if serial_number > 0:
97+
serial_number_as_hex = f"{serial_number:x}"
98+
if len(serial_number_as_hex) % 2 == 1:
99+
serial_number_as_hex = f"0{serial_number_as_hex}"
100+
return serial_number_as_hex
101+
# Representation is always a multiple of 8 bits, we need to compute this size
102+
output_bin_len = 8 - (serial_number.bit_length() % 8) + serial_number.bit_length()
103+
return f"{serial_number & ((1 << output_bin_len) - 1):x}"
104+
105+
78106
def parse_x509(file: str | bytes | Path) -> CertificateRecord:
79107
"""Parses a PEM file. Returns a CertificateREcord. Does not parse a public key embedded in a x509 certificate."""
80108

@@ -112,5 +140,6 @@ def parse_x509(file: str | bytes | Path) -> CertificateRecord:
112140
subject_dn=",".join(subject),
113141
fingerprint=(md5, crt.sha1.hex(), crt.sha256.hex()),
114142
serial_number=crt.serial_number,
143+
serial_number_hex=format_serial_number_as_hex(crt.serial_number),
115144
pem=crt.dump(),
116145
)

dissect/target/plugins/os/windows/certlog.py

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from typing import TYPE_CHECKING, Union
3+
from typing import TYPE_CHECKING, Any, Union
44

55
from dissect.database.ese.tools import certlog
66
from dissect.database.exception import Error
@@ -10,7 +10,7 @@
1010
from dissect.target.plugin import Plugin, export
1111

1212
if TYPE_CHECKING:
13-
from collections.abc import Iterator
13+
from collections.abc import Callable, Iterator
1414
from pathlib import Path
1515

1616
from dissect.target.target import Target
@@ -43,12 +43,12 @@
4343
CertificateRecord = TargetRecordDescriptor(
4444
"filesystem/windows/certlog/certificate",
4545
[
46-
("string", "certificate_hash2"),
46+
("digest", "fingerprint"),
4747
("string", "certificate_template"),
4848
("string", "common_name"),
4949
("string", "country"),
5050
("string", "device_serial_number"),
51-
("string", "distinguished_name"),
51+
("string", "subject_dn"),
5252
("string", "domain_component"),
5353
("string", "email"),
5454
("string", "given_name"),
@@ -58,6 +58,7 @@
5858
("string", "organizational_unit"),
5959
("string", "public_key_algorithm"),
6060
("string", "serial_number_hex"),
61+
("varint", "serial_number"),
6162
("string", "state_or_province"),
6263
("string", "street_address"),
6364
("string", "subject_key_identifier"),
@@ -69,8 +70,8 @@
6970
("string", "enrollment_flags"),
7071
("string", "general_flags"),
7172
("string", "issuer_name_id"),
72-
("datetime", "not_after"),
73-
("datetime", "not_before"),
73+
("datetime", "not_valid_after"),
74+
("datetime", "not_valid_before"),
7475
("string", "private_key_flags"),
7576
("string", "public_key"),
7677
("varint", "public_key_length"),
@@ -95,7 +96,7 @@
9596
("string", "device_serial_number"),
9697
("string", "disposition"),
9798
("string", "disposition_message"),
98-
("string", "distinguished_name"),
99+
("string", "subject_dn"),
99100
("string", "domain_component"),
100101
("string", "email"),
101102
("string", "endorsement_certificate_hash"),
@@ -172,13 +173,14 @@
172173
"$AttributeValue": "common_name",
173174
"$CRLPublishError": "crl_publish_error",
174175
"$CallerName": "caller_name",
175-
"$CertificateHash2": "certificate_hash2",
176+
"$CertificateHash": "fingerprint",
177+
"$CertificateHash2": "fingerprint",
176178
"$CertificateTemplate": "certificate_template",
177179
"$CommonName": "common_name",
178180
"$Country": "country",
179181
"$DeviceSerialNumber": "device_serial_number",
180182
"$DispositionMessage": "disposition_message",
181-
"$DistinguishedName": "distinguished_name",
183+
"$DistinguishedName": "subject_dn",
182184
"$DomainComponent": "domain_component",
183185
"$EMail": "email",
184186
"$EndorsementCertificateHash": "endorsement_certificate_hash",
@@ -221,8 +223,8 @@
221223
"NameId": "name_id",
222224
"NextPublish": "next_publish",
223225
"NextUpdate": "next_update",
224-
"NotAfter": "not_after",
225-
"NotBefore": "not_before",
226+
"NotAfter": "not_valid_after",
227+
"NotBefore": "not_valid_before",
226228
"Number": "number",
227229
"PrivateKeyFlags": "private_key_flags",
228230
"PropagationComplete": "propagation_complete",
@@ -254,6 +256,44 @@
254256
}
255257

256258

259+
def format_fingerprint(input_hash: str | None) -> tuple[str | None, str | None, str | None]:
260+
if input_hash:
261+
input_hash = input_hash.replace(" ", "")
262+
# hash is expected to be a sha1, but as it not documented, we make this function more flexible if hash is
263+
# in another standard format (md5/sha256), especially in the future
264+
match len(input_hash):
265+
case 32:
266+
return input_hash, None, None
267+
case 40:
268+
return None, input_hash, None
269+
case 64:
270+
return None, None, input_hash
271+
case _:
272+
raise ValueError(
273+
"Unexpected hash size found while processing certlog "
274+
f"$CertificateHash/$CertificateHash2 column: len {len(input_hash)}, content {input_hash}"
275+
)
276+
return None, None, None
277+
278+
279+
def format_serial_number(serial_number_as_hex: str | None) -> str | None:
280+
if not serial_number_as_hex:
281+
return None
282+
return serial_number_as_hex.replace(" ", "")
283+
284+
285+
def serial_number_as_int(serial_number_as_hex: str | None) -> int | None:
286+
if not serial_number_as_hex:
287+
return None
288+
return int(serial_number_as_hex, 16)
289+
290+
291+
FORMATING_FUNC: dict[str, Callable[[Any], Any]] = {
292+
"fingerprint": format_fingerprint,
293+
"serial_number_hex": format_serial_number,
294+
}
295+
296+
257297
class CertLogPlugin(Plugin):
258298
"""Return all available data stored in CertLog databases.
259299
@@ -302,8 +342,27 @@ def read_records(self, table_name: str, record_type: CertLogRecord) -> Iterator[
302342
record_values = {}
303343
for column, value in column_values:
304344
new_column = FIELD_MAPPINGS.get(column)
305-
if new_column:
345+
if new_column in FORMATING_FUNC:
346+
try:
347+
value = FORMATING_FUNC[new_column](value)
348+
except Exception as e:
349+
self.target.log.warning("Error formatting column %s (%s): %s", new_column, column, value)
350+
self.target.log.debug("", exc_info=e)
351+
value = None
352+
if new_column and new_column not in record_values:
306353
record_values[new_column] = value
354+
# Serial number is format as int and string, to ease search of a specific sn in both format
355+
if new_column == "serial_number_hex":
356+
record_values["serial_number"] = serial_number_as_int(value)
357+
elif new_column:
358+
self.target.log.debug(
359+
"Unexpected element while processing %s entries : %s column already exists "
360+
"(mapped from original column name %s). This may be cause by two column that were not"
361+
" expected to be present in the same time.",
362+
table_name,
363+
new_column,
364+
column,
365+
)
307366
else:
308367
self.target.log.debug(
309368
"Unexpected column for table %s in CA %s: %s", table_name, ca_name, column
File renamed without changes.
File renamed without changes.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:d6f36dde378f080a05358ae6f572d1ef6a42725e23caafa076a7a532ad576888
3+
size 1440
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:c2950633419e7dcd5b2830d6a6a05ac8fa675c58e70f19cc1ae23c7fb739b6ea
3+
size 1704
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:bdf320de0e673176fe131dab252bda76a1e0a8ae1f46a382e2da0caf3cfa8db4
3+
size 1464
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:370814279fa55bb7968081924bf1e508495ae1d87079e651f4a9e32359ab2dc2
3+
size 1464
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:6b5bff12a5eed82a63a815975e8e277b2400b993594e0d7a553ae5751e6b7805
3+
size 1464
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:4a24d908118cf2e6d4818250fabdbadc63aae32f9bf4f1abb2ebeba24f6948e5
3+
size 1464

0 commit comments

Comments
 (0)