From f77a9570e973dda7247c653754e62d5a9a895658 Mon Sep 17 00:00:00 2001 From: srdusr <99972264+srdusr@users.noreply.github.com> Date: Thu, 2 Apr 2026 20:55:00 +0200 Subject: Per-OS CA install instructions (mitmuxd -install-ca) Trusting the CA was previously "import ca.pem into whatever's making the requests" with no further help. -install-ca generates the CA if needed and prints copy-pasteable, OS-specific steps, then exits without starting the proxy. Deliberately instructions-only, never auto-executing anything: Linux trust-store tooling varies enough across distros (trust vs update-ca-trust vs update-ca-certificates) that guessing wrong and running the wrong command unattended is worse than asking, and installing a root CA is a system-wide trust change affecting every TLS connection on the machine, not just mitmux's own traffic - running the printed command themselves keeps the user in control of that. internal/ca/install.go: InstallInstructions(goos, caPath) dispatches by OS. Linux detects trust (p11-kit - Arch, also on Fedora) / update-ca-trust (RHEL/Fedora/CentOS) / update-ca-certificates (Debian/Ubuntu/Gentoo) via PATH lookup and prints whichever is actually present, plus separate certutil/NSS instructions for Firefox/Chrome's own certificate store (which doesn't always follow the system trust store on Linux). macOS (security add-trusted-cert) and Windows (certutil -addstore / Import-Certificate) are implemented from each platform's standard documented tooling but not verified live - no macOS/Windows machine was available to test against, unlike Linux. commandExists is a package var (not a direct exec.LookPath call) so tests can fake which tools are "present" and exercise every detection branch deterministically, independent of what's actually installed on whatever machine runs `go test`. Verified live: built mitmuxd, ran -install-ca against a throwaway CA dir on this (Arch Linux) machine - correctly detected `trust` and `certutil` on PATH and printed accurate commands, confirmed the CA files were actually generated, confirmed no proxy/daemon process was left running (exits immediately after printing), and confirmed running it a second time reuses the existing CA (identical file hash) rather than regenerating. go build/vet/gofmt/test/mod tidy all clean. --- internal/ca/install_test.go | 84 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 internal/ca/install_test.go (limited to 'internal/ca/install_test.go') diff --git a/internal/ca/install_test.go b/internal/ca/install_test.go new file mode 100644 index 0000000..0f01f1f --- /dev/null +++ b/internal/ca/install_test.go @@ -0,0 +1,84 @@ +package ca + +import ( + "strings" + "testing" +) + +// withCommands temporarily replaces commandExists with a fake that only +// reports the given names as present, restoring the real one after. +func withCommands(t *testing.T, present ...string) { + t.Helper() + set := make(map[string]bool, len(present)) + for _, p := range present { + set[p] = true + } + orig := commandExists + commandExists = func(name string) bool { return set[name] } + t.Cleanup(func() { commandExists = orig }) +} + +func TestLinuxInstructionsPrefersTrust(t *testing.T) { + withCommands(t, "trust", "update-ca-trust", "update-ca-certificates") + got := linuxInstructions("/tmp/ca.pem") + if !strings.Contains(got, "sudo trust anchor --store /tmp/ca.pem") { + t.Errorf("expected trust anchor command when trust is available, got:\n%s", got) + } +} + +func TestLinuxInstructionsFallsBackToUpdateCaTrust(t *testing.T) { + withCommands(t, "update-ca-trust") + got := linuxInstructions("/tmp/ca.pem") + if !strings.Contains(got, "update-ca-trust") || strings.Contains(got, "trust anchor") { + t.Errorf("expected update-ca-trust path, got:\n%s", got) + } +} + +func TestLinuxInstructionsFallsBackToUpdateCaCertificates(t *testing.T) { + withCommands(t, "update-ca-certificates") + got := linuxInstructions("/tmp/ca.pem") + if !strings.Contains(got, "update-ca-certificates") { + t.Errorf("expected update-ca-certificates path, got:\n%s", got) + } +} + +func TestLinuxInstructionsNoneFound(t *testing.T) { + withCommands(t) + got := linuxInstructions("/tmp/ca.pem") + if !strings.Contains(got, "No known trust-store tool") { + t.Errorf("expected a no-tool-found message, got:\n%s", got) + } +} + +func TestLinuxInstructionsCertutilPresence(t *testing.T) { + withCommands(t, "certutil") + withCert := linuxInstructions("/tmp/ca.pem") + if !strings.Contains(withCert, "certutil -d sql:") { + t.Errorf("expected certutil NSS instructions when certutil is present, got:\n%s", withCert) + } + + withCommands(t) + withoutCert := linuxInstructions("/tmp/ca.pem") + if !strings.Contains(withoutCert, "Import manually") { + t.Errorf("expected manual-import fallback when certutil is absent, got:\n%s", withoutCert) + } +} + +func TestInstallInstructionsDispatchesByOS(t *testing.T) { + withCommands(t) + tests := []struct { + goos string + want string + }{ + {"linux", "trust store"}, + {"darwin", "security add-trusted-cert"}, + {"windows", "certutil -addstore"}, + {"plan9", "No install steps known"}, + } + for _, tt := range tests { + got := InstallInstructions(tt.goos, "/tmp/ca.pem") + if !strings.Contains(got, tt.want) { + t.Errorf("InstallInstructions(%q, ...) = %q, want it to contain %q", tt.goos, got, tt.want) + } + } +} -- cgit v1.2.3