#!/usr/bin/env python3 # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """ This utility takes a series of https://crt.sh/ identifiers and writes to stdout all of those certs' distinguished name or SPKI fields in hex, with an array of all those. You'll need to post-process this list to handle any duplicates. Requires Python 3. """ import argparse import io import re import sys import requests from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.x509.oid import NameOID from pyasn1.codec.der import decoder, encoder from pyasn1_modules import pem, rfc5280 assert sys.version_info >= (3, 2), "Requires Python 3.2 or later" def hex_string_for_struct(bytes): return ["0x{:02X}".format(x) for x in bytes] def hex_string_human_readable(bytes): return ["{:02X}".format(x) for x in bytes] def nameOIDtoString(oid): if oid == NameOID.COUNTRY_NAME: return "C" if oid == NameOID.COMMON_NAME: return "CN" if oid == NameOID.LOCALITY_NAME: return "L" if oid == NameOID.ORGANIZATION_NAME: return "O" if oid == NameOID.ORGANIZATIONAL_UNIT_NAME: return "OU" raise Exception("Unknown OID: {}".format(oid)) def print_block(pemData, identifierType="DN", crtshId=None): substrate = pem.readPemFromFile(io.StringIO(pemData.decode("utf-8"))) cert, _ = decoder.decode(substrate, asn1Spec=rfc5280.Certificate()) octets = None if identifierType == "DN": der_subject = encoder.encode(cert["tbsCertificate"]["subject"]) octets = hex_string_for_struct(der_subject) elif identifierType == "SPKI": der_spki = encoder.encode(cert["tbsCertificate"]["subjectPublicKeyInfo"]) octets = hex_string_for_struct(der_spki) else: raise Exception("Unknown identifier type: " + identifierType) cert = x509.load_pem_x509_certificate(pemData, default_backend()) common_name = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0] block_name = "CA{}{}".format( re.sub(r"[-:=_. ]", "", common_name.value), identifierType ) fingerprint = hex_string_human_readable(cert.fingerprint(hashes.SHA256())) dn_parts = [ "/{id}={value}".format(id=nameOIDtoString(part.oid), value=part.value) for part in cert.subject ] distinguished_name = "".join(dn_parts) print("// {dn}".format(dn=distinguished_name)) print("// SHA256 Fingerprint: " + ":".join(fingerprint[:16])) print("// " + ":".join(fingerprint[16:])) if crtshId: print("// https://crt.sh/?id={crtsh} (crt.sh ID={crtsh})".format(crtsh=crtshId)) print("static const uint8_t {}[{}] = ".format(block_name, len(octets)) + "{") while len(octets) > 0: print(" " + ", ".join(octets[:13]) + ",") octets = octets[13:] print("};") print() return block_name if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument( "-spki", action="store_true", help="Create a list of subject public key info fields", ) parser.add_argument( "-dn", action="store_true", help="Create a list of subject distinguished name fields", ) parser.add_argument("-listname", help="Name of the final DataAndLength block") parser.add_argument( "certId", nargs="+", help="A list of PEM files on disk or crt.sh IDs" ) args = parser.parse_args() if not args.dn and not args.spki: parser.print_help() raise Exception("You must select either DN or SPKI matching") blocks = [] print( "// Script from security/manager/tools/crtshToIdentifyingStruct/" + "crtshToIdentifyingStruct.py" ) print("// Invocation: {}".format(" ".join(sys.argv))) print() identifierType = None if args.dn: identifierType = "DN" else: identifierType = "SPKI" for certId in args.certId: # Try a local file first, then crt.sh try: with open(certId, "rb") as pemFile: blocks.append( print_block(pemFile.read(), identifierType=identifierType) ) except OSError: r = requests.get("https://crt.sh/?d={}".format(certId)) r.raise_for_status() blocks.append( print_block(r.content, crtshId=certId, identifierType=identifierType) ) print("static const DataAndLength " + args.listname + "[]= {") for structName in blocks: if len(structName) < 33: print(" { " + "{name}, sizeof({name}) ".format(name=structName) + "},") else: print(" { " + "{},".format(structName)) print(" sizeof({})".format(structName) + " },") print("};")