mirror of
https://github.com/mjl-/mox.git
synced 2025-07-12 17:44:35 +03:00
implement the plus variants of scram, to bind the authentication exchange to the tls connection
to get the security benefits (detecting mitm attempts), explicitly configure clients to use a scram plus variant, e.g. scram-sha-256-plus. unfortunately, not many clients support it yet. imapserver scram plus support seems to work with the latest imtest (imap test client) from cyrus-sasl. no success yet with mutt (with gsasl) though.
This commit is contained in:
@ -869,7 +869,12 @@ func (c *conn) cmdHello(p *parser, ehlo bool) {
|
||||
if c.submission {
|
||||
// ../rfc/4954:123
|
||||
if c.tls || !c.requireTLSForAuth {
|
||||
c.bwritelinef("250-AUTH SCRAM-SHA-256 SCRAM-SHA-1 CRAM-MD5 PLAIN LOGIN")
|
||||
// We always mention the SCRAM PLUS variants, even if TLS is not active: It is a
|
||||
// hint to the client that a TLS connection can use TLS channel binding during
|
||||
// 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 ")
|
||||
}
|
||||
@ -1190,22 +1195,35 @@ func (c *conn) cmdAuth(p *parser) {
|
||||
// ../rfc/4954:276
|
||||
c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
|
||||
|
||||
case "SCRAM-SHA-1", "SCRAM-SHA-256":
|
||||
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
|
||||
|
||||
authVariant = strings.ToLower(mech)
|
||||
var h func() hash.Hash
|
||||
if authVariant == "scram-sha-1" {
|
||||
h = sha1.New
|
||||
} else {
|
||||
h = sha256.New
|
||||
}
|
||||
|
||||
// Passwords cannot be retrieved or replayed from the trace.
|
||||
|
||||
authVariant = strings.ToLower(mech)
|
||||
var h func() hash.Hash
|
||||
switch authVariant {
|
||||
case "scram-sha-1", "scram-sha-1-plus":
|
||||
h = sha1.New
|
||||
case "scram-sha-256", "scram-sha-256-plus":
|
||||
h = sha256.New
|
||||
default:
|
||||
xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeSys3Other0}, "missing scram auth method case")
|
||||
}
|
||||
|
||||
var cs *tls.ConnectionState
|
||||
channelBindingRequired := strings.HasSuffix(authVariant, "-plus")
|
||||
if channelBindingRequired && !c.tls {
|
||||
// ../rfc/4954:630
|
||||
xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "scram plus mechanism requires tls connection")
|
||||
}
|
||||
if c.tls {
|
||||
xcs := c.conn.(*tls.Conn).ConnectionState()
|
||||
cs = &xcs
|
||||
}
|
||||
c0 := xreadInitial()
|
||||
ss, err := scram.NewServer(h, c0)
|
||||
ss, err := scram.NewServer(h, c0, cs, channelBindingRequired)
|
||||
xcheckf(err, "starting scram")
|
||||
c.log.Debug("scram auth", slog.String("authentication", ss.Authentication))
|
||||
acc, _, err := store.OpenEmail(c.log, ss.Authentication)
|
||||
@ -1229,10 +1247,13 @@ func (c *conn) cmdAuth(p *parser) {
|
||||
acc.WithRLock(func() {
|
||||
err := acc.DB.Read(context.TODO(), func(tx *bstore.Tx) error {
|
||||
password, err := bstore.QueryTx[store.Password](tx).Get()
|
||||
if authVariant == "scram-sha-1" {
|
||||
switch authVariant {
|
||||
case "scram-sha-1", "scram-sha-1-plus":
|
||||
xscram = password.SCRAMSHA1
|
||||
} else {
|
||||
case "scram-sha-256", "scram-sha-256-plus":
|
||||
xscram = password.SCRAMSHA256
|
||||
default:
|
||||
xsmtpServerErrorf(codes{smtp.C554TransactionFailed, smtp.SeSys3Other0}, "missing scram auth credentials case")
|
||||
}
|
||||
if err == bstore.ErrAbsent || err == nil && (len(xscram.Salt) == 0 || xscram.Iterations == 0 || len(xscram.SaltedPassword) == 0) {
|
||||
c.log.Info("scram auth attempt without derived secrets set, save password again to store secrets", slog.String("address", ss.Authentication))
|
||||
|
@ -89,7 +89,7 @@ type testserver struct {
|
||||
comm *store.Comm
|
||||
cid int64
|
||||
resolver dns.Resolver
|
||||
auth []sasl.Client
|
||||
auth func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error)
|
||||
user, pass string
|
||||
submission bool
|
||||
requiretls bool
|
||||
@ -159,11 +159,11 @@ func (ts *testserver) run(fn func(helloErr error, client *smtpclient.Client)) {
|
||||
close(serverdone)
|
||||
}()
|
||||
|
||||
var auth []sasl.Client
|
||||
if len(ts.auth) > 0 {
|
||||
auth = ts.auth
|
||||
} else if ts.user != "" {
|
||||
auth = append(auth, sasl.NewClientPlain(ts.user, ts.pass))
|
||||
auth := ts.auth
|
||||
if auth == nil && ts.user != "" {
|
||||
auth = func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error) {
|
||||
return sasl.NewClientPlain(ts.user, ts.pass), nil
|
||||
}
|
||||
}
|
||||
|
||||
ourHostname := mox.Conf.Static.HostnameDomain
|
||||
@ -234,10 +234,12 @@ func TestSubmission(t *testing.T) {
|
||||
}
|
||||
mox.Conf.Dynamic.Domains["mox.example"] = dom
|
||||
|
||||
testAuth := func(authfn func(user, pass string) sasl.Client, user, pass string, expErr *smtpclient.Error) {
|
||||
testAuth := func(authfn func(user, pass string, cs *tls.ConnectionState) sasl.Client, user, pass string, expErr *smtpclient.Error) {
|
||||
t.Helper()
|
||||
if authfn != nil {
|
||||
ts.auth = []sasl.Client{authfn(user, pass)}
|
||||
ts.auth = func(mechanisms []string, cs *tls.ConnectionState) (sasl.Client, error) {
|
||||
return authfn(user, pass, cs), nil
|
||||
}
|
||||
} else {
|
||||
ts.auth = nil
|
||||
}
|
||||
@ -258,12 +260,22 @@ func TestSubmission(t *testing.T) {
|
||||
|
||||
ts.submission = true
|
||||
testAuth(nil, "", "", &smtpclient.Error{Permanent: true, Code: smtp.C530SecurityRequired, Secode: smtp.SePol7Other0})
|
||||
authfns := []func(user, pass string) sasl.Client{
|
||||
sasl.NewClientPlain,
|
||||
sasl.NewClientLogin,
|
||||
sasl.NewClientCRAMMD5,
|
||||
sasl.NewClientSCRAMSHA1,
|
||||
sasl.NewClientSCRAMSHA256,
|
||||
authfns := []func(user, pass string, cs *tls.ConnectionState) sasl.Client{
|
||||
func(user, pass string, cs *tls.ConnectionState) sasl.Client { return sasl.NewClientPlain(user, pass) },
|
||||
func(user, pass string, cs *tls.ConnectionState) sasl.Client { return sasl.NewClientLogin(user, pass) },
|
||||
func(user, pass string, cs *tls.ConnectionState) sasl.Client { return sasl.NewClientCRAMMD5(user, pass) },
|
||||
func(user, pass string, cs *tls.ConnectionState) sasl.Client {
|
||||
return sasl.NewClientSCRAMSHA1(user, pass, false)
|
||||
},
|
||||
func(user, pass string, cs *tls.ConnectionState) sasl.Client {
|
||||
return sasl.NewClientSCRAMSHA256(user, pass, false)
|
||||
},
|
||||
func(user, pass string, cs *tls.ConnectionState) sasl.Client {
|
||||
return sasl.NewClientSCRAMSHA1PLUS(user, pass, *cs)
|
||||
},
|
||||
func(user, pass string, cs *tls.ConnectionState) sasl.Client {
|
||||
return sasl.NewClientSCRAMSHA256PLUS(user, pass, *cs)
|
||||
},
|
||||
}
|
||||
for _, fn := range authfns {
|
||||
testAuth(fn, "mjl@mox.example", "test", &smtpclient.Error{Secode: smtp.SePol7AuthBadCreds8}) // Bad (short) password.
|
||||
|
Reference in New Issue
Block a user