package imap import ( "bufio" "context" "crypto/tls" "net" "strings" "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 := fakeMailboxStore{mailboxes: map[string][]Message{ "INBOX": {{SequenceNumber: 1, UID: 101, Flags: []string{}}}, }} 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 } } // sendTaggedOn sendet ein getaggtes Kommando über einen beliebigen // net.Conn (Klartext oder TLS) und liest bis zur getaggten // Abschlusszeile. func sendTaggedOn(t *testing.T, conn net.Conn, reader *bufio.Reader, tag, command string) (lines []string) { t.Helper() if _, err := conn.Write([]byte(tag + " " + command + "\r\n")); err != nil { t.Fatalf("kommando senden: %v", err) } for { _ = conn.SetReadDeadline(time.Now().Add(3 * time.Second)) line, err := reader.ReadString('\n') if err != nil { t.Fatalf("antwort lesen: %v", err) } line = strings.TrimRight(line, "\r\n") lines = append(lines, line) if strings.HasPrefix(line, tag+" ") { return lines } } } // TestLogin_RequiresTLS ist die geforderte Pflichtprüfung 2 (ING-06): // Login-Versuch ohne TLS/STARTTLS wird verweigert, sobald der Server // TLS anbietet (Akzeptanzkriterium 1). func TestLogin_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') lines := sendTaggedOn(t, conn, reader, "A1", "LOGIN alice geheim123") last := lines[len(lines)-1] if !strings.Contains(last, "NO") { t.Fatalf("erwartete NO (TLS erforderlich), habe: %v", lines) } } // TestStartTLS_UpgradesConnectionAndAllowsLogin belegt, dass LOGIN nach // erfolgreichem STARTTLS akzeptiert wird. func TestStartTLS_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') lines := sendTaggedOn(t, conn, reader, "A1", "STARTTLS") if !strings.Contains(lines[len(lines)-1], "OK") { t.Fatalf("erwartete OK für STARTTLS, habe: %v", lines) } tlsConn := tls.Client(conn, &tls.Config{InsecureSkipVerify: true}) if err := tlsConn.Handshake(); err != nil { t.Fatalf("tls-handshake: %v", err) } tlsReader := bufio.NewReader(tlsConn) lines = sendTaggedOn(t, tlsConn, tlsReader, "A2", "LOGIN alice geheim123") if !strings.Contains(lines[len(lines)-1], "OK") { t.Fatalf("erwartete OK nach STARTTLS, habe: %v", lines) } } // TestTLS_CertificateRotationWithoutDroppingExistingSessions ist die // geforderte Pflichtprüfung 3 (ING-06): eine bereits etablierte // TLS-Session bleibt nach einer Zertifikatsrotation 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() 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') sendTaggedOn(t, firstConn, r1, "A1", "STARTTLS") firstTLS := tls.Client(firstConn, &tls.Config{InsecureSkipVerify: true}) if err := firstTLS.Handshake(); err != nil { t.Fatalf("tls-handshake 1: %v", err) } if string(firstTLS.ConnectionState().PeerCertificates[0].Raw) != string(certA.Certificate[0]) { t.Fatalf("erste verbindung hätte zertifikat a bekommen sollen") } store.Replace(certB) 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') sendTaggedOn(t, secondConn, r2, "A1", "STARTTLS") secondTLS := tls.Client(secondConn, &tls.Config{InsecureSkipVerify: true}) if err := secondTLS.Handshake(); err != nil { t.Fatalf("tls-handshake 2: %v", err) } if string(secondTLS.ConnectionState().PeerCertificates[0].Raw) != string(certB.Certificate[0]) { t.Fatalf("zweite verbindung hätte NACH der rotation zertifikat b bekommen sollen") } // Erste, bereits etablierte Verbindung bleibt ungestört nutzbar. tlsReader1 := bufio.NewReader(firstTLS) lines := sendTaggedOn(t, firstTLS, tlsReader1, "A2", "LOGIN alice geheim123") if !strings.Contains(lines[len(lines)-1], "OK") { t.Fatalf("erste verbindung nach rotation nicht mehr funktionsfähig: %v", lines) } }