You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

155 lines
4.5 KiB
Python

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()