mirror of
https://github.com/mjl-/mox.git
synced 2024-12-26 16:33:47 +03:00
add scram-sha-256 for smtp
similar to imap. the code should be merged. this also reads the abort-line after authentication failure.
This commit is contained in:
parent
b40bb257d7
commit
020d0bb0fb
5 changed files with 128 additions and 17 deletions
|
@ -90,6 +90,11 @@ func TestAuthenticateSCRAMSHA256(t *testing.T) {
|
||||||
} else if err == nil || !errors.Is(err, serverFinalError) {
|
} else if err == nil || !errors.Is(err, serverFinalError) {
|
||||||
t.Fatalf("server final, got err %#v, expected %#v", err, serverFinalError)
|
t.Fatalf("server final, got err %#v, expected %#v", err, serverFinalError)
|
||||||
}
|
}
|
||||||
|
if serverFinalError != nil {
|
||||||
|
tc.writelinef("*")
|
||||||
|
} else {
|
||||||
|
tc.writelinef("")
|
||||||
|
}
|
||||||
_, result, err := tc.client.Response()
|
_, result, err := tc.client.Response()
|
||||||
tc.check(err, "read response")
|
tc.check(err, "read response")
|
||||||
if string(result.Status) != strings.ToUpper(status) {
|
if string(result.Status) != strings.ToUpper(status) {
|
||||||
|
|
|
@ -1370,6 +1370,7 @@ func (c *conn) cmdAuthenticate(tag, cmd string, p *parser) {
|
||||||
|
|
||||||
case "SCRAM-SHA-256":
|
case "SCRAM-SHA-256":
|
||||||
// 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: 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 = "scram-sha-256"
|
authVariant = "scram-sha-256"
|
||||||
|
|
||||||
|
@ -1409,9 +1410,6 @@ func (c *conn) cmdAuthenticate(tag, cmd string, p *parser) {
|
||||||
xcheckf(err, "read tx")
|
xcheckf(err, "read tx")
|
||||||
})
|
})
|
||||||
s1, err := ss.ServerFirst(password.SCRAMSHA256.Iterations, password.SCRAMSHA256.Salt)
|
s1, err := ss.ServerFirst(password.SCRAMSHA256.Iterations, password.SCRAMSHA256.Salt)
|
||||||
if err != nil {
|
|
||||||
xsyntaxErrorf("server first: %w", err)
|
|
||||||
}
|
|
||||||
xcheckf(err, "scram first server step")
|
xcheckf(err, "scram first server step")
|
||||||
c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(s1)))
|
c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(s1)))
|
||||||
c2 := xreadContinuation()
|
c2 := xreadContinuation()
|
||||||
|
@ -1420,6 +1418,7 @@ func (c *conn) cmdAuthenticate(tag, cmd string, p *parser) {
|
||||||
c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(s3)))
|
c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(s3)))
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
c.readline(false) // Should be "*" for cancellation.
|
||||||
if errors.Is(err, scram.ErrInvalidProof) {
|
if errors.Is(err, scram.ErrInvalidProof) {
|
||||||
authResult = "badcreds"
|
authResult = "badcreds"
|
||||||
xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
|
xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials")
|
||||||
|
@ -2779,7 +2778,7 @@ func (c *conn) linkOrCopyFile(dst, src string) error {
|
||||||
if df != nil {
|
if df != nil {
|
||||||
err = os.Remove(df.Name())
|
err = os.Remove(df.Name())
|
||||||
c.xsanity(err, "removing unfinished dst file")
|
c.xsanity(err, "removing unfinished dst file")
|
||||||
df.Close()
|
err = df.Close()
|
||||||
c.xsanity(err, "closing unfinished dst file")
|
c.xsanity(err, "closing unfinished dst file")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
|
@ -32,9 +32,9 @@ var (
|
||||||
C504ParamNotImpl = 504
|
C504ParamNotImpl = 504
|
||||||
C521HostNoMail = 521 // ../rfc/7504:179
|
C521HostNoMail = 521 // ../rfc/7504:179
|
||||||
C530SecurityRequired = 530 // ../rfc/3207:148 ../rfc/4954:623
|
C530SecurityRequired = 530 // ../rfc/3207:148 ../rfc/4954:623
|
||||||
C538EncReqForAuth = 538 // ../rfc/4954:630
|
|
||||||
C534AuthMechWeak = 534 // ../rfc/4954:593
|
C534AuthMechWeak = 534 // ../rfc/4954:593
|
||||||
C535AuthBadCreds = 535 // ../rfc/4954:600
|
C535AuthBadCreds = 535 // ../rfc/4954:600
|
||||||
|
C538EncReqForAuth = 538 // ../rfc/4954:630
|
||||||
C550MailboxUnavail = 550
|
C550MailboxUnavail = 550
|
||||||
C551UserNotLocal = 551
|
C551UserNotLocal = 551
|
||||||
C552MailboxFull = 552
|
C552MailboxFull = 552
|
||||||
|
|
|
@ -139,6 +139,21 @@ func (p *parser) takefn1(what string, fn func(c rune, i int) bool) string {
|
||||||
return p.remainder()
|
return p.remainder()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *parser) takefn1case(what string, fn func(c rune, i int) bool) string {
|
||||||
|
if p.empty() {
|
||||||
|
p.xerrorf("need at least one char for %s", what)
|
||||||
|
}
|
||||||
|
for i, c := range p.orig[p.o:] {
|
||||||
|
if !fn(c, i) {
|
||||||
|
if i == 0 {
|
||||||
|
p.xerrorf("expected at least one char for %s", what)
|
||||||
|
}
|
||||||
|
return p.xtaken(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return p.remainder()
|
||||||
|
}
|
||||||
|
|
||||||
func (p *parser) takefn(fn func(c rune, i int) bool) string {
|
func (p *parser) takefn(fn func(c rune, i int) bool) string {
|
||||||
for i, c := range p.upper[p.o:] {
|
for i, c := range p.upper[p.o:] {
|
||||||
if !fn(c, i) {
|
if !fn(c, i) {
|
||||||
|
@ -413,7 +428,7 @@ func (p *parser) xnumber(maxDigits int) int64 {
|
||||||
// sasl mechanism, for AUTH command.
|
// sasl mechanism, for AUTH command.
|
||||||
// ../rfc/4422:436
|
// ../rfc/4422:436
|
||||||
func (p *parser) xsaslMech() string {
|
func (p *parser) xsaslMech() string {
|
||||||
return p.takefn1("sasl-mech", func(c rune, i int) bool {
|
return p.takefn1case("sasl-mech", func(c rune, i int) bool {
|
||||||
return i < 20 && (c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '-' || c == '_')
|
return i < 20 && (c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '-' || c == '_')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,8 @@ import (
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
|
||||||
|
"github.com/mjl-/bstore"
|
||||||
|
|
||||||
"github.com/mjl-/mox/config"
|
"github.com/mjl-/mox/config"
|
||||||
"github.com/mjl-/mox/dkim"
|
"github.com/mjl-/mox/dkim"
|
||||||
"github.com/mjl-/mox/dmarc"
|
"github.com/mjl-/mox/dmarc"
|
||||||
|
@ -36,6 +38,7 @@ import (
|
||||||
"github.com/mjl-/mox/moxvar"
|
"github.com/mjl-/mox/moxvar"
|
||||||
"github.com/mjl-/mox/publicsuffix"
|
"github.com/mjl-/mox/publicsuffix"
|
||||||
"github.com/mjl-/mox/queue"
|
"github.com/mjl-/mox/queue"
|
||||||
|
"github.com/mjl-/mox/scram"
|
||||||
"github.com/mjl-/mox/smtp"
|
"github.com/mjl-/mox/smtp"
|
||||||
"github.com/mjl-/mox/spf"
|
"github.com/mjl-/mox/spf"
|
||||||
"github.com/mjl-/mox/store"
|
"github.com/mjl-/mox/store"
|
||||||
|
@ -659,10 +662,9 @@ func (c *conn) cmdHello(p *parser, ehlo bool) {
|
||||||
c.bwritelinef("250-STARTTLS")
|
c.bwritelinef("250-STARTTLS")
|
||||||
}
|
}
|
||||||
if c.submission {
|
if c.submission {
|
||||||
// todo future: implement SCRAM-SHA-256.
|
|
||||||
// ../rfc/4954:123
|
// ../rfc/4954:123
|
||||||
if c.tls || !c.requireTLSForAuth {
|
if c.tls || !c.requireTLSForAuth {
|
||||||
c.bwritelinef("250-AUTH PLAIN")
|
c.bwritelinef("250-AUTH PLAIN SCRAM-SHA-256")
|
||||||
} else {
|
} else {
|
||||||
c.bwritelinef("250-AUTH ")
|
c.bwritelinef("250-AUTH ")
|
||||||
}
|
}
|
||||||
|
@ -757,16 +759,8 @@ func (c *conn) cmdAuth(p *parser) {
|
||||||
// ../rfc/4954:699
|
// ../rfc/4954:699
|
||||||
p.xspace()
|
p.xspace()
|
||||||
mech := p.xsaslMech()
|
mech := p.xsaslMech()
|
||||||
switch mech {
|
|
||||||
case "PLAIN":
|
|
||||||
authVariant = "plain"
|
|
||||||
|
|
||||||
// ../rfc/4954:343
|
|
||||||
// ../rfc/4954:326
|
|
||||||
if !c.tls && c.requireTLSForAuth {
|
|
||||||
xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "authentication requires tls")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
xreadInitial := func() []byte {
|
||||||
var auth string
|
var auth string
|
||||||
if p.empty() {
|
if p.empty() {
|
||||||
c.writelinef("%d ", smtp.C334ContinueAuth) // ../rfc/4954:205
|
c.writelinef("%d ", smtp.C334ContinueAuth) // ../rfc/4954:205
|
||||||
|
@ -793,6 +787,34 @@ func (c *conn) cmdAuth(p *parser) {
|
||||||
// ../rfc/4954:235
|
// ../rfc/4954:235
|
||||||
xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "invalid base64: %s", err)
|
xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "invalid base64: %s", err)
|
||||||
}
|
}
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
xreadContinuation := func() []byte {
|
||||||
|
line := c.readline()
|
||||||
|
if line == "*" {
|
||||||
|
authResult = "aborted"
|
||||||
|
xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Other0, "authentication aborted")
|
||||||
|
}
|
||||||
|
buf, err := base64.StdEncoding.DecodeString(line)
|
||||||
|
if err != nil {
|
||||||
|
// ../rfc/4954:235
|
||||||
|
xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5Syntax2, "invalid base64: %s", err)
|
||||||
|
}
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
switch mech {
|
||||||
|
case "PLAIN":
|
||||||
|
authVariant = "plain"
|
||||||
|
|
||||||
|
// ../rfc/4954:343
|
||||||
|
// ../rfc/4954:326
|
||||||
|
if !c.tls && c.requireTLSForAuth {
|
||||||
|
xsmtpUserErrorf(smtp.C538EncReqForAuth, smtp.SePol7EncReqForAuth11, "authentication requires tls")
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := xreadInitial()
|
||||||
plain := bytes.Split(buf, []byte{0})
|
plain := bytes.Split(buf, []byte{0})
|
||||||
if len(plain) != 3 {
|
if len(plain) != 3 {
|
||||||
xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "auth data should have 3 nul-separated tokens, got %d", len(plain))
|
xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "auth data should have 3 nul-separated tokens, got %d", len(plain))
|
||||||
|
@ -819,6 +841,76 @@ func (c *conn) cmdAuth(p *parser) {
|
||||||
// ../rfc/4954:276
|
// ../rfc/4954:276
|
||||||
c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
|
c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
|
||||||
|
|
||||||
|
case "SCRAM-SHA-256":
|
||||||
|
// 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 = "scram-sha-256"
|
||||||
|
|
||||||
|
c0 := xreadInitial()
|
||||||
|
ss, err := scram.NewServer(c0)
|
||||||
|
xcheckf(err, "starting scram")
|
||||||
|
c.log.Info("scram auth", mlog.Field("authentication", ss.Authentication))
|
||||||
|
acc, _, err := store.OpenEmail(ss.Authentication)
|
||||||
|
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.
|
||||||
|
xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible")
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if acc != nil {
|
||||||
|
err := acc.Close()
|
||||||
|
if err != nil {
|
||||||
|
c.log.Errorx("closing account", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if ss.Authorization != "" && ss.Authorization != ss.Authentication {
|
||||||
|
xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "authentication with authorization for different user not supported")
|
||||||
|
}
|
||||||
|
var password store.Password
|
||||||
|
acc.WithRLock(func() {
|
||||||
|
err := acc.DB.Read(func(tx *bstore.Tx) error {
|
||||||
|
password, err = bstore.QueryTx[store.Password](tx).Get()
|
||||||
|
xsc := password.SCRAMSHA256
|
||||||
|
if err == bstore.ErrAbsent || err == nil && (len(xsc.Salt) == 0 || xsc.Iterations == 0 || len(xsc.SaltedPassword) == 0) {
|
||||||
|
xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible")
|
||||||
|
}
|
||||||
|
xcheckf(err, "fetching credentials")
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
xcheckf(err, "read tx")
|
||||||
|
})
|
||||||
|
s1, err := ss.ServerFirst(password.SCRAMSHA256.Iterations, password.SCRAMSHA256.Salt)
|
||||||
|
xcheckf(err, "scram first server step")
|
||||||
|
c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s1))) // ../rfc/4954:187
|
||||||
|
c2 := xreadContinuation()
|
||||||
|
s3, err := ss.Finish(c2, password.SCRAMSHA256.SaltedPassword)
|
||||||
|
if len(s3) > 0 {
|
||||||
|
c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(s3))) // ../rfc/4954:187
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
c.readline() // Should be "*" for cancellation.
|
||||||
|
if errors.Is(err, scram.ErrInvalidProof) {
|
||||||
|
authResult = "badcreds"
|
||||||
|
xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad credentials")
|
||||||
|
}
|
||||||
|
xcheckf(err, "server final")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client must still respond, but there is nothing to say. See ../rfc/9051:6221
|
||||||
|
// The message should be empty. todo: should we require it is empty?
|
||||||
|
xreadContinuation()
|
||||||
|
|
||||||
|
authResult = "ok"
|
||||||
|
c.authFailed = 0
|
||||||
|
c.account = acc
|
||||||
|
acc = nil // Cancel cleanup.
|
||||||
|
c.username = ss.Authentication
|
||||||
|
// ../rfc/4954:276
|
||||||
|
c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// todo future: implement scram-sha-256 ../rfc/7677
|
// todo future: implement scram-sha-256 ../rfc/7677
|
||||||
// todo future: possibly implement cram-md5, at least where we allow PLAIN. ../rfc/4954:348
|
// todo future: possibly implement cram-md5, at least where we allow PLAIN. ../rfc/4954:348
|
||||||
|
|
Loading…
Reference in a new issue