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/docker/archive/Dockerfile b/docker/archive/Dockerfile index bce1d261..9a5281e6 100644 --- a/docker/archive/Dockerfile +++ b/docker/archive/Dockerfile @@ -5,6 +5,8 @@ WORKDIR /app # Install Python for main.py ENV PYTHONUNBUFFERED=1 +# Deno trusts the system store, which apt keeps current, instead of its bundled roots. +ENV DENO_TLS_CA_STORE=system RUN apt update && apt install -y python3 python3-dev python3-venv curl unzip && ln -sf python3 /usr/bin/python RUN python3 -m venv /app/venv RUN . /app/venv/bin/activate 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 63286426..cb9785cd 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..7258ab2c --- /dev/null +++ b/wrolpi/test/test_trust_store.py @@ -0,0 +1,31 @@ +""" +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'), + ('docker/archive/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()