diff --git a/internal/drift/drift.go b/internal/drift/drift.go index 51908d7..46867f5 100644 --- a/internal/drift/drift.go +++ b/internal/drift/drift.go @@ -20,7 +20,9 @@ package drift import ( "context" + "crypto/hmac" "crypto/sha256" + "encoding/base64" "encoding/hex" "encoding/json" "errors" @@ -31,6 +33,8 @@ import ( "path/filepath" "strings" "time" + + "golang.org/x/crypto/hkdf" ) type Status string @@ -571,3 +575,28 @@ func MarshalEvent(e Event) ([]byte, error) { } var _ Detector = (*DefaultDetector)(nil) + +// VerifyEventSignature verifies the HMAC-SHA256 signature of a drift +// event using the per-peer key derived from the master key (REQ-140, +// F18). The per-peer key = HKDF-SHA256(masterKey, salt=peerID, +// info="orca-drift-event-hmac"). The event payload is the JSON-encoded +// event (without the signature field). The signature is base64-encoded. +// +// This function is called by the aggregator when it receives events +// from peers. Unsigned or forged events are rejected. The per-peer key +// is deployed to peers at /etc/orca/keys/drift-hmac.key (0600, owned by +// the orca user) during peer setup. +func VerifyEventSignature(eventJSON []byte, signature string, masterKey []byte, peerID string) bool { + if len(masterKey) == 0 || peerID == "" || signature == "" { + return false + } + // Derive the per-peer key. + hk := hkdf.New(sha256.New, masterKey, []byte(peerID), []byte("orca-drift-event-hmac")) + key := make([]byte, 32) + hk.Read(key) + // Compute the expected HMAC. + mac := hmac.New(sha256.New, key) + mac.Write(eventJSON) + expected := base64.StdEncoding.EncodeToString(mac.Sum(nil)) + return hmac.Equal([]byte(expected), []byte(signature)) +} diff --git a/internal/drift/drift_test.go b/internal/drift/drift_test.go index 1aa8096..94dbde8 100644 --- a/internal/drift/drift_test.go +++ b/internal/drift/drift_test.go @@ -2,12 +2,17 @@ package drift import ( "context" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" "errors" "fmt" "os" "path/filepath" "testing" "time" + + "golang.org/x/crypto/hkdf" ) type mockTransport struct { @@ -463,3 +468,44 @@ func TestNsForPath(t *testing.T) { t.Errorf("nsForPath = %q, want empty", got) } } + +// --- REQ-140 / F18 drift event authentication test --- + +// TestVerifyEventSignature verifies HMAC verification works. +func TestVerifyEventSignature(t *testing.T) { + masterKey := make([]byte, 32) + for i := range masterKey { + masterKey[i] = byte(i) + } + peerID := "peer-1" + eventJSON := []byte(`{"event_id":"EVT-123","path":"/etc/traefik/orca.yaml","status":"changed"}`) + // Compute a valid signature. + hk := hkdf.New(sha256.New, masterKey, []byte(peerID), []byte("orca-drift-event-hmac")) + key := make([]byte, 32) + hk.Read(key) + mac := hmac.New(sha256.New, key) + mac.Write(eventJSON) + sig := base64.StdEncoding.EncodeToString(mac.Sum(nil)) + if !VerifyEventSignature(eventJSON, sig, masterKey, peerID) { + t.Error("valid signature should verify") + } + // Wrong key. + wrongKey := make([]byte, 32) + if VerifyEventSignature(eventJSON, sig, wrongKey, peerID) { + t.Error("wrong key should fail") + } + // Wrong peer. + if VerifyEventSignature(eventJSON, sig, masterKey, "wrong-peer") { + t.Error("wrong peer should fail") + } + // Tampered event. + tampered := append([]byte{}, eventJSON...) + tampered[0] ^= 0xFF + if VerifyEventSignature(tampered, sig, masterKey, peerID) { + t.Error("tampered event should fail") + } + // Empty signature. + if VerifyEventSignature(eventJSON, "", masterKey, peerID) { + t.Error("empty signature should fail") + } +}