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, rest = 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(re.sub(r'[-:=_. ]', '', common_name.value), identifierType) 73 74 fingerprint = hex_string_human_readable(cert.fingerprint(hashes.SHA256())) 75 76 dn_parts = ["/{id}={value}".format(id=nameOIDtoString(part.oid), 77 value=part.value) for part in cert.subject] 78 distinguished_name = "".join(dn_parts) 79 80 print("// {dn}".format(dn=distinguished_name)) 81 print("// SHA256 Fingerprint: " + ":".join(fingerprint[:16])) 82 print("// " + ":".join(fingerprint[16:])) 83 if crtshId: 84 print("// https://crt.sh/?id={crtsh} (crt.sh ID={crtsh})" 85 .format(crtsh=crtshId)) 86 print("static const uint8_t {}[{}] = ".format(block_name, len(octets)) + "{") 87 88 while len(octets) > 0: 89 print(" " + ", ".join(octets[:13]) + ",") 90 octets = octets[13:] 91 92 print("};") 93 print() 94 95 return block_name 96 97 98if __name__ == "__main__": 99 parser = argparse.ArgumentParser() 100 parser.add_argument("-spki", action="store_true", 101 help="Create a list of subject public key info fields") 102 parser.add_argument("-dn", action="store_true", 103 help="Create a list of subject distinguished name fields") 104 parser.add_argument("-listname", 105 help="Name of the final DataAndLength block") 106 parser.add_argument("certId", nargs="+", 107 help="A list of PEM files on disk or crt.sh IDs") 108 args = parser.parse_args() 109 110 if not args.dn and not args.spki: 111 parser.print_help() 112 raise Exception("You must select either DN or SPKI matching") 113 114 blocks = [] 115 116 print("// Script from security/manager/tools/crtshToIdentifyingStruct/" + 117 "crtshToIdentifyingStruct.py") 118 print("// Invocation: {}".format(" ".join(sys.argv))) 119 print() 120 121 identifierType = None 122 if args.dn: 123 identifierType = "DN" 124 else: 125 identifierType = "SPKI" 126 127 for certId in args.certId: 128 # Try a local file first, then crt.sh 129 try: 130 with open(certId, "rb") as pemFile: 131 blocks.append(print_block(pemFile.read(), identifierType=identifierType)) 132 except OSError: 133 r = requests.get('https://crt.sh/?d={}'.format(certId)) 134 r.raise_for_status() 135 blocks.append(print_block(r.content, crtshId=certId, identifierType=identifierType)) 136 137 print("static const DataAndLength " + args.listname + "[]= {") 138 for structName in blocks: 139 if len(structName) < 33: 140 print(" { " + "{name}, sizeof({name}) ".format(name=structName) + "},") 141 else: 142 print(" { " + "{},".format(structName)) 143 print(" sizeof({})".format(structName) + " },") 144 print("};") 145