Files
MyLuxy--overdrive/ps5-react/tools/ca_bundle.py
T

72 lines
3.0 KiB
Python

# Copyright (C) 2026 half144 and PS5 React contributors
# SPDX-License-Identifier: GPL-3.0-or-later
# Additional attribution term: see LICENSE-ATTRIBUTION.
"""Mozilla's root certificates as curl publishes them (MPL-2.0), pinned in the lock file.
Old firmwares' own certificate store lacks roots that sites use today; networking apps ship this
bundle and the console merges it with the system store at start (native/ps5/network_platform.cpp).
`python3 tools/ca_bundle.py` pins the newest dated copy from curl.se, after the same checks a build runs.
"""
import base64
import binascii
import json
import re
import ssl
import sys
import urllib.request
from common import DEPS, LOCK, ROOT, digest, fetch
PAGE = "https://curl.se/docs/caextract.html"
BLOCK = re.compile(rb"-----BEGIN CERTIFICATE-----\r?\n(.+?)-----END CERTIFICATE-----", re.S)
def validate(path):
"""Every block decodes, OpenSSL loads every one, and the count looks like Mozilla's store."""
data = path.read_bytes()
if len(data) > 1024 * 1024 or b"\0" in data:
raise RuntimeError(f"{path.name}: not a PEM bundle")
blocks = BLOCK.findall(data)
if data.count(b"-----BEGIN") != len(blocks) or not 100 <= len(blocks) <= 400:
raise RuntimeError(f"{path.name}: {len(blocks)} certificates, expected 100-400")
try:
for block in blocks: base64.b64decode(block, validate=False)
except binascii.Error as error:
raise RuntimeError(f"{path.name}: undecodable certificate: {error}")
loaded = ssl.create_default_context(cafile=str(path)).cert_store_stats()["x509_ca"]
if loaded != len(blocks):
raise RuntimeError(f"{path.name}: OpenSSL loaded {loaded} of {len(blocks)} certificates")
return len(blocks)
def ca_bundle():
item = LOCK["caBundle"]
path = fetch(item["url"], DEPS / "ca-bundle" / f"cacert-{item['version']}.pem", item["sha256"])
validate(path)
return path
def update():
with urllib.request.urlopen(PAGE, timeout=60) as page:
dates = sorted(set(re.findall(r"cacert-(\d{4}-\d{2}-\d{2})\.pem", page.read().decode())))
if not dates: raise RuntimeError(f"No dated bundle listed on {PAGE}")
version = dates[-1]
url = f"https://curl.se/ca/cacert-{version}.pem"
path = DEPS / "ca-bundle" / f"cacert-{version}.pem"
path.parent.mkdir(parents=True, exist_ok=True)
with urllib.request.urlopen(url, timeout=120) as source:
path.write_bytes(source.read())
count = validate(path)
lock_path = ROOT / "dependencies.lock.json"
lock = json.loads(lock_path.read_text())
previous = lock.get("caBundle", {}).get("version")
lock["caBundle"] = {"version": version, "url": url, "sha256": digest(path)}
lock_path.write_bytes((json.dumps(lock, indent=2) + "\n").encode())
print(f"caBundle {previous} -> {version}: {count} certificates")
if __name__ == "__main__":
if sys.argv[1:] not in ([], ["--check"]):
sys.exit("usage: ca_bundle.py [--check]")
if sys.argv[1:] == ["--check"]: print(f"{ca_bundle().name}: {validate(ca_bundle())} certificates")
else: update()