package moduletrust import ( "context" "crypto/ed25519" "errors" "net/http" "sync" "testing" "time" ) func TestIssueAndVerify_RoundTrip(t *testing.T) { km, err := NewKeyManager() if err != nil { t.Fatalf("new key manager: %v", err) } token, err := km.Issue("user-1", "acme", time.Hour) if err != nil { t.Fatalf("issue: %v", err) } v := NewVerifier(time.Hour, func(ctx context.Context) (map[string]ed25519.PublicKey, error) { return km.PublicKeySet(), nil }) claims, err := v.Verify(context.Background(), token) if err != nil { t.Fatalf("verify: %v", err) } if claims.Subject != "user-1" || claims.TenantSlug != "acme" { t.Fatalf("claims unerwartet: %+v", claims) } } // Akzeptanzkriterium 1: Verifikation lokal, kein Request pro Aufruf. func TestVerify_DoesNotFetchPerCall(t *testing.T) { km, err := NewKeyManager() if err != nil { t.Fatalf("new key manager: %v", err) } token, err := km.Issue("user-1", "acme", time.Hour) if err != nil { t.Fatalf("issue: %v", err) } var mu sync.Mutex fetchCalls := 0 v := NewVerifier(time.Hour, func(ctx context.Context) (map[string]ed25519.PublicKey, error) { mu.Lock() fetchCalls++ mu.Unlock() return km.PublicKeySet(), nil }) for i := 0; i < 10; i++ { if _, err := v.Verify(context.Background(), token); err != nil { t.Fatalf("verify %d: %v", i, err) } } mu.Lock() defer mu.Unlock() if fetchCalls != 1 { t.Fatalf("erwartet genau 1 fetch fuer 10 Verify-Aufrufe innerhalb der TTL, habe %d", fetchCalls) } } // Akzeptanzkriterium 2 + Pruefung 1: Core simuliert nicht erreichbar, // bereits authentifizierte Nutzer bleiben funktionsfaehig (Fail-Open mit // letztbekanntem Schluesselstand). func TestVerify_FailsOpenWhenCoreUnreachableButStaleKeysExist(t *testing.T) { km, err := NewKeyManager() if err != nil { t.Fatalf("new key manager: %v", err) } token, err := km.Issue("user-1", "acme", time.Hour) if err != nil { t.Fatalf("issue: %v", err) } coreDown := false v := NewVerifier(30*time.Millisecond, func(ctx context.Context) (map[string]ed25519.PublicKey, error) { if coreDown { return nil, errors.New("core nicht erreichbar (simuliert)") } return km.PublicKeySet(), nil }) // Cache vorwaermen, waehrend Core noch erreichbar ist. if _, err := v.Verify(context.Background(), token); err != nil { t.Fatalf("verify (warm): %v", err) } // "Core abschalten" und TTL ablaufen lassen. coreDown = true time.Sleep(50 * time.Millisecond) if _, err := v.Verify(context.Background(), token); err != nil { t.Fatalf("verify sollte trotz core-ausfall mit letztbekanntem stand funktionieren: %v", err) } } // Akzeptanzkriterium 2 + Pruefung 2: neue sicherheitskritische Aktionen // (z.B. neuer Login) schlagen bei Core-Ausfall klar fehl statt unsicher // durchgelassen zu werden — auch wenn ein (aelterer) Cache-Stand existiert. func TestRequireFreshKeys_FailsClosedWhenCoreUnreachable(t *testing.T) { km, err := NewKeyManager() if err != nil { t.Fatalf("new key manager: %v", err) } coreDown := false v := NewVerifier(time.Hour, func(ctx context.Context) (map[string]ed25519.PublicKey, error) { if coreDown { return nil, errors.New("core nicht erreichbar (simuliert)") } return km.PublicKeySet(), nil }) // Cache vorwaermen (existiert jetzt ein "veralteter" gueltiger Stand). if _, _, err := v.cache.Get(context.Background()); err != nil { t.Fatalf("warm cache: %v", err) } coreDown = true if err := v.RequireFreshKeys(context.Background()); err == nil { t.Fatal("erwartet fehler (fail-closed) bei core-ausfall, habe nil") } } // Akzeptanzkriterium 3 + Pruefung 3: Schluesselrotation ohne Ausfallzeit — // ein bereits ausgestelltes Token bleibt nach Rotation weiterhin // verifizierbar, ein zweites (simuliertes) Modul bekommt beide Schluessel. func TestRotate_NoDowntimeForAlreadyIssuedTokens(t *testing.T) { km, err := NewKeyManager() if err != nil { t.Fatalf("new key manager: %v", err) } oldToken, err := km.Issue("user-1", "acme", time.Hour) if err != nil { t.Fatalf("issue (alt): %v", err) } if _, err := km.Rotate(); err != nil { t.Fatalf("rotate: %v", err) } newToken, err := km.Issue("user-2", "acme", time.Hour) if err != nil { t.Fatalf("issue (neu): %v", err) } // Simuliertes zweites Modul: fragt den vollstaendigen Schluesselsatz ab. moduleB := NewVerifier(time.Hour, func(ctx context.Context) (map[string]ed25519.PublicKey, error) { return km.PublicKeySet(), nil }) if _, err := moduleB.Verify(context.Background(), oldToken); err != nil { t.Fatalf("altes token sollte nach rotation weiterhin gueltig sein: %v", err) } if _, err := moduleB.Verify(context.Background(), newToken); err != nil { t.Fatalf("neues token sollte gueltig sein: %v", err) } } func TestVerify_RejectsUnknownKid(t *testing.T) { km1, _ := NewKeyManager() km2, _ := NewKeyManager() // komplett anderer, unbekannter schluessel token, err := km1.Issue("user-1", "acme", time.Hour) if err != nil { t.Fatalf("issue: %v", err) } v := NewVerifier(time.Hour, func(ctx context.Context) (map[string]ed25519.PublicKey, error) { return km2.PublicKeySet(), nil // kennt km1s schluessel nicht }) if _, err := v.Verify(context.Background(), token); !errors.Is(err, ErrInvalidToken) { t.Fatalf("erwartet ErrInvalidToken, habe %v", err) } } func TestJWKSRoundTrip(t *testing.T) { km, _ := NewKeyManager() km.Rotate() var buf []byte rec := &captureWriter{} km.ServeJWKS(rec, nil) buf = rec.body parsed, err := ParseJWKS(buf) if err != nil { t.Fatalf("parse jwks: %v", err) } if len(parsed) != len(km.PublicKeySet()) { t.Fatalf("erwartet %d schluessel, habe %d", len(km.PublicKeySet()), len(parsed)) } } type captureWriter struct { body []byte header http.Header } func (w *captureWriter) Header() http.Header { if w.header == nil { w.header = http.Header{} } return w.header } func (w *captureWriter) Write(p []byte) (int, error) { w.body = append(w.body, p...); return len(p), nil } func (w *captureWriter) WriteHeader(statusCode int) {}