Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ scripts/deploy.sh [<build>] # ship: CI green, then build a release
## Detail Docs

- [docs/specs/2026-09-29-processor-service-design.md](docs/specs/2026-09-29-processor-service-design.md) — the spec: decisions, grants, runtime, failure table, versioning, cutover, testing, contract quick reference
- [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) — prerequisites, the broker credential handoff, `/etc/processor/`, releases and `scripts/deploy.sh`, the drift check, liveness, containment, install and rollback, operate, co-core bumps, Tailscale DNS, Node.js
- [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) — prerequisites, the broker credential handoff, `/etc/processor/`, releases and `scripts/deploy.sh`, the drift check, liveness, containment, install and rollback, operate, co-core bumps, TLS trust, Tailscale DNS, Node.js
- [docs/plans/](docs/plans/) — implementation plans
- [docs/SKILLS.md](docs/SKILLS.md) — vendored agent skills: inventory, overrides (none), the refresh hook, worktrees
- [docs/SOCRATICODE.md](docs/SOCRATICODE.md) — semantic search: tools, the shared store, link stubs, the pin, the memory decision
Expand Down
4 changes: 4 additions & 0 deletions docs/DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,10 @@ co-core is pinned `==`, and Processor owns the pin (spec D5, §5; amended 2026-1
4. **Notice to Watcher** before deploy: the new `processor_version`, and the moved digests. A change to the contract needs coordination with Watcher first, not just notice. The contract is the `content.process` / `content.derived` models and `canonical_text` semantics. A change to essence resolution is notice, not coordination (spec §5, #57).
5. Merge, then `scripts/deploy.sh`.

## TLS trust

The service's TLS trust is certifi's bundle in the release venv, not the system store: GCS (`google-auth` / `google-cloud-storage` over `requests`) and the drift check's GitHub calls verify against `certifi.where()`, and nothing sets `SSL_CERT_FILE` / `REQUESTS_CA_BUNDLE`. A CA distrust (exe.dev's system-store remediation doesn't reach the venv) is a certifi bump: raise `constraint-dependencies` in `pyproject.toml` and the floor in `tests/test_tls_trust.py`, `uv lock --upgrade-package certifi` (the diff moves certifi alone, never `uv lock --upgrade`), merge, then `scripts/deploy.sh`. The restart is the fix: a running process keeps the bundle it loaded. Check from the release venv: `/srv/processor/live/.venv/bin/python -I -c "import certifi; print(certifi.__version__)"`. certifi isn't co-core, so `processor_version` doesn't move and Watcher needs no notice. Since #66: ≥ 2026.10.19, without `emSign ECC Root CA - G3` (the eMudhra compromise, https://exe.dev/security/emudhra-compromise).

## Tailscale DNS

Since #8 (2026-10-01) this VM runs Tailscale with `--accept-dns=true`, as every cohort node does (observo#631, notifier#43 D8). It had run with DNS off since provisioning: the `tailscale up` of 2026-09-29, from an Observo session (observo#629), passed `--accept-dns=false`, and nothing records a reason.
Expand Down
32 changes: 32 additions & 0 deletions docs/plans/2026-10-23-66-certifi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
title: certifi ≥ 2026.10.19, so the venv stops trusting the emSign G3 chain (#66)
date: 2026-10-23
status: per #66's hand-off from epic #24 (2026-10-23)
---

# certifi ≥ 2026.10.19

## Problem

The services' TLS trust is certifi's bundle, not the system store. `uv.lock` has certifi 2026.07.22, which still holds `emSign ECC Root CA - G3`, the root of the eMudhra-compromised chains (exe.dev advisory). From the live venv, `requests.get` on the advisory's test host returns 200. GCS (`google-auth` / `google-cloud-storage` over `requests`) and the drift check's GitHub calls use it. Nothing overrides certifi (`SSL_CERT_FILE` / `REQUESTS_CA_BUNDLE` / `CURL_CA_BUNDLE`: none in `deploy/`, `src/`, or the unit). 2026.10.19 drops G3 and keeps emSign G1 and C3.

## Approach

- `uv lock --upgrade-package certifi`: the diff moves certifi alone. Never `uv lock --upgrade` (co-core's pin; `tests/test_pin.py`).
- A floor that survives a re-lock: `[tool.uv] constraint-dependencies = ["certifi>=2026.10.19"]`, commented with #66 and the advisory. certifi stays transitive, out of `[project] dependencies`.
- New `tests/test_tls_trust.py` (not `test_pin.py`: that is co-core's pin and its contract):
- the lock resolves certifi ≥ 2026.10.19, and so does the installed package, compared as dated parts (`2026.7.22` in the lock, `2026.07.22` installed);
- the installed bundle, parsed as certificates (`ssl` `get_ca_certs()`, not the comment text), has no subject `emSign ECC Root CA - G3`. Positive control: it does have `emSign Root CA - G1`, so the matcher finds the family it looks for.
- DEPLOYMENT.md, a `## TLS trust` note: the trust is certifi's bundle from the release venv; a CA distrust is a certifi bump, a lock change and a deploy; exe.dev's system-store remediation doesn't reach it; the restart is the fix.

## Test

Red first on today's lock (2026.07.22 and G3 present), green after the lock and `uv sync`.

## Ship

PR (`Refs #66`) → FF merge → `git pull --ff-only` → `scripts/deploy.sh` (`uv.lock` and `pyproject.toml` are deploy paths). Live checks, from `/srv/processor/live/.venv` as `exedev`: certifi ≥ 2026.10.19; the noct host raises `SSLError`; `https://storage.googleapis.com/` answers; `starting` has the new build, `processor_version` `0.38.0+1`, `child_containment: required`, `NRestarts=0`; the next command acks; the next drift run is `ok`/202. `processor_version` doesn't move, so no Watcher notice (D5).

## Open questions

None.
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,8 @@ packages = ["src/processor"]
# cannobserv release tag by scripts/build_wheelhouse.sh. find-links locks by
# filename, not hash, so either source satisfies the same uv.lock.
find-links = ["./.wheelhouse"]
# certifi is transitive (requests, httpx), and its bundle is the services' TLS trust,
# not the system store. 2026.10.19 drops emSign ECC Root CA - G3, the root of the
# eMudhra-compromised chains (https://exe.dev/security/emudhra-compromise, #66); the
# floor keeps a re-lock from resolving an older one. tests/test_tls_trust.py checks it.
constraint-dependencies = ["certifi>=2026.10.19"]
51 changes: 51 additions & 0 deletions tests/test_tls_trust.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""The services' TLS trust is certifi's bundle, and it must not trust emSign ECC G3 (#66).

GCS (``google-auth`` / ``google-cloud-storage`` over ``requests``) and the drift check's
GitHub calls verify against ``certifi.where()``, not the system store, so exe.dev's
system-store remediation of the eMudhra compromise doesn't reach them
(https://exe.dev/security/emudhra-compromise). certifi 2026.10.19 drops
``emSign ECC Root CA - G3``, the compromised chains' root. A later lock must not bring
it back: ``[tool.uv] constraint-dependencies`` holds the floor, and these tests check it.
"""

import ssl
import tomllib
from pathlib import Path

import certifi

ROOT = Path(__file__).resolve().parent.parent
FLOOR = (2026, 10, 19)
DISTRUSTED = "emSign ECC Root CA - G3"


def _dated(version: str) -> tuple[int, ...]:
# The lock normalises 2026.07.22 to 2026.7.22; compare parts, not strings.
return tuple(int(part) for part in version.split("."))


def _bundle_common_names() -> set[str]:
# The certificates themselves, not cacert.pem's comment lines.
certs = ssl.create_default_context(cafile=certifi.where()).get_ca_certs()
return {dict(rdn[0] for rdn in c["subject"]).get("commonName", "") for c in certs}


def test_dated_compares_parts() -> None:
assert _dated("2026.7.22") == _dated("2026.07.22") < FLOOR <= _dated("2026.10.19")


def test_lock_resolves_certifi_at_the_floor() -> None:
lock = tomllib.loads((ROOT / "uv.lock").read_text())
(locked,) = [p["version"] for p in lock["package"] if p["name"] == "certifi"]
assert _dated(locked) >= FLOOR, locked


def test_installed_certifi_is_at_the_floor() -> None:
assert _dated(certifi.__version__) >= FLOOR, certifi.__version__


def test_the_bundle_does_not_trust_emsign_ecc_g3() -> None:
names = _bundle_common_names()
# Control: the matcher sees eMudhra's roots, and the one 2026.10.19 keeps.
assert "emSign Root CA - G1" in names
assert DISTRUSTED not in names
11 changes: 7 additions & 4 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading