| 1 | package stripeapi |
| 2 | |
| 3 | import ( |
| 4 | "crypto/hmac" |
| 5 | "crypto/sha256" |
| 6 | "encoding/hex" |
| 7 | "errors" |
| 8 | "strconv" |
| 9 | "strings" |
| 10 | "time" |
| 11 | ) |
| 12 | |
| 13 | // webhookTolerance is how old a signed delivery may be: Stripe's own |
| 14 | // libraries' five minutes, which is what stops a captured one replaying. |
| 15 | const webhookTolerance = 5 * time.Minute |
| 16 | |
| 17 | // ErrBadSignature is every way a delivery can fail the check: the header |
| 18 | // is malformed, stale, or signed by none of the secrets. |
| 19 | var ErrBadSignature = errors.New("stripeapi: webhook signature does not check") |
| 20 | |
| 21 | // VerifySignature checks a Stripe-Signature header ("t=…,v1=…[,v1=…]") |
| 22 | // over body against any of secrets (more than one while rotating). |
| 23 | func VerifySignature(header string, body []byte, secrets []string, now time.Time) error { |
| 24 | var ts string |
| 25 | var sigs [][]byte |
| 26 | for _, part := range strings.Split(header, ",") { |
| 27 | k, v, _ := strings.Cut(strings.TrimSpace(part), "=") |
| 28 | switch k { |
| 29 | case "t": |
| 30 | ts = v |
| 31 | case "v1": |
| 32 | if b, err := hex.DecodeString(v); err == nil { |
| 33 | sigs = append(sigs, b) |
| 34 | } |
| 35 | } |
| 36 | } |
| 37 | t, err := strconv.ParseInt(ts, 10, 64) |
| 38 | if err != nil || len(sigs) == 0 { |
| 39 | return ErrBadSignature |
| 40 | } |
| 41 | if d := now.Sub(time.Unix(t, 0)); d > webhookTolerance || d < -webhookTolerance { |
| 42 | return ErrBadSignature |
| 43 | } |
| 44 | for _, secret := range secrets { |
| 45 | mac := hmac.New(sha256.New, []byte(secret)) |
| 46 | mac.Write([]byte(ts)) |
| 47 | mac.Write([]byte(".")) |
| 48 | mac.Write(body) |
| 49 | want := mac.Sum(nil) |
| 50 | for _, s := range sigs { |
| 51 | if hmac.Equal(s, want) { |
| 52 | return nil |
| 53 | } |
| 54 | } |
| 55 | } |
| 56 | return ErrBadSignature |
| 57 | } |
| 58 | |