Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor verifySignature for re-use #721

Merged
merged 4 commits into from
Nov 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 4 additions & 42 deletions api/webhook/taskcluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ package webhook

import (
"context"
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -42,29 +39,14 @@ func tcWebhookHandler(w http.ResponseWriter, r *http.Request) {
return
}

ctx := shared.NewAppEngineContext(r)
log := shared.GetLogger(ctx)

payload, err := ioutil.ReadAll(r.Body)
r.Body.Close()
payload, err := verifyAndGetPayload(r)
if err != nil {
log.Errorf("%v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

secret, err := getSecret(ctx)
if err != nil {
log.Errorf("%v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

if !verifySignature(payload, r.Header.Get("X-Hub-Signature"), secret) {
http.Error(w, "HMAC verification failed", http.StatusUnauthorized)
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}

ctx := appengine.NewContext(r)
log := shared.GetLogger(ctx)
log.Debugf("GitHub Delivery: %s", r.Header.Get("X-GitHub-Delivery"))

processed, err := handleStatusEvent(ctx, payload)
Expand Down Expand Up @@ -267,26 +249,6 @@ func getAuth(ctx context.Context) (username string, password string, err error)
return u.Username, u.Password, err
}

func getSecret(ctx context.Context) (token string, err error) {
var t shared.Token
key := datastore.NewKey(ctx, "Token", "github-tc-webhook-secret", 0, nil)
err = datastore.Get(ctx, key, &t)
return t.Secret, err
}

func verifySignature(message []byte, signature string, secret string) bool {
// https://developer.github.com/webhooks/securing/
signature = strings.TrimPrefix(signature, "sha1=")
messageMAC, err := hex.DecodeString(signature)
if err != nil {
return false
}
mac := hmac.New(sha1.New, []byte(secret))
mac.Write(message)
expectedMAC := mac.Sum(nil)
return hmac.Equal(messageMAC, expectedMAC)
}

func createAllRuns(log shared.Logger,
client *http.Client,
api,
Expand Down
17 changes: 0 additions & 17 deletions api/webhook/taskcluster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,23 +112,6 @@ func TestExtractResultURLs(t *testing.T) {
}, urls)
}

func TestVerifySignature(t *testing.T) {
message := []byte("test")
signature := "a053ee211b4693456ef071e336f74ab699250318"
secret := "95bfab9afa719185ee7c3658356b166b7f45349a"
assert.True(t, verifySignature(
message, signature, secret))
assert.False(t, verifySignature(
[]byte("foobar"), signature, secret))
assert.False(t, verifySignature(
message, "875a5feef4cde4265d6d5d21c304d755903ccb60", secret))
assert.False(t, verifySignature(
message, signature, "875a5feef4cde4265d6d5d21c304d755903ccb60"))
// Test an ill-formed (odd-length) signature.
assert.False(t, verifySignature(
message, "875a5feef4cde4265d6d5d21c304d755903ccb6", secret))
}

func TestCreateAllRuns_success(t *testing.T) {
var requested uint32
requested = 0
Expand Down
49 changes: 49 additions & 0 deletions api/webhook/verify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package webhook

import (
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
"errors"
"io/ioutil"
"net/http"
"strings"

"github.com/web-platform-tests/wpt.fyi/shared"
)

func verifyAndGetPayload(r *http.Request) ([]byte, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: this is fine for now, but I imagine you'd like to reuse this method in checks to verify the payload? (I saw your TODO in that PR.)

When that happens, we'd need to export (and perhaps move) this function, and add tokenName as an argument.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do :)

ctx := shared.NewAppEngineContext(r)
log := shared.GetLogger(ctx)

payload, err := ioutil.ReadAll(r.Body)
r.Body.Close()
if err != nil {
log.Errorf("Failed to read request body: %s", err.Error())
return nil, errors.New("Failed to read request body")
}

secret, err := shared.GetSecret(ctx, "github-tc-webhook-secret")
if err != nil {
log.Errorf("Failed to get verification secret: %s", err.Error())
return nil, errors.New("Internal error")
}

if !verifySignature(payload, r.Header.Get("X-Hub-Signature"), secret) {
return nil, errors.New("HMAC verification failed")
}
return payload, nil
}

func verifySignature(message []byte, signature string, secret string) bool {
// https://developer.github.com/webhooks/securing/
signature = strings.TrimPrefix(signature, "sha1=")
messageMAC, err := hex.DecodeString(signature)
if err != nil {
return false
}
mac := hmac.New(sha1.New, []byte(secret))
mac.Write(message)
expectedMAC := mac.Sum(nil)
return hmac.Equal(messageMAC, expectedMAC)
}
25 changes: 25 additions & 0 deletions api/webhook/verify_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// +build small

package webhook

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestVerifySignature(t *testing.T) {
message := []byte("test")
signature := "a053ee211b4693456ef071e336f74ab699250318"
secret := "95bfab9afa719185ee7c3658356b166b7f45349a"
assert.True(t, verifySignature(message, signature, secret))

assert.False(t, verifySignature([]byte("foobar"), signature, secret))
assert.False(t, verifySignature(
message, "875a5feef4cde4265d6d5d21c304d755903ccb60", secret))
assert.False(t, verifySignature(
message, signature, "875a5feef4cde4265d6d5d21c304d755903ccb60"))
// Test an ill-formed (odd-length) signature.
assert.False(t, verifySignature(
message, "875a5feef4cde4265d6d5d21c304d755903ccb6", secret))
}
11 changes: 11 additions & 0 deletions shared/datastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,3 +279,14 @@ func IsFeatureEnabled(ctx context.Context, flagName string) bool {
}
return flag.Enabled
}

// GetSecret is a helper wrapper for loading a token's secret from the datastore
// by name.
func GetSecret(ctx context.Context, tokenName string) (string, error) {
key := datastore.NewKey(ctx, "Token", tokenName, 0, nil)
var token Token
if err := datastore.Get(ctx, key, &token); err != nil {
return "", err
}
return token.Secret, nil
}
19 changes: 19 additions & 0 deletions shared/datastore_medium_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -442,3 +442,22 @@ func TestIsFeatureEnabled(t *testing.T) {
datastore.Put(ctx, key, &shared.Flag{Enabled: true})
assert.True(t, shared.IsFeatureEnabled(ctx, flagName))
}

func TestGetSecret(t *testing.T) {
i, err := sharedtest.NewAEInstance(true)
assert.Nil(t, err)
defer i.Close()
r, err := i.NewRequest("GET", "/", nil)
assert.Nil(t, err)
tokenName := "foo"
ctx := shared.NewAppEngineContext(r)
key := datastore.NewKey(ctx, "Token", tokenName, 0, nil)
secret, err := shared.GetSecret(ctx, tokenName)
assert.NotNil(t, err)
assert.Equal(t, "", secret)
// Token.
datastore.Put(ctx, key, &shared.Token{Secret: "bar"})
secret, err = shared.GetSecret(ctx, tokenName)
assert.Nil(t, err)
assert.Equal(t, "bar", secret)
}