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