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
188 lines
5.6 KiB
Go
188 lines
5.6 KiB
Go
package crypto
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"io"
|
|
"testing"
|
|
)
|
|
|
|
// TestGenerateDEK_NeverReused ist Akzeptanzkriterium 1: DEK wird pro
|
|
// Objekt neu erzeugt, nie wiederverwendet.
|
|
func TestGenerateDEK_NeverReused(t *testing.T) {
|
|
seen := map[string]bool{}
|
|
for i := 0; i < 50; i++ {
|
|
dek, err := GenerateDEK()
|
|
if err != nil {
|
|
t.Fatalf("generatedek: %v", err)
|
|
}
|
|
if len(dek) != DEKSize {
|
|
t.Fatalf("dek-laenge = %d, want %d", len(dek), DEKSize)
|
|
}
|
|
key := string(dek)
|
|
if seen[key] {
|
|
t.Fatal("generatedek lieferte denselben schluessel zweimal")
|
|
}
|
|
seen[key] = true
|
|
}
|
|
}
|
|
|
|
// TestEncryptDecryptStream_RoundTrip ist Pruefung 1: Round-Trip liefert
|
|
// identischen Klartext.
|
|
func TestEncryptDecryptStream_RoundTrip(t *testing.T) {
|
|
dek, err := GenerateDEK()
|
|
if err != nil {
|
|
t.Fatalf("generatedek: %v", err)
|
|
}
|
|
plaintext := []byte("Rechnung 2026-0001 — vertraulicher Inhalt")
|
|
|
|
ciphertext, err := EncryptStream(dek, bytes.NewReader(plaintext))
|
|
if err != nil {
|
|
t.Fatalf("encryptstream: %v", err)
|
|
}
|
|
ciphertextBytes, err := io.ReadAll(ciphertext)
|
|
if err != nil {
|
|
t.Fatalf("chiffretext lesen: %v", err)
|
|
}
|
|
if bytes.Equal(ciphertextBytes, plaintext) {
|
|
t.Fatal("chiffretext ist identisch zum klartext - keine verschluesselung stattgefunden")
|
|
}
|
|
|
|
decrypted, err := DecryptStream(dek, bytes.NewReader(ciphertextBytes))
|
|
if err != nil {
|
|
t.Fatalf("decryptstream: %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)
|
|
}
|
|
}
|
|
|
|
// TestDecryptStream_RejectsTamperedCiphertext ist Pruefung 2: manipulierter
|
|
// Chiffretext wird bei Decrypt erkannt und abgelehnt (GCM-Auth-Tag).
|
|
func TestDecryptStream_RejectsTamperedCiphertext(t *testing.T) {
|
|
dek, err := GenerateDEK()
|
|
if err != nil {
|
|
t.Fatalf("generatedek: %v", err)
|
|
}
|
|
ciphertext, err := EncryptStream(dek, bytes.NewReader([]byte("geheimer inhalt")))
|
|
if err != nil {
|
|
t.Fatalf("encryptstream: %v", err)
|
|
}
|
|
tampered, err := io.ReadAll(ciphertext)
|
|
if err != nil {
|
|
t.Fatalf("chiffretext lesen: %v", err)
|
|
}
|
|
// Ein Byte in der Mitte des Chiffretexts kippen (nach dem Nonce-Praefix).
|
|
tampered[len(tampered)-1] ^= 0xFF
|
|
|
|
if _, err := DecryptStream(dek, bytes.NewReader(tampered)); !errors.Is(err, ErrDecryptFailed) {
|
|
t.Fatalf("manipulierter chiffretext: erwartet ErrDecryptFailed, habe %v", err)
|
|
}
|
|
}
|
|
|
|
// TestDecryptStream_WrongKeyRejected prueft den zweiten Fall, in dem
|
|
// Entschluesselung scheitern muss: falscher Schluessel statt Manipulation.
|
|
func TestDecryptStream_WrongKeyRejected(t *testing.T) {
|
|
dek1, _ := GenerateDEK()
|
|
dek2, _ := GenerateDEK()
|
|
ciphertext, err := EncryptStream(dek1, bytes.NewReader([]byte("inhalt")))
|
|
if err != nil {
|
|
t.Fatalf("encryptstream: %v", err)
|
|
}
|
|
ciphertextBytes, _ := io.ReadAll(ciphertext)
|
|
|
|
if _, err := DecryptStream(dek2, bytes.NewReader(ciphertextBytes)); !errors.Is(err, ErrDecryptFailed) {
|
|
t.Fatalf("falscher schluessel: erwartet ErrDecryptFailed, habe %v", err)
|
|
}
|
|
}
|
|
|
|
// TestWrapUnwrapDEK_RoundTrip prueft das Envelope-Verpacken des DEK selbst.
|
|
func TestWrapUnwrapDEK_RoundTrip(t *testing.T) {
|
|
kek := make([]byte, KEKSize)
|
|
for i := range kek {
|
|
kek[i] = byte(i)
|
|
}
|
|
dek, err := GenerateDEK()
|
|
if err != nil {
|
|
t.Fatalf("generatedek: %v", err)
|
|
}
|
|
|
|
wrapped, err := WrapDEK(kek, dek)
|
|
if err != nil {
|
|
t.Fatalf("wrapdek: %v", err)
|
|
}
|
|
if bytes.Equal(wrapped, dek) {
|
|
t.Fatal("wrapdek lieferte den unveraenderten dek")
|
|
}
|
|
|
|
unwrapped, err := UnwrapDEK(kek, wrapped)
|
|
if err != nil {
|
|
t.Fatalf("unwrapdek: %v", err)
|
|
}
|
|
if !bytes.Equal(unwrapped, dek) {
|
|
t.Fatal("entpackter dek stimmt nicht mit original ueberein")
|
|
}
|
|
}
|
|
|
|
// TestRewrapDEK_RotationKeepsObjectReadable ist Pruefung 3: KEK-Rotation
|
|
// getestet, alte Objekte weiterhin lesbar — OHNE dass der Chiffretext des
|
|
// Objekts selbst jemals angefasst wird (Akzeptanzkriterium 3).
|
|
func TestRewrapDEK_RotationKeepsObjectReadable(t *testing.T) {
|
|
oldKEK := bytes.Repeat([]byte{0x01}, KEKSize)
|
|
newKEK := bytes.Repeat([]byte{0x02}, KEKSize)
|
|
|
|
dek, err := GenerateDEK()
|
|
if err != nil {
|
|
t.Fatalf("generatedek: %v", err)
|
|
}
|
|
plaintext := []byte("altes objekt, vor der rotation verschluesselt")
|
|
ciphertext, err := EncryptStream(dek, bytes.NewReader(plaintext))
|
|
if err != nil {
|
|
t.Fatalf("encryptstream: %v", err)
|
|
}
|
|
ciphertextBytes, _ := io.ReadAll(ciphertext)
|
|
|
|
wrappedOld, err := WrapDEK(oldKEK, dek)
|
|
if err != nil {
|
|
t.Fatalf("wrapdek (alt): %v", err)
|
|
}
|
|
|
|
// Rotation: NUR der DEK-Wrapper wird neu verpackt, der Chiffretext
|
|
// (ciphertextBytes) wird nicht angefasst.
|
|
wrappedNew, err := RewrapDEK(oldKEK, newKEK, wrappedOld)
|
|
if err != nil {
|
|
t.Fatalf("rewrapdek: %v", err)
|
|
}
|
|
if bytes.Equal(wrappedNew, wrappedOld) {
|
|
t.Fatal("rewrapdek lieferte denselben wrapper wie vor der rotation")
|
|
}
|
|
|
|
// Der alte KEK kann den NEUEN Wrapper nicht mehr entpacken (Rotation
|
|
// war wirksam).
|
|
if _, err := UnwrapDEK(oldKEK, wrappedNew); !errors.Is(err, ErrDecryptFailed) {
|
|
t.Fatal("alter kek konnte den nach der rotation neu verpackten dek weiterhin entpacken")
|
|
}
|
|
|
|
// Mit dem NEUEN KEK und dem neuen Wrapper ist das unveraenderte, alte
|
|
// Objekt weiterhin vollstaendig lesbar.
|
|
dekAfterRotation, err := UnwrapDEK(newKEK, wrappedNew)
|
|
if err != nil {
|
|
t.Fatalf("unwrapdek (neu) nach rotation: %v", err)
|
|
}
|
|
decrypted, err := DecryptStream(dekAfterRotation, bytes.NewReader(ciphertextBytes))
|
|
if err != nil {
|
|
t.Fatalf("decryptstream nach rotation: %v", err)
|
|
}
|
|
got, err := io.ReadAll(decrypted)
|
|
if err != nil {
|
|
t.Fatalf("klartext lesen: %v", err)
|
|
}
|
|
if !bytes.Equal(got, plaintext) {
|
|
t.Fatalf("altes objekt nach rotation = %q, want %q", got, plaintext)
|
|
}
|
|
}
|