package crypto import ( "crypto/tls" "errors" "io" "log" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "sync/atomic" "testing" ) func newKnownServers(t *testing.T) *KnownServers { t.Helper() return &KnownServers{Path: filepath.Join(t.TempDir(), "known_servers")} } func TestKnownServers(t *testing.T) { ks := newKnownServers(t) sc, _ := LoadOrCreateServerCert(t.TempDir()) other, _ := LoadOrCreateServerCert(t.TempDir()) if _, ok, err := ks.Lookup("ingress:8443"); ok || err != nil { t.Fatalf("leere Datei: ok=%v err=%v", ok, err) } if err := ks.Add("ingress:8443", sc.Fingerprint); err != nil { t.Fatalf("Add: %v", err) } if fp, ok, err := ks.Lookup("ingress:8443"); !ok || err != nil || fp != sc.Fingerprint { t.Errorf("Lookup = %q, %v, %v", fp, ok, err) } if err := ks.Add("ingress:8443", sc.Fingerprint); err != nil { t.Errorf("gleichen Eintrag erneut hinzufügen: %v", err) } if err := ks.Add("ingress:8443", other.Fingerprint); !errors.Is(err, ErrFingerprintMismatch) { t.Errorf("Pin überschreiben: erwartet ErrFingerprintMismatch, bekommen %v", err) } if err := ks.Add("anderer:8443", other.Fingerprint); err != nil { t.Errorf("zweiter Server: %v", err) } data, _ := os.ReadFile(ks.Path) if n := strings.Count(string(data), "ingress:8443"); n != 1 { t.Errorf("ingress:8443 steht %d-mal in der Datei", n) } info, _ := os.Stat(ks.Path) if info.Mode().Perm() != 0o600 { t.Errorf("Dateirechte %o, erwartet 600", info.Mode().Perm()) } } func TestKnownServers_FileFormat(t *testing.T) { ks := newKnownServers(t) sc, _ := LoadOrCreateServerCert(t.TempDir()) content := "# Kommentar\n\n ingress:8443 " + string(sc.Fingerprint) + " \n" os.WriteFile(ks.Path, []byte(content), 0o600) if fp, ok, err := ks.Lookup("ingress:8443"); !ok || err != nil || fp != sc.Fingerprint { t.Errorf("Lookup = %q, %v, %v", fp, ok, err) } for name, bad := range map[string]string{ "ein Feld": "ingress:8443\n", "drei Felder": "ingress:8443 " + string(sc.Fingerprint) + " x\n", "kaputter Fingerprint": "ingress:8443 abc\n", } { t.Run(name, func(t *testing.T) { os.WriteFile(ks.Path, []byte(bad), 0o600) if _, _, err := ks.Lookup("ingress:8443"); !errors.Is(err, ErrCorruptKnownServers) { t.Errorf("erwartet ErrCorruptKnownServers, bekommen %v", err) } }) } if err := ks.Add("mit leerzeichen", sc.Fingerprint); err == nil { t.Error("Servername mit Leerzeichen akzeptiert") } } // tlsServer startet einen Test-Server mit dem Zertifikat aus einem eigenen // Datenverzeichnis und zählt, ob ein Request ihn erreicht. func tlsServer(t *testing.T) (*httptest.Server, ServerCert, *atomic.Int32) { t.Helper() sc, err := LoadOrCreateServerCert(t.TempDir()) if err != nil { t.Fatal(err) } var hits atomic.Int32 srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { hits.Add(1) })) srv.TLS = ServerTLSConfig(sc) srv.Config.ErrorLog = log.New(io.Discard, "", 0) // abgewiesene Handshakes sind hier gewollt srv.StartTLS() t.Cleanup(srv.Close) return srv, sc, &hits } func get(t *testing.T, srv *httptest.Server, cfg *tls.Config) error { t.Helper() c := &http.Client{Transport: &http.Transport{TLSClientConfig: cfg}} resp, err := c.Get(srv.URL) if err == nil { resp.Body.Close() } return err } func TestClientTLS_FirstContactWithExpectedFingerprint(t *testing.T) { srv, sc, hits := tlsServer(t) ks := newKnownServers(t) addr := srv.Listener.Addr().String() if err := get(t, srv, ClientTLSConfig(addr, ks, sc.Fingerprint)); err != nil { t.Fatalf("erster Kontakt mit korrektem Fingerprint: %v", err) } if hits.Load() != 1 { t.Error("Request kam nicht an") } if fp, ok, _ := ks.Lookup(addr); !ok || fp != sc.Fingerprint { t.Errorf("Pin nicht gespeichert: %q %v", fp, ok) } // Folgekontakt ohne Token-Fingerprint: der Pin genügt. if err := get(t, srv, ClientTLSConfig(addr, ks, "")); err != nil { t.Errorf("Folgekontakt mit Pin: %v", err) } } // Kein blindes TOFU: Ohne Pin und ohne erwarteten Fingerprint wird nicht verbunden. func TestClientTLS_NoBlindFirstContact(t *testing.T) { srv, _, hits := tlsServer(t) ks := newKnownServers(t) addr := srv.Listener.Addr().String() if err := get(t, srv, ClientTLSConfig(addr, ks, "")); !errors.Is(err, ErrUnknownServer) { t.Errorf("erwartet ErrUnknownServer, bekommen %v", err) } if hits.Load() != 0 { t.Error("Request hat den Server erreicht") } if _, ok, _ := ks.Lookup(addr); ok { t.Error("unbekannter Server wurde gepinnt") } } // Angreifer beim Erstkontakt: Er präsentiert sein eigenes Zertifikat, der // Token enthält aber den Fingerprint des echten Servers. func TestClientTLS_MITMOnFirstContact(t *testing.T) { mitm, _, hits := tlsServer(t) real, _ := LoadOrCreateServerCert(t.TempDir()) ks := newKnownServers(t) addr := mitm.Listener.Addr().String() if err := get(t, mitm, ClientTLSConfig(addr, ks, real.Fingerprint)); !errors.Is(err, ErrFingerprintMismatch) { t.Errorf("erwartet ErrFingerprintMismatch, bekommen %v", err) } if hits.Load() != 0 { t.Error("Request (inkl. Token) hat den Angreifer erreicht") } if _, ok, _ := ks.Lookup(addr); ok { t.Error("Angreifer wurde gepinnt") } } // Angreifer nach dem Erstkontakt: Der Pin gewinnt, auch wenn ein (neuer) // Token-Fingerprint auf den Angreifer passen würde. func TestClientTLS_PinWinsLater(t *testing.T) { mitm, mitmCert, hits := tlsServer(t) real, _ := LoadOrCreateServerCert(t.TempDir()) ks := newKnownServers(t) addr := mitm.Listener.Addr().String() if err := ks.Add(addr, real.Fingerprint); err != nil { t.Fatal(err) } for _, expected := range []Fingerprint{"", mitmCert.Fingerprint} { if err := get(t, mitm, ClientTLSConfig(addr, ks, expected)); !errors.Is(err, ErrFingerprintMismatch) { t.Errorf("expected=%q: erwartet ErrFingerprintMismatch, bekommen %v", expected, err) } } if hits.Load() != 0 { t.Error("Request hat den Angreifer erreicht") } } func TestClientTLSConfig_OnlyTLS13(t *testing.T) { cfg := ClientTLSConfig("x:1", newKnownServers(t), "") if cfg.MinVersion != tls.VersionTLS13 { t.Errorf("MinVersion = %x, erwartet TLS 1.3", cfg.MinVersion) } }