Some background to what I'm trying to achieve (you don't need to read it if you're not interested, just for reference).
I have exported a certificate template from AD into an LDIF-file using the following command:
ldifde -m -v -d "CN=MyTemplate,CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,DC=domain,DC=com" -f MyTemplate.ldf
The template contains the following record:
pKIOverlapPeriod:: AICmCv/e//8=
It seems like this is a base64-encoded Windows filetime structure, possibly with some sort of encoding on top(?).
From Microsoft's website
The FILETIME structure is a 64-bit value that represents the number of 100-nanosecond intervals that have elapsed since January 1, 1601, Coordinated Universal Time (UTC).
I tried to parse it into hex and got 0080a60affdeffff
. However, I want to parse it into something like "6 weeks" or "2 years".
Thus, I wrote a Python program to parse the LDIF and convert the pKIOverlapPeriod
but I don't get the expected output.
The actual output is:
pKIOverlapPeriod:
unit: days
value: 41911
Since I have configured the overlap to be "6 weeks" in the certificate template, this is the output I expect:
pKIOverlapPeriod:
unit: days
value: 42
The Python code I use looks like this:
# pip3 install ldif pyyaml
from ldif import LDIFParser
import os
import sys
import json
import yaml
# Converts a Win32 FILETIME structure to a dictionary.
def filetime_to_dict(filetime):
# This variable is supposed to contain the number of 100-nanosecond intervals since January 1, 1601...
intervals = int.from_bytes(filetime, byteorder = 'big')
return {
"unit": "days",
"value": int(intervals // (1E7 * 60 * 60 * 24))
}
parser = LDIFParser(open(os.path.join(os.getcwd(), sys.argv[1]), "rb"))
for dn, records in parser.parse():
template = {}
for key in records:
# Special magic for pKIOverlapPeriod goes here
if key == 'pKIOverlapPeriod':
template[key] = filetime_to_dict(records[key][0])
continue
# end of magic
if len(records[key]) == 1:
template[key] = records[key][0]
else:
template[key] = records[key]
data = yaml.dump(
yaml.load(
json.dumps(template, default = str),
Loader = yaml.SafeLoader),
default_flow_style = False)
print(data)
The LDIF looks like this:
dn: CN=AteaComputer,CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,DC=atea,DC=se
changetype: add
cn: AteaComputer
displayName: Atea Computer
distinguishedName:
CN=AteaComputer,CN=Certificate Templates,CN=Public Key Services,CN=Services,CN
=Configuration,DC=atea,DC=se
dSCorePropagationData: 20220601093015.0Z
dSCorePropagationData: 20220518190731.0Z
dSCorePropagationData: 16010101000000.0Z
flags: 131680
instanceType: 4
msPKI-Cert-Template-OID:
1.3.6.1.4.1.311.21.8.12474934.3506392.5459122.6785906.4016631.21.8298576.73677
34
msPKI-Certificate-Application-Policy: 1.3.6.1.5.5.7.3.1
msPKI-Certificate-Application-Policy: 1.3.6.1.5.5.7.3.2
msPKI-Certificate-Name-Flag: 134217728
msPKI-Enrollment-Flag: 32
msPKI-Minimal-Key-Size: 256
msPKI-Private-Key-Flag: 101056512
msPKI-RA-Application-Policies:
msPKI-Asymmetric-Algorithm`PZPWSTR`ECDH_P256`msPKI-Hash-Algorithm`PZPWSTR`SHA2
56`msPKI-Key-Usage`DWORD`16777215`msPKI-Symmetric-Algorithm`PZPWSTR`3DES`msPKI
-Symmetric-Key-Length`DWORD`168`
msPKI-RA-Signature: 0
msPKI-Template-Minor-Revision: 1
msPKI-Template-Schema-Version: 4
name: AteaComputer
objectCategory:
CN=PKI-Certificate-Template,CN=Schema,CN=Configuration,DC=atea,DC=se
objectClass: top
objectClass: pKICertificateTemplate
pKICriticalExtensions: 2.5.29.15
pKIDefaultKeySpec: 1
pKIExpirationPeriod:: AEA5hy7h/v8=
pKIExtendedKeyUsage: 1.3.6.1.5.5.7.3.1
pKIExtendedKeyUsage: 1.3.6.1.5.5.7.3.2
pKIKeyUsage:: iA==
pKIMaxIssuingDepth: 0
pKIOverlapPeriod:: AICmCv/e//8=
revision: 104
showInAdvancedViewOnly: TRUE
uSNChanged: 53271
uSNCreated: 28782
whenChanged: 20220601093015.0Z
whenCreated: 20220518190731.0Z
What did I do wrong? I have cross-checked my implementation against e.g. Python's winfiletime, with the same result, so I'm starting to suspect that the bytes need to be decoded before I can convert it into an int.
After fiddling around with this I came up with the following:
def filetime_to_dict(filetime):
input = 18446744073709551616 - int.from_bytes(filetime, byteorder = 'little')
if intervals % (1E7 * 60 * 60 * 24 * 365) == 0:
return {
"unit": "years",
"value": int(intervals / (1E7 * 60 * 60 * 24 * 365))
}
if intervals % (1E7 * 60 * 60 * 24 * 30) == 0:
return {
"unit": "months",
"value": int(intervals / (1E7 * 60 * 60 * 24 * 30))
}
if intervals % (1E7 * 60 * 60 * 24 * 7) == 0:
return {
"unit": "weeks",
"value": int(intervals / (1E7 * 60 * 60 * 24 * 7))
}
if intervals % (1E7 * 60 * 60 * 24) == 0:
return {
"unit": "days",
"value": int(intervals / (1E7 * 60 * 60 * 24))
}
if intervals % (1E7 * 60 * 60) == 0:
return {
"unit": "hours",
"value": int(intervals / (1E7 * 60 * 60))
}
return {
"unit": "filetime",
"value": filetime
}