srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/ca/install.go112
-rw-r--r--internal/ca/install_test.go84
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)
+ }
+ }
+}