mirror of
https://github.com/mjl-/mox.git
synced 2025-07-14 09:34:36 +03:00
implement tls client certificate authentication
the imap & smtp servers now allow logging in with tls client authentication and the "external" sasl authentication mechanism. email clients like thunderbird, fairemail, k9, macos mail implement it. this seems to be the most secure among the authentication mechanism commonly implemented by clients. a useful property is that an account can have a separate tls public key for each device/email client. with tls client cert auth, authentication is also bound to the tls connection. a mitm cannot pass the credentials on to another tls connection, similar to scram-*-plus. though part of scram-*-plus is that clients verify that the server knows the client credentials. for tls client auth with imap, we send a "preauth" untagged message by default. that puts the connection in authenticated state. given the imap connection state machine, further authentication commands are not allowed. some clients don't recognize the preauth message, and try to authenticate anyway, which fails. a tls public key has a config option to disable preauth, keeping new connections in unauthenticated state, to work with such email clients. for smtp (submission), we don't require an explicit auth command. both for imap and smtp, we allow a client to authenticate with another mechanism than "external". in that case, credentials are verified, and have to be for the same account as the tls client auth, but the adress can be another one than the login address configured with the tls public key. only the public key is used to identify the account that is authenticating. we ignore the rest of the certificate. expiration dates, names, constraints, etc are not verified. no certificate authorities are involved. users can upload their own (minimal) certificate. the account web interface shows openssl commands you can run to generate a private key, minimal cert, and a p12 file (the format that email clients seem to like...) containing both private key and certificate. the imapclient & smtpclient packages can now also use tls client auth. and so does "mox sendmail", either with a pem file with private key and certificate, or with just an ed25519 private key. there are new subcommands "mox config tlspubkey ..." for adding/removing/listing tls public keys from the cli, by the admin.
This commit is contained in:
@ -12,6 +12,7 @@ import (
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
@ -22,7 +23,6 @@ import (
|
||||
"net"
|
||||
"net/textproto"
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
"slices"
|
||||
"sort"
|
||||
@ -272,8 +272,14 @@ func listen1(protocol, name, ip string, port int, hostname dns.Domain, tlsConfig
|
||||
if err != nil {
|
||||
log.Fatalx("smtp: listen for smtp", err, slog.String("protocol", protocol), slog.String("listener", name))
|
||||
}
|
||||
if xtls {
|
||||
ln = tls.NewListener(ln, tlsConfig)
|
||||
|
||||
// Each listener gets its own copy of the config, so session keys between different
|
||||
// ports on same listener aren't shared. We rotate session keys explicitly in this
|
||||
// base TLS config because each connection clones the TLS config before using. The
|
||||
// base TLS config would never get automatically managed/rotated session keys.
|
||||
if tlsConfig != nil {
|
||||
tlsConfig = tlsConfig.Clone()
|
||||
mox.StartTLSSessionTicketKeyRefresher(mox.Shutdown, log, tlsConfig)
|
||||
}
|
||||
|
||||
serve := func() {
|
||||
@ -320,7 +326,7 @@ type conn struct {
|
||||
slow bool // If set, reads are done with a 1 second sleep, and writes are done 1 byte at a time, to keep spammers busy.
|
||||
lastlog time.Time // Used for printing the delta time since the previous logging for this connection.
|
||||
submission bool // ../rfc/6409:19 applies
|
||||
tlsConfig *tls.Config
|
||||
baseTLSConfig *tls.Config
|
||||
localIP net.IP
|
||||
remoteIP net.IP
|
||||
hostname dns.Domain
|
||||
@ -342,6 +348,8 @@ type conn struct {
|
||||
ehlo bool // If set, we had EHLO instead of HELO.
|
||||
|
||||
authFailed int // Number of failed auth attempts. For slowing down remote with many failures.
|
||||
authSASL bool // Whether SASL authentication was done.
|
||||
authTLS bool // Whether we did TLS client cert authentication.
|
||||
username string // Only when authenticated.
|
||||
account *store.Account // Only when authenticated.
|
||||
|
||||
@ -385,17 +393,208 @@ func isClosed(err error) bool {
|
||||
return errors.Is(err, errIO) || moxio.IsClosed(err)
|
||||
}
|
||||
|
||||
// makeTLSConfig makes a new tls config that is bound to the connection for
|
||||
// possible client certificate authentication in case of submission.
|
||||
func (c *conn) makeTLSConfig() *tls.Config {
|
||||
if !c.submission {
|
||||
return c.baseTLSConfig
|
||||
}
|
||||
|
||||
// We clone the config so we can set VerifyPeerCertificate below to a method bound
|
||||
// to this connection. Earlier, we set session keys explicitly on the base TLS
|
||||
// config, so they can be used for this connection too.
|
||||
tlsConf := c.baseTLSConfig.Clone()
|
||||
|
||||
// Allow client certificate authentication, for use with the sasl "external"
|
||||
// authentication mechanism.
|
||||
tlsConf.ClientAuth = tls.RequestClientCert
|
||||
|
||||
// We verify the client certificate during the handshake. The TLS handshake is
|
||||
// initiated explicitly for incoming connections and during starttls, so we can
|
||||
// immediately extract the account name and address used for authentication.
|
||||
tlsConf.VerifyPeerCertificate = c.tlsClientAuthVerifyPeerCert
|
||||
|
||||
return tlsConf
|
||||
}
|
||||
|
||||
// tlsClientAuthVerifyPeerCert can be used as tls.Config.VerifyPeerCertificate, and
|
||||
// sets authentication-related fields on conn. This is not called on resumed TLS
|
||||
// connections.
|
||||
func (c *conn) tlsClientAuthVerifyPeerCert(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
|
||||
if len(rawCerts) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If we had too many authentication failures from this IP, don't attempt
|
||||
// authentication. If this is a new incoming connetion, it is closed after the TLS
|
||||
// handshake.
|
||||
if !mox.LimiterFailedAuth.CanAdd(c.remoteIP, time.Now(), 1) {
|
||||
return nil
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(rawCerts[0])
|
||||
if err != nil {
|
||||
c.log.Debugx("parsing tls client certificate", err)
|
||||
return err
|
||||
}
|
||||
if err := c.tlsClientAuthVerifyPeerCertParsed(cert); err != nil {
|
||||
c.log.Debugx("verifying tls client certificate", err)
|
||||
return fmt.Errorf("verifying client certificate: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// tlsClientAuthVerifyPeerCertParsed verifies a client certificate. Called both for
|
||||
// fresh and resumed TLS connections.
|
||||
func (c *conn) tlsClientAuthVerifyPeerCertParsed(cert *x509.Certificate) error {
|
||||
if c.account != nil {
|
||||
return fmt.Errorf("cannot authenticate with tls client certificate after previous authentication")
|
||||
}
|
||||
|
||||
authResult := "error"
|
||||
defer func() {
|
||||
metrics.AuthenticationInc("submission", "tlsclientauth", authResult)
|
||||
if authResult == "ok" {
|
||||
mox.LimiterFailedAuth.Reset(c.remoteIP, time.Now())
|
||||
} else {
|
||||
mox.LimiterFailedAuth.Add(c.remoteIP, time.Now(), 1)
|
||||
}
|
||||
}()
|
||||
|
||||
// For many failed auth attempts, slow down verification attempts.
|
||||
if c.authFailed > 3 && authFailDelay > 0 {
|
||||
mox.Sleep(mox.Context, time.Duration(c.authFailed-3)*authFailDelay)
|
||||
}
|
||||
c.authFailed++ // Compensated on success.
|
||||
defer func() {
|
||||
// On the 3rd failed authentication, start responding slowly. Successful auth will
|
||||
// cause fast responses again.
|
||||
if c.authFailed >= 3 {
|
||||
c.setSlow(true)
|
||||
}
|
||||
}()
|
||||
|
||||
shabuf := sha256.Sum256(cert.RawSubjectPublicKeyInfo)
|
||||
fp := base64.RawURLEncoding.EncodeToString(shabuf[:])
|
||||
pubKey, err := store.TLSPublicKeyGet(context.TODO(), fp)
|
||||
if err != nil {
|
||||
if err == bstore.ErrAbsent {
|
||||
authResult = "badcreds"
|
||||
}
|
||||
return fmt.Errorf("looking up tls public key with fingerprint %s: %v", fp, err)
|
||||
}
|
||||
|
||||
// Verify account exists and still matches address.
|
||||
acc, _, err := store.OpenEmail(c.log, pubKey.LoginAddress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening account for address %s for public key %s: %w", pubKey.LoginAddress, fp, err)
|
||||
}
|
||||
defer func() {
|
||||
if acc != nil {
|
||||
err := acc.Close()
|
||||
c.log.Check(err, "close account")
|
||||
}
|
||||
}()
|
||||
if acc.Name != pubKey.Account {
|
||||
return fmt.Errorf("tls client public key %s is for account %s, but email address %s is for account %s", fp, pubKey.Account, pubKey.LoginAddress, acc.Name)
|
||||
}
|
||||
|
||||
authResult = "ok"
|
||||
c.authFailed = 0
|
||||
c.account = acc
|
||||
acc = nil // Prevent cleanup by defer.
|
||||
c.username = pubKey.LoginAddress
|
||||
c.authTLS = true
|
||||
c.log.Debug("tls client authenticated with client certificate",
|
||||
slog.String("fingerprint", fp),
|
||||
slog.String("username", c.username),
|
||||
slog.String("account", c.account.Name),
|
||||
slog.Any("remote", c.remoteIP))
|
||||
return nil
|
||||
}
|
||||
|
||||
// xtlsHandshakeAndAuthenticate performs the TLS handshake, and verifies a client
|
||||
// certificate if present.
|
||||
func (c *conn) xtlsHandshakeAndAuthenticate(conn net.Conn) {
|
||||
tlsConn := tls.Server(conn, c.makeTLSConfig())
|
||||
c.conn = tlsConn
|
||||
|
||||
cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
|
||||
ctx, cancel := context.WithTimeout(cidctx, time.Minute)
|
||||
defer cancel()
|
||||
c.log.Debug("starting tls server handshake")
|
||||
if !c.submission {
|
||||
metricDeliveryStarttls.Inc()
|
||||
}
|
||||
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||
if !c.submission {
|
||||
// Errors from crypto/tls mostly aren't typed. We'll have to look for strings...
|
||||
reason := "other"
|
||||
if errors.Is(err, io.EOF) {
|
||||
reason = "eof"
|
||||
} else if alert, ok := mox.AsTLSAlert(err); ok {
|
||||
reason = tlsrpt.FormatAlert(alert)
|
||||
} else {
|
||||
s := err.Error()
|
||||
if strings.Contains(s, "tls: client offered only unsupported versions") {
|
||||
reason = "unsupportedversions"
|
||||
} else if strings.Contains(s, "tls: first record does not look like a TLS handshake") {
|
||||
reason = "nottls"
|
||||
} else if strings.Contains(s, "tls: unsupported SSLv2 handshake received") {
|
||||
reason = "sslv2"
|
||||
}
|
||||
}
|
||||
metricDeliveryStarttlsErrors.WithLabelValues(reason).Inc()
|
||||
}
|
||||
panic(fmt.Errorf("tls handshake: %s (%w)", err, errIO))
|
||||
}
|
||||
cancel()
|
||||
|
||||
cs := tlsConn.ConnectionState()
|
||||
if cs.DidResume && len(cs.PeerCertificates) > 0 {
|
||||
// Verify client after session resumption.
|
||||
err := c.tlsClientAuthVerifyPeerCertParsed(cs.PeerCertificates[0])
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("tls verify client certificate after resumption: %s (%w)", err, errIO))
|
||||
}
|
||||
}
|
||||
|
||||
attrs := []slog.Attr{
|
||||
slog.Any("version", tlsVersion(cs.Version)),
|
||||
slog.String("ciphersuite", tls.CipherSuiteName(cs.CipherSuite)),
|
||||
slog.String("sni", cs.ServerName),
|
||||
slog.Bool("resumed", cs.DidResume),
|
||||
slog.Int("clientcerts", len(cs.PeerCertificates)),
|
||||
}
|
||||
if c.account != nil {
|
||||
attrs = append(attrs,
|
||||
slog.String("account", c.account.Name),
|
||||
slog.String("username", c.username),
|
||||
)
|
||||
}
|
||||
c.log.Debug("tls handshake completed", attrs...)
|
||||
}
|
||||
|
||||
type tlsVersion uint16
|
||||
|
||||
func (v tlsVersion) String() string {
|
||||
return strings.ReplaceAll(strings.ToLower(tls.VersionName(uint16(v))), " ", "-")
|
||||
}
|
||||
|
||||
// completely reset connection state as if greeting has just been sent.
|
||||
// ../rfc/3207:210
|
||||
func (c *conn) reset() {
|
||||
c.ehlo = false
|
||||
c.hello = dns.IPDomain{}
|
||||
c.username = ""
|
||||
if c.account != nil {
|
||||
err := c.account.Close()
|
||||
c.log.Check(err, "closing account")
|
||||
if !c.authTLS {
|
||||
c.username = ""
|
||||
if c.account != nil {
|
||||
err := c.account.Close()
|
||||
c.log.Check(err, "closing account")
|
||||
}
|
||||
c.account = nil
|
||||
}
|
||||
c.account = nil
|
||||
c.authSASL = false
|
||||
c.rset()
|
||||
}
|
||||
|
||||
@ -593,7 +792,7 @@ func (c *conn) writelinef(format string, args ...any) {
|
||||
|
||||
var cleanClose struct{} // Sentinel value for panic/recover indicating clean close of connection.
|
||||
|
||||
func serve(listenerName string, cid int64, hostname dns.Domain, tlsConfig *tls.Config, nc net.Conn, resolver dns.Resolver, submission, tls bool, maxMessageSize int64, requireTLSForAuth, requireTLSForDelivery, requireTLS bool, dnsBLs []dns.Domain, firstTimeSenderDelay time.Duration) {
|
||||
func serve(listenerName string, cid int64, hostname dns.Domain, tlsConfig *tls.Config, nc net.Conn, resolver dns.Resolver, submission, xtls bool, maxMessageSize int64, requireTLSForAuth, requireTLSForDelivery, requireTLS bool, dnsBLs []dns.Domain, firstTimeSenderDelay time.Duration) {
|
||||
var localIP, remoteIP net.IP
|
||||
if a, ok := nc.LocalAddr().(*net.TCPAddr); ok {
|
||||
localIP = a.IP
|
||||
@ -613,11 +812,11 @@ func serve(listenerName string, cid int64, hostname dns.Domain, tlsConfig *tls.C
|
||||
origConn: nc,
|
||||
conn: nc,
|
||||
submission: submission,
|
||||
tls: tls,
|
||||
tls: xtls,
|
||||
extRequireTLS: requireTLS,
|
||||
resolver: resolver,
|
||||
lastlog: time.Now(),
|
||||
tlsConfig: tlsConfig,
|
||||
baseTLSConfig: tlsConfig,
|
||||
localIP: localIP,
|
||||
remoteIP: remoteIP,
|
||||
hostname: hostname,
|
||||
@ -643,8 +842,8 @@ func serve(listenerName string, cid int64, hostname dns.Domain, tlsConfig *tls.C
|
||||
return l
|
||||
})
|
||||
c.tr = moxio.NewTraceReader(c.log, "RC: ", c)
|
||||
c.tw = moxio.NewTraceWriter(c.log, "LS: ", c)
|
||||
c.r = bufio.NewReader(c.tr)
|
||||
c.tw = moxio.NewTraceWriter(c.log, "LS: ", c)
|
||||
c.w = bufio.NewWriter(c.tw)
|
||||
|
||||
metricConnection.WithLabelValues(c.kind()).Inc()
|
||||
@ -652,7 +851,7 @@ func serve(listenerName string, cid int64, hostname dns.Domain, tlsConfig *tls.C
|
||||
slog.Any("remote", c.conn.RemoteAddr()),
|
||||
slog.Any("local", c.conn.LocalAddr()),
|
||||
slog.Bool("submission", submission),
|
||||
slog.Bool("tls", tls),
|
||||
slog.Bool("tls", xtls),
|
||||
slog.String("listener", listenerName))
|
||||
|
||||
defer func() {
|
||||
@ -677,6 +876,12 @@ func serve(listenerName string, cid int64, hostname dns.Domain, tlsConfig *tls.C
|
||||
}
|
||||
}()
|
||||
|
||||
if xtls {
|
||||
// Start TLS on connection. We perform the handshake explicitly, so we can set a
|
||||
// timeout, do client certificate authentication, log TLS details afterwards.
|
||||
c.xtlsHandshakeAndAuthenticate(c.conn)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-mox.Shutdown.Done():
|
||||
// ../rfc/5321:2811 ../rfc/5321:1666 ../rfc/3463:420
|
||||
@ -905,7 +1110,7 @@ func (c *conn) cmdHello(p *parser, ehlo bool) {
|
||||
c.bwritelinef("250-PIPELINING") // ../rfc/2920:108
|
||||
c.bwritelinef("250-SIZE %d", c.maxMessageSize) // ../rfc/1870:70
|
||||
// ../rfc/3207:237
|
||||
if !c.tls && c.tlsConfig != nil {
|
||||
if !c.tls && c.baseTLSConfig != nil {
|
||||
// ../rfc/3207:90
|
||||
c.bwritelinef("250-STARTTLS")
|
||||
} else if c.extRequireTLS {
|
||||
@ -914,6 +1119,7 @@ func (c *conn) cmdHello(p *parser, ehlo bool) {
|
||||
c.bwritelinef("250-REQUIRETLS")
|
||||
}
|
||||
if c.submission {
|
||||
var mechs string
|
||||
// ../rfc/4954:123
|
||||
if c.tls || !c.requireTLSForAuth {
|
||||
// We always mention the SCRAM PLUS variants, even if TLS is not active: It is a
|
||||
@ -921,10 +1127,12 @@ func (c *conn) cmdHello(p *parser, ehlo bool) {
|
||||
// authentication. The client should select the bare variant when TLS isn't
|
||||
// present, and also not indicate the server supports the PLUS variant in that
|
||||
// case, or it would trigger the mechanism downgrade detection.
|
||||
c.bwritelinef("250-AUTH SCRAM-SHA-256-PLUS SCRAM-SHA-256 SCRAM-SHA-1-PLUS SCRAM-SHA-1 CRAM-MD5 PLAIN LOGIN")
|
||||
} else {
|
||||
c.bwritelinef("250-AUTH ")
|
||||
mechs = "SCRAM-SHA-256-PLUS SCRAM-SHA-256 SCRAM-SHA-1-PLUS SCRAM-SHA-1 CRAM-MD5 PLAIN LOGIN"
|
||||
}
|
||||
if c.tls && len(c.conn.(*tls.Conn).ConnectionState().PeerCertificates) > 0 {
|
||||
mechs = "EXTERNAL " + mechs
|
||||
}
|
||||
c.bwritelinef("250-AUTH %s", mechs)
|
||||
// ../rfc/4865:127
|
||||
t := time.Now().Add(queue.FutureReleaseIntervalMax).UTC() // ../rfc/4865:98
|
||||
c.bwritelinef("250-FUTURERELEASE %d %s", queue.FutureReleaseIntervalMax/time.Second, t.Format(time.RFC3339))
|
||||
@ -949,7 +1157,7 @@ func (c *conn) cmdStarttls(p *parser) {
|
||||
if c.account != nil {
|
||||
xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "cannot starttls after authentication")
|
||||
}
|
||||
if c.tlsConfig == nil {
|
||||
if c.baseTLSConfig == nil {
|
||||
xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "starttls not offered")
|
||||
}
|
||||
|
||||
@ -967,61 +1175,13 @@ func (c *conn) cmdStarttls(p *parser) {
|
||||
|
||||
// We add the cid to the output, to help debugging in case of a failing TLS connection.
|
||||
c.writecodeline(smtp.C220ServiceReady, smtp.SeOther00, "go! ("+mox.ReceivedID(c.cid)+")", nil)
|
||||
tlsConn := tls.Server(conn, c.tlsConfig)
|
||||
cidctx := context.WithValue(mox.Context, mlog.CidKey, c.cid)
|
||||
ctx, cancel := context.WithTimeout(cidctx, time.Minute)
|
||||
defer cancel()
|
||||
c.log.Debug("starting tls server handshake")
|
||||
metricDeliveryStarttls.Inc()
|
||||
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||
// Errors from crypto/tls mostly aren't typed. We'll have to look for strings...
|
||||
reason := "other"
|
||||
if errors.Is(err, io.EOF) {
|
||||
reason = "eof"
|
||||
} else if alert, ok := asTLSAlert(err); ok {
|
||||
reason = tlsrpt.FormatAlert(alert)
|
||||
} else {
|
||||
s := err.Error()
|
||||
if strings.Contains(s, "tls: client offered only unsupported versions") {
|
||||
reason = "unsupportedversions"
|
||||
} else if strings.Contains(s, "tls: first record does not look like a TLS handshake") {
|
||||
reason = "nottls"
|
||||
} else if strings.Contains(s, "tls: unsupported SSLv2 handshake received") {
|
||||
reason = "sslv2"
|
||||
}
|
||||
}
|
||||
metricDeliveryStarttlsErrors.WithLabelValues(reason).Inc()
|
||||
panic(fmt.Errorf("starttls handshake: %s (%w)", err, errIO))
|
||||
}
|
||||
cancel()
|
||||
tlsversion, ciphersuite := moxio.TLSInfo(tlsConn)
|
||||
c.log.Debug("tls server handshake done", slog.String("tls", tlsversion), slog.String("ciphersuite", ciphersuite))
|
||||
c.conn = tlsConn
|
||||
c.tr = moxio.NewTraceReader(c.log, "RC: ", c)
|
||||
c.tw = moxio.NewTraceWriter(c.log, "LS: ", c)
|
||||
c.r = bufio.NewReader(c.tr)
|
||||
c.w = bufio.NewWriter(c.tw)
|
||||
|
||||
c.xtlsHandshakeAndAuthenticate(conn)
|
||||
|
||||
c.reset() // ../rfc/3207:210
|
||||
c.tls = true
|
||||
}
|
||||
|
||||
func asTLSAlert(err error) (alert uint8, ok bool) {
|
||||
// If the remote client aborts the connection, it can send an alert indicating why.
|
||||
// crypto/tls gives us a net.OpError with "Op" set to "remote error", an an Err
|
||||
// with the unexported type "alert", a uint8. So we try to read it.
|
||||
|
||||
var opErr *net.OpError
|
||||
if !errors.As(err, &opErr) || opErr.Op != "remote error" || opErr.Err == nil {
|
||||
return
|
||||
}
|
||||
v := reflect.ValueOf(opErr.Err)
|
||||
if v.Kind() != reflect.Uint8 || v.Type().Name() != "alert" {
|
||||
return
|
||||
}
|
||||
return uint8(v.Uint()), true
|
||||
}
|
||||
|
||||
// ../rfc/4954:139
|
||||
func (c *conn) cmdAuth(p *parser) {
|
||||
c.xneedHello()
|
||||
@ -1029,7 +1189,7 @@ func (c *conn) cmdAuth(p *parser) {
|
||||
if !c.submission {
|
||||
xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "authentication only allowed on submission ports")
|
||||
}
|
||||
if c.account != nil {
|
||||
if c.authSASL {
|
||||
// ../rfc/4954:152
|
||||
xsmtpUserErrorf(smtp.C503BadCmdSeq, smtp.SeProto5BadCmdOrSeq1, "already authenticated")
|
||||
}
|
||||
@ -1062,7 +1222,7 @@ func (c *conn) cmdAuth(p *parser) {
|
||||
}
|
||||
}()
|
||||
|
||||
var authVariant string
|
||||
var authVariant string // Only known strings, used in metrics.
|
||||
authResult := "error"
|
||||
defer func() {
|
||||
metrics.AuthenticationInc("submission", authVariant, authResult)
|
||||
@ -1129,6 +1289,18 @@ func (c *conn) cmdAuth(p *parser) {
|
||||
return buf
|
||||
}
|
||||
|
||||
// The various authentication mechanisms set account and username. We may already
|
||||
// have an account and username from TLS client authentication. Afterwards, we
|
||||
// check that the account is the same.
|
||||
var account *store.Account
|
||||
var username string
|
||||
defer func() {
|
||||
if account != nil {
|
||||
err := account.Close()
|
||||
c.log.Check(err, "close account")
|
||||
}
|
||||
}()
|
||||
|
||||
switch mech {
|
||||
case "PLAIN":
|
||||
authVariant = "plain"
|
||||
@ -1148,31 +1320,24 @@ func (c *conn) cmdAuth(p *parser) {
|
||||
xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "auth data should have 3 nul-separated tokens, got %d", len(plain))
|
||||
}
|
||||
authz := norm.NFC.String(string(plain[0]))
|
||||
authc := norm.NFC.String(string(plain[1]))
|
||||
username = norm.NFC.String(string(plain[1]))
|
||||
password := string(plain[2])
|
||||
|
||||
if authz != "" && authz != authc {
|
||||
if authz != "" && authz != username {
|
||||
authResult = "badcreds"
|
||||
xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "cannot assume other role")
|
||||
}
|
||||
|
||||
acc, err := store.OpenEmailAuth(c.log, authc, password)
|
||||
var err error
|
||||
account, err = store.OpenEmailAuth(c.log, username, password)
|
||||
if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
|
||||
// ../rfc/4954:274
|
||||
authResult = "badcreds"
|
||||
c.log.Info("failed authentication attempt", slog.String("username", authc), slog.Any("remote", c.remoteIP))
|
||||
c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
|
||||
xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
|
||||
}
|
||||
xcheckf(err, "verifying credentials")
|
||||
|
||||
authResult = "ok"
|
||||
c.authFailed = 0
|
||||
c.setSlow(false)
|
||||
c.account = acc
|
||||
c.username = authc
|
||||
// ../rfc/4954:276
|
||||
c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
|
||||
|
||||
case "LOGIN":
|
||||
// LOGIN is obsoleted in favor of PLAIN, only implemented to support legacy
|
||||
// clients, see Internet-Draft (I-D):
|
||||
@ -1193,7 +1358,7 @@ func (c *conn) cmdAuth(p *parser) {
|
||||
// I-D says maximum length must be 64 bytes. We allow more, for long user names
|
||||
// (domains).
|
||||
encChal := base64.StdEncoding.EncodeToString([]byte("Username:"))
|
||||
username := string(xreadInitial(encChal))
|
||||
username = string(xreadInitial(encChal))
|
||||
username = norm.NFC.String(username)
|
||||
|
||||
// Again, client should ignore the challenge, we send the same as the example in
|
||||
@ -1205,7 +1370,8 @@ func (c *conn) cmdAuth(p *parser) {
|
||||
password := string(xreadContinuation())
|
||||
c.xtrace(mlog.LevelTrace) // Restore.
|
||||
|
||||
acc, err := store.OpenEmailAuth(c.log, username, password)
|
||||
var err error
|
||||
account, err = store.OpenEmailAuth(c.log, username, password)
|
||||
if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
|
||||
// ../rfc/4954:274
|
||||
authResult = "badcreds"
|
||||
@ -1214,14 +1380,6 @@ func (c *conn) cmdAuth(p *parser) {
|
||||
}
|
||||
xcheckf(err, "verifying credentials")
|
||||
|
||||
authResult = "ok"
|
||||
c.authFailed = 0
|
||||
c.setSlow(false)
|
||||
c.account = acc
|
||||
c.username = username
|
||||
// ../rfc/4954:276
|
||||
c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "hello ancient smtp implementation", nil)
|
||||
|
||||
case "CRAM-MD5":
|
||||
authVariant = strings.ToLower(mech)
|
||||
|
||||
@ -1236,26 +1394,21 @@ func (c *conn) cmdAuth(p *parser) {
|
||||
if len(t) != 2 || len(t[1]) != 2*md5.Size {
|
||||
xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "malformed cram-md5 response")
|
||||
}
|
||||
addr := norm.NFC.String(t[0])
|
||||
c.log.Debug("cram-md5 auth", slog.String("address", addr))
|
||||
acc, _, err := store.OpenEmail(c.log, addr)
|
||||
username = norm.NFC.String(t[0])
|
||||
c.log.Debug("cram-md5 auth", slog.String("username", username))
|
||||
var err error
|
||||
account, _, err = store.OpenEmail(c.log, username)
|
||||
if err != nil && errors.Is(err, store.ErrUnknownCredentials) {
|
||||
c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
|
||||
c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
|
||||
xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
|
||||
}
|
||||
xcheckf(err, "looking up address")
|
||||
defer func() {
|
||||
if acc != nil {
|
||||
err := acc.Close()
|
||||
c.log.Check(err, "closing account")
|
||||
}
|
||||
}()
|
||||
var ipadhash, opadhash hash.Hash
|
||||
acc.WithRLock(func() {
|
||||
err := acc.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
|
||||
account.WithRLock(func() {
|
||||
err := account.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
|
||||
password, err := bstore.QueryTx[store.Password](tx).Get()
|
||||
if err == bstore.ErrAbsent {
|
||||
c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
|
||||
c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
|
||||
xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
|
||||
}
|
||||
if err != nil {
|
||||
@ -1270,8 +1423,8 @@ func (c *conn) cmdAuth(p *parser) {
|
||||
})
|
||||
if ipadhash == nil || opadhash == nil {
|
||||
missingDerivedSecrets = true
|
||||
c.log.Info("cram-md5 auth attempt without derived secrets set, save password again to store secrets", slog.String("username", addr))
|
||||
c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
|
||||
c.log.Info("cram-md5 auth attempt without derived secrets set, save password again to store secrets", slog.String("username", username))
|
||||
c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
|
||||
xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
|
||||
}
|
||||
|
||||
@ -1280,19 +1433,10 @@ func (c *conn) cmdAuth(p *parser) {
|
||||
opadhash.Write(ipadhash.Sum(nil))
|
||||
digest := fmt.Sprintf("%x", opadhash.Sum(nil))
|
||||
if digest != t[1] {
|
||||
c.log.Info("failed authentication attempt", slog.String("username", addr), slog.Any("remote", c.remoteIP))
|
||||
c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
|
||||
xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
|
||||
}
|
||||
|
||||
authResult = "ok"
|
||||
c.authFailed = 0
|
||||
c.setSlow(false)
|
||||
c.account = acc
|
||||
acc = nil // Cancel cleanup.
|
||||
c.username = addr
|
||||
// ../rfc/4954:276
|
||||
c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
|
||||
|
||||
case "SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1":
|
||||
// todo: improve handling of errors during scram. e.g. invalid parameters. should we abort the imap command, or continue until the end and respond with a scram-level error?
|
||||
// todo: use single implementation between ../imapserver/server.go and ../smtpserver/server.go
|
||||
@ -1326,31 +1470,25 @@ func (c *conn) cmdAuth(p *parser) {
|
||||
c.log.Infox("scram protocol error", err, slog.Any("remote", c.remoteIP))
|
||||
xsmtpUserErrorf(smtp.C455BadParams, smtp.SePol7Other0, "scram protocol error: %s", err)
|
||||
}
|
||||
authc := norm.NFC.String(ss.Authentication)
|
||||
c.log.Debug("scram auth", slog.String("authentication", authc))
|
||||
acc, _, err := store.OpenEmail(c.log, authc)
|
||||
username = norm.NFC.String(ss.Authentication)
|
||||
c.log.Debug("scram auth", slog.String("authentication", username))
|
||||
account, _, err = store.OpenEmail(c.log, username)
|
||||
if err != nil {
|
||||
// todo: we could continue scram with a generated salt, deterministically generated
|
||||
// from the username. that way we don't have to store anything but attackers cannot
|
||||
// learn if an account exists. same for absent scram saltedpassword below.
|
||||
c.log.Info("failed authentication attempt", slog.String("username", authc), slog.Any("remote", c.remoteIP))
|
||||
c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
|
||||
xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible")
|
||||
}
|
||||
defer func() {
|
||||
if acc != nil {
|
||||
err := acc.Close()
|
||||
c.log.Check(err, "closing account")
|
||||
}
|
||||
}()
|
||||
if ss.Authorization != "" && ss.Authorization != ss.Authentication {
|
||||
if ss.Authorization != "" && ss.Authorization != username {
|
||||
xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "authentication with authorization for different user not supported")
|
||||
}
|
||||
var xscram store.SCRAM
|
||||
acc.WithRLock(func() {
|
||||
err := acc.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
|
||||
account.WithRLock(func() {
|
||||
err := account.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
|
||||
password, err := bstore.QueryTx[store.Password](tx).Get()
|
||||
if err == bstore.ErrAbsent {
|
||||
c.log.Info("failed authentication attempt", slog.String("username", authc), slog.Any("remote", c.remoteIP))
|
||||
c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
|
||||
xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass")
|
||||
}
|
||||
xcheckf(err, "fetching credentials")
|
||||
@ -1364,8 +1502,8 @@ func (c *conn) cmdAuth(p *parser) {
|
||||
}
|
||||
if len(xscram.Salt) == 0 || xscram.Iterations == 0 || len(xscram.SaltedPassword) == 0 {
|
||||
missingDerivedSecrets = true
|
||||
c.log.Info("scram auth attempt without derived secrets set, save password again to store secrets", slog.String("address", authc))
|
||||
c.log.Info("failed authentication attempt", slog.String("username", authc), slog.Any("remote", c.remoteIP))
|
||||
c.log.Info("scram auth attempt without derived secrets set, save password again to store secrets", slog.String("address", username))
|
||||
c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
|
||||
xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible")
|
||||
}
|
||||
return nil
|
||||
@ -1384,14 +1522,14 @@ func (c *conn) cmdAuth(p *parser) {
|
||||
c.readline() // Should be "*" for cancellation.
|
||||
if errors.Is(err, scram.ErrInvalidProof) {
|
||||
authResult = "badcreds"
|
||||
c.log.Info("failed authentication attempt", slog.String("username", authc), slog.Any("remote", c.remoteIP))
|
||||
c.log.Info("failed authentication attempt", slog.String("username", username), slog.Any("remote", c.remoteIP))
|
||||
xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad credentials")
|
||||
} else if errors.Is(err, scram.ErrChannelBindingsDontMatch) {
|
||||
authResult = "badchanbind"
|
||||
c.log.Warn("bad channel binding during authentication, potential mitm", slog.String("username", authc), slog.Any("remote", c.remoteIP))
|
||||
c.log.Warn("bad channel binding during authentication, potential mitm", slog.String("username", username), slog.Any("remote", c.remoteIP))
|
||||
xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7MsgIntegrity7, "channel bindings do not match, potential mitm")
|
||||
} else if errors.Is(err, scram.ErrInvalidEncoding) {
|
||||
c.log.Infox("bad scram protocol message", err, slog.String("username", authc), slog.Any("remote", c.remoteIP))
|
||||
c.log.Infox("bad scram protocol message", err, slog.String("username", username), slog.Any("remote", c.remoteIP))
|
||||
xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7Other0, "bad scram protocol message")
|
||||
}
|
||||
xcheckf(err, "server final")
|
||||
@ -1401,19 +1539,65 @@ func (c *conn) cmdAuth(p *parser) {
|
||||
// The message should be empty. todo: should we require it is empty?
|
||||
xreadContinuation()
|
||||
|
||||
authResult = "ok"
|
||||
c.authFailed = 0
|
||||
c.setSlow(false)
|
||||
c.account = acc
|
||||
acc = nil // Cancel cleanup.
|
||||
c.username = authc
|
||||
// ../rfc/4954:276
|
||||
c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
|
||||
case "EXTERNAL":
|
||||
authVariant = strings.ToLower(mech)
|
||||
|
||||
// ../rfc/4422:1618
|
||||
buf := xreadInitial("")
|
||||
username = string(buf)
|
||||
|
||||
if !c.tls {
|
||||
// ../rfc/4954:630
|
||||
xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "tls required for tls client certificate authentication")
|
||||
}
|
||||
if c.account == nil {
|
||||
xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "missing client certificate, required for tls client certificate authentication")
|
||||
}
|
||||
|
||||
if username == "" {
|
||||
username = c.username
|
||||
}
|
||||
var err error
|
||||
account, _, err = store.OpenEmail(c.log, username)
|
||||
xcheckf(err, "looking up username from tls client authentication")
|
||||
|
||||
default:
|
||||
// ../rfc/4954:176
|
||||
xsmtpUserErrorf(smtp.C504ParamNotImpl, smtp.SeProto5BadParams4, "mechanism %s not supported", mech)
|
||||
}
|
||||
|
||||
// We may already have TLS credentials. We allow an additional SASL authentication,
|
||||
// possibly with different username, but the account must be the same.
|
||||
if c.account != nil {
|
||||
if account != c.account {
|
||||
c.log.Debug("sasl authentication for different account than tls client authentication, aborting connection",
|
||||
slog.String("saslmechanism", authVariant),
|
||||
slog.String("saslaccount", account.Name),
|
||||
slog.String("tlsaccount", c.account.Name),
|
||||
slog.String("saslusername", username),
|
||||
slog.String("tlsusername", c.username),
|
||||
)
|
||||
xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "authentication failed, tls client certificate public key belongs to another account")
|
||||
} else if username != c.username {
|
||||
c.log.Debug("sasl authentication for different username than tls client certificate authentication, switching to sasl username",
|
||||
slog.String("saslmechanism", authVariant),
|
||||
slog.String("saslusername", username),
|
||||
slog.String("tlsusername", c.username),
|
||||
slog.String("account", c.account.Name),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
c.account = account
|
||||
account = nil // Prevent cleanup.
|
||||
}
|
||||
c.username = username
|
||||
|
||||
authResult = "ok"
|
||||
c.authSASL = true
|
||||
c.authFailed = 0
|
||||
c.setSlow(false)
|
||||
// ../rfc/4954:276
|
||||
c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
|
||||
}
|
||||
|
||||
// ../rfc/5321:1879 ../rfc/5321:1025
|
||||
|
@ -82,19 +82,23 @@ test email, unique.
|
||||
`, "\n", "\r\n")
|
||||
|
||||
type testserver struct {
|
||||
t *testing.T
|
||||
acc *store.Account
|
||||
switchStop func()
|
||||
comm *store.Comm
|
||||
cid int64
|
||||
resolver dns.Resolver
|
||||
auth func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error)
|
||||
user, pass string
|
||||
submission bool
|
||||
requiretls bool
|
||||
dnsbls []dns.Domain
|
||||
tlsmode smtpclient.TLSMode
|
||||
tlspkix bool
|
||||
t *testing.T
|
||||
acc *store.Account
|
||||
switchStop func()
|
||||
comm *store.Comm
|
||||
cid int64
|
||||
resolver dns.Resolver
|
||||
auth func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error)
|
||||
user, pass string
|
||||
immediateTLS bool
|
||||
serverConfig *tls.Config
|
||||
clientConfig *tls.Config
|
||||
clientCert *tls.Certificate // Passed to smtpclient for starttls authentication.
|
||||
submission bool
|
||||
requiretls bool
|
||||
dnsbls []dns.Domain
|
||||
tlsmode smtpclient.TLSMode
|
||||
tlspkix bool
|
||||
}
|
||||
|
||||
const password0 = "te\u0301st \u00a0\u2002\u200a" // NFD and various unicode spaces.
|
||||
@ -103,9 +107,23 @@ const password1 = "tést " // PRECIS normalized, with NF
|
||||
func newTestServer(t *testing.T, configPath string, resolver dns.Resolver) *testserver {
|
||||
limitersInit() // Reset rate limiters.
|
||||
|
||||
ts := testserver{t: t, cid: 1, resolver: resolver, tlsmode: smtpclient.TLSOpportunistic}
|
||||
|
||||
log := mlog.New("smtpserver", nil)
|
||||
|
||||
ts := testserver{
|
||||
t: t,
|
||||
cid: 1,
|
||||
resolver: resolver,
|
||||
tlsmode: smtpclient.TLSOpportunistic,
|
||||
serverConfig: &tls.Config{
|
||||
Certificates: []tls.Certificate{fakeCert(t, false)},
|
||||
},
|
||||
}
|
||||
|
||||
// Ensure session keys, for tests that check resume and authentication.
|
||||
ctx, cancel := context.WithCancel(ctxbg)
|
||||
defer cancel()
|
||||
mox.StartTLSSessionTicketKeyRefresher(ctx, log, ts.serverConfig)
|
||||
|
||||
mox.Context = ctxbg
|
||||
mox.ConfigStaticPath = configPath
|
||||
mox.MustLoadConfig(true, false)
|
||||
@ -116,6 +134,8 @@ func newTestServer(t *testing.T, configPath string, resolver dns.Resolver) *test
|
||||
tcheck(t, err, "dmarcdb init")
|
||||
err = tlsrptdb.Init()
|
||||
tcheck(t, err, "tlsrptdb init")
|
||||
err = store.Init(ctxbg)
|
||||
tcheck(t, err, "store init")
|
||||
|
||||
ts.acc, err = store.OpenAccount(log, "mjl")
|
||||
tcheck(t, err, "open account")
|
||||
@ -139,6 +159,8 @@ func (ts *testserver) close() {
|
||||
tcheck(ts.t, err, "dmarcdb close")
|
||||
err = tlsrptdb.Close()
|
||||
tcheck(ts.t, err, "tlsrptdb close")
|
||||
err = store.Close()
|
||||
tcheck(ts.t, err, "store close")
|
||||
ts.comm.Unregister()
|
||||
queue.Shutdown()
|
||||
ts.switchStop()
|
||||
@ -180,8 +202,9 @@ func (ts *testserver) run(fn func(helloErr error, client *smtpclient.Client)) {
|
||||
ourHostname := mox.Conf.Static.HostnameDomain
|
||||
remoteHostname := dns.Domain{ASCII: "mox.example"}
|
||||
opts := smtpclient.Opts{
|
||||
Auth: auth,
|
||||
RootCAs: mox.Conf.Static.TLS.CertPool,
|
||||
Auth: auth,
|
||||
RootCAs: mox.Conf.Static.TLS.CertPool,
|
||||
ClientCert: ts.clientCert,
|
||||
}
|
||||
log := pkglog.WithCid(ts.cid - 1)
|
||||
client, err := smtpclient.New(ctxbg, log.Logger, conn, ts.tlsmode, ts.tlspkix, ourHostname, remoteHostname, opts)
|
||||
@ -206,13 +229,14 @@ func (ts *testserver) runRaw(fn func(clientConn net.Conn)) {
|
||||
defer func() { <-serverdone }()
|
||||
|
||||
go func() {
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{fakeCert(ts.t)},
|
||||
}
|
||||
serve("test", ts.cid-2, dns.Domain{ASCII: "mox.example"}, tlsConfig, serverConn, ts.resolver, ts.submission, false, 100<<20, false, false, ts.requiretls, ts.dnsbls, 0)
|
||||
serve("test", ts.cid-2, dns.Domain{ASCII: "mox.example"}, ts.serverConfig, serverConn, ts.resolver, ts.submission, ts.immediateTLS, 100<<20, false, false, ts.requiretls, ts.dnsbls, 0)
|
||||
close(serverdone)
|
||||
}()
|
||||
|
||||
if ts.immediateTLS {
|
||||
clientConn = tls.Client(clientConn, ts.clientConfig)
|
||||
}
|
||||
|
||||
fn(clientConn)
|
||||
}
|
||||
|
||||
@ -228,10 +252,17 @@ func (ts *testserver) smtpErr(err error, expErr *smtpclient.Error) {
|
||||
// Just a cert that appears valid. SMTP client will not verify anything about it
|
||||
// (that is opportunistic TLS for you, "better some than none"). Let's enjoy this
|
||||
// one moment where it makes life easier.
|
||||
func fakeCert(t *testing.T) tls.Certificate {
|
||||
privKey := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)) // Fake key, don't use this for real!
|
||||
func fakeCert(t *testing.T, randomkey bool) tls.Certificate {
|
||||
seed := make([]byte, ed25519.SeedSize)
|
||||
if randomkey {
|
||||
cryptorand.Read(seed)
|
||||
}
|
||||
privKey := ed25519.NewKeyFromSeed(seed) // Fake key, don't use this for real!
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1), // Required field...
|
||||
// Valid period is needed to get session resumption enabled.
|
||||
NotBefore: time.Now().Add(-time.Minute),
|
||||
NotAfter: time.Now().Add(time.Hour),
|
||||
}
|
||||
localCertBuf, err := x509.CreateCertificate(cryptorand.Reader, template, template, privKey.Public(), privKey)
|
||||
if err != nil {
|
||||
@ -330,6 +361,108 @@ func TestSubmission(t *testing.T) {
|
||||
testAuth(fn, "mo\u0301x@mox.example", password0, nil)
|
||||
testAuth(fn, "mo\u0301x@mox.example", password1, nil)
|
||||
}
|
||||
|
||||
// Create a certificate, register its public key with account, and make a tls
|
||||
// client config that sends the certificate.
|
||||
clientCert0 := fakeCert(ts.t, true)
|
||||
tlspubkey, err := store.ParseTLSPublicKeyCert(clientCert0.Certificate[0])
|
||||
tcheck(t, err, "parse certificate")
|
||||
tlspubkey.Account = "mjl"
|
||||
tlspubkey.LoginAddress = "mjl@mox.example"
|
||||
err = store.TLSPublicKeyAdd(ctxbg, &tlspubkey)
|
||||
tcheck(t, err, "add tls public key to account")
|
||||
ts.immediateTLS = true
|
||||
ts.clientConfig = &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
Certificates: []tls.Certificate{
|
||||
clientCert0,
|
||||
},
|
||||
}
|
||||
|
||||
// No explicit address in EXTERNAL.
|
||||
testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
|
||||
return sasl.NewClientExternal(user)
|
||||
}, "", "", nil)
|
||||
|
||||
// Same username in EXTERNAL as configured for key.
|
||||
testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
|
||||
return sasl.NewClientExternal(user)
|
||||
}, "mjl@mox.example", "", nil)
|
||||
|
||||
// Different username in EXTERNAL as configured for key, but same account.
|
||||
testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
|
||||
return sasl.NewClientExternal(user)
|
||||
}, "móx@mox.example", "", nil)
|
||||
|
||||
// Different username as configured for key, but same account, but not EXTERNAL auth.
|
||||
testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
|
||||
return sasl.NewClientSCRAMSHA256PLUS(user, pass, *cs)
|
||||
}, "móx@mox.example", password0, nil)
|
||||
|
||||
// Different account results in error.
|
||||
testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
|
||||
return sasl.NewClientExternal(user)
|
||||
}, "☺@mox.example", "", &smtpclient.Error{Code: smtp.C535AuthBadCreds, Secode: smtp.SePol7AuthBadCreds8})
|
||||
|
||||
// Starttls with client cert should authenticate too.
|
||||
ts.immediateTLS = false
|
||||
ts.clientCert = &clientCert0
|
||||
testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
|
||||
return sasl.NewClientExternal(user)
|
||||
}, "", "", nil)
|
||||
ts.immediateTLS = true
|
||||
ts.clientCert = nil
|
||||
|
||||
// Add a client session cache, so our connections will be resumed. We are testing
|
||||
// that the credentials are applied to resumed connections too.
|
||||
ts.clientConfig.ClientSessionCache = tls.NewLRUClientSessionCache(10)
|
||||
testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
|
||||
if cs.DidResume {
|
||||
panic("tls connection was resumed")
|
||||
}
|
||||
return sasl.NewClientExternal(user)
|
||||
}, "", "", nil)
|
||||
testAuth(func(user, pass string, cs *tls.ConnectionState) sasl.Client {
|
||||
if !cs.DidResume {
|
||||
panic("tls connection was not resumed")
|
||||
}
|
||||
return sasl.NewClientExternal(user)
|
||||
}, "", "", nil)
|
||||
|
||||
// Unknown client certificate should fail the connection.
|
||||
serverConn, clientConn := net.Pipe()
|
||||
serverdone := make(chan struct{})
|
||||
defer func() { <-serverdone }()
|
||||
|
||||
go func() {
|
||||
defer serverConn.Close()
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{fakeCert(ts.t, false)},
|
||||
}
|
||||
serve("test", ts.cid-2, dns.Domain{ASCII: "mox.example"}, tlsConfig, serverConn, ts.resolver, ts.submission, ts.immediateTLS, 100<<20, false, false, false, ts.dnsbls, 0)
|
||||
close(serverdone)
|
||||
}()
|
||||
|
||||
defer clientConn.Close()
|
||||
|
||||
// Authentication with an unknown/untrusted certificate should fail.
|
||||
clientCert1 := fakeCert(ts.t, true)
|
||||
ts.clientConfig.ClientSessionCache = nil
|
||||
ts.clientConfig.Certificates = []tls.Certificate{
|
||||
clientCert1,
|
||||
}
|
||||
clientConn = tls.Client(clientConn, ts.clientConfig)
|
||||
// note: It's not enough to do a handshake and check if that was successful. If the
|
||||
// client cert is not acceptable, we only learn after the handshake, when the first
|
||||
// data messages are exchanged.
|
||||
buf := make([]byte, 100)
|
||||
_, err = clientConn.Read(buf)
|
||||
if err == nil {
|
||||
t.Fatalf("tls handshake with unknown client certificate succeeded")
|
||||
}
|
||||
if alert, ok := mox.AsTLSAlert(err); !ok || alert != 42 {
|
||||
t.Fatalf("got err %#v, expected tls 'bad certificate' alert", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Test delivery from external MTA.
|
||||
@ -1247,7 +1380,7 @@ func TestNonSMTP(t *testing.T) {
|
||||
|
||||
go func() {
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{fakeCert(ts.t)},
|
||||
Certificates: []tls.Certificate{fakeCert(ts.t, false)},
|
||||
}
|
||||
serve("test", ts.cid-2, dns.Domain{ASCII: "mox.example"}, tlsConfig, serverConn, ts.resolver, ts.submission, false, 100<<20, false, false, false, ts.dnsbls, 0)
|
||||
close(serverdone)
|
||||
|
Reference in New Issue
Block a user