diff --git a/internal/events/trigger/forward_event.go b/internal/events/trigger/forward_event.go index eee6099..af920bd 100644 --- a/internal/events/trigger/forward_event.go +++ b/internal/events/trigger/forward_event.go @@ -109,13 +109,12 @@ func ForwardEvent(p ForwardParamters) (*http.Response, error) { func getSignatureHeader(req *http.Request, id string, secret string, transport string, timestamp string, payload []byte) { mac := hmac.New(sha256.New, []byte(secret)) - ts, _ := time.Parse(time.RFC3339Nano, timestamp) switch transport { case models.TransportWebhook: req.Header.Set("Twitch-Eventsub-Message-Timestamp", timestamp) - prefix := ts.AppendFormat([]byte(id), time.RFC3339Nano) - mac.Write(prefix) + // The signature must cover the exact timestamp string sent in the header + mac.Write([]byte(id + timestamp)) mac.Write(payload) req.Header.Set("Twitch-Eventsub-Message-Signature", fmt.Sprintf("sha256=%x", mac.Sum(nil))) } diff --git a/internal/events/trigger/forward_event_test.go b/internal/events/trigger/forward_event_test.go index 3bac204..213a0aa 100644 --- a/internal/events/trigger/forward_event_test.go +++ b/internal/events/trigger/forward_event_test.go @@ -12,6 +12,7 @@ import ( "testing" "time" + "github.com/twitchdev/twitch-cli/internal/models" "github.com/twitchdev/twitch-cli/test_setup" ) @@ -47,3 +48,50 @@ func TestForwardEventEventsub(t *testing.T) { })) defer ts.Close() } + +func TestForwardEventSignatureUsesTimestampHeader(t *testing.T) { + a := test_setup.SetupTestEnv(t) + + secret := "potaytoes" + id := "test-message-id" + + // Valid RFC3339Nano timestamps that are not in Go's canonical output form + timestamps := []string{ + "2023-01-01T00:00:00.000Z", + "2023-01-01T00:00:00.120Z", + "2023-01-01T00:00:00+00:00", + } + + for _, timestamp := range timestamps { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + a.Nil(err) + + // Signature is HMAC-SHA256 over message id + timestamp header + body, as documented by Twitch + mac := hmac.New(sha256.New, []byte(secret)) + mac.Write([]byte(r.Header.Get("Twitch-Eventsub-Message-Id"))) + mac.Write([]byte(r.Header.Get("Twitch-Eventsub-Message-Timestamp"))) + mac.Write(body) + + a.Equal(timestamp, r.Header.Get("Twitch-Eventsub-Message-Timestamp")) + a.Equal(fmt.Sprintf("sha256=%x", mac.Sum(nil)), r.Header.Get("Twitch-Eventsub-Message-Signature"), timestamp) + w.WriteHeader(http.StatusAccepted) + })) + + resp, err := ForwardEvent(ForwardParamters{ + ID: id, + ForwardAddress: ts.URL, + JSON: []byte(`{"test":true}`), + Transport: models.TransportWebhook, + Timestamp: timestamp, + Secret: secret, + Event: "channel.follow", + Type: EventSubMessageTypeNotification, + SubscriptionVersion: "2", + }) + a.Nil(err) + a.Equal(http.StatusAccepted, resp.StatusCode) + resp.Body.Close() + ts.Close() + } +}