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