Files
mrcharles 05ec1a4142 events: verify, normalize, append-only store with provider-id dedup
Signature verification (gitea hex HMAC-SHA256 over the raw body via
constant-time hmac.Equal; redmine/discourse constant-time shared-secret
headers) with one generic ErrUnverified so rejects give attackers no
oracle. Tolerant normalization of the known Redmine/Discourse/Gitea
payload variants into one Event record (canonical subject ids, actor,
title, repo, sha256 payload digest) plus the DESIGN action mapping
(dispatch_turn / respond_turn / pipeline_step / ignore). Store: JSONL
under state dir, 0600, dedup keyed on provider event id (delivery
header, payload-digest fallback), index rebuilt at startup so replays
across restarts still dedup; torn tail lines skipped, not fatal.
2026-08-28 21:38:16 -05:00

99 lines
2.6 KiB
Go

package events
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"strings"
"testing"
)
func hmacHex(body []byte, secret string) string {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
return hex.EncodeToString(mac.Sum(nil))
}
func TestVerifyGiteaHMAC(t *testing.T) {
body := []byte(`{"action":"approved","number":5}`)
secret := "gitea-hook-secret"
cases := []struct {
name string
sig string
wantErr bool
}{
{"valid signature", hmacHex(body, secret), false},
{"missing header value", "", true},
{"all-zero signature", strings.Repeat("00", 32), true},
{"wrong secret", hmacHex(body, "other-secret"), true},
{"not hex", "not-hex-at-all", true},
{"truncated signature", hmacHex(body, secret)[:32], true},
{"uppercase hex still valid", strings.ToUpper(hmacHex(body, secret)), false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := VerifyGiteaHMAC(body, tc.sig, secret)
if tc.wantErr && err == nil {
t.Fatalf("expected error, got nil")
}
if !tc.wantErr && err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err != nil && !IsUnverified(err) {
t.Errorf("error %v is not ErrUnverified", err)
}
})
}
}
func TestVerifyGiteaHMACBodySensitivity(t *testing.T) {
secret := "s"
sig := hmacHex([]byte(`{"a":1}`), secret)
if err := VerifyGiteaHMAC([]byte(`{"a":2}`), sig, secret); err == nil {
t.Fatal("signature over a different body must not verify")
}
}
func TestVerifySharedSecret(t *testing.T) {
const header = "X-Test-Secret"
secret := "shared-hook-secret"
cases := []struct {
name string
value string
wantErr bool
}{
{"valid", secret, false},
{"missing header value", "", true},
{"wrong secret", "wrong", true},
{"prefix of secret", secret[:10], true},
{"case differs", strings.ToUpper(secret), true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := VerifySharedSecret(header, tc.value, secret)
if tc.wantErr && err == nil {
t.Fatalf("expected error, got nil")
}
if !tc.wantErr && err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
}
}
func TestSignGiteaMatchesVerify(t *testing.T) {
body, secret := []byte(`{}`), "k"
if err := VerifyGiteaHMAC(body, SignGitea(body, secret), secret); err != nil {
t.Fatalf("SignGitea does not verify: %v", err)
}
if GiteaSignatureHeader() != "X-Gitea-Signature" {
t.Errorf("unexpected header name %q", GiteaSignatureHeader())
}
// sha256 import guard: helper must agree with a direct hmac.
if SignGitea(body, secret) != hmacHex(body, secret) {
t.Errorf("SignGitea disagrees with hmacHex")
}
}