mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-12 16:19:29 -10:00
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.
143 lines
4.8 KiB
Python
Executable File
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()
|