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:
@ -60,22 +60,31 @@ func TestAuthenticatePlain(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuthenticateSCRAMSHA1(t *testing.T) {
|
||||
testAuthenticateSCRAM(t, "SCRAM-SHA-1", sha1.New)
|
||||
testAuthenticateSCRAM(t, false, "SCRAM-SHA-1", sha1.New)
|
||||
}
|
||||
|
||||
func TestAuthenticateSCRAMSHA256(t *testing.T) {
|
||||
testAuthenticateSCRAM(t, "SCRAM-SHA-256", sha256.New)
|
||||
testAuthenticateSCRAM(t, false, "SCRAM-SHA-256", sha256.New)
|
||||
}
|
||||
|
||||
func testAuthenticateSCRAM(t *testing.T, method string, h func() hash.Hash) {
|
||||
tc := start(t)
|
||||
func TestAuthenticateSCRAMSHA1PLUS(t *testing.T) {
|
||||
testAuthenticateSCRAM(t, true, "SCRAM-SHA-1-PLUS", sha1.New)
|
||||
}
|
||||
|
||||
func TestAuthenticateSCRAMSHA256PLUS(t *testing.T) {
|
||||
testAuthenticateSCRAM(t, true, "SCRAM-SHA-256-PLUS", sha256.New)
|
||||
}
|
||||
|
||||
func testAuthenticateSCRAM(t *testing.T, tls bool, method string, h func() hash.Hash) {
|
||||
tc := startArgs(t, true, tls, true, true, "mjl")
|
||||
tc.client.AuthenticateSCRAM(method, h, "mjl@mox.example", "testtest")
|
||||
tc.close()
|
||||
|
||||
auth := func(status string, serverFinalError error, username, password string) {
|
||||
t.Helper()
|
||||
|
||||
sc := scram.NewClient(h, username, "")
|
||||
noServerPlus := false
|
||||
sc := scram.NewClient(h, username, "", noServerPlus, tc.client.TLSConnectionState())
|
||||
clientFirst, err := sc.ClientFirst()
|
||||
tc.check(err, "scram clientFirst")
|
||||
tc.client.LastTag = "x001"
|
||||
@ -116,7 +125,7 @@ func testAuthenticateSCRAM(t *testing.T, method string, h func() hash.Hash) {
|
||||
}
|
||||
}
|
||||
|
||||
tc = start(t)
|
||||
tc = startArgs(t, true, tls, true, true, "mjl")
|
||||
auth("no", scram.ErrInvalidProof, "mjl@mox.example", "badpass")
|
||||
auth("no", scram.ErrInvalidProof, "mjl@mox.example", "")
|
||||
// todo: server aborts due to invalid username. we should probably make client continue with fake determinisitically generated salt and result in error in the end.
|
||||
|
@ -150,13 +150,18 @@ var authFailDelay = time.Second // After authentication failure.
|
||||
// SPECIAL-USE: ../rfc/6154
|
||||
// LIST-STATUS: ../rfc/5819
|
||||
// ID: ../rfc/2971
|
||||
// AUTH=SCRAM-SHA-256: ../rfc/7677 ../rfc/5802
|
||||
// AUTH=SCRAM-SHA-1: ../rfc/5802
|
||||
// AUTH=SCRAM-SHA-256-PLUS and AUTH=SCRAM-SHA-256: ../rfc/7677 ../rfc/5802
|
||||
// AUTH=SCRAM-SHA-1-PLUS and AUTH=SCRAM-SHA-1: ../rfc/5802
|
||||
// AUTH=CRAM-MD5: ../rfc/2195
|
||||
// APPENDLIMIT, we support the max possible size, 1<<63 - 1: ../rfc/7889:129
|
||||
// CONDSTORE: ../rfc/7162:411
|
||||
// QRESYNC: ../rfc/7162:1323
|
||||
const serverCapabilities = "IMAP4rev2 IMAP4rev1 ENABLE LITERAL+ IDLE SASL-IR BINARY UNSELECT UIDPLUS ESEARCH SEARCHRES MOVE UTF8=ONLY LIST-EXTENDED SPECIAL-USE LIST-STATUS AUTH=SCRAM-SHA-256 AUTH=SCRAM-SHA-1 AUTH=CRAM-MD5 ID APPENDLIMIT=9223372036854775807 CONDSTORE QRESYNC"
|
||||
//
|
||||
// We always announce support for SCRAM PLUS-variants, also on connections without
|
||||
// TLS. The client should not be selecting PLUS variants on non-TLS connections,
|
||||
// instead opting to do the bare SCRAM variant without indicating the server claims
|
||||
// to support the PLUS variant (skipping the server downgrade detection check).
|
||||
const serverCapabilities = "IMAP4rev2 IMAP4rev1 ENABLE LITERAL+ IDLE SASL-IR BINARY UNSELECT UIDPLUS ESEARCH SEARCHRES MOVE UTF8=ONLY LIST-EXTENDED SPECIAL-USE LIST-STATUS AUTH=SCRAM-SHA-256-PLUS AUTH=SCRAM-SHA-256 AUTH=SCRAM-SHA-1-PLUS AUTH=SCRAM-SHA-1 AUTH=CRAM-MD5 ID APPENDLIMIT=9223372036854775807 CONDSTORE QRESYNC"
|
||||
|
||||
type conn struct {
|
||||
cid int64
|
||||
@ -1654,22 +1659,34 @@ func (c *conn) cmdAuthenticate(tag, cmd string, p *parser) {
|
||||
acc = nil // Cancel cleanup.
|
||||
c.username = addr
|
||||
|
||||
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(authType)
|
||||
var h func() hash.Hash
|
||||
if authVariant == "scram-sha-1" {
|
||||
h = sha1.New
|
||||
} else {
|
||||
h = sha256.New
|
||||
}
|
||||
|
||||
// No plaintext credentials, we can log these normally.
|
||||
|
||||
authVariant = strings.ToLower(authType)
|
||||
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:
|
||||
xserverErrorf("missing case for scram variant")
|
||||
}
|
||||
|
||||
var cs *tls.ConnectionState
|
||||
requireChannelBinding := strings.HasSuffix(authVariant, "-plus")
|
||||
if requireChannelBinding && !c.tls {
|
||||
xuserErrorf("cannot use plus variant with tls channel binding without tls")
|
||||
}
|
||||
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, requireChannelBinding)
|
||||
if err != nil {
|
||||
xsyntaxErrorf("starting scram: %s", err)
|
||||
}
|
||||
@ -1694,10 +1711,13 @@ func (c *conn) cmdAuthenticate(tag, cmd string, 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:
|
||||
xserverErrorf("missing case for scram credentials")
|
||||
}
|
||||
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))
|
||||
|
Reference in New Issue
Block a user