Files
nexarch/mail/internal/tlscert/tlscert.go
T
sysops b22ab67bb2 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.
2026-09-01 01:17:40 +02:00

105 lines
3.5 KiB
Go

// Package tlscert bündelt die für ING-06 gemeinsam benötigte
// TLS-Grundlage von IMAP-, POP3- und SMTP-Server: ein hot-reloadbares
// Zertifikat (Akzeptanzkriterium 3: Zertifikatswechsel ohne
// Verbindungsabriss für bestehende Sessions) und eine gehärtete
// tls.Config (Akzeptanzkriterium 2: keine schwachen Cipher-Suiten,
// keine veralteten TLS-Versionen).
package tlscert
import (
"context"
"crypto/tls"
"fmt"
"net"
"sync"
)
// Store hält das aktuell aktive Zertifikat. GetCertificate wird von
// crypto/tls bei JEDEM neuen Handshake aufgerufen und liefert dadurch
// immer den aktuellen Stand — ein Replace wirkt sich nur auf NEUE
// Handshakes aus, bereits etablierte Verbindungen laufen mit ihrem beim
// Handshake ausgehandelten Zertifikat unverändert weiter (kein
// Verbindungsabriss, Akzeptanzkriterium 3).
type Store struct {
mu sync.RWMutex
cert *tls.Certificate
}
// NewStore erstellt einen Store mit einem bereits geladenen Zertifikat.
func NewStore(cert tls.Certificate) *Store {
return &Store{cert: &cert}
}
// LoadFromFiles lädt Zertifikat und privaten Schlüssel aus PEM-Dateien.
// Die Pfade kommen ausschließlich vom Aufrufer (Umgebungsvariable),
// nicht aus dem Code.
func LoadFromFiles(certPath, keyPath string) (*Store, error) {
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return nil, fmt.Errorf("tlscert: zertifikat laden: %w", err)
}
return NewStore(cert), nil
}
// Replace tauscht das aktive Zertifikat atomar aus.
func (s *Store) Replace(cert tls.Certificate) {
s.mu.Lock()
defer s.mu.Unlock()
s.cert = &cert
}
// ReplaceFromFiles lädt ein neues Zertifikat aus Dateien und ersetzt
// damit das aktive (z. B. nach Zertifikatserneuerung durch eine externe
// ACME-Komponente).
func (s *Store) ReplaceFromFiles(certPath, keyPath string) error {
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return fmt.Errorf("tlscert: zertifikat laden: %w", err)
}
s.Replace(cert)
return nil
}
// GetCertificate erfüllt tls.Config.GetCertificate.
func (s *Store) GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) {
s.mu.RLock()
defer s.mu.RUnlock()
return s.cert, nil
}
// hardenedCipherSuites sind ausschließlich AEAD-Suiten für TLS 1.2
// (kein CBC, kein RC4, kein 3DES) — Akzeptanzkriterium 2. TLS 1.3 hat
// ohnehin nur feste, starke Suiten und braucht keine explizite Liste.
var hardenedCipherSuites = []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
}
// Config liefert eine gehärtete tls.Config: TLS 1.2 als Mindestversion
// (Akzeptanzkriterium 2, deaktiviert SSLv3/TLS1.0/TLS1.1), ausschließlich
// AEAD-Cipher-Suiten für TLS 1.2, Zertifikat immer über s.GetCertificate
// aufgelöst (Akzeptanzkriterium 3).
func (s *Store) Config() *tls.Config {
return &tls.Config{
MinVersion: tls.VersionTLS12,
CipherSuites: hardenedCipherSuites,
GetCertificate: s.GetCertificate,
}
}
// UpgradeServer führt den TLS-Serverhandschlag auf einer bestehenden
// Klartextverbindung durch (STARTTLS-Muster, gemeinsam genutzt von
// IMAP-, POP3- und SMTP-Session) und liefert die resultierende
// *tls.Conn.
func UpgradeServer(ctx context.Context, conn net.Conn, cfg *tls.Config) (*tls.Conn, error) {
tlsConn := tls.Server(conn, cfg)
if err := tlsConn.HandshakeContext(ctx); err != nil {
return nil, err
}
return tlsConn, nil
}