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.
99 lines
2.6 KiB
Go
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")
|
|
}
|
|
}
|