crtshToIdentifyingStruct.py (4883B)
1 #!/usr/bin/env python3 2 # 3 # This Source Code Form is subject to the terms of the Mozilla Public 4 # License, v. 2.0. If a copy of the MPL was not distributed with this 5 # file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 7 """ 8 This utility takes a series of https://crt.sh/ identifiers and writes to 9 stdout all of those certs' distinguished name or SPKI fields in hex, with an 10 array of all those. You'll need to post-process this list to handle any 11 duplicates. 12 13 Requires Python 3. 14 """ 15 16 import argparse 17 import io 18 import re 19 import sys 20 21 import requests 22 from cryptography import x509 23 from cryptography.hazmat.backends import default_backend 24 from cryptography.hazmat.primitives import hashes 25 from cryptography.x509.oid import NameOID 26 from pyasn1.codec.der import decoder, encoder 27 from pyasn1_modules import pem, rfc5280 28 29 assert sys.version_info >= (3, 2), "Requires Python 3.2 or later" 30 31 32 def hex_string_for_struct(bytes): 33 return [f"0x{x:02X}" for x in bytes] 34 35 36 def hex_string_human_readable(bytes): 37 return [f"{x:02X}" for x in bytes] 38 39 40 def nameOIDtoString(oid): 41 if oid == NameOID.COUNTRY_NAME: 42 return "C" 43 if oid == NameOID.COMMON_NAME: 44 return "CN" 45 if oid == NameOID.LOCALITY_NAME: 46 return "L" 47 if oid == NameOID.ORGANIZATION_NAME: 48 return "O" 49 if oid == NameOID.ORGANIZATIONAL_UNIT_NAME: 50 return "OU" 51 raise Exception(f"Unknown OID: {oid}") 52 53 54 def print_block(pemData, identifierType="DN", crtshId=None): 55 substrate = pem.readPemFromFile(io.StringIO(pemData.decode("utf-8"))) 56 cert, _ = decoder.decode(substrate, asn1Spec=rfc5280.Certificate()) 57 octets = None 58 59 if identifierType == "DN": 60 der_subject = encoder.encode(cert["tbsCertificate"]["subject"]) 61 octets = hex_string_for_struct(der_subject) 62 elif identifierType == "SPKI": 63 der_spki = encoder.encode(cert["tbsCertificate"]["subjectPublicKeyInfo"]) 64 octets = hex_string_for_struct(der_spki) 65 else: 66 raise Exception("Unknown identifier type: " + identifierType) 67 68 cert = x509.load_pem_x509_certificate(pemData, default_backend()) 69 common_name = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0] 70 block_name = "CA{}{}".format( 71 re.sub(r"[-:=_. ]", "", common_name.value), identifierType 72 ) 73 74 fingerprint = hex_string_human_readable(cert.fingerprint(hashes.SHA256())) 75 76 dn_parts = [f"/{nameOIDtoString(part.oid)}={part.value}" for part in cert.subject] 77 distinguished_name = "".join(dn_parts) 78 79 print(f"// {distinguished_name}") 80 print("// SHA256 Fingerprint: " + ":".join(fingerprint[:16])) 81 print("// " + ":".join(fingerprint[16:])) 82 if crtshId: 83 print(f"// https://crt.sh/?id={crtshId} (crt.sh ID={crtshId})") 84 print(f"static const uint8_t {block_name}[{len(octets)}] = " + "{") 85 86 while len(octets) > 0: 87 print(" " + ", ".join(octets[:13]) + ",") 88 octets = octets[13:] 89 90 print("};") 91 print() 92 93 return block_name 94 95 96 if __name__ == "__main__": 97 parser = argparse.ArgumentParser() 98 parser.add_argument( 99 "-spki", 100 action="store_true", 101 help="Create a list of subject public key info fields", 102 ) 103 parser.add_argument( 104 "-dn", 105 action="store_true", 106 help="Create a list of subject distinguished name fields", 107 ) 108 parser.add_argument("-listname", help="Name of the final DataAndLength block") 109 parser.add_argument( 110 "certId", nargs="+", help="A list of PEM files on disk or crt.sh IDs" 111 ) 112 args = parser.parse_args() 113 114 if not args.dn and not args.spki: 115 parser.print_help() 116 raise Exception("You must select either DN or SPKI matching") 117 118 blocks = [] 119 120 print( 121 "// Script from security/manager/tools/crtshToIdentifyingStruct/" 122 + "crtshToIdentifyingStruct.py" 123 ) 124 print("// Invocation: {}".format(" ".join(sys.argv))) 125 print() 126 127 identifierType = None 128 if args.dn: 129 identifierType = "DN" 130 else: 131 identifierType = "SPKI" 132 133 for certId in args.certId: 134 # Try a local file first, then crt.sh 135 try: 136 with open(certId, "rb") as pemFile: 137 blocks.append( 138 print_block(pemFile.read(), identifierType=identifierType) 139 ) 140 except OSError: 141 r = requests.get(f"https://crt.sh/?d={certId}") 142 r.raise_for_status() 143 blocks.append( 144 print_block(r.content, crtshId=certId, identifierType=identifierType) 145 ) 146 147 print("static const DataAndLength " + args.listname + "[]= {") 148 for structName in blocks: 149 if len(structName) < 33: 150 print(" { " + f"{structName}, sizeof({structName}) " + "},") 151 else: 152 print(" { " + f"{structName},") 153 print(f" sizeof({structName})" + " },") 154 print("};")