diff --git a/docs/Hook-Rules.md b/docs/Hook-Rules.md index 52947aa..68b83d3 100644 --- a/docs/Hook-Rules.md +++ b/docs/Hook-Rules.md @@ -186,7 +186,39 @@ For the regex syntax, check out } ``` -### 4. Match Whitelisted IP range +### 4. Match payload-hash-sha256 +```json +{ + "match": + { + "type": "payload-hash-sha256", + "secret": "yoursecret", + "parameter": + { + "source": "header", + "name": "X-Signature" + } + } +} +``` + +### 5. Match payload-hash-sha512 +```json +{ + "match": + { + "type": "payload-hash-sha512", + "secret": "yoursecret", + "parameter": + { + "source": "header", + "name": "X-Signature" + } + } +} +``` + +### 6. Match Whitelisted IP range The IP can be IPv4- or IPv6-formatted, using [CIDR notation](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing#CIDR_blocks). To match a single IP address only, use `/32`. @@ -200,7 +232,7 @@ The IP can be IPv4- or IPv6-formatted, using [CIDR notation](https://en.wikipedi } ``` -### 5. Match scalr-signature +### 7. Match scalr-signature The trigger rule checks the scalr signature and also checks that the request was signed less than 5 minutes before it was received. A unqiue signing key is generated for each webhook endpoint URL you register in Scalr. diff --git a/hook/hook.go b/hook/hook.go index 98fb975..20a2664 100644 --- a/hook/hook.go +++ b/hook/hook.go @@ -5,6 +5,7 @@ import ( "crypto/hmac" "crypto/sha1" "crypto/sha256" + "crypto/sha512" "encoding/base64" "encoding/hex" "encoding/json" @@ -134,6 +135,27 @@ func CheckPayloadSignature256(payload []byte, secret string, signature string) ( return expectedMAC, err } +// CheckPayloadSignature512 calculates and verifies SHA512 signature of the given payload +func CheckPayloadSignature512(payload []byte, secret string, signature string) (string, error) { + if secret == "" { + return "", errors.New("signature validation secret can not be empty") + } + + signature = strings.TrimPrefix(signature, "sha512=") + + mac := hmac.New(sha512.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 +} + func CheckScalrSignature(headers map[string]interface{}, body []byte, signingKey string, checkDate bool) (bool, error) { // Check for the signature and date headers if _, ok := headers["X-Signature"]; !ok { @@ -748,6 +770,7 @@ const ( MatchRegex string = "regex" MatchHashSHA1 string = "payload-hash-sha1" MatchHashSHA256 string = "payload-hash-sha256" + MatchHashSHA512 string = "payload-hash-sha512" IPWhitelist string = "ip-whitelist" ScalrSignature string = "scalr-signature" ) @@ -773,6 +796,9 @@ func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, bod case MatchHashSHA256: _, err := CheckPayloadSignature256(*body, r.Secret, arg) return err == nil, err + case MatchHashSHA512: + _, err := CheckPayloadSignature512(*body, r.Secret, arg) + return err == nil, err } } return false, nil diff --git a/hook/hook_test.go b/hook/hook_test.go index e8a98bb..c2dc16d 100644 --- a/hook/hook_test.go +++ b/hook/hook_test.go @@ -94,6 +94,33 @@ func TestCheckPayloadSignature256(t *testing.T) { } } +var checkPayloadSignature512Tests = []struct { + payload []byte + secret string + signature string + mac string + ok bool +}{ + {[]byte(`{"a": "z"}`), "secret", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", true}, + {[]byte(`{"a": "z"}`), "secret", "sha512=4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", true}, + // failures + {[]byte(`{"a": "z"}`), "secret", "74a0081f5b5988f4f3e8b8dd34dadc6291611f2e6260635a7e1535f8e95edb97ff520ba8b152e8ca5760ac42639854f3242e29efc81be73a8bf52d474d31ffea", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", false}, + {[]byte(`{"a": "z"}`), "", "74a0081f5b5988f4f3e8b8dd34dadc6291611f2e6260635a7e1535f8e95edb97ff520ba8b152e8ca5760ac42639854f3242e29efc81be73a8bf52d474d31ffea", "", false}, +} + +func TestCheckPayloadSignature512(t *testing.T) { + for _, tt := range checkPayloadSignature512Tests { + mac, err := CheckPayloadSignature512(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 && tt.mac != "" && strings.Contains(err.Error(), tt.mac) { + t.Errorf("error message should not disclose expected mac: %s", err) + } + } +} + var checkScalrSignatureTests = []struct { description string headers map[string]interface{}