srdusr
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--PLAN.md25
-rw-r--r--README.md17
-rw-r--r--cmd/mitmuxd/main.go12
-rw-r--r--internal/ca/install.go112
-rw-r--r--internal/ca/install_test.go84
5 files changed, 244 insertions, 6 deletions
diff --git a/PLAN.md b/PLAN.md
index d874f69..28b80e3 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -146,8 +146,29 @@ configured once before `ctrl+r` starts an attack and apply for that run
only - matching Burp, which doesn't retroactively re-grep already-fired
requests if you change the options mid-attack.
-Still open from "worth considering": CA install UX per OS, multiple
-proxy listeners and upstream proxy chaining. Neither is started yet.
+Shipped since: CA install UX per OS (`mitmuxd -install-ca`) - generates
+the CA if needed, prints copy-pasteable install steps for the detected
+platform, and exits without starting the proxy. Deliberately
+instructions-only, never auto-executing: trust-store tooling varies
+enough across Linux distros that guessing wrong and running the wrong
+command unattended is worse than asking, and installing a root CA is a
+system-wide trust change that affects every TLS connection on the
+machine, not just mitmux's own traffic - the user running the printed
+command themselves keeps them in control of that. On Linux, detects
+`trust` (p11-kit - Arch, and Fedora also ships it)/`update-ca-trust`
+(RHEL/Fedora/CentOS)/`update-ca-certificates` (Debian/Ubuntu/Gentoo)
+via PATH lookup and picks 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`) instructions are implemented but, unlike the
+Linux path, not verified live - no macOS/Windows machine was available
+to test against; only the command text itself (sourced from each
+platform's standard, documented tooling) is confirmed correct by
+inspection.
+
+Still open from "worth considering": multiple proxy listeners and
+upstream proxy chaining. Not started yet.
Skipped deliberately (from the research, matches this tool's stated
scope): active/passive vulnerability scanning, plugin marketplace,
diff --git a/README.md b/README.md
index 67536c7..f65e3fe 100644
--- a/README.md
+++ b/README.md
@@ -94,8 +94,18 @@ go build -o bin/mitmux ./cmd/mitmux
2. **Trust the CA.** To intercept HTTPS without constant certificate
warnings, import `ca.pem` into whatever's making the requests -
your browser's certificate store, `curl --cacert`, a mobile device's
- trusted-certificate settings, etc. (Automated per-OS trust-store
- installation isn't implemented yet - see `PLAN.md`.)
+ trusted-certificate settings, etc. For copy-pasteable, OS-specific
+ steps (Linux: whichever of `trust`/`update-ca-trust`/
+ `update-ca-certificates` is actually on your system, plus Firefox's
+ own NSS store; macOS: Keychain; Windows: `certutil`/PowerShell), run:
+
+ ```sh
+ ./bin/mitmuxd -install-ca
+ ```
+
+ This only prints commands - it never runs anything against your
+ trust store itself. Installing a root CA is a system-wide trust
+ change, so you run the printed command yourself.
3. **Point a client at the proxy.** e.g.:
@@ -316,7 +326,8 @@ reasoning behind each:
- Match-and-replace: headers only, no body rules yet
- Intruder: Sniper attack only (no battering ram / pitchfork / cluster
bomb), sequential sending, capped at 1000 requests per attack
-- No automated CA installation into OS/browser trust stores
+- `mitmuxd -install-ca` prints per-OS trust-store install steps; it
+ never runs them for you (see Quick start above for why)
- No WebSocket interception
- No client (mutual-TLS) certificate support
- No active or passive vulnerability scanning, no plugin system - this
diff --git a/cmd/mitmuxd/main.go b/cmd/mitmuxd/main.go
index 9db31e5..9c77516 100644
--- a/cmd/mitmuxd/main.go
+++ b/cmd/mitmuxd/main.go
@@ -7,11 +7,13 @@ package main
import (
"context"
"flag"
+ "fmt"
"log"
"net"
"os"
"os/signal"
"path/filepath"
+ "runtime"
"syscall"
"time"
@@ -26,6 +28,7 @@ func main() {
caDir := flag.String("ca-dir", "", "directory for CA cert/key and history db (default: XDG config dir)")
dbPath := flag.String("db", "", "path to history database (default: <ca-dir>/history.db)")
socketPath := flag.String("socket", "", "control socket path (default: $XDG_RUNTIME_DIR/mitmux.sock, else <ca-dir>/mitmux.sock)")
+ installCA := flag.Bool("install-ca", false, "generate the CA if needed, print OS-specific trust-store install steps, and exit (doesn't start the proxy)")
flag.Parse()
dir := *caDir
@@ -41,7 +44,14 @@ func main() {
if err != nil {
log.Fatalf("load CA: %v", err)
}
- log.Printf("CA ready: %s (install %s/ca.pem in your client's trust store to avoid TLS warnings)", root.Cert.Subject.CommonName, dir)
+
+ if *installCA {
+ fmt.Printf("CA certificate: %s\n\n", ca.CertPath(dir))
+ fmt.Print(ca.InstallInstructions(runtime.GOOS, ca.CertPath(dir)))
+ return
+ }
+
+ log.Printf("CA ready: %s (install %s/ca.pem in your client's trust store to avoid TLS warnings, or run 'mitmuxd -install-ca' for OS-specific steps)", root.Cert.Subject.CommonName, dir)
dbFile := *dbPath
if dbFile == "" {
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)
+ }
+ }
+}