// Package clientcert manages client (mutual-TLS) certificates: which // certificate mitmux presents to an upstream server that requires one, // selected by matching the request's hostname the same way scope rules // do (see internal/scope) - substring match by default, or a regex - so // the "which rule applies to this host" mental model stays identical // throughout the tool. package clientcert import ( "crypto/tls" "fmt" "regexp" "strings" ) // Cert is one client certificate, scoped to hosts matching Pattern. type Cert struct { ID int64 `json:"id"` Enabled bool `json:"enabled"` Name string `json:"name"` Pattern string `json:"pattern"` IsRegex bool `json:"is_regex"` CertPEM []byte `json:"cert_pem"` KeyPEM []byte `json:"key_pem"` } func (c Cert) matches(host string) bool { if c.IsRegex { re, err := regexp.Compile(c.Pattern) if err != nil { return false } return re.MatchString(host) } return strings.Contains(strings.ToLower(host), strings.ToLower(c.Pattern)) } // FindFor returns the first enabled cert whose pattern matches host, or // nil if none applies - mitmux then just doesn't present a client // certificate for that connection, same as if mutual TLS weren't // configured at all. First-match-wins on ID order, same convention as // match-and-replace rules' Position ordering, minus the extra field: // client certs are keyed by host, not layered edits, so insertion order // is a reasonable enough tiebreaker without adding one. func FindFor(certs []Cert, host string) *Cert { for i := range certs { if certs[i].Enabled && certs[i].matches(host) { return &certs[i] } } return nil } // TLSCertificate parses c's PEM-encoded cert/key pair into the form // crypto/tls needs to present it during a handshake. func (c Cert) TLSCertificate() (tls.Certificate, error) { cert, err := tls.X509KeyPair(c.CertPEM, c.KeyPEM) if err != nil { return tls.Certificate{}, fmt.Errorf("parse client certificate %q: %w", c.Name, err) } return cert, nil }