srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/internal/store
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2026-06-16 22:57:00 +0200
committersrdusr <[email protected]>2026-06-16 22:57:00 +0200
commit23c8ab359c2108654d57176e233d2c099b398f31 (patch)
tree3e2d1c66d987b4b771da69e67bd8a0c9ad2fc3db /internal/store
parent6114567258bcad0517a0d881168711aaacdba5d5 (diff)
downloadmitmux-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.go64
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()