From 9abdb1fffb0edd1205dd8af68ae0a7705603db17 Mon Sep 17 00:00:00 2001 From: Ian Roberts Date: Sun, 27 Oct 2024 20:56:41 +0000 Subject: [PATCH] feat: new rule type specifically for signature checks Move the signature checking rules out of MatchRule into their own dedicated SignatureRule, configured as "check-signature" in the hooks file. This takes an algorithm, secret and Argument giving the source of the signature, and by default behaves exactly like the old payload-hmac- match rules. However it can also take a second optional Argument to customize how to generate the "string to sign", allowing signatures to be computed over something other than the full request body content. This could be a single header or payload item but more likely will be a "template" argument to combine items from different places in the request, such as the body content and one or more headers, e.g. to compute a signature over the X-Request-Id header, Date header, and request body, concatenated with CRLF, you could specify check-signature: algorithm: sha512 secret: 5uper5eecret signature: source: header name: X-Hook-Signature string-to-sign: source: template name: | {{- printf "%s\r\n" (.GetHeader "x-request-id") -}} {{- printf "%s\r\n" (.GetHeader "date") -}} {{- .BodyText -}} --- internal/hook/hook.go | 217 ++++++++++++++++++++++++++----------- internal/hook/hook_test.go | 157 +++++++++------------------ 2 files changed, 202 insertions(+), 172 deletions(-) diff --git a/internal/hook/hook.go b/internal/hook/hook.go index f0afb79..52ded35 100644 --- a/internal/hook/hook.go +++ b/internal/hook/hook.go @@ -196,45 +196,6 @@ func ValidateMAC(payload []byte, mac hash.Hash, signatures []string) (string, er return actualMAC, e } -// CheckPayloadSignature calculates and verifies SHA1 signature of the given payload -func CheckPayloadSignature(payload []byte, secret, signature string) (string, error) { - if secret == "" { - return "", errors.New("signature validation secret can not be empty") - } - - // Extract the signatures. - signatures := ExtractSignatures(signature, "sha1=") - - // Validate the MAC. - return ValidateMAC(payload, hmac.New(sha1.New, []byte(secret)), signatures) -} - -// CheckPayloadSignature256 calculates and verifies SHA256 signature of the given payload -func CheckPayloadSignature256(payload []byte, secret, signature string) (string, error) { - if secret == "" { - return "", errors.New("signature validation secret can not be empty") - } - - // Extract the signatures. - signatures := ExtractSignatures(signature, "sha256=") - - // Validate the MAC. - return ValidateMAC(payload, hmac.New(sha256.New, []byte(secret)), signatures) -} - -// CheckPayloadSignature512 calculates and verifies SHA512 signature of the given payload -func CheckPayloadSignature512(payload []byte, secret, signature string) (string, error) { - if secret == "" { - return "", errors.New("signature validation secret can not be empty") - } - - // Extract the signatures. - signatures := ExtractSignatures(signature, "sha512=") - - // Validate the MAC. - return ValidateMAC(payload, hmac.New(sha512.New, []byte(secret)), signatures) -} - func CheckScalrSignature(r *Request, signingKey string, checkDate bool) (bool, error) { if r.Headers == nil { return false, nil @@ -854,7 +815,12 @@ func (h *Hooks) LoadFromFile(path string, asTemplate bool, delimsStr string) err file = buf.Bytes() } - return yaml.Unmarshal(file, h) + err := yaml.Unmarshal(file, h) + if err != nil { + return err + } + + return h.postProcess() } // Append appends hooks unless the new hooks contain a hook with an ID that already exists @@ -882,12 +848,81 @@ func (h *Hooks) Match(id string) *Hook { return nil } +func (h *Hooks) postProcess() error { + for i := range *h { + rules := (*h)[i].TriggerRule + if rules != nil { + if err := postProcess(rules); err != nil { + return err + } + } + } + return nil +} + // Rules is a structure that contains one of the valid rule types type Rules struct { - And *AndRule `json:"and,omitempty"` - Or *OrRule `json:"or,omitempty"` - Not *NotRule `json:"not,omitempty"` - Match *MatchRule `json:"match,omitempty"` + And *AndRule `json:"and,omitempty"` + Or *OrRule `json:"or,omitempty"` + Not *NotRule `json:"not,omitempty"` + Match *MatchRule `json:"match,omitempty"` + Signature *SignatureRule `json:"check-signature,omitempty"` +} + +// postProcess is called on each Rules instance after loading it from JSON/YAML, +// to replace any legacy constructs with their modern equivalents. +func postProcess(r *Rules) error { + if r.And != nil { + for i := range *(r.And) { + if err := postProcess(&(*r.And)[i]); err != nil { + return err + } + } + } + if r.Or != nil { + for i := range *(r.Or) { + if err := postProcess(&(*r.Or)[i]); err != nil { + return err + } + } + } + if r.Not != nil { + return postProcess((*Rules)(r.Not)) + } + if r.Match != nil { + // convert any signature matching rules to the equivalent SignatureRule + if r.Match.Type == MatchHashSHA1 || r.Match.Type == MatchHMACSHA1 { + log.Printf(`warn: use of deprecated match type %s; use a check-signature rule instead`, r.Match.Type) + r.Signature = &SignatureRule{ + Algorithm: AlgorithmSHA1, + Secret: r.Match.Secret, + Signature: r.Match.Parameter, + } + r.Match = nil + return nil + } + if r.Match.Type == MatchHashSHA256 || r.Match.Type == MatchHMACSHA256 { + log.Printf(`warn: use of deprecated match type %s; use a check-signature rule instead`, r.Match.Type) + r.Signature = &SignatureRule{ + Algorithm: AlgorithmSHA256, + Secret: r.Match.Secret, + Signature: r.Match.Parameter, + } + r.Match = nil + return nil + } + if r.Match.Type == MatchHashSHA512 || r.Match.Type == MatchHMACSHA512 { + log.Printf(`warn: use of deprecated match type %s; use a check-signature rule instead`, r.Match.Type) + r.Signature = &SignatureRule{ + Algorithm: AlgorithmSHA512, + Secret: r.Match.Secret, + Signature: r.Match.Parameter, + } + r.Match = nil + return nil + } + } + return nil } // Evaluate finds the first rule property that is not nil and returns the value @@ -902,6 +937,8 @@ func (r Rules) Evaluate(req *Request) (bool, error) { return r.Not.Evaluate(req) case r.Match != nil: return r.Match.Evaluate(req) + case r.Signature != nil: + return r.Signature.Evaluate(req) } return false, nil @@ -976,16 +1013,19 @@ type MatchRule struct { // Constants for the MatchRule type const ( - MatchValue string = "value" - MatchRegex string = "regex" + MatchValue string = "value" + MatchRegex string = "regex" + IPWhitelist string = "ip-whitelist" + ScalrSignature string = "scalr-signature" + + // legacy match types that have migrated to SignatureRule + MatchHMACSHA1 string = "payload-hmac-sha1" MatchHMACSHA256 string = "payload-hmac-sha256" MatchHMACSHA512 string = "payload-hmac-sha512" MatchHashSHA1 string = "payload-hash-sha1" MatchHashSHA256 string = "payload-hash-sha256" MatchHashSHA512 string = "payload-hash-sha512" - IPWhitelist string = "ip-whitelist" - ScalrSignature string = "scalr-signature" ) // Evaluate MatchRule will return based on the type @@ -1004,29 +1044,74 @@ func (r MatchRule) Evaluate(req *Request) (bool, error) { return compare(arg, r.Value), nil case MatchRegex: return regexp.MatchString(r.Regex, arg) - case MatchHashSHA1: - log.Print(`warn: use of deprecated option payload-hash-sha1; use payload-hmac-sha1 instead`) - fallthrough - case MatchHMACSHA1: - _, err := CheckPayloadSignature(req.Body, r.Secret, arg) - return err == nil, err - case MatchHashSHA256: - log.Print(`warn: use of deprecated option payload-hash-sha256: use payload-hmac-sha256 instead`) - fallthrough - case MatchHMACSHA256: - _, err := CheckPayloadSignature256(req.Body, r.Secret, arg) - return err == nil, err - case MatchHashSHA512: - log.Print(`warn: use of deprecated option payload-hash-sha512: use payload-hmac-sha512 instead`) - fallthrough - case MatchHMACSHA512: - _, err := CheckPayloadSignature512(req.Body, r.Secret, arg) - return err == nil, err } } return false, err } +type SignatureRule struct { + Algorithm string `json:"algorithm,omitempty"` + Secret string `json:"secret,omitempty"` + Signature Argument `json:"signature,omitempty"` + Prefix string `json:"prefix,omitempty"` + StringToSign *Argument `json:"string-to-sign,omitempty"` +} + +// Constants for the SignatureRule type +const ( + AlgorithmSHA1 string = "sha1" + AlgorithmSHA256 string = "sha256" + AlgorithmSHA512 string = "sha512" +) + +// Evaluate extracts the signature payload and signature value from the request +// and checks whether the signature matches +func (r SignatureRule) Evaluate(req *Request) (bool, error) { + if r.Secret == "" { + return false, errors.New("signature validation secret can not be empty") + } + + var hashConstructor func() hash.Hash + switch r.Algorithm { + case AlgorithmSHA1: + hashConstructor = sha1.New + case AlgorithmSHA256: + hashConstructor = sha256.New + case AlgorithmSHA512: + hashConstructor = sha512.New + default: + return false, fmt.Errorf("unknown hash algorithm %s", r.Algorithm) + } + + prefix := r.Prefix + if prefix == "" { + // default prefix is "sha1=" for SHA1, etc. + prefix = fmt.Sprintf("%s=", r.Algorithm) + } + + // find the signature + sig, err := r.Signature.Get(req) + if err != nil { + return false, fmt.Errorf("could not extract signature string: %w", err) + } + + // determine the payload that is signed + payload := req.Body + if r.StringToSign != nil { + payloadStr, err := r.StringToSign.Get(req) + if err != nil { + return false, fmt.Errorf("could not build string-to-sign: %w", err) + } + payload = []byte(payloadStr) + } + + // check the signature + signatures := ExtractSignatures(sig, prefix) + _, err = ValidateMAC(payload, hmac.New(hashConstructor, []byte(r.Secret)), signatures) + + return err == nil, err +} + // compare is a helper function for constant time string comparisons. func compare(a, b string) bool { return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1 diff --git a/internal/hook/hook_test.go b/internal/hook/hook_test.go index aeb5131..3b76490 100644 --- a/internal/hook/hook_test.go +++ b/internal/hook/hook_test.go @@ -40,100 +40,6 @@ func TestGetParameter(t *testing.T) { } } -var checkPayloadSignatureTests = []struct { - payload []byte - secret string - signature string - mac string - ok bool -}{ - {[]byte(`{"a": "z"}`), "secret", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", true}, - {[]byte(`{"a": "z"}`), "secret", "sha1=b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", true}, - {[]byte(`{"a": "z"}`), "secret", "sha1=XXXe04cbb22afa8ffbff8796fc1894ed27badd9e,sha1=b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", true}, - {[]byte(``), "secret", "25af6174a0fcecc4d346680a72b7ce644b9a88e8", "25af6174a0fcecc4d346680a72b7ce644b9a88e8", true}, - // failures - {[]byte(`{"a": "z"}`), "secret", "XXXe04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", false}, - {[]byte(`{"a": "z"}`), "secret", "sha1=XXXe04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", false}, - {[]byte(`{"a": "z"}`), "secret", "sha1=XXXe04cbb22afa8ffbff8796fc1894ed27badd9e,sha1=XXXe04cbb22afa8ffbff8796fc1894ed27badd9e", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", false}, - {[]byte(`{"a": "z"}`), "secreX", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "900225703e9342328db7307692736e2f7cc7b36e", false}, - {[]byte(`{"a": "z"}`), "", "b17e04cbb22afa8ffbff8796fc1894ed27badd9e", "", false}, - {[]byte(``), "secret", "XXXf6174a0fcecc4d346680a72b7ce644b9a88e8", "25af6174a0fcecc4d346680a72b7ce644b9a88e8", false}, -} - -func TestCheckPayloadSignature(t *testing.T) { - for _, tt := range checkPayloadSignatureTests { - mac, err := CheckPayloadSignature(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 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}, - {[]byte(`{"a": "z"}`), "secret", "sha256=XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89,sha256=f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", true}, - {[]byte(``), "secret", "f9e66e179b6747ae54108f82f8ade8b3c25d76fd30afde6c395822c530196169", "f9e66e179b6747ae54108f82f8ade8b3c25d76fd30afde6c395822c530196169", true}, - // failures - {[]byte(`{"a": "z"}`), "secret", "XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", false}, - {[]byte(`{"a": "z"}`), "secret", "sha256=XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", false}, - {[]byte(`{"a": "z"}`), "secret", "sha256=XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89,sha256=XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", false}, - {[]byte(`{"a": "z"}`), "", "XXX7af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89", "", false}, - {[]byte(``), "secret", "XXX66e179b6747ae54108f82f8ade8b3c25d76fd30afde6c395822c530196169", "f9e66e179b6747ae54108f82f8ade8b3c25d76fd30afde6c395822c530196169", 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 && tt.mac != "" && strings.Contains(err.Error(), tt.mac) { - t.Errorf("error message should not disclose expected mac: %s", err) - } - } -} - -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}, - {[]byte(``), "secret", "b0e9650c5faf9cd8ae02276671545424104589b3656731ec193b25d01b07561c27637c2d4d68389d6cf5007a8632c26ec89ba80a01c77a6cdd389ec28db43901", "b0e9650c5faf9cd8ae02276671545424104589b3656731ec193b25d01b07561c27637c2d4d68389d6cf5007a8632c26ec89ba80a01c77a6cdd389ec28db43901", true}, - // failures - {[]byte(`{"a": "z"}`), "secret", "74a0081f5b5988f4f3e8b8dd34dadc6291611f2e6260635a7e1535f8e95edb97ff520ba8b152e8ca5760ac42639854f3242e29efc81be73a8bf52d474d31ffea", "4ab17cc8ec668ead8bf498f87f8f32848c04d5ca3c9bcfcd3db9363f0deb44e580b329502a7fdff633d4d8fca301cc5c94a55a2fec458c675fb0ff2655898324", false}, - {[]byte(`{"a": "z"}`), "", "74a0081f5b5988f4f3e8b8dd34dadc6291611f2e6260635a7e1535f8e95edb97ff520ba8b152e8ca5760ac42639854f3242e29efc81be73a8bf52d474d31ffea", "", false}, - {[]byte(``), "secret", "XXX9650c5faf9cd8ae02276671545424104589b3656731ec193b25d01b07561c27637c2d4d68389d6cf5007a8632c26ec89ba80a01c77a6cdd389ec28db43901", "b0e9650c5faf9cd8ae02276671545424104589b3656731ec193b25d01b07561c27637c2d4d68389d6cf5007a8632c26ec89ba80a01c77a6cdd389ec28db43901", 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{} @@ -456,7 +362,7 @@ func TestHooksTemplateLoadFromFile(t *testing.T) { continue } - s := (*h.Match("webhook").TriggerRule.And)[0].Match.Secret + s := (*h.Match("webhook").TriggerRule.And)[0].Signature.Secret if s != secret { t.Errorf("Expected secret of %q, got %q", secret, s) } @@ -492,22 +398,12 @@ var matchRuleTests = []struct { }{ {"value", "", "", "z", "", Argument{"header", "a", "", false, nil}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false}, {"regex", "^z", "", "z", "", Argument{"header", "a", "", false, nil}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", true, false}, - {"payload-hmac-sha1", "", "secret", "", "", Argument{"header", "a", "", false, nil}, map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), "", true, false}, - {"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", "", false, nil}, map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), "", true, false}, - {"payload-hmac-sha256", "", "secret", "", "", Argument{"header", "a", "", false, nil}, map[string]interface{}{"A": "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89"}, nil, nil, []byte(`{"a": "z"}`), "", true, false}, - {"payload-hash-sha256", "", "secret", "", "", Argument{"header", "a", "", false, nil}, map[string]interface{}{"A": "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89"}, nil, nil, []byte(`{"a": "z"}`), "", true, false}, // failures {"value", "", "", "X", "", Argument{"header", "a", "", false, nil}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false}, {"regex", "^X", "", "", "", Argument{"header", "a", "", false, nil}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, false}, {"value", "", "2", "X", "", Argument{"header", "a", "", false, nil}, map[string]interface{}{"Y": "z"}, nil, nil, []byte{}, "", false, true}, // reference invalid header // errors - {"regex", "*", "", "", "", Argument{"header", "a", "", false, nil}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, true}, // invalid regex - {"payload-hmac-sha1", "", "secret", "", "", Argument{"header", "a", "", false, nil}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac - {"payload-hash-sha1", "", "secret", "", "", Argument{"header", "a", "", false, nil}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac - {"payload-hmac-sha256", "", "secret", "", "", Argument{"header", "a", "", false, nil}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac - {"payload-hash-sha256", "", "secret", "", "", Argument{"header", "a", "", false, nil}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac - {"payload-hmac-sha512", "", "secret", "", "", Argument{"header", "a", "", false, nil}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac - {"payload-hash-sha512", "", "secret", "", "", Argument{"header", "a", "", false, nil}, map[string]interface{}{"A": ""}, nil, nil, []byte{}, "", false, true}, // invalid hmac + {"regex", "*", "", "", "", Argument{"header", "a", "", false, nil}, map[string]interface{}{"A": "z"}, nil, nil, []byte{}, "", false, true}, // invalid regex // 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 @@ -542,6 +438,55 @@ func TestMatchRule(t *testing.T) { } } +var signatureRuleTests = []struct { + algorithm, secret string + sigSource Argument + stringToSign *Argument + headers, query, payload map[string]interface{} + body []byte + ok bool + err bool +}{ + {"sha1", "secret", Argument{"header", "a", "", false, nil}, nil, map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), true, false}, + {"sha1", "secret", Argument{"header", "a", "", false, nil}, nil, map[string]interface{}{"A": "b17e04cbb22afa8ffbff8796fc1894ed27badd9e"}, nil, nil, []byte(`{"a": "z"}`), true, false}, + {"sha256", "secret", Argument{"header", "a", "", false, nil}, nil, map[string]interface{}{"A": "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89"}, nil, nil, []byte(`{"a": "z"}`), true, false}, + {"sha256", "secret", Argument{"header", "a", "", false, nil}, nil, map[string]interface{}{"A": "f417af3a21bd70379b5796d5f013915e7029f62c580fb0f500f59a35a6f04c89"}, nil, nil, []byte(`{"a": "z"}`), true, false}, + // errors + {"sha1", "secret", Argument{"header", "a", "", false, nil}, nil, map[string]interface{}{"A": ""}, nil, nil, []byte{}, false, true}, // invalid hmac + {"sha1", "secret", Argument{"header", "a", "", false, nil}, nil, map[string]interface{}{"A": ""}, nil, nil, []byte{}, false, true}, // invalid hmac + {"sha256", "secret", Argument{"header", "a", "", false, nil}, nil, map[string]interface{}{"A": ""}, nil, nil, []byte{}, false, true}, // invalid hmac + {"sha256", "secret", Argument{"header", "a", "", false, nil}, nil, map[string]interface{}{"A": ""}, nil, nil, []byte{}, false, true}, // invalid hmac + {"sha512", "secret", Argument{"header", "a", "", false, nil}, nil, map[string]interface{}{"A": ""}, nil, nil, []byte{}, false, true}, // invalid hmac + {"sha512", "secret", Argument{"header", "a", "", false, nil}, nil, map[string]interface{}{"A": ""}, nil, nil, []byte{}, false, true}, // invalid hmac + + // template to build custom string-to-sign + {"sha256", "secret", Argument{"header", "a", "", false, nil}, &Argument{"template", "{{ printf \"%s\\n%s\" .BodyText (.GetHeader \"x-id\") }}", "", false, nil}, map[string]interface{}{"A": "sha256=4f1d62e6e6de1e31537a5faefabeffd7dce115bc499584feefbf8db6d2da4027", "X-Id": "test"}, nil, nil, []byte(`{"a": "z"}`), true, false}, + {"sha256", "secret", Argument{"header", "a", "", false, nil}, &Argument{"template", "{{ printf \"%s\\n%s\" .BodyText (.GetHeader \"x-id\") }}", "", false, nil}, map[string]interface{}{"A": "sha256=4f1d62e6e6de1e31537a5faefabeffd7dce115bc499584feefbf8db6d2da4027", "X-Id": "unexpected"}, nil, nil, []byte(`{"a": "z"}`), false, true}, +} + +func TestSignatureRule(t *testing.T) { + for i, tt := range signatureRuleTests { + if tt.stringToSign != nil { + // post process the argument, as it would have been if it were loaded from a hooks file + tt.stringToSign.postProcess() + } + r := SignatureRule{tt.algorithm, tt.secret, tt.sigSource, "", tt.stringToSign} + req := &Request{ + Headers: tt.headers, + Query: tt.query, + Payload: tt.payload, + Body: tt.body, + RawRequest: &http.Request{ + RemoteAddr: "", + }, + } + ok, err := r.Evaluate(req) + if ok != tt.ok || (err != nil) != tt.err { + t.Errorf("%d failed to match %#v:\nexpected ok: %#v, err: %v\ngot ok: %#v, err: %v", i, r, tt.ok, tt.err, ok, err) + } + } +} + var andRuleTests = []struct { desc string // description of the test case rule AndRule