diff --git a/cmd/client.go b/cmd/client.go
@@ -1,16 +1,24 @@
package cmd
import (
- "crypto/x509"
"fmt"
"log"
- "os"
+ "strings"
"github.com/djnnvx/mic/fingerprint"
"github.com/djnnvx/mic/proxy"
"github.com/spf13/cobra"
)
+// proxyURL renders a listen address as something a user can paste into curl -x.
+// A bare ":8080" needs a host.
+func proxyURL(listen string) string {
+ if strings.HasPrefix(listen, ":") {
+ return "localhost" + listen
+ }
+ return listen
+}
+
func newClientCmd() *cobra.Command {
var (
listen string
@@ -27,12 +35,10 @@ func newClientCmd() *cobra.Command {
p := &proxy.Proxy{ListenAddr: listen}
if caPath != "" {
- caCert, err := os.ReadFile(caPath)
+ pool, err := loadCAPool(caPath)
if err != nil {
- return fmt.Errorf("reading CA certificate: %w", err)
+ return err
}
- pool := x509.NewCertPool()
- pool.AppendCertsFromPEM(caCert)
p.CAPool = pool
}
@@ -45,14 +51,17 @@ func newClientCmd() *cobra.Command {
log.Printf("[+] Using TLS fingerprint: %s", fp.Name())
}
- if interceptCert != "" || interceptKey != "" {
+ if (interceptCert == "") != (interceptKey == "") {
+ return fmt.Errorf("--intercept-cert and --intercept-key must be given together")
+ }
+ if interceptCert != "" {
ca, err := proxy.LoadOrGenerateCA(interceptCert, interceptKey)
if err != nil {
return fmt.Errorf("loading/generating intercept CA: %w", err)
}
p.LocalCA = ca
- log.Printf("[+] MitM CA ready. Import %s, then: curl --cacert %s -x http://localhost%s https://<target>",
- interceptCert, interceptCert, listen)
+ log.Printf("[+] MitM CA ready. Import %s, then: curl --cacert %s -x http://%s https://<target>",
+ interceptCert, interceptCert, proxyURL(listen))
}
log.Printf("[+] Mode: client (HTTP CONNECT proxy on %s)", listen)
diff --git a/cmd/client_test.go b/cmd/client_test.go
@@ -0,0 +1,54 @@
+package cmd
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+// run executes the client command with args, using an unroutable listen port so
+// that anything reaching p.Run() fails there instead of binding a real socket.
+func runClient(t *testing.T, args ...string) error {
+ t.Helper()
+ c := newClientCmd()
+ c.SetOut(os.NewFile(0, os.DevNull))
+ c.SetErr(os.NewFile(0, os.DevNull))
+ c.SetArgs(append([]string{"--listen", "127.0.0.1:99999"}, args...))
+ return c.Execute()
+}
+
+func TestGarbageCAFileIsRefused(t *testing.T) {
+ path := filepath.Join(t.TempDir(), "ca.pem")
+ if err := os.WriteFile(path, []byte("this is not a certificate"), 0o600); err != nil {
+ t.Fatal(err)
+ }
+ err := runClient(t, "--ca", path)
+ if err == nil || !strings.Contains(err.Error(), "no certificates found") {
+ t.Errorf("garbage CA accepted; got err=%v, want one naming the bad file", err)
+ }
+}
+
+func TestHalfSpecifiedInterceptPairIsRefused(t *testing.T) {
+ dir := t.TempDir()
+ for _, flag := range []string{"--intercept-cert", "--intercept-key"} {
+ err := runClient(t, flag, filepath.Join(dir, "x.pem"))
+ if err == nil || !strings.Contains(err.Error(), "must be given together") {
+ t.Errorf("%s alone accepted; got err=%v", flag, err)
+ }
+ if _, statErr := os.Stat(filepath.Join(dir, "x.pem")); statErr == nil {
+ t.Errorf("%s alone wrote a CA file it should have refused to create", flag)
+ }
+ }
+}
+
+func TestProxyURL(t *testing.T) {
+ for in, want := range map[string]string{
+ ":8080": "localhost:8080",
+ "127.0.0.1:8080": "127.0.0.1:8080",
+ } {
+ if got := proxyURL(in); got != want {
+ t.Errorf("proxyURL(%q) = %q; want %q", in, got, want)
+ }
+ }
+}
diff --git a/cmd/root.go b/cmd/root.go
@@ -1,12 +1,28 @@
package cmd
import (
+ "crypto/x509"
"fmt"
"os"
"github.com/spf13/cobra"
)
+// loadCAPool reads a PEM bundle into a cert pool. AppendCertsFromPEM reports
+// failure only through its return value, and an empty pool makes every upstream
+// handshake fail with "unknown authority" instead of naming the real cause.
+func loadCAPool(path string) (*x509.CertPool, error) {
+ pem, err := os.ReadFile(path)
+ if err != nil {
+ return nil, fmt.Errorf("reading CA certificate: %w", err)
+ }
+ pool := x509.NewCertPool()
+ if !pool.AppendCertsFromPEM(pem) {
+ return nil, fmt.Errorf("no certificates found in %s", path)
+ }
+ return pool, nil
+}
+
var SKID_ASCII_ART = `
diff --git a/cmd/server.go b/cmd/server.go
@@ -1,11 +1,9 @@
package cmd
import (
- "crypto/x509"
"errors"
"fmt"
"log"
- "os"
"github.com/djnnvx/mic/fingerprint"
"github.com/djnnvx/mic/proxy"
@@ -39,12 +37,10 @@ func newServerCmd() *cobra.Command {
}
if caPath != "" {
- caCert, err := os.ReadFile(caPath)
+ pool, err := loadCAPool(caPath)
if err != nil {
- return fmt.Errorf("reading CA certificate: %w", err)
+ return err
}
- pool := x509.NewCertPool()
- pool.AppendCertsFromPEM(caCert)
p.CAPool = pool
}
| | | |