Alte Schannel-Stacks brechen an einem TLS-1.3-ClientHello kommentarlos ab: sie nehmen die TCP-Verbindung an, lesen den ClientHello und setzen zurueck, ohne ein Zertifikat zu schicken. Herunterhandeln hilft dann nicht — TLS 1.3 darf gar nicht erst angeboten werden. max_tls_version (Default 1.3) deckelt die Hoechstversion; die Validierung lehnt eine Hoechstversion unterhalb der Mindestversion ab. check erkennt zusaetzlich zurueckgesetzte Verbindungen und nennt die beiden plausiblen Ursachen: fehlendes LDAPS-Zertifikat auf dem Server oder TLS-1.3-Inkompatibilitaet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NBHF4R9EAejDJUMdwr6C68
132 lines
4.1 KiB
Go
132 lines
4.1 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/go-ldap/ldap/v3"
|
|
)
|
|
|
|
// conn ist der Ausschnitt der LDAP-Verbindung, den der Authenticator braucht.
|
|
// Das Interface existiert, damit Tests ohne echten Verzeichnisdienst laufen.
|
|
type conn interface {
|
|
Bind(username, password string) error
|
|
Search(req *ldap.SearchRequest) (*ldap.SearchResult, error)
|
|
Close() error
|
|
}
|
|
|
|
// ldapConn adaptiert *ldap.Conn an conn (Close hat dort keine Fehlerrückgabe).
|
|
type ldapConn struct{ *ldap.Conn }
|
|
|
|
func (c ldapConn) Close() error { c.Conn.Close(); return nil }
|
|
|
|
// ParseTLSVersion übersetzt "1.0".."1.3" in die Konstante von crypto/tls.
|
|
// Ein leerer oder unbekannter Wert ergibt TLS 1.2 — im Zweifel das Sichere.
|
|
func ParseTLSVersion(s string) uint16 {
|
|
switch s {
|
|
case "1.0":
|
|
return tls.VersionTLS10
|
|
case "1.1":
|
|
return tls.VersionTLS11
|
|
case "1.3":
|
|
return tls.VersionTLS13
|
|
default:
|
|
return tls.VersionTLS12
|
|
}
|
|
}
|
|
|
|
// tlsConfigFor baut die TLS-Konfiguration. Die Zertifikatsprüfung ist immer
|
|
// aktiv — für LDAP gibt es bewusst keine Insecure-Option. Lediglich die
|
|
// Protokoll-Mindestversion ist konfigurierbar, damit Altsysteme wie ein
|
|
// Windows Server 2012 R2 ohne TLS 1.2 erreichbar bleiben.
|
|
func tlsConfigFor(server, caFile string, minVersion, maxVersion uint16) (*tls.Config, error) {
|
|
if minVersion == 0 {
|
|
minVersion = tls.VersionTLS12
|
|
}
|
|
if maxVersion == 0 {
|
|
maxVersion = tls.VersionTLS13
|
|
}
|
|
if maxVersion < minVersion {
|
|
maxVersion = minVersion
|
|
}
|
|
cfg := &tls.Config{ServerName: server, MinVersion: minVersion, MaxVersion: maxVersion}
|
|
if minVersion < tls.VersionTLS12 {
|
|
// Go bietet die CBC-Suiten alter Schannel-Stacks nicht mehr von sich
|
|
// aus an. Ohne sie scheitert der Handshake mit einem 2012 R2, der auf
|
|
// TLS 1.0/1.1 festgenagelt ist, bereits an der Cipher-Auswahl.
|
|
cfg.CipherSuites = legacyCipherSuites()
|
|
}
|
|
if caFile == "" {
|
|
return cfg, nil
|
|
}
|
|
pem, err := os.ReadFile(caFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ad.ca_file %s: %w", caFile, err)
|
|
}
|
|
pool := x509.NewCertPool()
|
|
if !pool.AppendCertsFromPEM(pem) {
|
|
return nil, fmt.Errorf("ad.ca_file %s enthält kein gültiges PEM-Zertifikat", caFile)
|
|
}
|
|
cfg.RootCAs = pool
|
|
return cfg, nil
|
|
}
|
|
|
|
// legacyCipherSuites ergänzt Gos sichere Vorgaben um die CBC-SHA256-Suiten,
|
|
// die Go als unsicher einstuft und deshalb nicht von sich aus anbietet, die
|
|
// aber auf alten Schannel-Stacks vorkommen.
|
|
//
|
|
// Die Suiten mit reinem RSA-Schlüsselaustausch (TLS_RSA_*) sind bewusst NICHT
|
|
// dabei: Go schaltet sie seit 1.22 unabhängig von dieser Liste ab. Braucht ein
|
|
// Domain Controller sie zwingend, hilft nur GODEBUG=tlsrsakex=1 in der
|
|
// systemd-Unit — siehe README.
|
|
//
|
|
// Wird nur benutzt, wenn der Betreiber die Mindestversion ausdrücklich unter
|
|
// TLS 1.2 gesetzt hat.
|
|
func legacyCipherSuites() []uint16 {
|
|
out := make([]uint16, 0, len(tls.CipherSuites())+2)
|
|
for _, cs := range tls.CipherSuites() {
|
|
out = append(out, cs.ID)
|
|
}
|
|
return append(out,
|
|
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
|
|
)
|
|
}
|
|
|
|
// realDialer erzeugt die Dial-Funktion für den Produktivbetrieb.
|
|
func realDialer(port int, tlsMode, caFile string, minVersion, maxVersion uint16, timeout time.Duration) func(context.Context, string) (conn, error) {
|
|
return func(ctx context.Context, server string) (conn, error) {
|
|
tlsCfg, err := tlsConfigFor(server, caFile, minVersion, maxVersion)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
addr := net.JoinHostPort(server, strconv.Itoa(port))
|
|
dialer := &net.Dialer{Timeout: timeout}
|
|
|
|
var c *ldap.Conn
|
|
if tlsMode == "starttls" {
|
|
c, err = ldap.DialURL("ldap://"+addr, ldap.DialWithDialer(dialer))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Verbindung zu %s: %w", addr, err)
|
|
}
|
|
if err := c.StartTLS(tlsCfg); err != nil {
|
|
c.Close()
|
|
return nil, fmt.Errorf("StartTLS zu %s: %w", addr, err)
|
|
}
|
|
} else {
|
|
c, err = ldap.DialURL("ldaps://"+addr,
|
|
ldap.DialWithDialer(dialer), ldap.DialWithTLSConfig(tlsCfg))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("LDAPS-Verbindung zu %s: %w", addr, err)
|
|
}
|
|
}
|
|
c.SetTimeout(timeout)
|
|
return ldapConn{c}, nil
|
|
}
|
|
}
|