fix: cross-platform CA trust (Arch/Fedora + Windows) #41

Merged
razvandimescu merged 4 commits from fix/cross-platform-ca-trust into main 2026-04-08 20:18:01 +08:00
razvandimescu commented 2026-04-08 19:13:10 +08:00 (Migrated from github.com)

Closes #35.

Summary

  • Linux: detect the trust store the distro ships (Debian / Fedora pki / Arch p11-kit) instead of hardcoding update-ca-certificates. Walks a const table in priority order, picks the first whose anchor dir exists. Fixes #35 on Arch and silently broken Fedora/RHEL/SUSE installs.
  • Windows: implement trust via certutil -addstore Root / certutil -delstore Root. Previously numa install succeeded on Windows but the trust step quietly errored, leaving HTTPS .numa requests throwing browser warnings.
  • Refactor: trust_ca / untrust_ca are now thin dispatchers calling per-platform helpers. CA_COMMON_NAME and CA_FILE_NAME centralized in tls.rs and reused from system_dns.rs and api.rs. untrust_ca_linux drops a TOCTOU pre-check and skips update-ca-certificates --fresh when nothing was removed.
  • Docs: README notes the Firefox/NSS limitation (Firefox keeps its own trust store and ignores the system one).

Test plan

  • cargo build + cargo clippy --lib clean on macOS
  • cargo test --lib system_dns — 3/3 pass
  • CI: cargo fmt --check + cargo build + cargo test + cargo clippy -- -D warnings on ubuntu-latest, macos-latest, windows-latest — all green (Windows compile-check is the most valuable signal here, validating the new certutil-based code path against the real windows-msvc toolchain)
  • tests/docker/install-trust.sh — hermetic contract test against debian:stable, fedora:latest, archlinux:latest in Docker, asserting the fixture cert lands in (and is removed from) each distro's system bundle. 3 passed, 0 failed locally on Apple Silicon.
  • tests/manual/install-trust-macos.sh — verified locally with the production numa CA in place (script's safety design uses a unique CN + by-hash deletion so coexistence is provably safe). Test cert added via security add-trusted-cert, found by CN, removed by hash, verified gone. Production CA untouched.
  • Windows: covered by CI compile-check + clippy + cargo test on windows-latest against the real windows-msvc toolchain. The Windows code path is 8 lines of certutil -addstore Root / -delstore Root against a well-documented stable Windows API. Behavioral test (running numa install on a real Windows machine) deferred to post-release user reports — acceptable risk for this PR's surface.

Test coverage shape

Platform Test Hermetic? In CI?
Linux (Debian/Fedora/Arch) tests/docker/install-trust.sh Yes (Docker) No (dev-only)
macOS tests/manual/install-trust-macos.sh No (mutates host keychain, cleans up) No (sudo + GUI prompt)
Windows CI compile-check + clippy + cargo test N/A Yes

tests/docker/ = hermetic, no host mutation. tests/manual/ = host-mutating, sudo, dev-only. Naming convention makes the contract obvious.

Notes

  • The CA trust path mismatch between crate::data_dir() (used by system_dns.rs) and ctx.data_dir (used by api.rs, respects [server] data_dir from numa.toml) is a latent pre-existing bug that this refactor inherited but did not introduce. Out of scope for this PR — will file a follow-up issue.
  • A standalone numa trust install / numa trust uninstall subcommand (so users running numa serve directly without numa install can still trust the CA) was deferred per the original plan discussion. Not blocking #35.
  • tests/docker/install-trust.sh and tests/manual/install-trust-macos.sh are intentionally not wired into CI — both are "before push" tools, like tests/integration.sh. The Docker script aligns with the future docker-integration migration plan in docs/implementation/integration-test-migration.md.

🤖 Generated with Claude Code

Closes #35. ## Summary - **Linux**: detect the trust store the distro ships (Debian / Fedora pki / Arch p11-kit) instead of hardcoding `update-ca-certificates`. Walks a const table in priority order, picks the first whose anchor dir exists. Fixes #35 on Arch and silently broken Fedora/RHEL/SUSE installs. - **Windows**: implement trust via `certutil -addstore Root` / `certutil -delstore Root`. Previously `numa install` succeeded on Windows but the trust step quietly errored, leaving HTTPS `.numa` requests throwing browser warnings. - **Refactor**: `trust_ca` / `untrust_ca` are now thin dispatchers calling per-platform helpers. `CA_COMMON_NAME` and `CA_FILE_NAME` centralized in `tls.rs` and reused from `system_dns.rs` and `api.rs`. `untrust_ca_linux` drops a TOCTOU pre-check and skips `update-ca-certificates --fresh` when nothing was removed. - **Docs**: README notes the Firefox/NSS limitation (Firefox keeps its own trust store and ignores the system one). ## Test plan - [x] `cargo build` + `cargo clippy --lib` clean on macOS - [x] `cargo test --lib system_dns` — 3/3 pass - [x] CI: `cargo fmt --check` + `cargo build` + `cargo test` + `cargo clippy -- -D warnings` on `ubuntu-latest`, `macos-latest`, `windows-latest` — all green (Windows compile-check is the most valuable signal here, validating the new `certutil`-based code path against the real `windows-msvc` toolchain) - [x] `tests/docker/install-trust.sh` — hermetic contract test against `debian:stable`, `fedora:latest`, `archlinux:latest` in Docker, asserting the fixture cert lands in (and is removed from) each distro's system bundle. **3 passed, 0 failed** locally on Apple Silicon. - [x] `tests/manual/install-trust-macos.sh` — verified locally with the production numa CA in place (script's safety design uses a unique CN + by-hash deletion so coexistence is provably safe). Test cert added via `security add-trusted-cert`, found by CN, removed by hash, verified gone. Production CA untouched. - [x] **Windows**: covered by CI compile-check + clippy + `cargo test` on `windows-latest` against the real `windows-msvc` toolchain. The Windows code path is 8 lines of `certutil -addstore Root` / `-delstore Root` against a well-documented stable Windows API. Behavioral test (running `numa install` on a real Windows machine) deferred to post-release user reports — acceptable risk for this PR's surface. ## Test coverage shape | Platform | Test | Hermetic? | In CI? | |---|---|---|---| | Linux (Debian/Fedora/Arch) | `tests/docker/install-trust.sh` | Yes (Docker) | No (dev-only) | | macOS | `tests/manual/install-trust-macos.sh` | No (mutates host keychain, cleans up) | No (sudo + GUI prompt) | | Windows | CI compile-check + clippy + cargo test | N/A | Yes | `tests/docker/` = hermetic, no host mutation. `tests/manual/` = host-mutating, sudo, dev-only. Naming convention makes the contract obvious. ## Notes - The CA trust path mismatch between `crate::data_dir()` (used by `system_dns.rs`) and `ctx.data_dir` (used by `api.rs`, respects `[server] data_dir` from `numa.toml`) is a **latent pre-existing bug** that this refactor inherited but did not introduce. Out of scope for this PR — will file a follow-up issue. - A standalone `numa trust install` / `numa trust uninstall` subcommand (so users running `numa serve` directly without `numa install` can still trust the CA) was deferred per the original plan discussion. Not blocking #35. - `tests/docker/install-trust.sh` and `tests/manual/install-trust-macos.sh` are intentionally **not wired into CI** — both are "before push" tools, like `tests/integration.sh`. The Docker script aligns with the future docker-integration migration plan in `docs/implementation/integration-test-migration.md`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.