diff options
| author | srdusr <[email protected]> | 2026-06-16 22:57:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2026-06-16 22:57:00 +0200 |
| commit | 23c8ab359c2108654d57176e233d2c099b398f31 (patch) | |
| tree | 3e2d1c66d987b4b771da69e67bd8a0c9ad2fc3db /internal/store | |
| parent | 6114567258bcad0517a0d881168711aaacdba5d5 (diff) | |
| download | mitmux-23c8ab359c2108654d57176e233d2c099b398f31.tar.gz mitmux-23c8ab359c2108654d57176e233d2c099b398f31.zip | |
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.
Diffstat (limited to 'internal/store')
| -rw-r--r-- | internal/store/store.go | 64 |
1 files changed, 64 insertions, 0 deletions
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() |