diff options
| author | srdusr <[email protected]> | 2026-04-02 20:55:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2026-04-02 20:55:00 +0200 |
| commit | f77a9570e973dda7247c653754e62d5a9a895658 (patch) | |
| tree | 00bd28c9f76e3ed6ea5bc1e0663acf2716e98128 /internal | |
| parent | b11e60dcccc836bf67b3720930600f7112f624dc (diff) | |
| download | mitmux-f77a9570e973dda7247c653754e62d5a9a895658.tar.gz mitmux-f77a9570e973dda7247c653754e62d5a9a895658.zip | |
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.
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/ca/install.go | 112 | ||||
| -rw-r--r-- | internal/ca/install_test.go | 84 |
2 files changed, 196 insertions, 0 deletions
diff --git a/internal/ca/install.go b/internal/ca/install.go new file mode 100644 index 0000000..bafcea4 --- /dev/null +++ b/internal/ca/install.go @@ -0,0 +1,112 @@ +package ca + +import ( + "fmt" + "os/exec" + "path/filepath" + "strings" +) + +// commandExists reports whether name is on PATH. Var, not a plain func +// call, so tests can substitute a fake lookup without touching the real +// PATH - trust-store tooling varies enough across distros that testing +// the actual detection logic (which command wins, in what order) matters +// more than testing against whatever happens to be installed on the +// machine running `go test`. +var commandExists = func(name string) bool { + _, err := exec.LookPath(name) + return err == nil +} + +// InstallInstructions returns copy-pasteable, OS-specific steps for +// trusting caPath as a root CA. It only ever prints commands - it never +// runs anything itself. Installing a root CA is a genuinely sensitive, +// system-wide trust change (and system trust-store tooling varies enough +// across distros that guessing wrong and auto-running the wrong command +// is worse than asking); the user running the printed command themselves +// keeps them in control of a change that affects every TLS connection on +// the machine, not just mitmux's own traffic. +func InstallInstructions(goos, caPath string) string { + switch goos { + case "linux": + return linuxInstructions(caPath) + case "darwin": + return darwinInstructions(caPath) + case "windows": + return windowsInstructions(caPath) + default: + return genericInstructions(caPath) + } +} + +func linuxInstructions(caPath string) string { + var b strings.Builder + fmt.Fprintf(&b, "System trust store (curl, most CLI tools, Chrome/Chromium):\n") + + switch { + case commandExists("trust"): + fmt.Fprintf(&b, " sudo trust anchor --store %s\n", caPath) + case commandExists("update-ca-trust"): + fmt.Fprintf(&b, " sudo cp %s /etc/pki/ca-trust/source/anchors/mitmux-ca.pem\n", caPath) + fmt.Fprintf(&b, " sudo update-ca-trust\n") + case commandExists("update-ca-certificates"): + fmt.Fprintf(&b, " sudo cp %s /usr/local/share/ca-certificates/mitmux-ca.crt\n", caPath) + fmt.Fprintf(&b, " sudo update-ca-certificates\n") + default: + fmt.Fprintf(&b, " No known trust-store tool (trust / update-ca-trust /\n") + fmt.Fprintf(&b, " update-ca-certificates) found on PATH. Check your distro's\n") + fmt.Fprintf(&b, " docs for how it manages /etc/ssl/certs.\n") + } + + fmt.Fprintf(&b, "\nFirefox (and Chrome/Chromium's own NSS store, which doesn't\n") + fmt.Fprintf(&b, "always follow the system trust store on Linux):\n") + if commandExists("certutil") { + fmt.Fprintf(&b, " certutil -d sql:$HOME/.mozilla/firefox/<your-profile> -A -n mitmux -t \"C,,\" -i %s\n", caPath) + fmt.Fprintf(&b, " (find <your-profile> with: ls ~/.mozilla/firefox | grep default)\n") + } else { + fmt.Fprintf(&b, " Import manually: Settings -> Privacy & Security -> Certificates\n") + fmt.Fprintf(&b, " -> View Certificates -> Authorities -> Import, select %s\n", caPath) + fmt.Fprintf(&b, " (or install nss-tools/libnss3-tools for certutil, which can\n") + fmt.Fprintf(&b, " script this instead)\n") + } + return b.String() +} + +func darwinInstructions(caPath string) string { + var b strings.Builder + fmt.Fprintf(&b, "System-wide (Keychain Access -> System, or via Terminal):\n") + fmt.Fprintf(&b, " sudo security add-trusted-cert -d -r trustRoot \\\n") + fmt.Fprintf(&b, " -k /Library/Keychains/System.keychain %s\n", caPath) + fmt.Fprintf(&b, "\nCurrent user only (no sudo, login keychain):\n") + fmt.Fprintf(&b, " security add-trusted-cert -d -r trustRoot \\\n") + fmt.Fprintf(&b, " -k ~/Library/Keychains/login.keychain-db %s\n", caPath) + fmt.Fprintf(&b, "\nFirefox uses its own certificate store, not the macOS Keychain -\n") + fmt.Fprintf(&b, "import %s manually via Settings -> Privacy & Security ->\n", caPath) + fmt.Fprintf(&b, "Certificates -> View Certificates -> Authorities -> Import.\n") + return b.String() +} + +func windowsInstructions(caPath string) string { + var b strings.Builder + fmt.Fprintf(&b, "From an elevated (Administrator) command prompt:\n") + fmt.Fprintf(&b, " certutil -addstore -f \"ROOT\" %s\n", caPath) + fmt.Fprintf(&b, "\nOr from an elevated PowerShell:\n") + fmt.Fprintf(&b, " Import-Certificate -FilePath %s -CertStoreLocation Cert:\\LocalMachine\\Root\n", caPath) + fmt.Fprintf(&b, "\nFirefox uses its own certificate store, not the Windows store -\n") + fmt.Fprintf(&b, "import %s manually via Settings -> Privacy & Security ->\n", caPath) + fmt.Fprintf(&b, "Certificates -> View Certificates -> Authorities -> Import.\n") + return b.String() +} + +func genericInstructions(caPath string) string { + return fmt.Sprintf("No install steps known for this OS - import %s into your\n"+ + "client's trust store manually (browser certificate settings, or\n"+ + "whatever --cacert / equivalent flag your TLS client offers).\n", caPath) +} + +// CertPath returns the path to the CA certificate PEM file inside dir +// (see EnsureCA), for callers that just need to point a user or a tool +// at it. +func CertPath(dir string) string { + return filepath.Join(dir, certFileName) +} 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) + } + } +} |