Files
mailgoserver/internal/imapserver/imapserver_tls_test.go
T

121 lines
4.1 KiB
Go

package imapserver_test
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"math/big"
"net"
"path/filepath"
"testing"
"time"
"github.com/emersion/go-imap/v2/imapclient"
"mailgoserver/internal/abuseguard"
"mailgoserver/internal/db"
"mailgoserver/internal/imapserver"
"mailgoserver/internal/mailstore"
)
// genTLSTestCert generates a throwaway self-signed cert for "127.0.0.1" — enough to
// exercise a real *tls.Conn handshake (the client skips verification instead of trusting
// a CA, since this test cares about the server-side connection-wrapping order, not
// certificate validation).
func genTLSTestCert(t *testing.T) tls.Certificate {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
template := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "127.0.0.1"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
if err != nil {
t.Fatal(err)
}
return tls.Certificate{Certificate: [][]byte{der}, PrivateKey: key}
}
// TestIMAPLoginOverTLSThroughAbuseguardListener reproduces main.go's real direct-TLS
// IMAP listener wiring: a raw TCP listener wrapped by abuseguard.GuardListener and
// imapserver.IdleTimeoutListener, with TLS applied outermost via tls.NewListener. This
// order matters: go-imap/v2 decides whether LOGIN is allowed (vs. LOGINDISABLED, "TLS is
// required to authenticate") via a c.conn.(*tls.Conn) type assertion on whatever
// Accept() returns — wrapping abuseguard's own conn type around an already-established
// tls.Conn (main.go's previous order) hides that from the library and makes every LOGIN
// fail before ever reaching Session.Login, even with correct credentials. Regression
// test for that bug.
func TestIMAPLoginOverTLSThroughAbuseguardListener(t *testing.T) {
dir := t.TempDir()
database, err := db.Open(filepath.Join(dir, "test.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { database.Close() })
domainID, err := database.CreateDomain("example.com")
if err != nil {
t.Fatal(err)
}
store := mailstore.New(database, mailstore.GenerateDEK(), t.TempDir())
dek := mailstore.GenerateDEK()
wrapped, nonce, err := store.WrapDEK(dek)
if err != nil {
t.Fatal(err)
}
portalHash, err := db.HashPassword("portal-password-unused")
if err != nil {
t.Fatal(err)
}
mailboxID, err := database.CreateMailbox("inbox@example.com", portalHash, domainID, 5*1024*1024*1024, wrapped, nonce)
if err != nil {
t.Fatal(err)
}
appPassword := db.GenerateAppPassword(25)
appHash, err := db.HashPassword(appPassword)
if err != nil {
t.Fatal(err)
}
if _, err := database.CreateAppPassword(mailboxID, "test client", appHash, nil); err != nil {
t.Fatal(err)
}
cert := genTLSTestCert(t)
tlsConfig := &tls.Config{Certificates: []tls.Certificate{cert}}
backend := &imapserver.Backend{DB: database, Mailstore: store}
tlsServer := imapserver.NewTLSServer(backend, tlsConfig)
rawListener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
guarded := imapserver.IdleTimeoutListener(abuseguard.GuardListener(rawListener, database, nil, nil), 30*time.Minute)
finalListener := tls.NewListener(guarded, tlsConfig)
go tlsServer.Serve(finalListener)
t.Cleanup(func() { tlsServer.Close() })
client, err := imapclient.DialTLS(finalListener.Addr().String(), &imapclient.Options{
TLSConfig: &tls.Config{InsecureSkipVerify: true},
})
if err != nil {
t.Fatalf("DialTLS: %v", err)
}
t.Cleanup(func() { client.Close() })
if err := client.Login("inbox@example.com", appPassword).Wait(); err != nil {
t.Fatalf("login over TLS through abuseguard.GuardListener: %v (this is exactly the bug: LOGIN rejected with PRIVACYREQUIRED because the wrapped conn no longer type-asserts to *tls.Conn)", err)
}
}