feat(mail): ING-06 TLS/STARTTLS-Absicherung für IMAP/POP3/SMTP

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.
This commit is contained in:
sysops
2026-09-01 01:17:40 +02:00
parent 6631bcbbdd
commit b22ab67bb2
16 changed files with 1263 additions and 38 deletions
+46 -1
View File
@@ -1,15 +1,22 @@
package imap
import (
"bufio"
"context"
"fmt"
"strconv"
"strings"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/tlscert"
)
// handleCapability ist in jedem Zustand erlaubt (RFC 3501 §6.1.1).
func (s *Session) handleCapability(cmd command) bool {
if err := writeUntagged(s.writer, "CAPABILITY IMAP4rev1"); err != nil {
caps := "CAPABILITY IMAP4rev1"
if s.tlsConfig != nil && !s.tlsActive {
caps += " STARTTLS"
}
if err := writeUntagged(s.writer, caps); err != nil {
return false
}
return s.writeErr(cmd.Tag, "OK", "CAPABILITY completed")
@@ -24,6 +31,12 @@ func (s *Session) handleLogin(ctx context.Context, cmd command) bool {
if len(cmd.Args) != 2 {
return s.writeErr(cmd.Tag, "BAD", "LOGIN requires username and password")
}
if s.tlsConfig != nil && !s.tlsActive {
// Akzeptanzkriterium 1 (ING-06): Anmeldedaten werden
// ausschließlich über TLS oder nach erfolgreichem STARTTLS
// akzeptiert, sobald der Server TLS überhaupt anbietet.
return s.writeErr(cmd.Tag, "NO", "LOGIN disabled without TLS, use STARTTLS")
}
if s.auth == nil {
return s.writeErr(cmd.Tag, "NO", "LOGIN not available")
}
@@ -154,6 +167,38 @@ func (s *Session) handleLogout(cmd command) bool {
return false
}
// handleStartTLS implementiert STARTTLS (RFC 3501 §6.2.1): TLS-Aufwertung
// der bestehenden Klartextverbindung, nur vor der Anmeldung erlaubt.
func (s *Session) handleStartTLS(ctx context.Context, cmd command) bool {
if s.tlsConfig == nil {
return s.writeErr(cmd.Tag, "NO", "STARTTLS not available")
}
if s.tlsActive {
return s.writeErr(cmd.Tag, "BAD", "TLS already active")
}
if s.state != NotAuthenticated {
return s.writeErr(cmd.Tag, "BAD", "STARTTLS not allowed in "+s.state.String()+" state")
}
if !s.writeErr(cmd.Tag, "OK", "Begin TLS negotiation now") {
return false
}
tlsConn, err := tlscert.UpgradeServer(ctx, s.conn, s.tlsConfig)
if err != nil {
// Handshake fehlgeschlagen -> Verbindung beenden, kein Rückfall
// auf Klartext.
return false
}
s.conn = tlsConn
// Reader/Writer NEU aufsetzen: verhindert, dass vor dem Handshake
// gepufferte Klartextdaten nach dem Wechsel noch als Kommandos
// verarbeitet werden (Command-Injection-Schutz).
s.reader = bufio.NewReaderSize(tlsConn, maxCommandLineBytes)
s.writer = bufio.NewWriter(tlsConn)
s.tlsActive = true
return true
}
// parseSequenceSet unterstützt die für FETCH gebräuchlichsten Formen:
// eine einzelne Zahl ("1"), eine kommagetrennte Liste ("1,3,5") und einen
// Bereich mit "*" als offenem Ende ("1:*"), aufgelöst gegen maxSeq (die