// ARC-08: End-zu-Ende-Rotationstest. Nutzt einen fake KEKProvider (echter // Testkonvention aus ARC-02, siehe encstorage_test.go) statt eines echten // HTTP-Aufrufs an Core API-12 — Core selbst hat keine rotierbare // Testschnittstelle über HTTP exponiert (nur der aktuelle KEK ist // abrufbar), die Rotation wird hier auf Höhe der KEKProvider-Schnittstelle // simuliert, exakt wie ARC-02 es für Fehlerfälle bereits tut. package crypto import ( "bytes" "context" "io" "strings" "sync" "testing" ) // rotatableKEKProvider liefert für einen Mandanten einen aktuell // gesetzten KEK, der zur Laufzeit "rotiert" werden kann (simuliert Core // API-10s RotateTenantKEK, dessen Effekt auf API-12 exakt darin besteht, // dass TenantKEK ab dann einen anderen Wert liefert). type rotatableKEKProvider struct { mu sync.Mutex current []byte } func (p *rotatableKEKProvider) TenantKEK(_ context.Context, _ string) ([]byte, error) { p.mu.Lock() defer p.mu.Unlock() return p.current, nil } func (p *rotatableKEKProvider) rotate(newKEK []byte) { p.mu.Lock() defer p.mu.Unlock() p.current = newKEK } // TestRotation_OldArchiveStaysReadableAfterMasterKeyRotation ist die // geforderte Pflichtprüfung 1: Rotation des Hauptschlüssels lässt // Altbestand weiterhin lesbar. func TestRotation_OldArchiveStaysReadableAfterMasterKeyRotation(t *testing.T) { tenant := "mandant-arc08-rotation-lesbar" store := setupKEKVersionStore(t, tenant) ctx := context.Background() provider := &rotatableKEKProvider{current: bytes.Repeat([]byte{0x51}, KEKSize)} svc := NewService(provider).WithVersionStore(store) // Objekt VOR der Rotation versiegeln. oldEnvelope, err := svc.Seal(ctx, tenant, strings.NewReader("altbestand vor rotation")) if err != nil { t.Fatalf("seal (alt): %v", err) } if oldEnvelope.KEKVersion != 1 { t.Fatalf("erwartete kek-version 1 vor rotation, habe %d", oldEnvelope.KEKVersion) } oldCiphertext, err := io.ReadAll(oldEnvelope.Ciphertext) if err != nil { t.Fatalf("chiffretext (alt) lesen: %v", err) } // Core rotiert den Tenant-Hauptschlüssel — TenantKEK liefert ab jetzt // einen komplett anderen Wert, exakt wie internal/kek.Store. // RotateTenantKEK es real bei Core bewirkt. provider.rotate(bytes.Repeat([]byte{0x52}, KEKSize)) // Objekt NACH der Rotation versiegeln (Akzeptanzkriterium 2: kein // Neuverschlüsseln des Altbestands nötig, nur neue Objekte nutzen den // neuen Schlüssel). newEnvelope, err := svc.Seal(ctx, tenant, strings.NewReader("neuer inhalt nach rotation")) if err != nil { t.Fatalf("seal (neu): %v", err) } if newEnvelope.KEKVersion != 2 { t.Fatalf("erwartete kek-version 2 nach rotation, habe %d", newEnvelope.KEKVersion) } // Altbestand bleibt über die aufgezeichnete Version lesbar // (Akzeptanzkriterium 3). openedOld, err := svc.OpenAtVersion(ctx, tenant, oldEnvelope.KEKVersion, oldEnvelope.WrappedDEK, bytes.NewReader(oldCiphertext)) if err != nil { t.Fatalf("openatversion (alt, nach rotation): %v", err) } plainOld, err := io.ReadAll(openedOld) if err != nil { t.Fatalf("altbestand lesen: %v", err) } if string(plainOld) != "altbestand vor rotation" { t.Fatalf("altbestand-inhalt stimmt nicht, habe %q", string(plainOld)) } // Der naive Open() (aktueller KEK) darf für das ALTE Objekt inzwischen // NICHT mehr funktionieren — das beweist, dass OpenAtVersion die // Rotation tatsächlich überbrückt, statt zufällig auch so zu klappen. if _, err := svc.Open(ctx, tenant, oldEnvelope.WrappedDEK, bytes.NewReader(oldCiphertext)); err == nil { t.Fatal("erwartete fehler bei Open() des altbestands mit dem NEUEN aktuellen kek, habe nil") } } // TestRotation_CompromisedOldKeyCanBeRevoked ist die geforderte // Pflichtprüfung 2: kompromittierter alter Schlüssel kann gezielt // gesperrt werden. func TestRotation_CompromisedOldKeyCanBeRevoked(t *testing.T) { tenant := "mandant-arc08-revoke-e2e" store := setupKEKVersionStore(t, tenant) ctx := context.Background() provider := &rotatableKEKProvider{current: bytes.Repeat([]byte{0x61}, KEKSize)} svc := NewService(provider).WithVersionStore(store) compromisedEnvelope, err := svc.Seal(ctx, tenant, strings.NewReader("mit kompromittiertem schlüssel versiegelt")) if err != nil { t.Fatalf("seal: %v", err) } compromisedCiphertext, err := io.ReadAll(compromisedEnvelope.Ciphertext) if err != nil { t.Fatalf("chiffretext lesen: %v", err) } provider.rotate(bytes.Repeat([]byte{0x62}, KEKSize)) if err := store.Revoke(ctx, tenant, compromisedEnvelope.KEKVersion); err != nil { t.Fatalf("kompromittierte version sperren: %v", err) } _, err = svc.OpenAtVersion(ctx, tenant, compromisedEnvelope.KEKVersion, compromisedEnvelope.WrappedDEK, bytes.NewReader(compromisedCiphertext)) if err == nil { t.Fatal("erwartete fehler beim lesen mit gesperrter kek-version, habe nil") } }