package stripeapi import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "errors" "strconv" "testing" "time" ) func signed(body string, at time.Time, secret string) string { ts := strconv.FormatInt(at.Unix(), 10) mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(ts + "." + body)) return "t=" + ts + ",v1=" + hex.EncodeToString(mac.Sum(nil)) } func TestVerifySignature(t *testing.T) { t.Parallel() now := time.Unix(1_800_000_000, 0) body := `{"id":"evt_1"}` ok := signed(body, now, "whsec_new") for name, c := range map[string]struct { header, body string secrets []string want bool }{ "good": {ok, body, []string{"whsec_new"}, true}, "rotating, either": {ok, body, []string{"whsec_old", "whsec_new"}, true}, "two v1s, one good": {ok + ",v1=00ff", body, []string{"whsec_new"}, true}, "wrong secret": {ok, body, []string{"whsec_old"}, false}, "body changed": {ok, `{"id":"evt_2"}`, []string{"whsec_new"}, false}, "too old": {signed(body, now.Add(-6*time.Minute), "whsec_new"), body, []string{"whsec_new"}, false}, "from the future": {signed(body, now.Add(6*time.Minute), "whsec_new"), body, []string{"whsec_new"}, false}, "no timestamp": {"v1=00", body, []string{"whsec_new"}, false}, "empty": {"", body, []string{"whsec_new"}, false}, "v0 only (Stripe's test scheme)": {"t=" + strconv.FormatInt(now.Unix(), 10) + ",v0=00", body, []string{"whsec_new"}, false}, } { err := VerifySignature(c.header, []byte(c.body), c.secrets, now) if got := err == nil; got != c.want { t.Errorf("%s: verified %v, want %v", name, got, c.want) } if err != nil && !errors.Is(err, ErrBadSignature) { t.Errorf("%s: error %v is not ErrBadSignature", name, err) } } } // The window is five minutes either side of now: just inside passes, // just outside fails. func TestVerifySignatureTolerance(t *testing.T) { t.Parallel() now := time.Unix(1_800_000_000, 0) body := `{"id":"evt_1"}` secrets := []string{"whsec"} for name, c := range map[string]struct { at time.Time want bool }{ "4m59s old": {now.Add(-4*time.Minute - 59*time.Second), true}, "4m59s ahead": {now.Add(4*time.Minute + 59*time.Second), true}, "5m01s old": {now.Add(-5*time.Minute - time.Second), false}, "5m01s ahead": {now.Add(5*time.Minute + time.Second), false}, } { err := VerifySignature(signed(body, c.at, "whsec"), []byte(body), secrets, now) if got := err == nil; got != c.want { t.Errorf("%s: verified %v (%v), want %v", name, got, err, c.want) } } } // While a secret is being rotated Stripe signs with both; the second // of two configured secrets is enough. func TestVerifySignatureRotatingSecrets(t *testing.T) { t.Parallel() now := time.Unix(1_800_000_000, 0) body := `{"id":"evt_1"}` header := signed(body, now, "whsec_second") if err := VerifySignature(header, []byte(body), []string{"whsec_first", "whsec_second"}, now); err != nil { t.Fatalf("second secret did not match: %v", err) } if err := VerifySignature(header, []byte(body), []string{"whsec_first", "whsec_third"}, now); !errors.Is(err, ErrBadSignature) { t.Fatalf("neither secret matches, got %v", err) } }