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) } }