From d6bf1637c765055f00bbd278c13b4fdf83dbf4c9 Mon Sep 17 00:00:00 2001 From: Mark Jansen Date: Sat, 13 Dec 2025 15:28:11 +0100 Subject: [PATCH] [SDK] Add script to update caroots.inf CORE-16744 --- sdk/tools/update_caroots.py | 178 ++++++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 sdk/tools/update_caroots.py diff --git a/sdk/tools/update_caroots.py b/sdk/tools/update_caroots.py new file mode 100644 index 00000000000..967632016d6 --- /dev/null +++ b/sdk/tools/update_caroots.py @@ -0,0 +1,178 @@ +""" +PROJECT: ReactOS tools +LICENSE: MIT (https://spdx.org/licenses/MIT) +PURPOSE: Script to update caroots.inf with the latest CA root certificates from Mozilla NSS +COPYRIGHT: Copyright 2025 Mark Jansen +""" + +from enum import Enum +from pathlib import Path +import argparse +import urllib.request +from datetime import datetime +from dataclasses import dataclass + +# Additional sources: +# https://hg.mozilla.org/releases/mozilla-release/raw-file/default/security/nss/lib/ckfw/builtins/certdata.txt +# https://hg.mozilla.org/projects/nss/raw-file/tip/lib/ckfw/builtins/certdata.txt + + +REPO_ROOT = Path(__file__).resolve().parent.parent.parent +CAROOTS_INF = REPO_ROOT / "boot" / "bootdata" / "caroots.inf" +DEFAULT_DOWNLOAD_URL = "https://hg.mozilla.org/mozilla-central/raw-file/default/security/nss/lib/ckfw/builtins/certdata.txt" +CAROOTS_HEADER = """; Auto-generated caroots.inf +; Do not edit manually. +; Source: {source_url} +; Generated on: {date} +; Number of certificates: {cert_count} +; Generated by sdk/tools/update_caroots.py + +[Version] +Signature = "$Windows NT$" + +[AddReg] + +""" + +CERTIFICATE_HEADER = """; "{cert_name}" ({cert_size} bytes) +HKLM,"SOFTWARE\\Microsoft\\SystemCertificates\\AuthRoot\\Certificates\\{fingerprint_sha1}","Blob",0x00000001,\\ + 0x20,0x00,0x00,0x00,\\ + 0x01,0x00,0x00,0x00,\\ + {cert_size_hex},\\ +{data} +""" + + +@dataclass +class Certificate: + name: str + fingerprint_sha1: str + data: bytes + + +def format_certificate(cert: Certificate) -> str: + # Format the data as comma-separated hex bytes, 16 bytes per line + hex_bytes = [f"0x{b:02X}" for b in cert.data] + lines = [] + for i in range(0, len(hex_bytes), 16): + line = ",".join(hex_bytes[i : i + 16]) + if i + 16 < len(hex_bytes): + line += ",\\" + lines.append(" " + line) + + cert_size = len(cert.data) + cert_size_hex = ",".join(f"0x{b:02X}" for b in cert_size.to_bytes(4, byteorder="little")) + + return CERTIFICATE_HEADER.format( + cert_name=cert.name, + fingerprint_sha1=cert.fingerprint_sha1, + cert_size=cert_size, + cert_size_hex=cert_size_hex, + data="\n".join(lines), + ) + + +class CurrentBlock(Enum): + Begin = 0 + Certificate = 1 + Trust = 2 + + +def parse_certificates2(certdata: str) -> list[Certificate]: + cert = None + result = [] + temp_value = None + block = CurrentBlock.Begin + + for line in certdata.splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + + if line == "CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST": + block = CurrentBlock.Trust + continue + elif line == "CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE": + block = CurrentBlock.Certificate + continue + elif line.startswith("CKA_LABEL"): + _, name, _ = line.split('"') + if block == CurrentBlock.Certificate: + cert = Certificate(name=name, fingerprint_sha1="", data=b"") + elif block == CurrentBlock.Trust: + assert cert is not None, line + assert name == cert.name + else: + pass + elif line == "CKA_VALUE MULTILINE_OCTAL": + assert cert is not None + temp_value = [] + assert cert.data == b"" + assert cert.fingerprint_sha1 == "" + elif line == "CKA_CERT_SHA1_HASH MULTILINE_OCTAL": + assert cert is not None + temp_value = [] + assert cert.data != b"" + assert cert.fingerprint_sha1 == "" + elif temp_value is not None: + assert cert is not None + if line == "END": + if cert.data == b"": + cert.data = bytes(temp_value) + else: + assert cert.fingerprint_sha1 == "" + cert.fingerprint_sha1 = "".join(f"{b:02X}" for b in temp_value) + temp_value = None + else: + for number in line.split("\\"): + if not number: + continue + temp_value.append(int(number, 8)) + + elif line.startswith("CKA_TRUST_SERVER_AUTH"): + assert cert is not None + if "CKT_NSS_TRUSTED_DELEGATOR" in line: + print(f"Trusted cert: {cert.name} ({len(cert.data)} bytes)") + result.append(cert) + cert = None + return result + + +def main(): + parser = argparse.ArgumentParser( + description="Update the caroots.inf file with the latest CA root certificates." + ) + parser.add_argument( + "--url", + type=str, + default=DEFAULT_DOWNLOAD_URL, + help="URL to download the certdata.txt file from.", + ) + args = parser.parse_args() + + print(f"Downloading certdata.txt from {args.url}...") + with urllib.request.urlopen(args.url) as response: + certdata = response.read().decode("utf-8") + + print("Parsing certdata.txt...") + certificates = parse_certificates2(certdata) + certificates.sort(key=lambda c: c.name.lower()) + print(f"Found {len(certificates)} trusted root certificates.") + print(f"Updating {CAROOTS_INF}...") + with CAROOTS_INF.open("w", encoding="utf-8", newline="\r\n") as f: + f.write( + CAROOTS_HEADER.format( + source_url=args.url, + date=datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + cert_count=len(certificates), + ) + ) + for cert in certificates: + f.write(format_certificate(cert)) + f.write("\n") + + print("Update complete.") + + +if __name__ == "__main__": + main()