1# -*- coding: utf-8 -*-
2import base64
3import binascii
4import codecs
5import subprocess
6import sys
7import urllib.request
8import urllib.parse
9import urllib.error
10
11import extra_constraints
12
13HEADER = """//!
14//! This library is automatically generated from the Mozilla certificate
15//! store via mkcert.org.  Don't edit it.
16//!
17//! The generation is done deterministically so you can verify it
18//! yourself by inspecting and re-running the generation process.
19//!
20
21#![forbid(unsafe_code,
22          unstable_features)]
23#![deny(trivial_casts,
24        trivial_numeric_casts,
25        unused_import_braces,
26        unused_extern_crates,
27        unused_qualifications)]
28"""
29
30CERT = """
31  %(comment)s
32  %(code)s,"""
33
34excluded_cas = [
35    # See https://bugzilla.mozilla.org/show_bug.cgi?id=1266574.
36    "Buypass Class 2 CA 1",
37
38    # https://blog.mozilla.org/security/2015/04/02/distrusting-new-cnnic-certificates/
39    # https://security.googleblog.com/2015/03/maintaining-digital-certificate-security.html
40    "China Internet Network Information Center",
41    "CNNIC",
42
43    # See https://bugzilla.mozilla.org/show_bug.cgi?id=1283326.
44    "RSA Security 2048 v3",
45
46    # https://bugzilla.mozilla.org/show_bug.cgi?id=1272158
47    "Root CA Generalitat Valenciana",
48
49    # See https://wiki.mozilla.org/CA:WoSign_Issues.
50    "StartCom",
51    "WoSign",
52
53    # See https://cabforum.org/pipermail/public/2016-September/008475.html.
54    # Both the ASCII and non-ASCII names are required.
55    "TÜRKTRUST",
56    "TURKTRUST",
57]
58
59
60def fetch_bundle():
61    proc = subprocess.Popen(['curl',
62                             'https://mkcert.org/generate/all/except/' +
63                                "+".join([urllib.parse.quote(x) for x in excluded_cas])],
64            stdout = subprocess.PIPE)
65    stdout, _ = proc.communicate()
66    return stdout.decode('utf-8')
67
68
69def split_bundle(bundle):
70    cert = ''
71    for line in bundle.splitlines():
72        if line.strip() != '':
73            cert += line + '\n'
74        if '-----END CERTIFICATE-----' in line:
75            yield cert
76            cert = ''
77
78
79def calc_spki_hash(cert):
80    """
81    Use openssl to sha256 hash the public key in the certificate.
82    """
83    proc = subprocess.Popen(
84            ['openssl', 'x509', '-noout', '-sha256', '-fingerprint'],
85            stdin = subprocess.PIPE,
86            stdout = subprocess.PIPE)
87    stdout, _ = proc.communicate(cert.encode('utf-8'))
88    stdout = stdout.decode('utf-8')
89    assert proc.returncode == 0
90    assert stdout.startswith('SHA256 Fingerprint=')
91    hash = stdout.replace('SHA256 Fingerprint=', '').replace(':', '')
92    hash = hash.strip()
93    assert len(hash) == 64
94    return hash.lower()
95
96
97def extract_header_spki_hash(cert):
98    """
99    Extract the sha256 hash of the public key in the header, for
100    cross-checking.
101    """
102    line = [ll for ll in cert.splitlines() if ll.startswith('# SHA256 Fingerprint: ')][0]
103    return line.replace('# SHA256 Fingerprint: ', '').replace(':', '').lower()
104
105
106def unwrap_pem(cert):
107    start = '-----BEGIN CERTIFICATE-----\n'
108    end = '-----END CERTIFICATE-----\n'
109    body = cert[cert.index(start)+len(start):cert.rindex(end)]
110    return base64.b64decode(body)
111
112
113def extract(msg, name):
114    lines = msg.splitlines()
115    value = [ll for ll in lines if ll.startswith(name + ': ')][0]
116    return value[len(name) + 2:].strip()
117
118
119def convert_cert(cert_der):
120    proc = subprocess.Popen(
121            ['target/debug/process_cert'],
122            stdin = subprocess.PIPE,
123            stdout = subprocess.PIPE)
124    stdout, _ = proc.communicate(cert_der)
125    stdout = stdout.decode('utf-8')
126    assert proc.returncode == 0
127    return dict(
128            subject = extract(stdout, 'Subject'),
129            spki = extract(stdout, 'SPKI'),
130            name_constraints = extract(stdout, 'Name-Constraints'))
131
132
133def commentify(cert):
134    lines = cert.splitlines()
135    lines = [ll[2:] if ll.startswith('# ') else ll for ll in lines]
136    return '/*\n   * ' + ('\n   * '.join(lines)) + '\n   */'
137
138
139def convert_bytes(hex):
140    bb = binascii.a2b_hex(hex)
141    encoded, _ = codecs.escape_encode(bb)
142    return encoded.decode('utf-8').replace('"', '\\"')
143
144
145def print_root(cert, data):
146    subject = convert_bytes(data['subject'])
147    spki = convert_bytes(data['spki'])
148    nc = data['name_constraints']
149    nc = ('Some(b"{}")'.format(convert_bytes(nc))) if nc != 'None' else nc
150
151    print("""  {}
152  webpki::TrustAnchor {{
153    subject: b"{}",
154    spki: b"{}",
155    name_constraints: {}
156  }},
157""".format(commentify(cert), subject, spki, nc))
158
159
160if __name__ == '__main__':
161    if sys.platform == "win32":
162        import os, msvcrt
163        msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
164
165    bundle = fetch_bundle()
166    open('fetched.pem', 'w').write(bundle)
167
168    certs = {}
169
170    for cert in split_bundle(bundle):
171        our_hash = calc_spki_hash(cert)
172        their_hash = extract_header_spki_hash(cert)
173        assert our_hash == their_hash
174
175        cert_der = unwrap_pem(cert)
176        data = convert_cert(cert_der)
177
178        imposed_nc = extra_constraints.get_imposed_name_constraints(data['subject'])
179        if imposed_nc:
180            data['name_constraints'] = binascii.b2a_hex(imposed_nc)
181
182        assert our_hash not in certs, 'duplicate cert'
183        certs[our_hash] = (cert, data)
184
185    print(HEADER)
186    print("""pub static TLS_SERVER_ROOTS: webpki::TLSServerTrustAnchors = webpki::TLSServerTrustAnchors(&[""")
187
188    # emit in sorted hash order for deterministic builds
189    for hash in sorted(certs):
190        cert, data = certs[hash]
191        print_root(cert, data)
192
193    print(']);')
194