From 23c8ab359c2108654d57176e233d2c099b398f31 Mon Sep 17 00:00:00 2001 From: srdusr <99972264+srdusr@users.noreply.github.com> Date: Tue, 16 Jun 2026 22:57:00 +0200 Subject: Client (mutual-TLS) certificates Adds internal/clientcert: a cert/key pair matched to hosts by the same substring-or-regex pattern model as scope.Rule, so mitmux can present a client certificate on an upstream TLS handshake that requires one - the previous behavior was a hard handshake failure with no way to authenticate. Wired into both places mitmux dials an https:// upstream over its own TLS client connection: proxy.go's handleConnect (live proxied traffic) and repeat.go's dialForRepeat (Repeater/Intruder resends), both through a new Server.clientCertFor(host) helper. Stored in a new client_certs table, mirroring the existing scope_rules persistence pattern. The TUI (`t` from history) is add-only like scope, for the same reason: delete and re-add covers changing anything, and it's a rarely-touched, low-cardinality list. The add form takes cert/key file paths and reads them once at save time - PEM content, not the path, is what's stored and later presented, so a cert keeps working even if the original file moves afterward. Verified live against a real mutual-TLS-requiring origin server: without a matching cert the handshake correctly fails; with one configured, the origin receives it and the request succeeds; toggling it off reproduces the failure, confirming the enable/disable path works end to end. --- internal/proxy/repeat.go | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) (limited to 'internal/proxy/repeat.go') diff --git a/internal/proxy/repeat.go b/internal/proxy/repeat.go index f682b99..8e7836d 100644 --- a/internal/proxy/repeat.go +++ b/internal/proxy/repeat.go @@ -36,7 +36,7 @@ func (s *Server) sendRaw(ctx context.Context, scheme, host string, raw []byte, s started := time.Now() method, path := parseRequestLine(raw) - conn, err := dialForRepeat(ctx, scheme, host) + conn, err := s.dialForRepeat(ctx, scheme, host) if err != nil { return s.recordRaw(started, time.Since(started), scheme, host, method, path, raw, nil, false, false, 0, err.Error(), source) } @@ -101,8 +101,11 @@ func (s *Server) recordRaw(started time.Time, duration time.Duration, scheme, ho } // dialForRepeat connects to host for scheme, forcing HTTP/1.1 over ALPN -// when TLS is involved (see Repeat's doc comment for why). -func dialForRepeat(ctx context.Context, scheme, host string) (net.Conn, error) { +// when TLS is involved (see Repeat's doc comment for why), presenting a +// client certificate if one is configured for hostname (see +// clientCertFor) - a resent or fuzzed request against a mutual-TLS host +// needs one just as much as a live proxied request does. +func (s *Server) dialForRepeat(ctx context.Context, scheme, host string) (net.Conn, error) { nd := &net.Dialer{Timeout: 10 * time.Second} if scheme != "https" { hostPort := host @@ -122,7 +125,11 @@ func dialForRepeat(ctx context.Context, scheme, host string) (net.Conn, error) { if err != nil { return nil, err } - conn := tls.Client(raw, &tls.Config{ServerName: hostname, NextProtos: []string{"http/1.1"}}) + cfg := &tls.Config{ServerName: hostname, NextProtos: []string{"http/1.1"}} + if cert := s.clientCertFor(hostname); cert != nil { + cfg.Certificates = []tls.Certificate{*cert} + } + conn := tls.Client(raw, cfg) if err := conn.HandshakeContext(ctx); err != nil { raw.Close() return nil, err -- cgit v1.2.3