diff --git a/README.md b/README.md index c8ae1a3..0be08c8 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,4 @@ [![ghit.me](https://ghit.me/badge.svg?repo=adnanh/webhook)](https://ghit.me/repo/adnanh/webhook) [![Join the chat at https://gitter.im/adnanh/webhook](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/adnanh/webhook?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Flattr this](https://button.flattr.com/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=adnanh&url=https%3A%2F%2Fwww.github.com%2Fadnanh%2Fwebhook) [Donate via PayPal](https://paypal.me/hookdoo) | [Patreon page](https://www.patreon.com/webhook) - - -# Hookdoo -hookdoo - -If you don't have time to waste configuring, hosting, debugging and maintaining your webhook instance, we offer a __SaaS__ solution that has all of the capabilities webhook provides, plus a lot more, and all that packaged in a nice friendly web interface. If you are interested, find out more at [hookdoo website](https://www.hookdoo.com/). If you have any questions, you can contact us at info@hookdoo.com - - # What is webhook? [webhook](https://github.com/adnanh/webhook/) is a lightweight configurable tool written in Go, that allows you to easily create HTTP endpoints (hooks) on your server, which you can use to execute configured commands. You can also pass data from the HTTP request (such as headers, payload or query variables) to your commands. [webhook](https://github.com/adnanh/webhook/) also allows you to specify rules which have to be satisfied in order for the hook to be triggered. diff --git a/hook/hook.go b/hook/hook.go index b653537..b336e54 100644 --- a/hook/hook.go +++ b/hook/hook.go @@ -3,6 +3,7 @@ package hook import ( "crypto/hmac" "crypto/sha1" + "crypto/sha256" "encoding/hex" "encoding/json" "errors" @@ -101,6 +102,25 @@ func CheckPayloadSignature(payload []byte, secret string, signature string) (str return expectedMAC, err } +// CheckPayloadSignature256 calculates and verifies SHA256 signature of the given payload +func CheckPayloadSignature256(payload []byte, secret string, signature string) (string, error) { + if strings.HasPrefix(signature, "sha256=") { + signature = signature[7:] + } + + mac := hmac.New(sha256.New, []byte(secret)) + _, err := mac.Write(payload) + if err != nil { + return "", err + } + expectedMAC := hex.EncodeToString(mac.Sum(nil)) + + if !hmac.Equal([]byte(signature), []byte(expectedMAC)) { + return expectedMAC, &SignatureError{signature} + } + return expectedMAC, err +} + // CheckIPWhitelist makes sure the provided remote address (of the form IP:port) falls within the provided IP range // (in CIDR form or a single IP address). func CheckIPWhitelist(remoteAddr string, ipRange string) (bool, error) { @@ -602,10 +622,11 @@ type MatchRule struct { // Constants for the MatchRule type const ( - MatchValue string = "value" - MatchRegex string = "regex" - MatchHashSHA1 string = "payload-hash-sha1" - IPWhitelist string = "ip-whitelist" + MatchValue string = "value" + MatchRegex string = "regex" + MatchHashSHA1 string = "payload-hash-sha1" + MatchHashSHA256 string = "payload-hash-sha256" + IPWhitelist string = "ip-whitelist" ) // Evaluate MatchRule will return based on the type @@ -623,6 +644,9 @@ func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, bod case MatchHashSHA1: _, err := CheckPayloadSignature(*body, r.Secret, arg) return err == nil, err + case MatchHashSHA256: + _, err := CheckPayloadSignature256(*body, r.Secret, arg) + return err == nil, err } } return false, nil diff --git a/hook/hook_test.go b/hook/hook_test.go index a274196..7828e00 100644 --- a/hook/hook_test.go +++ b/hook/hook_test.go @@ -33,6 +33,32 @@ func TestCheckPayloadSignature(t *testing.T) { } } +var checkPayloadSignature256Tests = []struct { + payload []byte + secret string + signature string + mac string + ok bool +}{ + {[]byte(`{"a": "z"}`), "secret", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true}, + {[]byte(`{"a": "z"}`), "secret", "sha256=f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true}, + // failures + {[]byte(`{"a": "z"}`), "secret", "XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", false}, +} + +func TestCheckPayloadSignature256(t *testing.T) { + for _, tt := range checkPayloadSignature256Tests { + mac, err := CheckPayloadSignature256(tt.payload, tt.secret, tt.signature) + if (err == nil) != tt.ok || mac != tt.mac { + t.Errorf("failed to check payload signature {%q, %q, %q}:\nexpected {mac:%#v, ok:%#v},\ngot {mac:%#v, ok:%#v}", tt.payload, tt.secret, tt.signature, tt.mac, tt.ok, mac, (err == nil)) + } + + if err != nil && strings.Contains(err.Error(), tt.mac) { + t.Errorf("error message should not disclose expected mac: %s", err) + } + } +} + var extractParameterTests = []struct { s string params interface{} @@ -249,9 +275,10 @@ var matchRuleTests = []struct { ok bool err bool }{ - {"value", "", "", "z", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false}, - {"regex", "^z", "", "z", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false}, - {"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), "", true, false}, + {"value", "", "", "z", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, true, false}, + {"regex", "^z", "", "z", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, true, false}, + {"payload-hash-sha1", "", "secret", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), true, false}, + {"payload-hash-sha256", "", "secret", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89"}, nil, nil, []byte(`{"a": "z"}`), true, false}, // failures {"value", "", "", "X", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false}, {"regex", "^X", "", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false}, @@ -259,6 +286,7 @@ var matchRuleTests = []struct { // errors {"regex", "*", "", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, true}, // invalid regex {"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac + {"payload-hash-sha256", "", "secret", "", Argument{"header", "a", ""}, &map[string]interface{}{"A": ""}, nil, nil, []byte{}, false, true}, // invalid hmac // IP whitelisting, valid cases {"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", true, false}, // valid IPv4, with range {"ip-whitelist", "", "", "", "192.168.0.1/24", Argument{}, nil, nil, nil, []byte{}, "192.168.0.2:9000", true, false}, // valid IPv4, with range