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