pythonactive-directorypkildif

Parse pKIOverlapPeriod from LDIF export into days


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.


Solution

  • 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
        }