package mtls import ( "crypto/tls" "io" "net/http" "net/http/httptest" "testing" "time" ) func newTLSServer(t *testing.T, cfg *tls.Config) *httptest.Server { t.Helper() server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("ok")) })) server.TLS = cfg server.StartTLS() return server } func fetch(t *testing.T, url string, clientCfg *tls.Config) (int, error) { t.Helper() client := &http.Client{ Transport: &http.Transport{TLSClientConfig: clientCfg}, Timeout: 3 * time.Second, } resp, err := client.Get(url) if err != nil { return 0, err } defer resp.Body.Close() _, _ = io.ReadAll(resp.Body) return resp.StatusCode, nil } // Akzeptanzkriterium 1: Zertifikatsverteilung ist ein reiner // Funktionsaufruf, das Ergebnis ist gueltiges, wiederverwendbares // PEM-Material. func TestIssueCert_ProducesValidPEMRoundTrip(t *testing.T) { authority, err := NewAuthority() if err != nil { t.Fatalf("newauthority: %v", err) } issued, err := authority.IssueCert("dms", time.Hour) if err != nil { t.Fatalf("issuecert: %v", err) } if _, err := issued.TLSCertificate(); err != nil { t.Fatalf("ausgestelltes zertifikat nicht als tls.Certificate ladbar: %v", err) } } // Akzeptanzkriterium 2 + Pruefung 1: eine Verbindung ohne gueltiges // Client-Zertifikat wird abgelehnt, eine mit gueltigem angenommen. func TestServerTLSConfig_RejectsMissingClientCert(t *testing.T) { authority, err := NewAuthority() if err != nil { t.Fatalf("newauthority: %v", err) } serverIssued, err := authority.IssueCert("core", time.Hour) if err != nil { t.Fatalf("server-zertifikat ausstellen: %v", err) } serverCert, err := serverIssued.TLSCertificate() if err != nil { t.Fatalf("server-zertifikat laden: %v", err) } server := newTLSServer(t, ServerTLSConfig(ModeRequired, serverCert, authority)) defer server.Close() // Client OHNE Zertifikat, aber mit korrektem RootCA-Vertrauen fuer den // Server — der Handshake muss trotzdem an der fehlenden Client-Auth // scheitern. _, err = fetch(t, server.URL, &tls.Config{RootCAs: authority.TrustPool()}) if err == nil { t.Fatal("erwartet fehlschlagenden handshake ohne client-zertifikat") } clientIssued, err := authority.IssueCert("mail", time.Hour) if err != nil { t.Fatalf("client-zertifikat ausstellen: %v", err) } clientCert, err := clientIssued.TLSCertificate() if err != nil { t.Fatalf("client-zertifikat laden: %v", err) } status, err := fetch(t, server.URL, ClientTLSConfig(ModeRequired, clientCert, authority)) if err != nil { t.Fatalf("erwartet erfolgreiche verbindung mit gueltigem client-zertifikat: %v", err) } if status != http.StatusOK { t.Fatalf("status = %d, want 200", status) } } // Akzeptanzkriterium 3 + Pruefung 2: Rotation der CA unterbricht laufenden // Betrieb nicht — ein VOR der Rotation ausgestelltes Leaf-Zertifikat // funktioniert danach weiter, UND ein NACH der Rotation neu ausgestelltes // funktioniert ebenfalls, gegen denselben, weiterlaufenden Server (kein // Neustart noetig, da TrustPool() denselben *x509.CertPool zurueckliefert, // den Rotate() erweitert). func TestAuthority_RotateWithoutDowntime(t *testing.T) { authority, err := NewAuthority() if err != nil { t.Fatalf("newauthority: %v", err) } serverIssued, err := authority.IssueCert("core", time.Hour) if err != nil { t.Fatalf("server-zertifikat ausstellen: %v", err) } serverCert, err := serverIssued.TLSCertificate() if err != nil { t.Fatalf("server-zertifikat laden: %v", err) } server := newTLSServer(t, ServerTLSConfig(ModeRequired, serverCert, authority)) defer server.Close() // EIN Server-Prozess ueber die gesamte Rotation hinweg — kein Neustart. preRotationIssued, err := authority.IssueCert("dms", time.Hour) if err != nil { t.Fatalf("client-zertifikat (vor rotation) ausstellen: %v", err) } preRotationCert, _ := preRotationIssued.TLSCertificate() if err := authority.Rotate(); err != nil { t.Fatalf("rotate: %v", err) } // VOR der Rotation ausgestelltes Zertifikat funktioniert WEITERHIN, ohne // dass der Server neu gestartet wurde. status, err := fetch(t, server.URL, ClientTLSConfig(ModeRequired, preRotationCert, authority)) if err != nil { t.Fatalf("vor-rotation-zertifikat haette weiterhin funktionieren sollen: %v", err) } if status != http.StatusOK { t.Fatalf("status = %d, want 200 (vor-rotation-zertifikat)", status) } // NACH der Rotation neu ausgestelltes Zertifikat funktioniert ebenfalls, // gegen DENSELBEN laufenden Server. postRotationIssued, err := authority.IssueCert("mail", time.Hour) if err != nil { t.Fatalf("client-zertifikat (nach rotation) ausstellen: %v", err) } postRotationCert, _ := postRotationIssued.TLSCertificate() status, err = fetch(t, server.URL, ClientTLSConfig(ModeRequired, postRotationCert, authority)) if err != nil { t.Fatalf("nach-rotation-zertifikat haette funktionieren sollen: %v", err) } if status != http.StatusOK { t.Fatalf("status = %d, want 200 (nach-rotation-zertifikat)", status) } } // Akzeptanzkriterium 3 + Pruefung 3: Stufe-1-Betrieb (ModeOff) funktioniert // weiterhin OHNE mTLS-Zwang — ein Client ganz ohne Zertifikat kommt durch. func TestServerTLSConfig_ModeOffWorksWithoutClientCert(t *testing.T) { authority, err := NewAuthority() if err != nil { t.Fatalf("newauthority: %v", err) } serverIssued, err := authority.IssueCert("core", time.Hour) if err != nil { t.Fatalf("server-zertifikat ausstellen: %v", err) } serverCert, err := serverIssued.TLSCertificate() if err != nil { t.Fatalf("server-zertifikat laden: %v", err) } server := newTLSServer(t, ServerTLSConfig(ModeOff, serverCert, authority)) defer server.Close() status, err := fetch(t, server.URL, ClientTLSConfig(ModeOff, tls.Certificate{}, authority)) if err != nil { t.Fatalf("stufe-1-betrieb (ModeOff) haette ohne client-zertifikat funktionieren sollen: %v", err) } if status != http.StatusOK { t.Fatalf("status = %d, want 200", status) } }