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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
lrnselfreliance 2026-10-22 19:28:27 -06:00 • committed by Roland
parent cbf69e3013
commit 9ccf19b82b
No known key found for this signature in database
GPG key ID: 1BA620CE6C9A4680
5 changed files with 41 additions and 0 deletions

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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()