From 9ccf19b82b0af04f4e5c95427d0f16697531812f Mon Sep 17 00:00:00 2001 From: lrnselfreliance <33263708+lrnselfreliance@users.noreply.github.com> Date: Thu, 22 Oct 2026 19:28:27 -0600 Subject: [PATCH] Distrust compromised eMudhra intermediates in certifi and Deno (#854) * Distrust compromised eMudhra intermediates in certifi and Deno The private keys of eight eMudhra intermediate CAs can be recovered from their certificates, so anyone can mint a trusted certificate for any host. * Require certifi>=2026.10.19; yt-dlp, requests and curl-cffi use its bundle, and 2026.10.19 removes the emSign ECC Root CA - G3 root they chain to. * Set DENO_TLS_CA_STORE=system for the API (systemd + Docker) and the archive container so Deno follows the apt-patched system store instead of roots that only change with a Deno upgrade. Co-Authored-By: Claude Opus 5.5 * Keep Deno's bundled roots in the archive container browserless/chrome:1.61.1 is Ubuntu 22.04 whose newest ca-certificates is from 2023, older than Deno 2.8.3's bundled roots. The system store only becomes the better choice once the archive base image is upgraded. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- docker/api/Dockerfile | 2 ++ etc/raspberrypios/wrolpi-api-dev.service | 3 +++ etc/raspberrypios/wrolpi-api.service | 3 +++ requirements.txt | 3 +++ wrolpi/test/test_trust_store.py | 30 ++++++++++++++++++++++++ 5 files changed, 41 insertions(+) create mode 100644 wrolpi/test/test_trust_store.py diff --git a/docker/api/Dockerfile b/docker/api/Dockerfile index f3c9b705..813e5a9f 100644 --- a/docker/api/Dockerfile +++ b/docker/api/Dockerfile @@ -1,5 +1,7 @@ FROM python:3.13-trixie ENV DOCKER=true +# Deno trusts the system store, which apt keeps current, instead of its bundled roots. +ENV DENO_TLS_CA_STORE=system WORKDIR /opt/wrolpi # Update dependencies for the services install. diff --git a/etc/raspberrypios/wrolpi-api-dev.service b/etc/raspberrypios/wrolpi-api-dev.service index 2f4c8fae..9271cd13 100644 --- a/etc/raspberrypios/wrolpi-api-dev.service +++ b/etc/raspberrypios/wrolpi-api-dev.service @@ -7,6 +7,9 @@ Conflicts=wrolpi-api.service Environment=PYTHONUNBUFFERED=1 # Sanic 25.x wants 'spawn' but our code uses 'fork' for multiprocessing. Environment=SANIC_START_METHOD_SET=1 +# Deno (single-file, yt-dlp EJS) trusts the system store, which apt keeps current, instead of its +# bundled roots. +Environment=DENO_TLS_CA_STORE=system # Required for yt-dlp to access GNOME keyring for browser cookie decryption. Environment=DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/%U/bus ExecStart=/opt/wrolpi/venv/bin/python -m sanic main:api_app --host 0.0.0.0 --port 8081 -w 2 --reload --debug diff --git a/etc/raspberrypios/wrolpi-api.service b/etc/raspberrypios/wrolpi-api.service index 724e606f..6ad89f7f 100644 --- a/etc/raspberrypios/wrolpi-api.service +++ b/etc/raspberrypios/wrolpi-api.service @@ -7,6 +7,9 @@ Conflicts=wrolpi-api-dev.service Environment=PYTHONUNBUFFERED=1 # Sanic 25.x wants 'spawn' but our code uses 'fork' for multiprocessing. Environment=SANIC_START_METHOD_SET=1 +# Deno (single-file, yt-dlp EJS) trusts the system store, which apt keeps current, instead of its +# bundled roots. +Environment=DENO_TLS_CA_STORE=system # Required for yt-dlp to access GNOME keyring for browser cookie decryption. Environment=DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/%U/bus ExecStart=/opt/wrolpi/venv/bin/python -O -m sanic main:api_app --host 0.0.0.0 --port 8081 -w 5 diff --git a/requirements.txt b/requirements.txt index 23f81ca1..7706fdfc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,9 @@ aiohttp==3.14.4 alembic==1.20.0 beautifulsoup4==4.15.0 cachetools==7.2.1 +# yt-dlp, requests and curl-cffi trust certifi's bundle, not the system store. 2026.10.19 removes +# the root of the eMudhra intermediates whose private keys are recoverable. +certifi>=2026.10.19 cryptography==50.0.2 ebooklib==0.20 feedparser==6.0.14 diff --git a/wrolpi/test/test_trust_store.py b/wrolpi/test/test_trust_store.py new file mode 100644 index 00000000..58623a13 --- /dev/null +++ b/wrolpi/test/test_trust_store.py @@ -0,0 +1,30 @@ +""" +Tests that WROLPi's TLS clients do not trust compromised certificate authorities. +""" +import certifi +import pytest + +from wrolpi.vars import PROJECT_DIR + +# Roots of the eMudhra intermediates whose private keys can be recovered from their certificates. +DISTRUSTED_ROOT_LABELS = ( + 'emSign ECC Root CA - G3', +) + + +@pytest.mark.parametrize('label', DISTRUSTED_ROOT_LABELS) +def test_certifi_distrusts_root(label): + """certifi's bundle (used by yt-dlp, requests and curl-cffi) must not contain a distrusted root.""" + labels = [line.split('"')[1] for line in certifi.contents().splitlines() if line.startswith('# Label: "')] + assert labels, 'certifi bundle has no labels; cannot verify distrusted roots are absent' + assert label not in labels + + +@pytest.mark.parametrize('path,setting', [ + ('etc/raspberrypios/wrolpi-api.service', 'Environment=DENO_TLS_CA_STORE=system'), + ('etc/raspberrypios/wrolpi-api-dev.service', 'Environment=DENO_TLS_CA_STORE=system'), + ('docker/api/Dockerfile', 'ENV DENO_TLS_CA_STORE=system'), +]) +def test_deno_uses_system_trust_store(path, setting): + """Deno's bundled roots only change with a Deno upgrade; the system store is patched by apt.""" + assert setting in (PROJECT_DIR / path).read_text().splitlines()