API-05: verteilte-jwt-verifikation-rechte-feature-flag-cache-kontrakt
internal/moduletrust: asymmetrische JWT-Signatur (Ed25519) mit JWKS- Verteilung, wie im Entscheidungsverlauf "Vertrauensstellung Core<->Module" (nexarch-state.json) festgelegt. Getrennt von IAM-02s HS256-Session-Cookie (Browser-Login bleibt unangetastet) — dies ist der Modul-zu-Core- Vertrauensmechanismus. KeyManager haelt ALLE noch gueltigen Schluesselpaare (nicht nur das aktuell signierende); Rotate() erzeugt einen neuen Schluessel, alte bleiben in PublicKeySet() erhalten — bereits ausgestellte Tokens bleiben dadurch nach einer Rotation weiterhin verifizierbar (Akzeptanzkriterium 3, keine Ausfallzeit). ServeJWKS/ParseJWKS sind der Verteilungsmechanismus. StaleCache[T] ist der generische Rechte-/Feature-Flag-Cache-Kontrakt (Akzeptanzkriterium 2), mit zwei explizit benannten und begruendeten Verhalten: Get() ist FAIL-OPEN (nutzt bei Core-Ausfall einen vorhandenen, abgelaufenen Stand weiter — ein bereits authentifiziertes Modul soll nicht hart blockieren), RequireFresh() ist FAIL-CLOSED (nie zwischengespeichert, schlaegt bei Core-Ausfall klar fehl — fuer sicherheitskritische Aktionen wie einen neuen Login). LIC-02s internal/flag.Service implementiert bereits denselben Kontrakt fuer Feature-Flags; StaleCache verallgemeinert dasselbe Muster fuer JWT-Schluessel, damit beide Faelle derselben dokumentierten Policy folgen statt zwei unterschiedlichen Ad-hoc-Loesungen. Verifier.Verify ruft KeyFetchFunc nur bei abgelaufener TTL auf, nicht pro Aufruf (Akzeptanzkriterium 1) — Signaturpruefung selbst ist immer lokal. Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS): 1. Core simuliert abgeschaltet, andere Module bleiben fuer bereits authentifizierte Nutzer funktionsfaehig bis TTL/Fail-Open greift — TestVerify_FailsOpenWhenCoreUnreachableButStaleKeysExist: Verify() funktioniert weiter mit letztbekanntem Schluesselstand. PASS. 2. Neue sicherheitskritische Aktion schlaegt bei Core-Ausfall klar fehl, statt andere Funktionen mitzureissen — TestRequireFreshKeys_FailsClosedWhenCoreUnreachable: Fehler trotz vorhandenem (aelterem) Cache-Stand. PASS. 3. Schluesselrotation ohne Downtime in einem simulierten zweiten Modul — TestRotate_NoDowntimeForAlreadyIssuedTokens: vor UND nach Rotation ausgestellte Tokens beide weiterhin gueltig fuer Modul B. PASS. Zusaetzlich: TestVerify_DoesNotFetchPerCall belegt Akzeptanzkriterium 1 direkt (10 Verify-Aufrufe, genau 1 Fetch). PASS. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
3d20d86a4f
commit
f7863fd4d2
@@ -0,0 +1,67 @@
|
||||
package moduletrust
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
var ErrInvalidToken = errors.New("moduletrust: ungueltiges token")
|
||||
|
||||
// KeyFetchFunc holt den aktuellen Schluesselsatz von Core (z.B. per HTTP-GET
|
||||
// auf ServeJWKS + ParseJWKS). Wird vom Verifier nur bei abgelaufener TTL
|
||||
// aufgerufen — NICHT bei jeder Verify()-Anfrage (Akzeptanzkriterium 1).
|
||||
type KeyFetchFunc func(ctx context.Context) (map[string]ed25519.PublicKey, error)
|
||||
|
||||
// Verifier ist die Modulseite von API-05: verifiziert JWTs LOKAL gegen einen
|
||||
// per StaleCache zwischengespeicherten Schluesselsatz, ohne pro Aufruf einen
|
||||
// synchronen Request an Core zu stellen.
|
||||
type Verifier struct {
|
||||
cache *StaleCache[map[string]ed25519.PublicKey]
|
||||
}
|
||||
|
||||
func NewVerifier(ttl time.Duration, fetch KeyFetchFunc) *Verifier {
|
||||
return &Verifier{cache: NewStaleCache(ttl, func(ctx context.Context) (map[string]ed25519.PublicKey, error) {
|
||||
return fetch(ctx)
|
||||
})}
|
||||
}
|
||||
|
||||
// Verify prueft die Signatur LOKAL gegen den (ggf. abgelaufenen, aber
|
||||
// vorhandenen) Schluesselsatz — FAIL-OPEN fuer bereits ausgestellte Tokens
|
||||
// (Akzeptanzkriterium 2): ist Core nicht erreichbar, aber ein alter
|
||||
// Schluesselsatz bekannt, wird damit weiter verifiziert.
|
||||
func (v *Verifier) Verify(ctx context.Context, tokenString string) (*Claims, error) {
|
||||
keys, _, err := v.cache.Get(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
claims := &Claims{}
|
||||
token, err := jwt.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (interface{}, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodEd25519); !ok {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
kid, _ := t.Header["kid"].(string)
|
||||
pub, ok := keys[kid]
|
||||
if !ok {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
return pub, nil
|
||||
})
|
||||
if err != nil || !token.Valid {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// RequireFreshKeys ruft IMMER frisch von Core ab (FAIL-CLOSED) — fuer
|
||||
// sicherheitskritische Aktionen wie einen komplett neuen Login
|
||||
// (Akzeptanzkriterium 2): schlaegt klar fehl, wenn Core nicht erreichbar
|
||||
// ist, statt auf einem veralteten Schluesselsatz zu vertrauen.
|
||||
func (v *Verifier) RequireFreshKeys(ctx context.Context) error {
|
||||
_, err := v.cache.RequireFresh(ctx)
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user