Neues Paket mail/internal/tlscert: hot-reloadbarer Zertifikat-Store (Store.GetCertificate wird bei jedem neuen TLS-Handshake aufgerufen, Replace tauscht atomar aus — bestehende Verbindungen bleiben mit ihrem ausgehandelten Zertifikat unberührt, Akzeptanzkriterium 3) sowie eine gehärtete tls.Config (MinVersion TLS 1.2, ausschließlich AEAD-Suiten für TLS 1.2, Akzeptanzkriterium 2). UpgradeServer führt den STARTTLS-Handschlag durch, gemeinsam genutzt von allen drei Protokollen. IMAP bekommt STARTTLS (RFC 3501), POP3 STLS (RFC 2595), SMTP STARTTLS (RFC 3207) — jeweils nur vor der Anmeldung erlaubt, Reader/Writer nach dem Handschlag neu aufgesetzt (Schutz vor Command-Injection durch vor dem Handshake gepufferte Klartextdaten). LOGIN (IMAP) und PASS (POP3) werden zurückgewiesen, solange der Server TLS anbietet, die Verbindung aber weder implizit noch per STARTTLS verschlüsselt ist (Akzeptanzkriterium 1). Implizites TLS (Port 993/995/465) braucht keine Codeänderung — Server.Serve nimmt jeden net.Listener entgegen, ein tls.NewListener-gewrapptes Listener liefert bereits *tls.Conn, von der Session per Typ-Assertion erkannt. Ohne TLS-Konfiguration bleibt das bisherige Klartextverhalten unverändert (Rückwärtskompatibilität zu ING-01/ING-02/ING-03). Alle drei Pflichtprüfungen mit echten Nachweisen durchgeführt: echter openssl-s_client-Scan gegen den laufenden SMTP-Server (TLS 1.3, starke AEAD-Suite bei normaler Verbindung; kein Cipher ausgehandelt bei erzwungenen CBC-Suiten) ergänzt um automatisierte crypto/tls-Negativtests (veraltete Version, schwache Suite — openssl 3.5.6 auf diesem Host verweigert das Erzwingen von Legacy-TLS clientseitig, im Prüfprotokoll begründet); Login-ohne-TLS wird in IMAP und POP3 nachweislich verweigert, nach STARTTLS/STLS nachweislich akzeptiert; Zertifikatsrotation im laufenden Betrieb in allen drei Protokollen ohne Unterbrechung bestehender Sessions, neue Verbindungen bekommen sofort das neue Zertifikat. go build/go vet/golangci-lint clean, gesamtes Mail-Modul (~28 Pakete) regressionsfrei getestet.
186 lines
5.8 KiB
Go
186 lines
5.8 KiB
Go
package pop3
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"crypto/tls"
|
|
"net"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/mail/internal/protoguard"
|
|
"gitea.perlbach24.de/scripte/nexarch/mail/internal/tlscert"
|
|
)
|
|
|
|
func startTLSTestServer(t *testing.T, tlsConfig *tls.Config) (addr string, stop func()) {
|
|
t.Helper()
|
|
auth := fakeAuthenticator{users: map[string]string{"alice": "geheim123"}}
|
|
store := newFakeMailboxStore()
|
|
srv := NewServerWithGuardAndTLSConfig(auth, store, protoguard.DefaultConfig(), tlsConfig)
|
|
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("listener: %v", err)
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
done := make(chan struct{})
|
|
go func() {
|
|
_ = srv.Serve(ctx, listener)
|
|
close(done)
|
|
}()
|
|
return listener.Addr().String(), func() {
|
|
cancel()
|
|
<-done
|
|
}
|
|
}
|
|
|
|
// TestPass_RequiresTLS ist die geforderte Pflichtprüfung 2 (ING-06):
|
|
// Login-Versuch (hier: PASS) ohne TLS/STLS wird verweigert, sobald der
|
|
// Server TLS anbietet (Akzeptanzkriterium 1).
|
|
func TestPass_RequiresTLS(t *testing.T) {
|
|
cert, err := tlscert.GenerateSelfSigned("127.0.0.1")
|
|
if err != nil {
|
|
t.Fatalf("testzertifikat erzeugen: %v", err)
|
|
}
|
|
store := tlscert.NewStore(cert)
|
|
addr, stop := startTLSTestServer(t, store.Config())
|
|
defer stop()
|
|
|
|
conn, err := net.DialTimeout("tcp", addr, 2*time.Second)
|
|
if err != nil {
|
|
t.Fatalf("dial: %v", err)
|
|
}
|
|
defer func() { _ = conn.Close() }()
|
|
reader := bufio.NewReader(conn)
|
|
_, _ = reader.ReadString('\n')
|
|
|
|
_, _ = conn.Write([]byte("USER alice\r\n"))
|
|
_, _ = reader.ReadString('\n')
|
|
_, _ = conn.Write([]byte("PASS geheim123\r\n"))
|
|
resp, err := reader.ReadString('\n')
|
|
if err != nil {
|
|
t.Fatalf("antwort lesen: %v", err)
|
|
}
|
|
if resp[0] != '-' {
|
|
t.Fatalf("erwartete -ERR (TLS erforderlich), habe: %q", resp)
|
|
}
|
|
}
|
|
|
|
// TestStls_UpgradesConnectionAndAllowsLogin belegt, dass PASS nach
|
|
// erfolgreichem STLS akzeptiert wird — die Kehrseite von
|
|
// TestPass_RequiresTLS.
|
|
func TestStls_UpgradesConnectionAndAllowsLogin(t *testing.T) {
|
|
cert, err := tlscert.GenerateSelfSigned("127.0.0.1")
|
|
if err != nil {
|
|
t.Fatalf("testzertifikat erzeugen: %v", err)
|
|
}
|
|
store := tlscert.NewStore(cert)
|
|
addr, stop := startTLSTestServer(t, store.Config())
|
|
defer stop()
|
|
|
|
conn, err := net.DialTimeout("tcp", addr, 2*time.Second)
|
|
if err != nil {
|
|
t.Fatalf("dial: %v", err)
|
|
}
|
|
defer func() { _ = conn.Close() }()
|
|
reader := bufio.NewReader(conn)
|
|
_, _ = reader.ReadString('\n')
|
|
|
|
_, _ = conn.Write([]byte("STLS\r\n"))
|
|
resp, _ := reader.ReadString('\n')
|
|
if resp[0] != '+' {
|
|
t.Fatalf("erwartete +OK für STLS, habe: %q", resp)
|
|
}
|
|
|
|
tlsConn := tls.Client(conn, &tls.Config{InsecureSkipVerify: true})
|
|
if err := tlsConn.Handshake(); err != nil {
|
|
t.Fatalf("tls-handshake: %v", err)
|
|
}
|
|
tlsReader := bufio.NewReader(tlsConn)
|
|
|
|
_, _ = tlsConn.Write([]byte("USER alice\r\n"))
|
|
_, _ = tlsReader.ReadString('\n')
|
|
_, _ = tlsConn.Write([]byte("PASS geheim123\r\n"))
|
|
resp, err = tlsReader.ReadString('\n')
|
|
if err != nil {
|
|
t.Fatalf("antwort über tls lesen: %v", err)
|
|
}
|
|
if resp[0] != '+' {
|
|
t.Fatalf("erwartete +OK nach STLS, habe: %q", resp)
|
|
}
|
|
}
|
|
|
|
// TestTLS_CertificateRotationWithoutDroppingExistingSessions ist die
|
|
// geforderte Pflichtprüfung 3 (ING-06): Zertifikatsrotation im
|
|
// laufenden Betrieb ohne Dienstunterbrechung — eine bereits etablierte
|
|
// TLS-Session bleibt nach dem Wechsel nutzbar, eine NEUE Verbindung
|
|
// bekommt das neue Zertifikat.
|
|
func TestTLS_CertificateRotationWithoutDroppingExistingSessions(t *testing.T) {
|
|
certA, err := tlscert.GenerateSelfSigned("127.0.0.1")
|
|
if err != nil {
|
|
t.Fatalf("zertifikat a erzeugen: %v", err)
|
|
}
|
|
certB, err := tlscert.GenerateSelfSigned("127.0.0.1")
|
|
if err != nil {
|
|
t.Fatalf("zertifikat b erzeugen: %v", err)
|
|
}
|
|
store := tlscert.NewStore(certA)
|
|
addr, stop := startTLSTestServer(t, store.Config())
|
|
defer stop()
|
|
|
|
// Erste Verbindung, VOR der Rotation aufgebaut, wird offen gehalten.
|
|
firstConn, err := net.DialTimeout("tcp", addr, 2*time.Second)
|
|
if err != nil {
|
|
t.Fatalf("dial 1: %v", err)
|
|
}
|
|
defer func() { _ = firstConn.Close() }()
|
|
r1 := bufio.NewReader(firstConn)
|
|
_, _ = r1.ReadString('\n')
|
|
_, _ = firstConn.Write([]byte("STLS\r\n"))
|
|
_, _ = r1.ReadString('\n')
|
|
firstTLS := tls.Client(firstConn, &tls.Config{InsecureSkipVerify: true})
|
|
if err := firstTLS.Handshake(); err != nil {
|
|
t.Fatalf("tls-handshake 1: %v", err)
|
|
}
|
|
firstPeerCert := firstTLS.ConnectionState().PeerCertificates[0].Raw
|
|
if string(firstPeerCert) != string(certA.Certificate[0]) {
|
|
t.Fatalf("erste verbindung hätte zertifikat a bekommen sollen")
|
|
}
|
|
|
|
// Rotation im laufenden Betrieb.
|
|
store.Replace(certB)
|
|
|
|
// Zweite, NEUE Verbindung bekommt das neue Zertifikat.
|
|
secondConn, err := net.DialTimeout("tcp", addr, 2*time.Second)
|
|
if err != nil {
|
|
t.Fatalf("dial 2: %v", err)
|
|
}
|
|
defer func() { _ = secondConn.Close() }()
|
|
r2 := bufio.NewReader(secondConn)
|
|
_, _ = r2.ReadString('\n')
|
|
_, _ = secondConn.Write([]byte("STLS\r\n"))
|
|
_, _ = r2.ReadString('\n')
|
|
secondTLS := tls.Client(secondConn, &tls.Config{InsecureSkipVerify: true})
|
|
if err := secondTLS.Handshake(); err != nil {
|
|
t.Fatalf("tls-handshake 2: %v", err)
|
|
}
|
|
secondPeerCert := secondTLS.ConnectionState().PeerCertificates[0].Raw
|
|
if string(secondPeerCert) != string(certB.Certificate[0]) {
|
|
t.Fatalf("zweite verbindung hätte NACH der rotation zertifikat b bekommen sollen")
|
|
}
|
|
|
|
// Erste, bereits etablierte Verbindung ist nach der Rotation weiter
|
|
// UNGESTÖRT nutzbar (kein Verbindungsabriss).
|
|
tlsReader1 := bufio.NewReader(firstTLS)
|
|
_, _ = firstTLS.Write([]byte("USER alice\r\n"))
|
|
_, _ = tlsReader1.ReadString('\n')
|
|
_, _ = firstTLS.Write([]byte("PASS geheim123\r\n"))
|
|
resp, err := tlsReader1.ReadString('\n')
|
|
if err != nil {
|
|
t.Fatalf("erste verbindung nach rotation unterbrochen: %v", err)
|
|
}
|
|
if resp[0] != '+' {
|
|
t.Fatalf("erste verbindung nach rotation nicht mehr funktionsfähig: %q", resp)
|
|
}
|
|
}
|