Webhook-to-SSE gateway with hierarchical topic routing and signature verification
1package main
2
3import (
4 "crypto/hmac"
5 "crypto/sha1"
6 "crypto/sha256"
7 "encoding/hex"
8 "errors"
9 "fmt"
10 "hash"
11 "net/http"
12 "strings"
13)
14
15type Verifier interface {
16 Verify(body []byte, headers http.Header, secret string, signatureHeader string) error
17}
18
19func NewVerifier(method string) (Verifier, error) {
20 switch method {
21 case "hmac-sha256":
22 return &hmacVerifier{prefix: "sha256=", newHash: sha256.New}, nil
23 case "hmac-sha1":
24 return &hmacVerifier{prefix: "sha1=", newHash: sha1.New}, nil
25 default:
26 return nil, fmt.Errorf("unknown verification method: %s", method)
27 }
28}
29
30type hmacVerifier struct {
31 prefix string
32 newHash func() hash.Hash
33}
34
35func (v *hmacVerifier) Verify(body []byte, headers http.Header, secret string, signatureHeader string) error {
36 sig := headers.Get(signatureHeader)
37 if sig == "" {
38 return errors.New("missing signature header")
39 }
40
41 sigHex, ok := strings.CutPrefix(sig, v.prefix)
42 if !ok {
43 return fmt.Errorf("signature missing expected prefix %q", v.prefix)
44 }
45
46 sigBytes, err := hex.DecodeString(sigHex)
47 if err != nil {
48 return fmt.Errorf("invalid hex in signature: %w", err)
49 }
50
51 mac := hmac.New(v.newHash, []byte(secret))
52 mac.Write(body)
53 expected := mac.Sum(nil)
54
55 if !hmac.Equal(sigBytes, expected) {
56 return errors.New("signature mismatch")
57 }
58 return nil
59}