fix: cross-platform CA trust (Arch/Fedora + Windows) (#41)
* fix: cross-platform CA trust (Arch/Fedora + Windows) Closes #35. trust_ca_linux now detects which trust store the distro ships and runs the matching refresh command, instead of hardcoding Debian's update-ca-certificates. Detection walks a const table in priority order, picking the first whose anchor dir exists: - debian: /usr/local/share/ca-certificates (update-ca-certificates) - pki: /etc/pki/ca-trust/source/anchors (update-ca-trust extract) - p11kit: /etc/ca-certificates/trust-source/anchors (trust extract-compat) Falls back with a clear error listing every backend tried. Adds Windows support via certutil -addstore Root / -delstore Root, removing the silent CA-trust gap on numa install (previously the service installed but the trust step quietly errored, leaving every HTTPS .numa request throwing browser warnings). Refactor: trust_ca and untrust_ca are now thin dispatchers calling per-platform helpers. CA_COMMON_NAME and CA_FILE_NAME are centralized in tls.rs and reused from system_dns.rs and api.rs. untrust_ca_linux no longer pre-checks file existence (TOCTOU) and skips the refresh when no file was actually removed. Test: tests/docker/install-trust.sh runs the install/uninstall contract against debian:stable, fedora:latest, and archlinux:latest in containers, asserting the cert lands in (and is removed from) the system bundle. All three pass locally. README notes the Firefox/NSS limitation (separate trust store). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: rustfmt fixes for trust_ca_linux helpers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: macOS CA trust contract test (manual) Adds tests/manual/install-trust-macos.sh — a sudo bash script that mirrors trust_ca_macos / untrust_ca_macos against a fixture cert with a unique CN. Designed to coexist with a running production numa: - Refuses to run if a real "Numa Local CA" is already in System.keychain (fail-closed protection for dogfood installs) - Uses a unique CN ("Numa Local CA Test <pid-timestamp>") so the test cert can never collide with production - Mirrors the by-hash deletion loop from untrust_ca_macos - Trap-cleanup on success or interrupt Lives under tests/manual/ to signal "host-mutating, dev-only" — distinct from tests/docker/install-trust.sh which is hermetic. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: relax bail-out in macOS trust test (safe alongside production) The bail-out was overly defensive. The test cert uses a unique CN ("Numa Local CA Test <pid-ts>") that is strictly longer than the production CN, so `security find-certificate -c $TEST_CN` cannot substring-match the production cert. All deletes are by-hash, which can only target the test cert's specific hash. Coexistence is provably safe; document the reasoning in the header comment block and replace the refusal with an informational notice. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit was merged in pull request #41.
This commit is contained in:
94
tests/manual/install-trust-macos.sh
Executable file
94
tests/manual/install-trust-macos.sh
Executable file
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Manual macOS CA trust contract test.
|
||||
#
|
||||
# Mirrors src/system_dns.rs::trust_ca_macos / untrust_ca_macos by running
|
||||
# the same `security` shell commands against a fixture cert with a unique
|
||||
# CN. Safe to run alongside a production numa install:
|
||||
#
|
||||
# - Test cert CN = "Numa Local CA Test <pid-ts>", always strictly longer
|
||||
# than the production CN "Numa Local CA". `security find-certificate -c`
|
||||
# does substring matching, so the test's search for $TEST_CN can never
|
||||
# match the production cert (the search term is longer than the prod CN).
|
||||
# - All deletes use `delete-certificate -Z <hash>`, which only touches the
|
||||
# cert with that exact hash. Production and test certs have different
|
||||
# hashes by construction (different key material), so the delete cannot
|
||||
# reach the production cert even if a CN search somehow returned both.
|
||||
#
|
||||
# Mutates the System keychain (briefly). Cleans up on success or interrupt.
|
||||
# Requires sudo for `security add-trusted-cert` and `delete-certificate`.
|
||||
#
|
||||
# Usage: ./tests/manual/install-trust-macos.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "$OSTYPE" != darwin* ]]; then
|
||||
echo "This test is macOS-only." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
GREEN="\033[32m"; RED="\033[31m"; RESET="\033[0m"
|
||||
|
||||
# Production constant from src/tls.rs::CA_COMMON_NAME — keep in sync.
|
||||
PROD_CN="Numa Local CA"
|
||||
KEYCHAIN="/Library/Keychains/System.keychain"
|
||||
|
||||
# Notice if production numa is already installed. We proceed regardless —
|
||||
# see header for why coexistence is safe (unique CN + by-hash deletion).
|
||||
if security find-certificate -c "$PROD_CN" "$KEYCHAIN" >/dev/null 2>&1; then
|
||||
echo " note: production '$PROD_CN' detected — proceeding alongside (test cert can't touch it)"
|
||||
echo
|
||||
fi
|
||||
|
||||
# Unique CN ensures the test cert can never collide with production.
|
||||
TEST_CN="Numa Local CA Test $$-$(date +%s)"
|
||||
FIXTURE_DIR=$(mktemp -d)
|
||||
|
||||
cleanup() {
|
||||
# Best-effort: remove any test certs by hash if still present.
|
||||
if security find-certificate -c "$TEST_CN" "$KEYCHAIN" >/dev/null 2>&1; then
|
||||
echo " cleanup: removing leftover test cert"
|
||||
security find-certificate -c "$TEST_CN" -a -Z "$KEYCHAIN" 2>/dev/null \
|
||||
| awk '/^SHA-1 hash:/ {print $NF}' \
|
||||
| while read -r hash; do
|
||||
sudo security delete-certificate -Z "$hash" "$KEYCHAIN" >/dev/null 2>&1 || true
|
||||
done
|
||||
fi
|
||||
rm -rf "$FIXTURE_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "── generating fixture CA ──"
|
||||
openssl req -x509 -newkey rsa:2048 -nodes -days 1 \
|
||||
-keyout "$FIXTURE_DIR/ca.key" \
|
||||
-out "$FIXTURE_DIR/ca.pem" \
|
||||
-subj "/CN=$TEST_CN" \
|
||||
-addext "basicConstraints=critical,CA:TRUE" \
|
||||
-addext "keyUsage=critical,keyCertSign,cRLSign" >/dev/null 2>&1
|
||||
echo " CN: $TEST_CN"
|
||||
echo
|
||||
|
||||
echo "── trust step (mirrors trust_ca_macos) ──"
|
||||
sudo security add-trusted-cert -d -r trustRoot -k "$KEYCHAIN" "$FIXTURE_DIR/ca.pem"
|
||||
if security find-certificate -c "$TEST_CN" "$KEYCHAIN" >/dev/null 2>&1; then
|
||||
printf " ${GREEN}✓${RESET} test cert found in keychain\n"
|
||||
else
|
||||
printf " ${RED}✗${RESET} test cert NOT found after add-trusted-cert\n"
|
||||
exit 1
|
||||
fi
|
||||
echo
|
||||
|
||||
echo "── untrust step (mirrors untrust_ca_macos) ──"
|
||||
security find-certificate -c "$TEST_CN" -a -Z "$KEYCHAIN" 2>/dev/null \
|
||||
| awk '/^SHA-1 hash:/ {print $NF}' \
|
||||
| while read -r hash; do
|
||||
sudo security delete-certificate -Z "$hash" "$KEYCHAIN" >/dev/null
|
||||
done
|
||||
if security find-certificate -c "$TEST_CN" "$KEYCHAIN" >/dev/null 2>&1; then
|
||||
printf " ${RED}✗${RESET} test cert STILL present after delete (regression)\n"
|
||||
exit 1
|
||||
fi
|
||||
printf " ${GREEN}✓${RESET} test cert removed from keychain\n"
|
||||
echo
|
||||
|
||||
printf "${GREEN}all checks passed${RESET}\n"
|
||||
Reference in New Issue
Block a user