194 lines
6.1 KiB
Go
194 lines
6.1 KiB
Go
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)
|
|
}
|
|
}
|