AUD-02: unveraenderliches-protokoll-append-only
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>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
b12d53f469
commit
1bfb2efd94
@@ -0,0 +1,62 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user