mirror of
https://github.com/mjl-/mox.git
synced 2025-06-28 01:48:15 +03:00

Intended for future use with chatmail servers. Standard email ports may be blocked on some networks, while the HTTPS port may be accessible. This is a squashed commit of PR #255 by s0ph0s-dog.
367 lines
13 KiB
Go
367 lines
13 KiB
Go
package imapserver
|
|
|
|
import (
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/md5"
|
|
"crypto/sha1"
|
|
"crypto/sha256"
|
|
"crypto/tls"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"hash"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/text/secure/precis"
|
|
|
|
"github.com/mjl-/mox/mox-"
|
|
"github.com/mjl-/mox/scram"
|
|
"github.com/mjl-/mox/store"
|
|
)
|
|
|
|
func TestAuthenticateLogin(t *testing.T) {
|
|
// NFD username and PRECIS-cleaned password.
|
|
tc := start(t)
|
|
tc.client.Login("mo\u0301x@mox.example", password1)
|
|
tc.close()
|
|
}
|
|
|
|
func TestAuthenticatePlain(t *testing.T) {
|
|
tc := start(t)
|
|
|
|
tc.transactf("no", "authenticate bogus ")
|
|
tc.transactf("bad", "authenticate plain not base64...")
|
|
tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000baduser\u0000badpass")))
|
|
tc.xcode("AUTHENTICATIONFAILED")
|
|
tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000badpass")))
|
|
tc.xcode("AUTHENTICATIONFAILED")
|
|
tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl\u0000badpass"))) // Need email, not account.
|
|
tc.xcode("AUTHENTICATIONFAILED")
|
|
tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000test")))
|
|
tc.xcode("AUTHENTICATIONFAILED")
|
|
tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000test"+password0)))
|
|
tc.xcode("AUTHENTICATIONFAILED")
|
|
tc.transactf("bad", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000")))
|
|
tc.xcode("")
|
|
tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("other\u0000mjl@mox.example\u0000"+password0)))
|
|
tc.xcode("AUTHORIZATIONFAILED")
|
|
tc.transactf("ok", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000"+password0)))
|
|
tc.close()
|
|
|
|
tc = start(t)
|
|
tc.transactf("ok", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("mjl@mox.example\u0000mjl@mox.example\u0000"+password0)))
|
|
tc.close()
|
|
|
|
// NFD username and PRECIS-cleaned password.
|
|
tc = start(t)
|
|
tc.transactf("ok", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("mo\u0301x@mox.example\u0000mo\u0301x@mox.example\u0000"+password1)))
|
|
tc.close()
|
|
|
|
tc = start(t)
|
|
tc.client.AuthenticatePlain("mjl@mox.example", password0)
|
|
tc.close()
|
|
|
|
tc = start(t)
|
|
defer tc.close()
|
|
|
|
tc.cmdf("", "authenticate plain")
|
|
tc.readprefixline("+ ")
|
|
tc.writelinef("*") // Aborts.
|
|
tc.readstatus("bad")
|
|
|
|
tc.cmdf("", "authenticate plain")
|
|
tc.readprefixline("+ ")
|
|
tc.writelinef("%s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000"+password0)))
|
|
tc.readstatus("ok")
|
|
}
|
|
|
|
func TestAuthenticateSCRAMSHA1(t *testing.T) {
|
|
testAuthenticateSCRAM(t, false, "SCRAM-SHA-1", sha1.New)
|
|
}
|
|
|
|
func TestAuthenticateSCRAMSHA256(t *testing.T) {
|
|
testAuthenticateSCRAM(t, false, "SCRAM-SHA-256", sha256.New)
|
|
}
|
|
|
|
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", password0)
|
|
tc.close()
|
|
|
|
auth := func(status string, serverFinalError error, username, password string) {
|
|
t.Helper()
|
|
|
|
noServerPlus := false
|
|
sc := scram.NewClient(h, username, "", noServerPlus, tc.client.TLSConnectionState())
|
|
clientFirst, err := sc.ClientFirst()
|
|
tc.check(err, "scram clientFirst")
|
|
tc.client.LastTag = "x001"
|
|
tc.writelinef("%s authenticate %s %s", tc.client.LastTag, method, base64.StdEncoding.EncodeToString([]byte(clientFirst)))
|
|
|
|
xreadContinuation := func() []byte {
|
|
line, _, result, rerr := tc.client.ReadContinuation()
|
|
tc.check(rerr, "read continuation")
|
|
if result.Status != "" {
|
|
tc.t.Fatalf("expected continuation")
|
|
}
|
|
buf, err := base64.StdEncoding.DecodeString(line)
|
|
tc.check(err, "parsing base64 from remote")
|
|
return buf
|
|
}
|
|
|
|
serverFirst := xreadContinuation()
|
|
clientFinal, err := sc.ServerFirst(serverFirst, password)
|
|
tc.check(err, "scram clientFinal")
|
|
tc.writelinef("%s", base64.StdEncoding.EncodeToString([]byte(clientFinal)))
|
|
|
|
serverFinal := xreadContinuation()
|
|
err = sc.ServerFinal(serverFinal)
|
|
if serverFinalError == nil {
|
|
tc.check(err, "scram serverFinal")
|
|
} else if err == nil || !errors.Is(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()
|
|
tc.check(err, "read response")
|
|
if string(result.Status) != strings.ToUpper(status) {
|
|
tc.t.Fatalf("got status %q, expected %q", result.Status, strings.ToUpper(status))
|
|
}
|
|
}
|
|
|
|
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.
|
|
// auth("no", nil, "other@mox.example", password0)
|
|
|
|
tc.transactf("no", "authenticate bogus ")
|
|
tc.transactf("bad", "authenticate %s not base64...", method)
|
|
tc.transactf("no", "authenticate %s %s", method, base64.StdEncoding.EncodeToString([]byte("bad data")))
|
|
|
|
// NFD username, with PRECIS-cleaned password.
|
|
auth("ok", nil, "mo\u0301x@mox.example", password1)
|
|
|
|
tc.close()
|
|
}
|
|
|
|
func TestAuthenticateCRAMMD5(t *testing.T) {
|
|
tc := start(t)
|
|
|
|
tc.transactf("no", "authenticate bogus ")
|
|
tc.transactf("bad", "authenticate CRAM-MD5 not base64...")
|
|
tc.transactf("bad", "authenticate CRAM-MD5 %s", base64.StdEncoding.EncodeToString([]byte("baddata")))
|
|
tc.transactf("bad", "authenticate CRAM-MD5 %s", base64.StdEncoding.EncodeToString([]byte("bad data")))
|
|
|
|
auth := func(status string, username, password string) {
|
|
t.Helper()
|
|
|
|
tc.client.LastTag = "x001"
|
|
tc.writelinef("%s authenticate CRAM-MD5", tc.client.LastTag)
|
|
|
|
xreadContinuation := func() []byte {
|
|
line, _, result, rerr := tc.client.ReadContinuation()
|
|
tc.check(rerr, "read continuation")
|
|
if result.Status != "" {
|
|
tc.t.Fatalf("expected continuation")
|
|
}
|
|
buf, err := base64.StdEncoding.DecodeString(line)
|
|
tc.check(err, "parsing base64 from remote")
|
|
return buf
|
|
}
|
|
|
|
chal := xreadContinuation()
|
|
pw, err := precis.OpaqueString.String(password)
|
|
if err == nil {
|
|
password = pw
|
|
}
|
|
h := hmac.New(md5.New, []byte(password))
|
|
h.Write([]byte(chal))
|
|
resp := fmt.Sprintf("%s %x", username, h.Sum(nil))
|
|
tc.writelinef("%s", base64.StdEncoding.EncodeToString([]byte(resp)))
|
|
|
|
_, result, err := tc.client.Response()
|
|
tc.check(err, "read response")
|
|
if string(result.Status) != strings.ToUpper(status) {
|
|
tc.t.Fatalf("got status %q, expected %q", result.Status, strings.ToUpper(status))
|
|
}
|
|
}
|
|
|
|
auth("no", "mjl@mox.example", "badpass")
|
|
auth("no", "mjl@mox.example", "")
|
|
auth("no", "other@mox.example", password0)
|
|
|
|
auth("ok", "mjl@mox.example", password0)
|
|
|
|
tc.close()
|
|
|
|
// NFD username, with PRECIS-cleaned password.
|
|
tc = start(t)
|
|
auth("ok", "mo\u0301x@mox.example", password1)
|
|
tc.close()
|
|
}
|
|
|
|
func TestAuthenticateTLSClientCert(t *testing.T) {
|
|
tc := startArgs(t, true, true, true, true, "mjl")
|
|
tc.transactf("no", "authenticate external ") // No TLS auth.
|
|
tc.close()
|
|
|
|
// Create a certificate, register its public key with account, and make a tls
|
|
// client config that sends the certificate.
|
|
clientCert0 := fakeCert(t, true)
|
|
clientConfig := tls.Config{
|
|
InsecureSkipVerify: true,
|
|
Certificates: []tls.Certificate{clientCert0},
|
|
}
|
|
|
|
tlspubkey, err := store.ParseTLSPublicKeyCert(clientCert0.Certificate[0])
|
|
tcheck(t, err, "parse certificate")
|
|
tlspubkey.Account = "mjl"
|
|
tlspubkey.LoginAddress = "mjl@mox.example"
|
|
tlspubkey.NoIMAPPreauth = true
|
|
|
|
addClientCert := func() error {
|
|
return store.TLSPublicKeyAdd(ctxbg, &tlspubkey)
|
|
}
|
|
|
|
// No preauth, explicit authenticate with TLS.
|
|
tc = startArgsMore(t, true, true, nil, &clientConfig, false, true, true, "mjl", addClientCert)
|
|
if tc.client.Preauth {
|
|
t.Fatalf("preauthentication while not configured for tls public key")
|
|
}
|
|
tc.transactf("ok", "authenticate external ")
|
|
tc.close()
|
|
|
|
// External with explicit username.
|
|
tc = startArgsMore(t, true, true, nil, &clientConfig, false, true, true, "mjl", addClientCert)
|
|
if tc.client.Preauth {
|
|
t.Fatalf("preauthentication while not configured for tls public key")
|
|
}
|
|
tc.transactf("ok", "authenticate external %s", base64.StdEncoding.EncodeToString([]byte("mjl@mox.example")))
|
|
tc.close()
|
|
|
|
// No preauth, also allow other mechanisms.
|
|
tc = startArgsMore(t, true, true, nil, &clientConfig, false, true, true, "mjl", addClientCert)
|
|
tc.transactf("ok", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000"+password0)))
|
|
tc.close()
|
|
|
|
// No preauth, also allow other username for same account.
|
|
tc = startArgsMore(t, true, true, nil, &clientConfig, false, true, true, "mjl", addClientCert)
|
|
tc.transactf("ok", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000móx@mox.example\u0000"+password0)))
|
|
tc.close()
|
|
|
|
// No preauth, other mechanism must be for same account.
|
|
acc, err := store.OpenAccount(pkglog, "other")
|
|
tcheck(t, err, "open account")
|
|
err = acc.SetPassword(pkglog, "test1234")
|
|
tcheck(t, err, "set password")
|
|
tc = startArgsMore(t, true, true, nil, &clientConfig, false, true, true, "mjl", addClientCert)
|
|
tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000other@mox.example\u0000test1234")))
|
|
tc.close()
|
|
|
|
// Starttls and external auth.
|
|
tc = startArgsMore(t, true, false, nil, &clientConfig, false, true, true, "mjl", addClientCert)
|
|
tc.client.Starttls(&clientConfig)
|
|
tc.transactf("ok", "authenticate external =")
|
|
tc.close()
|
|
|
|
tlspubkey.NoIMAPPreauth = false
|
|
err = store.TLSPublicKeyUpdate(ctxbg, &tlspubkey)
|
|
tcheck(t, err, "update tls public key")
|
|
|
|
// With preauth, no authenticate command needed/allowed.
|
|
// Already set up tls session ticket cache, for next test.
|
|
serverConfig := tls.Config{
|
|
Certificates: []tls.Certificate{fakeCert(t, false)},
|
|
}
|
|
ctx, cancel := context.WithCancel(ctxbg)
|
|
defer cancel()
|
|
mox.StartTLSSessionTicketKeyRefresher(ctx, pkglog, &serverConfig)
|
|
clientConfig.ClientSessionCache = tls.NewLRUClientSessionCache(10)
|
|
tc = startArgsMore(t, true, true, &serverConfig, &clientConfig, false, true, true, "mjl", addClientCert)
|
|
if !tc.client.Preauth {
|
|
t.Fatalf("not preauthentication while configured for tls public key")
|
|
}
|
|
cs := tc.conn.(*tls.Conn).ConnectionState()
|
|
if cs.DidResume {
|
|
t.Fatalf("tls connection was resumed")
|
|
}
|
|
tc.transactf("no", "authenticate external ") // Not allowed, already in authenticated state.
|
|
tc.close()
|
|
|
|
// Authentication works with TLS resumption.
|
|
tc = startArgsMore(t, true, true, &serverConfig, &clientConfig, false, true, true, "mjl", addClientCert)
|
|
if !tc.client.Preauth {
|
|
t.Fatalf("not preauthentication while configured for tls public key")
|
|
}
|
|
cs = tc.conn.(*tls.Conn).ConnectionState()
|
|
if !cs.DidResume {
|
|
t.Fatalf("tls connection was not resumed")
|
|
}
|
|
// Check that operations that require an account work.
|
|
tc.client.Enable("imap4rev2")
|
|
received, err := time.Parse(time.RFC3339, "2022-11-16T10:01:00+01:00")
|
|
tc.check(err, "parse time")
|
|
tc.client.Append("inbox", nil, &received, []byte(exampleMsg))
|
|
tc.client.Select("inbox")
|
|
tc.close()
|
|
|
|
// Authentication with unknown key should fail.
|
|
// todo: less duplication, change startArgs so this can be merged into it.
|
|
err = store.Close()
|
|
tcheck(t, err, "store close")
|
|
os.RemoveAll("../testdata/imap/data")
|
|
err = store.Init(ctxbg)
|
|
tcheck(t, err, "store init")
|
|
mox.Context = ctxbg
|
|
mox.ConfigStaticPath = filepath.FromSlash("../testdata/imap/mox.conf")
|
|
mox.MustLoadConfig(true, false)
|
|
switchStop := store.Switchboard()
|
|
defer switchStop()
|
|
|
|
serverConn, clientConn := net.Pipe()
|
|
defer clientConn.Close()
|
|
|
|
done := make(chan struct{})
|
|
defer func() { <-done }()
|
|
connCounter++
|
|
cid := connCounter
|
|
go func() {
|
|
defer serverConn.Close()
|
|
serve("test", cid, &serverConfig, serverConn, true, false, false)
|
|
close(done)
|
|
}()
|
|
|
|
clientConfig.ClientSessionCache = nil
|
|
clientConn = tls.Client(clientConn, &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)
|
|
}
|
|
}
|