Envelope-Encryption: DEK (32 Byte) frisch je Objekt, AES-256-GCM fuer Objektinhalt, DEK selbst mit Tenant-KEK verpackt (WrapDEK/UnwrapDEK). KEK kommt ausschliesslich ueber HTTPKEKProvider von Core API-10 (internal/kek.Handler.TenantKEKHandler-Vertrag, Service-Credential- authentifiziert), wird nie persistiert - jeder Seal/Open-Aufruf bezieht ihn frisch. RewrapDEK fuer KEK-Rotation ohne Neuverschluesselung der Objekte (nur der DEK-Wrapper wird neu verpackt, Chiffretext bleibt unveraendert). Auf 192.168.1.131 verifiziert: Round-Trip Encrypt/Decrypt, manipulierter Chiffretext UND falscher Schluessel werden beide ueber GCM-Auth-Tag abgelehnt, KEK-Rotation real getestet (RewrapDEK, altes Objekt danach mit neuem KEK weiterhin lesbar, alter KEK funktioniert nicht mehr). Befund dokumentiert: Cores internal/kek.Handler (Gegenstelle fuer den KEK-Bezug) ist noch in keinem cmd/*/main.go verdrahtet, dieselbe Fehlerklasse wie FDN-03/QA-05/AUD-06. HTTPKEKProvider daher gegen den dokumentierten Vertrag getestet, nicht gegen eine laufende Core-Instanz. Siehe dms/docs/FDN-09-PRUEFPROTOKOLL.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
78 lines
2.3 KiB
Go
78 lines
2.3 KiB
Go
package crypto
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io"
|
|
"testing"
|
|
)
|
|
|
|
// fakeKEKProvider liefert einen fest hinterlegten Tenant-KEK, ohne echten
|
|
// HTTP-Aufruf — fuer Tests, die Service isoliert vom Core-Kontrakt pruefen
|
|
// wollen (der Kontrakt selbst ist in kekprovider_test.go geprueft).
|
|
type fakeKEKProvider struct {
|
|
kek []byte
|
|
calls int
|
|
}
|
|
|
|
func (f *fakeKEKProvider) TenantKEK(ctx context.Context, tenantSlug string) ([]byte, error) {
|
|
f.calls++
|
|
return f.kek, nil
|
|
}
|
|
|
|
// TestService_SealOpen_RoundTrip ist der End-to-End-Nachweis fuer
|
|
// Pruefung 1 ueber Service statt die Einzelfunktionen.
|
|
func TestService_SealOpen_RoundTrip(t *testing.T) {
|
|
kekProvider := &fakeKEKProvider{kek: bytes.Repeat([]byte{0x07}, KEKSize)}
|
|
svc := NewService(kekProvider)
|
|
ctx := context.Background()
|
|
plaintext := []byte("vertraulicher dokumentinhalt")
|
|
|
|
env, err := svc.Seal(ctx, "acme", bytes.NewReader(plaintext))
|
|
if err != nil {
|
|
t.Fatalf("seal: %v", err)
|
|
}
|
|
ciphertextBytes, err := io.ReadAll(env.Ciphertext)
|
|
if err != nil {
|
|
t.Fatalf("chiffretext lesen: %v", err)
|
|
}
|
|
if bytes.Equal(ciphertextBytes, plaintext) {
|
|
t.Fatal("chiffretext ist identisch zum klartext")
|
|
}
|
|
if len(env.WrappedDEK) == 0 {
|
|
t.Fatal("erwartet nicht-leeren wrappeddek")
|
|
}
|
|
|
|
decrypted, err := svc.Open(ctx, "acme", env.WrappedDEK, bytes.NewReader(ciphertextBytes))
|
|
if err != nil {
|
|
t.Fatalf("open: %v", err)
|
|
}
|
|
got, err := io.ReadAll(decrypted)
|
|
if err != nil {
|
|
t.Fatalf("klartext lesen: %v", err)
|
|
}
|
|
if !bytes.Equal(got, plaintext) {
|
|
t.Fatalf("entschluesselter klartext = %q, want %q", got, plaintext)
|
|
}
|
|
}
|
|
|
|
// TestService_Seal_NeverPersistsKEK ist Akzeptanzkriterium 2 (struktureller
|
|
// Nachweis): Service haelt den KEK nicht ueber einen Aufruf hinaus fest —
|
|
// jeder Seal/Open-Aufruf bezieht ihn frisch ueber KEKProvider.
|
|
func TestService_Seal_NeverPersistsKEK(t *testing.T) {
|
|
kekProvider := &fakeKEKProvider{kek: bytes.Repeat([]byte{0x09}, KEKSize)}
|
|
svc := NewService(kekProvider)
|
|
ctx := context.Background()
|
|
|
|
if _, err := svc.Seal(ctx, "acme", bytes.NewReader([]byte("a"))); err != nil {
|
|
t.Fatalf("seal 1: %v", err)
|
|
}
|
|
if _, err := svc.Seal(ctx, "acme", bytes.NewReader([]byte("b"))); err != nil {
|
|
t.Fatalf("seal 2: %v", err)
|
|
}
|
|
|
|
if kekProvider.calls != 2 {
|
|
t.Fatalf("erwartet 2 kek-abrufe (einer je Seal-Aufruf, kein Caching), habe %d", kekProvider.calls)
|
|
}
|
|
}
|