Append-only per Trigger (nicht nur GRANT/REVOKE): audit_events_prevent_mutation() wirft bei jedem UPDATE/DELETE auf audit_events eine Exception, unabhaengig von der verbindenden Rolle (Akzeptanzkriterium 1). internal/audit/four_eyes.go: Vier-Augen-Prinzip fuer sicherheitskritische Entscheidungen (Loeschbestaetigung, Rechtevergabe), 1:1 nach archivdms- Vorbild. Request erzeugt einen Klartext-Code (wird ausserhalb des Systems an eine ZWEITE Person uebermittelt) und speichert nur dessen SHA-256-Hash. Confirm sperrt die Zeile mit FOR UPDATE (Akzeptanzkriterium 2 — serialisiert zwei gleichzeitige Bestaetigungsversuche, verhindert doppelte Ausfuehrung), weist eine Bestaetigung durch dieselbe Person wie die anfordernde ab (ErrSameActor, echtes Vier-Augen-Prinzip statt nur Code-Pruefung), und vergleicht den Code timing-safe (Akzeptanzkriterium 3). internal/audit/timingsafe.go: timingSafeEqual als projektweite Referenz- implementierung (subtle.ConstantTimeCompare) fuer sicherheitsrelevante Vergleiche — andere Module (z.B. Archive CMP-06 Freigabelinks) uebernehmen dasselbe Muster laut IAM-02-Konvention. Nebenbei behoben: AUD-01s eigener Test nutzte einen festen Tenant-Slug mit DELETE-basiertem Cleanup — seit dem neuen Append-only-Trigger schlaegt dieses Cleanup lautlos fehl, wodurch Zeilen sich ueber Testlaeufe hinweg summierten und die Zaehl-Assertion brach. Auf eindeutigen Slug pro Lauf umgestellt (direkte, notwendige Folge dieser Kachel, keine Umgestaltung von AUD-01 selbst). Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS): 1. Direkter UPDATE/DELETE-Versuch von der Datenbank abgewiesen — TestAppendOnly_RejectsUpdateAndDelete: beide Operationen scheitern, Eintrag bleibt unveraendert erhalten. PASS. 2. Vier-Augen-Prinzip mit FOR-UPDATE-Lock race-frei unter parallelen Anfragen — TestFourEyes_ConcurrentConfirmIsRaceFree: zwei gleichzeitige Bestaetigungsversuche fuer denselben Vorgang, genau einer erfolgreich, der andere ErrAlreadyDecided. PASS. 3. Timing-safe Vergleich per Laufzeitmessung stichprobenartig verifiziert — TestTimingSafeEqual_NoEarlyExitTiming: Mismatch am Anfang (603µs) vs. am Ende (574µs) ueber 20000 Iterationen, kein Hinweis auf Short-Circuit- Vergleich (Ratio innerhalb Faktor 3 Toleranz). PASS. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
63 lines
2.1 KiB
Go
63 lines
2.1 KiB
Go
package audit
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestTimingSafeEqual_Correctness(t *testing.T) {
|
|
a := hashCode("geheimnis-a")
|
|
b := hashCode("geheimnis-a")
|
|
c := hashCode("geheimnis-b")
|
|
|
|
if !timingSafeEqual(a, b) {
|
|
t.Fatal("identische hashes sollten gleich sein")
|
|
}
|
|
if timingSafeEqual(a, c) {
|
|
t.Fatal("unterschiedliche hashes sollten ungleich sein")
|
|
}
|
|
if timingSafeEqual(a, []byte("kuerzer")) {
|
|
t.Fatal("unterschiedliche laenge sollte ungleich sein")
|
|
}
|
|
}
|
|
|
|
// Akzeptanzkriterium 3 + Pruefung 3: Timing-safe Vergleich stichprobenartig
|
|
// per Laufzeitmessung verifiziert — ein Mismatch am Anfang darf nicht
|
|
// messbar schneller sein als ein Mismatch am Ende (klassisches Merkmal
|
|
// eines NICHT timing-safen Vergleichs wie bytes.Equal mit Short-Circuit).
|
|
func TestTimingSafeEqual_NoEarlyExitTiming(t *testing.T) {
|
|
reference := hashCode("referenzwert-fuer-timing-test")
|
|
|
|
mismatchAtStart := make([]byte, len(reference))
|
|
copy(mismatchAtStart, reference)
|
|
mismatchAtStart[0] ^= 0xFF
|
|
|
|
mismatchAtEnd := make([]byte, len(reference))
|
|
copy(mismatchAtEnd, reference)
|
|
mismatchAtEnd[len(mismatchAtEnd)-1] ^= 0xFF
|
|
|
|
const iterations = 20000
|
|
startDur := measure(iterations, func() { timingSafeEqual(reference, mismatchAtStart) })
|
|
endDur := measure(iterations, func() { timingSafeEqual(reference, mismatchAtEnd) })
|
|
|
|
t.Logf("mismatch am anfang: %s, mismatch am ende: %s (%d iterationen)", startDur, endDur, iterations)
|
|
|
|
ratio := float64(startDur) / float64(endDur)
|
|
// Grosszuegige Toleranz (Faktor 3), da es ein Stichprobentest auf einer
|
|
// geteilten Testmaschine ist, kein isolierter Benchmark — es geht darum,
|
|
// eine grobe Short-Circuit-Implementierung zuverlaessig aufzudecken
|
|
// (die haette typischerweise eine Groessenordnung Unterschied), nicht um
|
|
// kryptographisch praezise Constant-Time-Beweise.
|
|
if ratio > 3.0 || ratio < 1.0/3.0 {
|
|
t.Fatalf("timing-unterschied zu gross (verdacht auf short-circuit-vergleich): ratio=%.2f", ratio)
|
|
}
|
|
}
|
|
|
|
func measure(iterations int, fn func()) time.Duration {
|
|
start := time.Now()
|
|
for i := 0; i < iterations; i++ {
|
|
fn()
|
|
}
|
|
return time.Since(start)
|
|
}
|