add config option to disable tls client auth during tls handshakes

to work around clients, like the gmail smtp client, that tries to
authenticate with a webpki-issued certificate (which we don't know).

i tried specifying a list of accepted (subjects of) CA certs during the
tls handshake (with just 1 entry, with "xmox.nl" as common name), which
clients can use to influence their cert selection.  however, the gmail
smtp client ignores it, so not a solution for the issue where this was
raised. also, specifying a list of accepted certs could cause other
clients to not send their client cert anymore, breaking existing setups.

i also considered only asking for tls client auth when at least one
account has a tls pubkey configured. but decided against it since any
account can add one on their own (without system admin interaction),
changing behaviour of the system and potentially breaking existing
submission/tls configs.

we now also print the "subject" and "issuer" of certs when tls client
auth fails, should be useful for future debugging.

for issue #359
This commit is contained in:
Mechiel Lukkien
2025-06-09 12:26:58 +02:00
parent f5b8c64b84
commit 833a67fe3d
9 changed files with 55 additions and 35 deletions

View File

@ -362,7 +362,7 @@ func TestAuthenticateTLSClientCert(t *testing.T) {
cid := connCounter
go func() {
defer serverConn.Close()
serve("test", cid, &serverConfig, serverConn, true, false, false, "")
serve("test", cid, &serverConfig, serverConn, true, false, false, false, "")
close(done)
}()

View File

@ -144,7 +144,7 @@ func FuzzServer(f *testing.F) {
err = serverConn.SetDeadline(time.Now().Add(time.Second))
flog(err, "set server deadline")
serve("test", cid, nil, serverConn, false, true, false, "")
serve("test", cid, nil, serverConn, false, false, true, false, "")
cid++
}

View File

@ -192,9 +192,10 @@ type conn struct {
cid int64
state state
conn net.Conn
connBroken bool // Once broken, we won't flush any more data.
tls bool // Whether TLS has been initialized.
viaHTTPS bool // Whether this connection came in via HTTPS (using TLS ALPN).
connBroken bool // Once broken, we won't flush any more data.
tls bool // Whether TLS has been initialized.
viaHTTPS bool // Whether this connection came in via HTTPS (using TLS ALPN).
noTLSClientAuth bool
br *bufio.Reader // From remote, with TLS unwrapped in case of TLS, and possibly wrapping inflate.
tr *moxio.TraceReader // Kept to change trace level when reading/writing cmd/auth/data.
line chan lineErr // If set, instead of reading from br, a line is read from this channel. For reading a line in IDLE while also waiting for mailbox/account updates.
@ -384,21 +385,23 @@ func Listen() {
listener := mox.Conf.Static.Listeners[name]
var tlsConfig *tls.Config
var noTLSClientAuth bool
if listener.TLS != nil {
tlsConfig = listener.TLS.Config
noTLSClientAuth = listener.TLS.ClientAuthDisabled
}
if listener.IMAP.Enabled {
port := config.Port(listener.IMAP.Port, 143)
for _, ip := range listener.IPs {
listen1("imap", name, ip, port, tlsConfig, false, listener.IMAP.NoRequireSTARTTLS)
listen1("imap", name, ip, port, tlsConfig, false, noTLSClientAuth, listener.IMAP.NoRequireSTARTTLS)
}
}
if listener.IMAPS.Enabled {
port := config.Port(listener.IMAPS.Port, 993)
for _, ip := range listener.IPs {
listen1("imaps", name, ip, port, tlsConfig, true, false)
listen1("imaps", name, ip, port, tlsConfig, true, noTLSClientAuth, false)
}
}
}
@ -406,7 +409,7 @@ func Listen() {
var servers []func()
func listen1(protocol, listenerName, ip string, port int, tlsConfig *tls.Config, xtls, noRequireSTARTTLS bool) {
func listen1(protocol, listenerName, ip string, port int, tlsConfig *tls.Config, xtls, noTLSClientAuth, noRequireSTARTTLS bool) {
log := mlog.New("imapserver", nil)
addr := net.JoinHostPort(ip, fmt.Sprintf("%d", port))
if os.Getuid() == 0 {
@ -439,7 +442,7 @@ func listen1(protocol, listenerName, ip string, port int, tlsConfig *tls.Config,
}
metricIMAPConnection.WithLabelValues(protocol).Inc()
go serve(listenerName, mox.Cid(), tlsConfig, conn, xtls, noRequireSTARTTLS, false, "")
go serve(listenerName, mox.Cid(), tlsConfig, conn, xtls, noTLSClientAuth, noRequireSTARTTLS, false, "")
}
}
@ -448,11 +451,11 @@ func listen1(protocol, listenerName, ip string, port int, tlsConfig *tls.Config,
// ServeTLSConn serves IMAP on a TLS connection.
func ServeTLSConn(listenerName string, conn *tls.Conn, tlsConfig *tls.Config) {
serve(listenerName, mox.Cid(), tlsConfig, conn, true, false, true, "")
serve(listenerName, mox.Cid(), tlsConfig, conn, true, true, false, true, "")
}
func ServeConnPreauth(listenerName string, cid int64, conn net.Conn, preauthAddress string) {
serve(listenerName, cid, nil, conn, false, true, false, preauthAddress)
serve(listenerName, cid, nil, conn, false, true, true, false, preauthAddress)
}
// Serve starts serving on all listeners, launching a goroutine per listener.
@ -766,7 +769,7 @@ var cleanClose struct{} // Sentinel value for panic/recover indicating clean clo
// preauthenticated.
//
// The connection is closed before returning.
func serve(listenerName string, cid int64, tlsConfig *tls.Config, nc net.Conn, xtls, noRequireSTARTTLS, viaHTTPS bool, preauthAddress string) {
func serve(listenerName string, cid int64, tlsConfig *tls.Config, nc net.Conn, xtls, noTLSClientAuth, noRequireSTARTTLS, viaHTTPS bool, preauthAddress string) {
var remoteIP net.IP
if a, ok := nc.RemoteAddr().(*net.TCPAddr); ok {
remoteIP = a.IP
@ -780,6 +783,7 @@ func serve(listenerName string, cid int64, tlsConfig *tls.Config, nc net.Conn, x
conn: nc,
tls: xtls,
viaHTTPS: viaHTTPS,
noTLSClientAuth: noTLSClientAuth,
lastlog: time.Now(),
baseTLSConfig: tlsConfig,
remoteIP: remoteIP,
@ -990,6 +994,10 @@ func (c *conn) makeTLSConfig() *tls.Config {
// config, so they can be used for this connection too.
tlsConf := c.baseTLSConfig.Clone()
if c.noTLSClientAuth {
return tlsConf
}
// Allow client certificate authentication, for use with the sasl "external"
// authentication mechanism.
tlsConf.ClientAuth = tls.RequestClientCert
@ -1089,7 +1097,7 @@ func (c *conn) tlsClientAuthVerifyPeerCertParsed(cert *x509.Certificate) error {
if err == bstore.ErrAbsent {
c.loginAttempt.Result = store.AuthBadCredentials
}
return fmt.Errorf("looking up tls public key with fingerprint %s: %v", fp, err)
return fmt.Errorf("looking up tls public key with fingerprint %s, subject %q, issuer %q: %v", fp, cert.Subject, cert.Issuer, err)
}
c.loginAttempt.LoginAddress = pubKey.LoginAddress
@ -1152,7 +1160,7 @@ func (c *conn) xtlsHandshakeAndAuthenticate(conn net.Conn) {
cancel()
cs := tlsConn.ConnectionState()
if cs.DidResume && len(cs.PeerCertificates) > 0 {
if cs.DidResume && len(cs.PeerCertificates) > 0 && !c.noTLSClientAuth {
// Verify client after session resumption.
err := c.tlsClientAuthVerifyPeerCertParsed(cs.PeerCertificates[0])
if err != nil {
@ -1167,6 +1175,7 @@ func (c *conn) xtlsHandshakeAndAuthenticate(conn net.Conn) {
slog.String("ciphersuite", ciphersuite),
slog.String("sni", cs.ServerName),
slog.Bool("resumed", cs.DidResume),
slog.Bool("notlsclientauth", c.noTLSClientAuth),
slog.Int("clientcerts", len(cs.PeerCertificates)),
}
if c.account != nil {
@ -2390,7 +2399,7 @@ func (c *conn) capabilities() string {
} else {
caps += " LOGINDISABLED"
}
if c.tls && len(c.conn.(*tls.Conn).ConnectionState().PeerCertificates) > 0 && !c.viaHTTPS {
if c.tls && len(c.conn.(*tls.Conn).ConnectionState().PeerCertificates) > 0 && !c.viaHTTPS && !c.noTLSClientAuth {
caps += " AUTH=EXTERNAL"
}
return caps

View File

@ -556,7 +556,7 @@ func startArgsMore(t *testing.T, uidonly, first, immediateTLS bool, serverConfig
cid := connCounter - 1
go func() {
const viaHTTPS = false
serve("test", cid, serverConfig, serverConn, immediateTLS, allowLoginWithoutTLS, viaHTTPS, "")
serve("test", cid, serverConfig, serverConn, immediateTLS, false, allowLoginWithoutTLS, viaHTTPS, "")
close(done)
}()
var tc *testconn