// Verify a CallQA webhook signature (Go 1.20+, standard library only). // // CallQA-Signature: t=,v1=.")> // // Run a tiny receiver locally: // // CALLQA_WEBHOOK_SECRET=whsec_... go run verify_webhook.go # listens on :8080 package main import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "io" "log" "math" "net/http" "os" "strconv" "strings" "time" ) const toleranceS = 300 type Event struct { ID string `json:"id"` Type string `json:"type"` Created int64 `json:"created"` Data map[string]interface{} `json:"data"` } // Verify checks the signature of a raw body and returns the decoded event. func Verify(secret, header string, body []byte, now time.Time) (*Event, error) { if header == "" { return nil, errors.New("missing CallQA-Signature header") } var ts string var sigs []string for _, part := range strings.Split(header, ",") { k, v, _ := strings.Cut(strings.TrimSpace(part), "=") switch k { case "t": ts = v case "v1": sigs = append(sigs, v) } } t, err := strconv.ParseInt(ts, 10, 64) if err != nil || len(sigs) == 0 { return nil, errors.New("malformed CallQA-Signature header") } if math.Abs(float64(now.Unix()-t)) > toleranceS { return nil, errors.New("timestamp outside the tolerance window") } mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(ts + ".")) mac.Write(body) expected := mac.Sum(nil) for _, s := range sigs { given, err := hex.DecodeString(s) if err == nil && hmac.Equal(given, expected) { var ev Event if err := json.Unmarshal(body, &ev); err != nil { return nil, err } return &ev, nil } } return nil, errors.New("signature mismatch") } func main() { secret := os.Getenv("CALLQA_WEBHOOK_SECRET") if secret == "" { log.Fatal("set CALLQA_WEBHOOK_SECRET") } http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) if err != nil { http.Error(w, "read error", http.StatusBadRequest) return } ev, err := Verify(secret, r.Header.Get("CallQA-Signature"), body, time.Now()) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } fmt.Printf("%s %s: %v\n", ev.ID, ev.Type, ev.Data) w.WriteHeader(http.StatusNoContent) }) log.Fatal(http.ListenAndServe(":8080", nil)) }