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/store/store.go | 64 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) (limited to 'internal/store/store.go') diff --git a/internal/store/store.go b/internal/store/store.go index befd5a6..0e57c3c 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -15,6 +15,7 @@ import ( _ "modernc.org/sqlite" + "mitmux/internal/clientcert" "mitmux/internal/rules" "mitmux/internal/scope" ) @@ -63,6 +64,16 @@ CREATE TABLE IF NOT EXISTS scope_rules ( pattern TEXT NOT NULL, is_regex INTEGER NOT NULL DEFAULT 0 ); + +CREATE TABLE IF NOT EXISTS client_certs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + enabled INTEGER NOT NULL DEFAULT 1, + name TEXT NOT NULL DEFAULT '', + pattern TEXT NOT NULL, + is_regex INTEGER NOT NULL DEFAULT 0, + cert_pem BLOB NOT NULL, + key_pem BLOB NOT NULL +); ` // Store is a handle to the history database. Safe for concurrent use. @@ -619,6 +630,59 @@ func (s *Store) DeleteScopeRule(id int64) error { return nil } +// ListClientCerts returns every client certificate, including disabled +// ones (the management view needs to show and let you re-enable those +// too). +func (s *Store) ListClientCerts() ([]clientcert.Cert, error) { + rows, err := s.db.Query(`SELECT id, enabled, name, pattern, is_regex, cert_pem, key_pem FROM client_certs ORDER BY id`) + if err != nil { + return nil, fmt.Errorf("list client certs: %w", err) + } + defer rows.Close() + + var out []clientcert.Cert + for rows.Next() { + var c clientcert.Cert + var enabled, isRegex int + if err := rows.Scan(&c.ID, &enabled, &c.Name, &c.Pattern, &isRegex, &c.CertPEM, &c.KeyPEM); err != nil { + return nil, fmt.Errorf("scan client cert row: %w", err) + } + c.Enabled = enabled != 0 + c.IsRegex = isRegex != 0 + out = append(out, c) + } + return out, rows.Err() +} + +// AddClientCert stores c and returns its assigned ID. +func (s *Store) AddClientCert(c clientcert.Cert) (int64, error) { + res, err := s.db.Exec( + `INSERT INTO client_certs (enabled, name, pattern, is_regex, cert_pem, key_pem) VALUES (?, ?, ?, ?, ?, ?)`, + boolToInt(c.Enabled), c.Name, c.Pattern, boolToInt(c.IsRegex), c.CertPEM, c.KeyPEM, + ) + if err != nil { + return 0, fmt.Errorf("add client cert: %w", err) + } + return res.LastInsertId() +} + +// SetClientCertEnabled toggles a client cert without touching its +// content. +func (s *Store) SetClientCertEnabled(id int64, enabled bool) error { + if _, err := s.db.Exec(`UPDATE client_certs SET enabled = ? WHERE id = ?`, boolToInt(enabled), id); err != nil { + return fmt.Errorf("set client cert %d enabled: %w", id, err) + } + return nil +} + +// DeleteClientCert removes a client certificate. +func (s *Store) DeleteClientCert(id int64) error { + if _, err := s.db.Exec(`DELETE FROM client_certs WHERE id = ?`, id); err != nil { + return fmt.Errorf("delete client cert %d: %w", id, err) + } + return nil +} + // DeleteEntry removes a single history entry and its search index row. func (s *Store) DeleteEntry(id int64) error { tx, err := s.db.Begin() -- cgit v1.2.3