-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add utilities for processing payload messages
Fixes #361. Change-Id: I1c80b8eb7816efc9b83e6b8be956430c68809d5e
- Loading branch information
Showing
2 changed files
with
200 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
// Copyright 2016 The go-github AUTHORS. All rights reserved. | ||
// | ||
// Use of this source code is governed by a BSD-style | ||
// license that can be found in the LICENSE file. | ||
|
||
// This file provides functions for validating payloads from GitHub Webhooks. | ||
// GitHub docs: https://developer.github.com/webhooks/securing/#validating-payloads-from-github | ||
|
||
package github | ||
|
||
import ( | ||
"crypto/hmac" | ||
"crypto/sha1" | ||
"crypto/sha256" | ||
"crypto/sha512" | ||
"encoding/hex" | ||
"errors" | ||
"fmt" | ||
"hash" | ||
"io/ioutil" | ||
"net/http" | ||
"strings" | ||
) | ||
|
||
const ( | ||
// sha1Prefix is the prefix used by GitHub before the HMAC hexdigest. | ||
sha1Prefix = "sha1" | ||
// sha256Prefix and sha512Prefix are provided for future compatibility. | ||
sha256Prefix = "sha256" | ||
sha512Prefix = "sha512" | ||
// signatureHeader is the GitHub header key used to pass the HMAC hexdigest. | ||
signatureHeader = "X-Hub-Signature" | ||
) | ||
|
||
// genMAC generates the HMAC signature for a message provided the secret key | ||
// and hashFunc. | ||
func genMAC(message, key []byte, hashFunc func() hash.Hash) []byte { | ||
mac := hmac.New(hashFunc, key) | ||
mac.Write(message) | ||
return mac.Sum(nil) | ||
} | ||
|
||
// checkMAC reports whether messageMAC is a valid HMAC tag for message. | ||
func checkMAC(message, messageMAC, key []byte, hashFunc func() hash.Hash) bool { | ||
expectedMAC := genMAC(message, key, hashFunc) | ||
return hmac.Equal(messageMAC, expectedMAC) | ||
} | ||
|
||
// messageMAC returns the hex-decoded HMAC tag from the signature and its | ||
// corresponding hash function. | ||
func messageMAC(signature string) ([]byte, func() hash.Hash, error) { | ||
if signature == "" { | ||
return nil, nil, errors.New("missing signature") | ||
} | ||
sigParts := strings.SplitN(signature, "=", 2) | ||
if len(sigParts) != 2 { | ||
return nil, nil, fmt.Errorf("error parsing signature %q", signature) | ||
} | ||
|
||
var hashFunc func() hash.Hash | ||
switch sigParts[0] { | ||
case sha1Prefix: | ||
hashFunc = sha1.New | ||
case sha256Prefix: | ||
hashFunc = sha256.New | ||
case sha512Prefix: | ||
hashFunc = sha512.New | ||
default: | ||
return nil, nil, fmt.Errorf("unknown hash type prefix: %q", sigParts[0]) | ||
} | ||
|
||
buf, err := hex.DecodeString(sigParts[1]) | ||
if err != nil { | ||
return nil, nil, fmt.Errorf("error decoding signature %q: %v", signature, err) | ||
} | ||
return buf, hashFunc, nil | ||
} | ||
|
||
// ValidatePayload validates an incoming GitHub Webhook event request | ||
// and returns the (JSON) payload. | ||
// secretKey is the GitHub Webhook secret message. | ||
// | ||
// Example usage: | ||
// | ||
// func (s *GitHubEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||
// payload, err := github.ValidatePayload(r, s.webhookSecretKey) | ||
// if err != nil { ... } | ||
// // Process payload... | ||
// } | ||
// | ||
func ValidatePayload(r *http.Request, secretKey []byte) (payload []byte, err error) { | ||
payload, err = ioutil.ReadAll(r.Body) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
sig := r.Header.Get(signatureHeader) | ||
if err := validateSignature(sig, payload, secretKey); err != nil { | ||
return nil, err | ||
} | ||
return payload, nil | ||
} | ||
|
||
// validateSignature validates the signature for the given payload. | ||
// signature is the GitHub hash signature delivered in the X-Hub-Signature header. | ||
// payload is the JSON payload sent by GitHub Webhooks. | ||
// secretKey is the GitHub Webhook secret message. | ||
// | ||
// GitHub docs: https://developer.github.com/webhooks/securing/#validating-payloads-from-github | ||
func validateSignature(signature string, payload, secretKey []byte) error { | ||
messageMAC, hashFunc, err := messageMAC(signature) | ||
if err != nil { | ||
return err | ||
} | ||
if !checkMAC(payload, messageMAC, secretKey, hashFunc) { | ||
return errors.New("payload signature check failed") | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
// Copyright 2016 The go-github AUTHORS. All rights reserved. | ||
// | ||
// Use of this source code is governed by a BSD-style | ||
// license that can be found in the LICENSE file. | ||
|
||
package github | ||
|
||
import ( | ||
"bytes" | ||
"net/http" | ||
"testing" | ||
) | ||
|
||
func TestValidatePayload(t *testing.T) { | ||
const defaultBody = `{"yo":true}` // All tests below use the default request body and signature. | ||
const defaultSignature = "sha1=126f2c800419c60137ce748d7672e77b65cf16d6" | ||
secretKey := []byte("0123456789abcdef") | ||
tests := []struct { | ||
signature string | ||
eventID string | ||
event string | ||
wantEventID string | ||
wantEvent string | ||
wantPayload string | ||
}{ | ||
// The following tests generate expected errors: | ||
{}, // Missing signature | ||
{signature: "yo"}, // Missing signature prefix | ||
{signature: "sha1=yo"}, // Signature not hex string | ||
{signature: "sha1=012345"}, // Invalid signature | ||
// The following tests expect err=nil: | ||
{ | ||
signature: defaultSignature, | ||
eventID: "dead-beef", | ||
event: "ping", | ||
wantEventID: "dead-beef", | ||
wantEvent: "ping", | ||
wantPayload: defaultBody, | ||
}, | ||
{ | ||
signature: defaultSignature, | ||
event: "ping", | ||
wantEvent: "ping", | ||
wantPayload: defaultBody, | ||
}, | ||
{ | ||
signature: "sha256=b1f8020f5b4cd42042f807dd939015c4a418bc1ff7f604dd55b0a19b5d953d9b", | ||
event: "ping", | ||
wantEvent: "ping", | ||
wantPayload: defaultBody, | ||
}, | ||
{ | ||
signature: "sha512=8456767023c1195682e182a23b3f5d19150ecea598fde8cb85918f7281b16079471b1329f92b912c4d8bd7455cb159777db8f29608b20c7c87323ba65ae62e1f", | ||
event: "ping", | ||
wantEvent: "ping", | ||
wantPayload: defaultBody, | ||
}, | ||
} | ||
|
||
for _, test := range tests { | ||
buf := bytes.NewBufferString(defaultBody) | ||
req, err := http.NewRequest("GET", "http://localhost/event", buf) | ||
if err != nil { | ||
t.Fatalf("NewRequest: %v", err) | ||
} | ||
if test.signature != "" { | ||
req.Header.Set(signatureHeader, test.signature) | ||
} | ||
|
||
got, err := ValidatePayload(req, secretKey) | ||
if err != nil { | ||
if test.wantPayload != "" { | ||
t.Errorf("ValidatePayload(%#v): err = %v, want nil", test, err) | ||
} | ||
continue | ||
} | ||
if string(got) != test.wantPayload { | ||
t.Errorf("ValidatePayload = %q, want %q", got, test.wantPayload) | ||
} | ||
} | ||
} |