CookbookVerify in Go

Verify webhooks in Go

⚠️
Preview — this feature is being built; the design below is the spec.

Check the syntic-signature header before trusting a webhook delivery. The header carries a timestamp t and one or more v1 signatures (several during secret rotation). The signature is the hex HMAC-SHA256 of t + "." + raw body, keyed with your endpoint’s signing secret. Reject the delivery if no v1 matches in constant time, or if t is more than 300 seconds away from your clock. Always hash the raw body bytes, before any JSON parsing.

package webhooks
 
import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"io"
	"math"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"
)
 
func Verify(header string, body []byte, secret string, now time.Time) bool {
	var t string
	var sigs []string
	for _, part := range strings.Split(header, ",") {
		k, v, ok := strings.Cut(strings.TrimSpace(part), "=")
		if !ok {
			continue
		}
		switch k {
		case "t":
			t = v
		case "v1":
			sigs = append(sigs, v)
		}
	}
	ts, err := strconv.ParseInt(t, 10, 64)
	if err != nil || math.Abs(float64(now.Unix()-ts)) > 300 {
		return false
	}
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(t + "."))
	mac.Write(body)
	expected := hex.EncodeToString(mac.Sum(nil))
	for _, s := range sigs {
		if hmac.Equal([]byte(s), []byte(expected)) {
			return true
		}
	}
	return false
}
 
func Handler(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
	if err != nil || !Verify(r.Header.Get("syntic-signature"), body, os.Getenv("SYNTIC_WEBHOOK_SECRET"), time.Now()) {
		http.Error(w, "bad signature", http.StatusBadRequest)
		return
	}
	enqueue(r.Header.Get("syntic-event-id"), body) // de-duplicate on the event id, process later
	w.WriteHeader(http.StatusOK)
}

Test it

Send a test event to your endpoint, or recompute a signature by hand as shown on Webhooks.

Was this page helpful?