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