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