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