#!/usr/bin/env python3 # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """Extract EV root data from a certificate and print a kEVInfos entry. Produces a block ready to paste into security/certverifier/ExtendedValidation.cpp. Usage: ./ev_cert_info.py [--oid OID] cert.cer Accepts DER (.cer/.crt) or PEM input; the format is auto-detected. The OID defaults to the CA/Browser Forum EV OID. If the OID is one already used in ExtendedValidation.cpp, its oidName description is filled in automatically; otherwise a placeholder is emitted for you to complete. Requires the cryptography package. """ import argparse import base64 import sys import textwrap from cryptography import x509 from cryptography.hazmat.primitives import hashes from cryptography.x509.oid import NameOID # Known EV policy OIDs and their oidName descriptions, as used in # security/certverifier/ExtendedValidation.cpp. Keep in sync with that file. OID_NAMES = { "1.2.156.112559.1.1.6.1": "GDCA EV OID", "1.2.392.200091.100.721.1": "SECOM EV OID", "1.2.616.1.113527.2.5.1.1": "Certum EV OID", "1.3.6.1.4.1.4788.2.202.1": "D-TRUST EV OID", "1.3.6.1.4.1.6449.1.2.1.5.1": "Comodo EV OID", "1.3.6.1.4.1.7879.13.24.1": "T-Systems EV OID", "1.3.6.1.4.1.8024.0.2.100.1.2": "QuoVadis EV OID", "1.3.6.1.4.1.14777.6.1.1": "Izenpe EV OID 1", "1.3.6.1.4.1.14777.6.1.2": "Izenpe EV OID 2", "1.3.6.1.4.1.40869.1.1.22.3": "TWCA EV OID", "1.3.159.1.17.1": "Actalis EV OID", "2.16.156.112554.3": "CFCA EV OID", "2.16.578.1.26.1.3.3": "Buypass EV OID", "2.16.756.5.14.7.4.8": "WISeKey EV OID", "2.16.840.1.114412.2.1": "DigiCert EV OID", "2.16.840.1.114413.1.7.23.3": "Go Daddy EV OID a", "2.16.840.1.114414.1.7.23.3": "Go Daddy EV OID b", "2.23.140.1.1": "CA/Browser Forum EV OID", } DEFAULT_OID = "2.23.140.1.1" ATTR_LABELS = { NameOID.COMMON_NAME: "CN", NameOID.ORGANIZATION_NAME: "O", NameOID.ORGANIZATIONAL_UNIT_NAME: "OU", NameOID.LOCALITY_NAME: "L", NameOID.STATE_OR_PROVINCE_NAME: "ST", NameOID.COUNTRY_NAME: "C", } def load_cert(path): data = open(path, "rb").read() if b"-----BEGIN CERTIFICATE-----" in data: return x509.load_pem_x509_certificate(data) return x509.load_der_x509_certificate(data) def format_name(name): """Format a distinguished name like the comments in ExtendedValidation.cpp, e.g. CN=...,O="...",C=JP (most-specific attribute first, values with commas quoted).""" parts = [] for rdn in reversed(list(name.rdns)): for attr in rdn: label = ATTR_LABELS.get(attr.oid, attr.oid.dotted_string) value = attr.value if "," in value: value = f'"{value}"' parts.append(f"{label}={value}") return ",".join(parts) def format_fingerprint(fp): hexbytes = [f"0x{b:02X}" for b in fp] lines = [] for i in range(0, len(hexbytes), 12): lines.append(", ".join(hexbytes[i : i + 12])) body = ",\n ".join(lines) return f" {{ {body} }}," def format_base64_literal(raw): """Split base64 into 64-char C string literals, one per line.""" b64 = base64.b64encode(raw).decode() chunks = textwrap.wrap(b64, 64) return "\n".join(f' "{c}"' for c in chunks) + "," def serial_content_octets(serial): """Serial as stored in the cert (content octets, no INTEGER tag/length).""" if serial == 0: return b"\x00" length = (serial.bit_length() + 8) // 8 return serial.to_bytes(length, "big") def emit(path, oid): cert = load_cert(path) if cert.issuer != cert.subject: raise ValueError("issuer does not match subject") fp = cert.fingerprint(hashes.SHA256()) issuer_der = cert.issuer.public_bytes() serial = serial_content_octets(cert.serial_number) oid_name = OID_NAMES.get(oid, "") print(" {") print(f" // {format_name(cert.issuer)}") print(f' "{oid}",') print(f' "{oid_name}",') print(format_fingerprint(fp)) print(format_base64_literal(issuer_der)) print(f' "{base64.b64encode(serial).decode()}",') print(" },") def main(): parser = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter ) parser.add_argument("cert", help="certificate file, DER or PEM") parser.add_argument( "--oid", default=DEFAULT_OID, help="EV policy OID (default: %(default)s)", ) args = parser.parse_args() try: emit(args.cert, args.oid) except Exception as e: print(f"error processing {args.cert}: {e}", file=sys.stderr) return 1 return 0 if __name__ == "__main__": sys.exit(main())