| # -*- coding: utf-8 -*- |
| import base64 |
| import binascii |
| import codecs |
| import subprocess |
| import sys |
| import urllib.request |
| import urllib.parse |
| import urllib.error |
| |
| import extra_constraints |
| |
| HEADER = """//! |
| //! This library is automatically generated from the Mozilla certificate |
| //! store via mkcert.org. Don't edit it. |
| //! |
| //! The generation is done deterministically so you can verify it |
| //! yourself by inspecting and re-running the generation process. |
| //! |
| |
| #![forbid(unsafe_code, |
| unstable_features)] |
| #![deny(trivial_casts, |
| trivial_numeric_casts, |
| unused_import_braces, |
| unused_extern_crates, |
| unused_qualifications)] |
| """ |
| |
| CERT = """ |
| %(comment)s |
| %(code)s,""" |
| |
| excluded_cas = [ |
| # See https://bugzilla.mozilla.org/show_bug.cgi?id=1266574. |
| "Buypass Class 2 CA 1", |
| |
| # https://blog.mozilla.org/security/2015/04/02/distrusting-new-cnnic-certificates/ |
| # https://security.googleblog.com/2015/03/maintaining-digital-certificate-security.html |
| "China Internet Network Information Center", |
| "CNNIC", |
| |
| # See https://bugzilla.mozilla.org/show_bug.cgi?id=1283326. |
| "RSA Security 2048 v3", |
| |
| # https://bugzilla.mozilla.org/show_bug.cgi?id=1272158 |
| "Root CA Generalitat Valenciana", |
| |
| # See https://wiki.mozilla.org/CA:WoSign_Issues. |
| "StartCom", |
| "WoSign", |
| |
| # See https://cabforum.org/pipermail/public/2016-September/008475.html. |
| # Both the ASCII and non-ASCII names are required. |
| "TÜRKTRUST", |
| "TURKTRUST", |
| ] |
| |
| |
| def fetch_bundle(): |
| proc = subprocess.Popen(['curl', |
| 'https://mkcert.org/generate/all/except/' + |
| "+".join([urllib.parse.quote(x) for x in excluded_cas])], |
| stdout = subprocess.PIPE) |
| stdout, _ = proc.communicate() |
| return stdout.decode('utf-8') |
| |
| |
| def split_bundle(bundle): |
| cert = '' |
| for line in bundle.splitlines(): |
| if line.strip() != '': |
| cert += line + '\n' |
| if '-----END CERTIFICATE-----' in line: |
| yield cert |
| cert = '' |
| |
| |
| def calc_spki_hash(cert): |
| """ |
| Use openssl to sha256 hash the public key in the certificate. |
| """ |
| proc = subprocess.Popen( |
| ['openssl', 'x509', '-noout', '-sha256', '-fingerprint'], |
| stdin = subprocess.PIPE, |
| stdout = subprocess.PIPE) |
| stdout, _ = proc.communicate(cert.encode('utf-8')) |
| stdout = stdout.decode('utf-8') |
| assert proc.returncode == 0 |
| assert stdout.startswith('SHA256 Fingerprint=') |
| hash = stdout.replace('SHA256 Fingerprint=', '').replace(':', '') |
| hash = hash.strip() |
| assert len(hash) == 64 |
| return hash.lower() |
| |
| |
| def extract_header_spki_hash(cert): |
| """ |
| Extract the sha256 hash of the public key in the header, for |
| cross-checking. |
| """ |
| line = [ll for ll in cert.splitlines() if ll.startswith('# SHA256 Fingerprint: ')][0] |
| return line.replace('# SHA256 Fingerprint: ', '').replace(':', '').lower() |
| |
| |
| def unwrap_pem(cert): |
| start = '-----BEGIN CERTIFICATE-----\n' |
| end = '-----END CERTIFICATE-----\n' |
| body = cert[cert.index(start)+len(start):cert.rindex(end)] |
| return base64.b64decode(body) |
| |
| |
| def extract(msg, name): |
| lines = msg.splitlines() |
| value = [ll for ll in lines if ll.startswith(name + ': ')][0] |
| return value[len(name) + 2:].strip() |
| |
| |
| def convert_cert(cert_der): |
| proc = subprocess.Popen( |
| ['target/debug/process_cert'], |
| stdin = subprocess.PIPE, |
| stdout = subprocess.PIPE) |
| stdout, _ = proc.communicate(cert_der) |
| stdout = stdout.decode('utf-8') |
| assert proc.returncode == 0 |
| return dict( |
| subject = extract(stdout, 'Subject'), |
| spki = extract(stdout, 'SPKI'), |
| name_constraints = extract(stdout, 'Name-Constraints')) |
| |
| |
| def commentify(cert): |
| lines = cert.splitlines() |
| lines = [ll[2:] if ll.startswith('# ') else ll for ll in lines] |
| return '/*\n * ' + ('\n * '.join(lines)) + '\n */' |
| |
| |
| def convert_bytes(hex): |
| bb = binascii.a2b_hex(hex) |
| encoded, _ = codecs.escape_encode(bb) |
| return encoded.decode('utf-8').replace('"', '\\"') |
| |
| |
| def print_root(cert, data): |
| subject = convert_bytes(data['subject']) |
| spki = convert_bytes(data['spki']) |
| nc = data['name_constraints'] |
| nc = ('Some(b"{}")'.format(convert_bytes(nc))) if nc != 'None' else nc |
| |
| print(""" {} |
| webpki::TrustAnchor {{ |
| subject: b"{}", |
| spki: b"{}", |
| name_constraints: {} |
| }}, |
| """.format(commentify(cert), subject, spki, nc)) |
| |
| |
| if __name__ == '__main__': |
| if sys.platform == "win32": |
| import os, msvcrt |
| msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) |
| |
| bundle = fetch_bundle() |
| open('fetched.pem', 'w').write(bundle) |
| |
| certs = {} |
| |
| for cert in split_bundle(bundle): |
| our_hash = calc_spki_hash(cert) |
| their_hash = extract_header_spki_hash(cert) |
| assert our_hash == their_hash |
| |
| cert_der = unwrap_pem(cert) |
| data = convert_cert(cert_der) |
| |
| imposed_nc = extra_constraints.get_imposed_name_constraints(data['subject']) |
| if imposed_nc: |
| data['name_constraints'] = binascii.b2a_hex(imposed_nc) |
| |
| assert our_hash not in certs, 'duplicate cert' |
| certs[our_hash] = (cert, data) |
| |
| print(HEADER) |
| print("""pub static TLS_SERVER_ROOTS: webpki::TlsServerTrustAnchors = webpki::TlsServerTrustAnchors(&[""") |
| |
| # emit in sorted hash order for deterministic builds |
| for hash in sorted(certs): |
| cert, data = certs[hash] |
| print_root(cert, data) |
| |
| print(']);') |