package events import ( "crypto/hmac" "crypto/sha256" "crypto/subtle" "encoding/hex" "errors" "fmt" ) // giteaSignatureHeader carries the hex HMAC-SHA256 of the raw body. const giteaSignatureHeader = "X-Gitea-Signature" // Default shared-secret header names (overridable per source in config; // gitea is always HMAC and ignores the override). const ( DefaultRedmineSecretHeader = "X-Redmine-Webhook-Secret" DefaultDiscourseSecretHeader = "X-Discourse-Webhook-Secret" ) // ErrUnverified marks every verification failure so callers reject with 401 // without distinguishing WHY (no oracle for attackers poking the receiver). var ErrUnverified = errors.New("webhook not verified") // VerifyGiteaHMAC checks the X-Gitea-Signature header (hex HMAC-SHA256 of // the raw request body) against the shared secret. Comparison is // constant-time; error text never echoes header values. func VerifyGiteaHMAC(body []byte, headerValue, secret string) error { if headerValue == "" { return fmt.Errorf("%w: missing %s header", ErrUnverified, giteaSignatureHeader) } got, err := hex.DecodeString(headerValue) if err != nil { return fmt.Errorf("%w: %s header is not valid hex", ErrUnverified, giteaSignatureHeader) } mac := hmac.New(sha256.New, []byte(secret)) mac.Write(body) if !hmac.Equal(got, mac.Sum(nil)) { return fmt.Errorf("%w: %s does not match body", ErrUnverified, giteaSignatureHeader) } return nil } // VerifySharedSecret constant-time compares the value of the provider's // secret header against the configured secret. func VerifySharedSecret(headerName, headerValue, secret string) error { if headerValue == "" { return fmt.Errorf("%w: missing %s header", ErrUnverified, headerName) } if subtle.ConstantTimeCompare([]byte(headerValue), []byte(secret)) != 1 { return fmt.Errorf("%w: %s does not verify", ErrUnverified, headerName) } return nil } // GiteaSignatureHeader exposes the fixed HMAC header name (config/docs). func GiteaSignatureHeader() string { return giteaSignatureHeader } // SignGitea computes the expected HMAC for a body+secret (tests, smoke). func SignGitea(body []byte, secret string) string { mac := hmac.New(sha256.New, []byte(secret)) mac.Write(body) return hex.EncodeToString(mac.Sum(nil)) }