import argparse import glob import os import sys from datetime import datetime from anytree import Node, RenderTree from anytree.exporter import DotExporter from cryptography import x509 from cryptography.exceptions import UnsupportedAlgorithm import helpers from helpers import format_key_type, format_oid_link, parse_certificate, table_line def edge_type_func(*_): return "--" def node_name_func(node): return f"{node.name}\n{hex(node.data.serial_number)}\n{format_key_type(node.data)}" def cert_to_markdown(cert: x509.Certificate): print("| Item | Value(s) |") print("|------|----------|") print("| Subject: |", cert.subject.rfc4514_string(), "|") print("| Issuer: |", cert.issuer.rfc4514_string(), "|") print(f"| Serial: | {hex(cert.serial_number)} ({cert.serial_number}) |") print(f"| Public Key: | {format_key_type(cert)} |") try: hash_name = cert.signature_hash_algorithm.name except UnsupportedAlgorithm: hash_name = format_oid_link(cert.signature_algorithm_oid) print(f"| Signature hash: | {hash_name} |") print(f"| Not before: | {cert.not_valid_before} |") print(f"| Not after: | {cert.not_valid_after} |") for ext in cert.extensions: if ext.oid == x509.OID_BASIC_CONSTRAINTS: if ext.value.path_length is not None: print(f"| Path length: | {ext.value.path_length} |") elif ext.oid == x509.ObjectIdentifier("1.3.6.1.5.5.7.1.3"): print( "| Qualified Certificate Statements: |" " see [RFC-3739](https://datatracker.ietf.org/doc/html/rfc3739.html) |" ) elif ext.oid == x509.ObjectIdentifier("2.5.29.16"): print( "| Private Key usage period | see [RFC-3280](https://tools.ietf.org/html/rfc3280.html) |" ) else: table_line( helpers.extension_label(ext.oid), [helpers.format_extension(cert.extensions, ext.oid)], ) def analyze_certificates(directory, include_old): nodes = {} node_parents = {} issuers = {} roots = [] certs = glob.glob("*.cer", root_dir=directory) certs += glob.glob("*.crt", root_dir=directory) certs += glob.glob("*.der", root_dir=directory) certs += glob.glob("*.pem", root_dir=directory) for cert in sorted(certs): certificate = parse_certificate(os.path.join(directory, cert)) if certificate is None: continue if not include_old: if certificate.not_valid_after < datetime.utcnow(): print( f"skip {cert} not valid after {certificate.not_valid_after}", file=sys.stderr, ) continue subject = certificate.subject.public_bytes() issuer = certificate.issuer.public_bytes() map_key = f"{issuer}-{certificate.serial_number}" if map_key in nodes: continue node = Node( certificate.subject.get_attributes_for_oid(x509.OID_COMMON_NAME)[0].value, data=certificate, ) nodes[map_key] = node if issuer == subject: roots.append(node) else: issuers.setdefault(issuer, []).append(map_key) node_parents[map_key] = issuer for item in nodes: issuer = nodes[item].data.subject.public_bytes() if issuer in issuers: for map_key in issuers[issuer]: nodes[map_key].parent = nodes[item] for root in roots: image_name = os.path.join( directory, "{}-{}.svg".format(root.name, root.data.serial_number) ) exp = DotExporter( root, graph="graph", options=["rankdir=LR"], edgetypefunc=edge_type_func, nodenamefunc=node_name_func, ) exp.to_picture(image_name) print(f"# {root.name}") print() print(f"![hierarchy for {root.name}]({image_name})") print() for _, _, node in RenderTree(root): cert = node.data print(f"## {node.name}") print() cert_to_markdown(cert) print() def main(): parser = argparse.ArgumentParser(prog="analyze_certs") parser.add_argument("directory", help="directory of certificate files") parser.add_argument( "include_old", action="store_false", help="include old certificates" ) args = parser.parse_args() analyze_certificates(args.directory, args.include_old) if __name__ == "__main__": main()