mirror of
https://github.com/mjl-/mox.git
synced 2025-07-10 07:14:40 +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:
@ -69,7 +69,7 @@ func TestClient(t *testing.T) {
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
|
||||
test := func(msg string, opts options, auths []sasl.Client, expClientErr, expDeliverErr, expServerErr error) {
|
||||
test := func(msg string, opts options, auth func(l []string, cs *tls.ConnectionState) (sasl.Client, error), expClientErr, expDeliverErr, expServerErr error) {
|
||||
t.Helper()
|
||||
|
||||
if opts.tlsMode == "" {
|
||||
@ -193,17 +193,32 @@ func TestClient(t *testing.T) {
|
||||
writeline("334 " + base64.StdEncoding.EncodeToString([]byte("<123.1234@host>")))
|
||||
readline("") // Proof
|
||||
writeline("235 2.7.0 auth ok")
|
||||
case "SCRAM-SHA-1", "SCRAM-SHA-256":
|
||||
case "SCRAM-SHA-256-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1":
|
||||
// Cannot fake/hardcode scram interactions.
|
||||
var h func() hash.Hash
|
||||
salt := scram.MakeRandom()
|
||||
var iterations int
|
||||
if t[0] == "SCRAM-SHA-1" {
|
||||
switch t[0] {
|
||||
case "SCRAM-SHA-1-PLUS", "SCRAM-SHA-1":
|
||||
h = sha1.New
|
||||
iterations = 2 * 4096
|
||||
} else {
|
||||
case "SCRAM-SHA-256-PLUS", "SCRAM-SHA-256":
|
||||
h = sha256.New
|
||||
iterations = 4096
|
||||
default:
|
||||
panic("missing case for scram")
|
||||
}
|
||||
var cs *tls.ConnectionState
|
||||
if strings.HasSuffix(t[0], "-PLUS") {
|
||||
if !haveTLS {
|
||||
writeline("501 scram plus without tls not possible")
|
||||
readline("QUIT")
|
||||
writeline("221 ok")
|
||||
result <- nil
|
||||
return
|
||||
}
|
||||
xcs := serverConn.(*tls.Conn).ConnectionState()
|
||||
cs = &xcs
|
||||
}
|
||||
saltedPassword := scram.SaltPassword(h, "test", salt, iterations)
|
||||
|
||||
@ -211,7 +226,7 @@ func TestClient(t *testing.T) {
|
||||
if err != nil {
|
||||
fail("bad base64: %w", err)
|
||||
}
|
||||
s, err := scram.NewServer(h, clientFirst)
|
||||
s, err := scram.NewServer(h, clientFirst, cs, cs != nil)
|
||||
if err != nil {
|
||||
fail("scram new server: %w", err)
|
||||
}
|
||||
@ -283,7 +298,7 @@ func TestClient(t *testing.T) {
|
||||
result <- err
|
||||
panic("stop")
|
||||
}
|
||||
c, err := New(ctx, log.Logger, clientConn, opts.tlsMode, opts.tlsPKIX, localhost, opts.tlsHostname, Opts{Auth: auths, RootCAs: opts.roots})
|
||||
client, err := New(ctx, log.Logger, clientConn, opts.tlsMode, opts.tlsPKIX, localhost, opts.tlsHostname, Opts{Auth: auth, RootCAs: opts.roots})
|
||||
if (err == nil) != (expClientErr == nil) || err != nil && !errors.As(err, reflect.New(reflect.ValueOf(expClientErr).Type()).Interface()) && !errors.Is(err, expClientErr) {
|
||||
fail("new client: got err %v, expected %#v", err, expClientErr)
|
||||
}
|
||||
@ -291,21 +306,21 @@ func TestClient(t *testing.T) {
|
||||
result <- nil
|
||||
return
|
||||
}
|
||||
err = c.Deliver(ctx, "postmaster@mox.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), opts.need8bitmime, opts.needsmtputf8, opts.needsrequiretls)
|
||||
err = client.Deliver(ctx, "postmaster@mox.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), opts.need8bitmime, opts.needsmtputf8, opts.needsrequiretls)
|
||||
if (err == nil) != (expDeliverErr == nil) || err != nil && !errors.Is(err, expDeliverErr) {
|
||||
fail("first deliver: got err %v, expected %v", err, expDeliverErr)
|
||||
}
|
||||
if err == nil {
|
||||
err = c.Reset()
|
||||
err = client.Reset()
|
||||
if err != nil {
|
||||
fail("reset: %v", err)
|
||||
}
|
||||
err = c.Deliver(ctx, "postmaster@mox.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), opts.need8bitmime, opts.needsmtputf8, opts.needsrequiretls)
|
||||
err = client.Deliver(ctx, "postmaster@mox.example", "mjl@mox.example", int64(len(msg)), strings.NewReader(msg), opts.need8bitmime, opts.needsmtputf8, opts.needsrequiretls)
|
||||
if (err == nil) != (expDeliverErr == nil) || err != nil && !errors.Is(err, expDeliverErr) {
|
||||
fail("second deliver: got err %v, expected %v", err, expDeliverErr)
|
||||
}
|
||||
}
|
||||
err = c.Close()
|
||||
err = client.Close()
|
||||
if err != nil {
|
||||
fail("close client: %v", err)
|
||||
}
|
||||
@ -357,11 +372,39 @@ test
|
||||
test(msg, options{ehlo: true, smtputf8: false, needsmtputf8: true, nodeliver: true}, nil, nil, ErrSMTPUTF8Unsupported, nil)
|
||||
test(msg, options{ehlo: true, starttls: true, tlsMode: TLSRequiredStartTLS, tlsPKIX: true, tlsHostname: dns.Domain{ASCII: "mismatch.example"}, nodeliver: true}, nil, ErrTLS, nil, &net.OpError{}) // Server TLS handshake is a net.OpError with "remote error" as text.
|
||||
test(msg, options{ehlo: true, maxSize: len(msg) - 1, nodeliver: true}, nil, nil, ErrSize, nil)
|
||||
test(msg, options{ehlo: true, auths: []string{"PLAIN"}}, []sasl.Client{sasl.NewClientPlain("test", "test")}, nil, nil, nil)
|
||||
test(msg, options{ehlo: true, auths: []string{"CRAM-MD5"}}, []sasl.Client{sasl.NewClientCRAMMD5("test", "test")}, nil, nil, nil)
|
||||
test(msg, options{ehlo: true, auths: []string{"SCRAM-SHA-1"}}, []sasl.Client{sasl.NewClientSCRAMSHA1("test", "test")}, nil, nil, nil)
|
||||
test(msg, options{ehlo: true, auths: []string{"SCRAM-SHA-256"}}, []sasl.Client{sasl.NewClientSCRAMSHA256("test", "test")}, nil, nil, nil)
|
||||
|
||||
authPlain := func(l []string, cs *tls.ConnectionState) (sasl.Client, error) {
|
||||
return sasl.NewClientPlain("test", "test"), nil
|
||||
}
|
||||
test(msg, options{ehlo: true, auths: []string{"PLAIN"}}, authPlain, nil, nil, nil)
|
||||
|
||||
authCRAMMD5 := func(l []string, cs *tls.ConnectionState) (sasl.Client, error) {
|
||||
return sasl.NewClientCRAMMD5("test", "test"), nil
|
||||
}
|
||||
test(msg, options{ehlo: true, auths: []string{"CRAM-MD5"}}, authCRAMMD5, nil, nil, nil)
|
||||
|
||||
// todo: add tests for failing authentication, also at various stages in SCRAM
|
||||
|
||||
authSCRAMSHA1 := func(l []string, cs *tls.ConnectionState) (sasl.Client, error) {
|
||||
return sasl.NewClientSCRAMSHA1("test", "test", false), nil
|
||||
}
|
||||
test(msg, options{ehlo: true, auths: []string{"SCRAM-SHA-1"}}, authSCRAMSHA1, nil, nil, nil)
|
||||
|
||||
authSCRAMSHA1PLUS := func(l []string, cs *tls.ConnectionState) (sasl.Client, error) {
|
||||
return sasl.NewClientSCRAMSHA1PLUS("test", "test", *cs), nil
|
||||
}
|
||||
test(msg, options{ehlo: true, starttls: true, auths: []string{"SCRAM-SHA-1-PLUS"}}, authSCRAMSHA1PLUS, nil, nil, nil)
|
||||
|
||||
authSCRAMSHA256 := func(l []string, cs *tls.ConnectionState) (sasl.Client, error) {
|
||||
return sasl.NewClientSCRAMSHA256("test", "test", false), nil
|
||||
}
|
||||
test(msg, options{ehlo: true, auths: []string{"SCRAM-SHA-256"}}, authSCRAMSHA256, nil, nil, nil)
|
||||
|
||||
authSCRAMSHA256PLUS := func(l []string, cs *tls.ConnectionState) (sasl.Client, error) {
|
||||
return sasl.NewClientSCRAMSHA256PLUS("test", "test", *cs), nil
|
||||
}
|
||||
test(msg, options{ehlo: true, starttls: true, auths: []string{"SCRAM-SHA-256-PLUS"}}, authSCRAMSHA256PLUS, nil, nil, nil)
|
||||
|
||||
test(msg, options{ehlo: true, requiretls: false, needsrequiretls: true, nodeliver: true}, nil, nil, ErrRequireTLSUnsupported, nil)
|
||||
|
||||
// Set an expired certificate. For non-strict TLS, we should still accept it.
|
||||
|
Reference in New Issue
Block a user