Files
RetroArch/tools/cacert_split.py
T
libretroadmin f76060572b net: keep the CA bundle as DER and read it in place
cacert.h carried the Mozilla roots as PEM text, which the built-in TLS
client base64-decoded into a heap copy of every certificate on first
use. Keep the bundle as the DER bytes instead: a quarter smaller in
the executable, and the trust store borrows the anchors from the
read-only data rather than decoding and copying them, which also drops
the 175 KiB of heap those copies took. The mbedTLS backend walks the
same parts certificate by certificate.

tools/cacert_split.py emits the DER form and still regenerates the
header from curl's cacert.pem or from itself; the PEM loader stays for
tests and the verify hooks.
2026-10-22 06:42:10 +02:00

143 lines
4.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""Write libretro-common/net/cacert.h: the CA bundle as DER, in C arrays
small enough for every compiler that builds it.
The certificates are kept as their DER bytes rather than as PEM text:
a quarter smaller, and the TLS client reads them in place (no base64
decode, no per-certificate copy) since one DER certificate is a
self-delimiting SEQUENCE and the next one starts where it ends.
MSVC before Visual Studio 2019 refuses a string literal longer than
65535 bytes after concatenation (fatal error C1091), so the bundle is
kept as several arrays, each well under the limit and each ending where
a certificate does, listed in cacert_der_parts[] / cacert_der_sizes[].
Every byte is a fixed 3-digit octal escape, so the source stays pure
ASCII and no compiler charset matters.
Usage:
cacert_split.py <cacert.pem | cacert.h> [out.h]
Reads the bundle from a PEM file (curl's cacert.pem from
https://curl.se/docs/caextract.html) or from an existing cacert.h, so
the header can be regenerated from itself; writes the header to out.h,
or to stdout.
"""
import base64
import re
import sys
# Well under MSVC's 65535, with room for the per-line bytes it counts
PART_LIMIT = 60000
# ISO C90 only guarantees 509 bytes per string literal
LINE_BYTES = 120
def literal_bytes(text):
"""The bytes the C string literals of a header spell, in order."""
out = bytearray()
for lit in re.findall(r'"((?:[^"\\]|\\.)*)"', text):
out += bytes(lit, "latin-1").decode("unicode_escape").encode("latin-1")
return bytes(out)
def certs_from_pem(text):
certs = []
for body in re.findall(r"-----BEGIN CERTIFICATE-----(.*?)-----END CERTIFICATE-----",
text, re.S):
certs.append(base64.b64decode("".join(body.split())))
return certs
def der_len(blob, pos):
"""The length of the DER SEQUENCE at blob[pos:], header included."""
if blob[pos] != 0x30:
raise SystemExit("not a DER SEQUENCE at %d" % pos)
l = blob[pos + 1]
if l < 0x80:
return 2 + l
n = l & 0x7f
if n == 0 or n > 3:
raise SystemExit("unsupported DER length at %d" % pos)
return 2 + n + int.from_bytes(blob[pos + 2:pos + 2 + n], "big")
def certs_from_der(blob):
certs, pos = [], 0
while pos < len(blob):
n = der_len(blob, pos)
certs.append(blob[pos:pos + n])
pos += n
return certs
def split(certs):
"""The certificates packed into parts no longer than PART_LIMIT
(with the NUL each array carries)."""
parts, cur = [], b""
for c in certs:
if len(c) + 1 > PART_LIMIT:
raise SystemExit("a single certificate is larger than a part")
if cur and len(cur) + len(c) + 1 > PART_LIMIT:
parts.append(cur)
cur = b""
cur += c
if cur:
parts.append(cur)
return parts
def c_lines(data):
out = []
for i in range(0, len(data), LINE_BYTES):
out.append('"%s"' % "".join("\\%03o" % b for b in data[i:i + LINE_BYTES]))
return out
def emit(parts, count):
o = []
o.append("/* CA certificates extracted from Mozilla")
o.append(" * https://curl.haxx.se/docs/caextract.html")
o.append(" *")
o.append(" * Generated by tools/cacert_split.py, which also regenerates it: the")
o.append(" * bundle is kept as DER, split where certificates end into parts")
o.append(" * under 64 KiB, since MSVC before 2019 refuses a longer string literal")
o.append(" * (C1091). Every part is a run of whole DER certificates; load them")
o.append(" * all. %d certificates." % count)
o.append(" */")
for i, part in enumerate(parts):
o.append("static const unsigned char cacert_der_%d[] = {" % i)
o.extend(c_lines(part))
o.append("};")
o.append("")
o.append("#define CACERT_DER_PARTS %d" % len(parts))
o.append("static const unsigned char *const cacert_der_parts[CACERT_DER_PARTS] = {")
o.append(",\n".join(" cacert_der_%d" % i for i in range(len(parts))))
o.append("};")
o.append("/* each literal carries a terminating NUL that is not DER */")
o.append("static const size_t cacert_der_sizes[CACERT_DER_PARTS] = {")
o.append(",\n".join(" sizeof(cacert_der_%d) - 1" % i for i in range(len(parts))))
o.append("};")
return "\n".join(o) + "\n"
def main():
src = open(sys.argv[1], encoding="latin-1").read()
if "-----BEGIN CERTIFICATE-----" in src and "static const" not in src:
certs = certs_from_pem(src)
else:
data = literal_bytes(src)
if b"-----BEGIN CERTIFICATE-----" in data:
certs = certs_from_pem(data.decode("latin-1"))
else:
certs = certs_from_der(data)
out = emit(split(certs), len(certs))
if len(sys.argv) > 2:
open(sys.argv[2], "w", encoding="latin-1", newline="\n").write(out)
else:
sys.stdout.write(out)
if __name__ == "__main__":
main()