Webhook-to-SSE gateway with hierarchical topic routing and signature verification
at main 59 lines 1.3 kB view raw
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}