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