| 1 | package stripeapi |
| 2 | |
| 3 | import ( |
| 4 | "crypto/hmac" |
| 5 | "crypto/sha256" |
| 6 | "encoding/hex" |
| 7 | "errors" |
| 8 | "strconv" |
| 9 | "testing" |
| 10 | "time" |
| 11 | ) |
| 12 | |
| 13 | func signed(body string, at time.Time, secret string) string { |
| 14 | ts := strconv.FormatInt(at.Unix(), 10) |
| 15 | mac := hmac.New(sha256.New, []byte(secret)) |
| 16 | mac.Write([]byte(ts + "." + body)) |
| 17 | return "t=" + ts + ",v1=" + hex.EncodeToString(mac.Sum(nil)) |
| 18 | } |
| 19 | |
| 20 | func TestVerifySignature(t *testing.T) { |
| 21 | t.Parallel() |
| 22 | now := time.Unix(1_800_000_000, 0) |
| 23 | body := `{"id":"evt_1"}` |
| 24 | ok := signed(body, now, "whsec_new") |
| 25 | for name, c := range map[string]struct { |
| 26 | header, body string |
| 27 | secrets []string |
| 28 | want bool |
| 29 | }{ |
| 30 | "good": {ok, body, []string{"whsec_new"}, true}, |
| 31 | "rotating, either": {ok, body, []string{"whsec_old", "whsec_new"}, true}, |
| 32 | "two v1s, one good": {ok + ",v1=00ff", body, []string{"whsec_new"}, true}, |
| 33 | "wrong secret": {ok, body, []string{"whsec_old"}, false}, |
| 34 | "body changed": {ok, `{"id":"evt_2"}`, []string{"whsec_new"}, false}, |
| 35 | "too old": {signed(body, now.Add(-6*time.Minute), "whsec_new"), body, []string{"whsec_new"}, false}, |
| 36 | "from the future": {signed(body, now.Add(6*time.Minute), "whsec_new"), body, []string{"whsec_new"}, false}, |
| 37 | "no timestamp": {"v1=00", body, []string{"whsec_new"}, false}, |
| 38 | "empty": {"", body, []string{"whsec_new"}, false}, |
| 39 | "v0 only (Stripe's test scheme)": {"t=" + strconv.FormatInt(now.Unix(), 10) + ",v0=00", body, []string{"whsec_new"}, false}, |
| 40 | } { |
| 41 | err := VerifySignature(c.header, []byte(c.body), c.secrets, now) |
| 42 | if got := err == nil; got != c.want { |
| 43 | t.Errorf("%s: verified %v, want %v", name, got, c.want) |
| 44 | } |
| 45 | if err != nil && !errors.Is(err, ErrBadSignature) { |
| 46 | t.Errorf("%s: error %v is not ErrBadSignature", name, err) |
| 47 | } |
| 48 | } |
| 49 | } |
| 50 | |
| 51 | // The window is five minutes either side of now: just inside passes, |
| 52 | // just outside fails. |
| 53 | func TestVerifySignatureTolerance(t *testing.T) { |
| 54 | t.Parallel() |
| 55 | now := time.Unix(1_800_000_000, 0) |
| 56 | body := `{"id":"evt_1"}` |
| 57 | secrets := []string{"whsec"} |
| 58 | for name, c := range map[string]struct { |
| 59 | at time.Time |
| 60 | want bool |
| 61 | }{ |
| 62 | "4m59s old": {now.Add(-4*time.Minute - 59*time.Second), true}, |
| 63 | "4m59s ahead": {now.Add(4*time.Minute + 59*time.Second), true}, |
| 64 | "5m01s old": {now.Add(-5*time.Minute - time.Second), false}, |
| 65 | "5m01s ahead": {now.Add(5*time.Minute + time.Second), false}, |
| 66 | } { |
| 67 | err := VerifySignature(signed(body, c.at, "whsec"), []byte(body), secrets, now) |
| 68 | if got := err == nil; got != c.want { |
| 69 | t.Errorf("%s: verified %v (%v), want %v", name, got, err, c.want) |
| 70 | } |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | // While a secret is being rotated Stripe signs with both; the second |
| 75 | // of two configured secrets is enough. |
| 76 | func TestVerifySignatureRotatingSecrets(t *testing.T) { |
| 77 | t.Parallel() |
| 78 | now := time.Unix(1_800_000_000, 0) |
| 79 | body := `{"id":"evt_1"}` |
| 80 | header := signed(body, now, "whsec_second") |
| 81 | if err := VerifySignature(header, []byte(body), []string{"whsec_first", "whsec_second"}, now); err != nil { |
| 82 | t.Fatalf("second secret did not match: %v", err) |
| 83 | } |
| 84 | if err := VerifySignature(header, []byte(body), []string{"whsec_first", "whsec_third"}, now); !errors.Is(err, ErrBadSignature) { |
| 85 | t.Fatalf("neither secret matches, got %v", err) |
| 86 | } |
| 87 | } |
| 88 | |