package stripeapi import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "errors" "strconv" "strings" "time" ) // webhookTolerance is how old a signed delivery may be: Stripe's own // libraries' five minutes, which is what stops a captured one replaying. const webhookTolerance = 5 * time.Minute // ErrBadSignature is every way a delivery can fail the check: the header // is malformed, stale, or signed by none of the secrets. var ErrBadSignature = errors.New("stripeapi: webhook signature does not check") // VerifySignature checks a Stripe-Signature header ("t=…,v1=…[,v1=…]") // over body against any of secrets (more than one while rotating). func VerifySignature(header string, body []byte, secrets []string, now time.Time) error { var ts string var sigs [][]byte for _, part := range strings.Split(header, ",") { k, v, _ := strings.Cut(strings.TrimSpace(part), "=") switch k { case "t": ts = v case "v1": if b, err := hex.DecodeString(v); err == nil { sigs = append(sigs, b) } } } t, err := strconv.ParseInt(ts, 10, 64) if err != nil || len(sigs) == 0 { return ErrBadSignature } if d := now.Sub(time.Unix(t, 0)); d > webhookTolerance || d < -webhookTolerance { return ErrBadSignature } for _, secret := range secrets { mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(ts)) mac.Write([]byte(".")) mac.Write(body) want := mac.Sum(nil) for _, s := range sigs { if hmac.Equal(s, want) { return nil } } } return ErrBadSignature }