diff --git a/docs/Hook-Examples.md b/docs/Hook-Examples.md index 23896a76..8ff543f7 100644 --- a/docs/Hook-Examples.md +++ b/docs/Hook-Examples.md @@ -257,6 +257,56 @@ Values in the request body can be accessed in the command or to the match rule b ] ``` +## Icoming Drone.io hook +```json +[ + { + "id": "redeploy-webhook", + "execute-command": "/home/adnan/deploy-go-webhook.sh", + "command-working-directory": "/home/adnan/go", + "response-message": "Executing deploy script", + "trigger-rule": + { + "and": [ + { + "match": + { + "type": "value", + "value": "build", + "parameter": { + "source": "header", + "name": "X-Drone-Event" + } + } + }, + { + "match": + { + "type": "value", + "value": "success", + "parameter": { + "source": "payload", + "name": "build.status" + } + } + }, + { + "match": + { + "type": "payload-hmac-sha256", + "secret": "600a2774d248847509ba27482330d513", + "parameter": { + "source": "header", + "name": "Signature" + } + } + } + ] + } + } +] +``` + ## A simple webhook with a secret key in GET query __Not recommended in production due to low security__ diff --git a/go.mod b/go.mod index bc7efb77..4a3a0e12 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/adnanh/webhook go 1.13 require ( + github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e github.com/clbanning/mxj v1.8.4 github.com/dustin/go-humanize v1.0.0 github.com/fsnotify/fsnotify v1.4.7 // indirect diff --git a/go.sum b/go.sum index a9a5f57c..13be2a67 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e h1:rl2Aq4ZODqTDkeSqQBy+fzpZPamacO1Srp8zq7jf2Sc= +github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e/go.mod h1:Xa6lInWHNQnuWoF0YPSsx+INFA9qk7/7pTjwb3PInkY= github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I= github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= diff --git a/internal/hook/hook.go b/internal/hook/hook.go index a9b4774c..28dda0a1 100644 --- a/internal/hook/hook.go +++ b/internal/hook/hook.go @@ -25,8 +25,10 @@ import ( "strings" "text/template" "time" + "net/http" "github.com/ghodss/yaml" + "github.com/99designs/httpsignatures-go" ) // Constants used to specify the parameter source @@ -212,6 +214,41 @@ func CheckPayloadSignature512(payload []byte, secret string, signature string) ( return ValidateMAC(payload, hmac.New(sha512.New, []byte(secret)), signatures) } +func CheckHmacSHA256(headers map[string]interface{}, body []byte, signingKey string) (bool, error) { + // Check headers for relevant parts + if _,ok := headers["Signature"]; !ok { + return false, errors.New("Missing Signature header") + } + if _,ok := headers["Digest"]; !ok { + return false, errors.New("Missing Digest header") + } + if _,ok := headers["Date"]; !ok { + return false, errors.New("Missing Date header") + } + if signingKey == "" { + return false, errors.New("Secret key is required and cannot be empty") + } + headerSignature := headers["Signature"].(string) + headerDate := headers["Date"].(string) + headerDigest := headers["Digest"].(string) + + tmpHttpRequest := &http.Request{ + Header: http.Header{ + "Date": []string{headerDate}, + "Digest": []string{headerDigest}, + }, + } + sig,err := httpsignatures.FromString(headerSignature) + if err != nil { + return false, errors.New("httpsignature error") + } + if !sig.IsValid(signingKey, tmpHttpRequest) { + return false, errors.New("Invalid Signature") + } + + return true, nil +} + 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 { @@ -835,6 +872,7 @@ const ( MatchHashSHA256 string = "payload-hash-sha256" MatchHashSHA512 string = "payload-hash-sha512" IPWhitelist string = "ip-whitelist" + MatchHmacSHA256 string = "payload-hmac-sha256" ScalrSignature string = "scalr-signature" ) @@ -847,6 +885,10 @@ func (r MatchRule) Evaluate(headers, query, payload *map[string]interface{}, bod return CheckScalrSignature(*headers, *body, r.Secret, true) } + if r.Type == MatchHmacSHA256 { + return CheckHmacSHA256(*headers, *body, r.Secret) + } + arg, err := r.Parameter.Get(headers, query, payload) if err == nil { switch r.Type { diff --git a/internal/hook/hook_test.go b/internal/hook/hook_test.go index 489fde24..65224cdf 100644 --- a/internal/hook/hook_test.go +++ b/internal/hook/hook_test.go @@ -181,6 +181,104 @@ func TestCheckScalrSignature(t *testing.T) { } } + +var checkHmacSHA256SignatureTests = []struct { + description string + headers map[string]interface{} + payload []byte + secret string + expectedSignature string + ok bool +}{ + { + "Valid Signature", + map[string]interface{}{ + "Date": "Thu, 10 Sep 2020 19:09:14 GMT", + "Digest": "SHA-256=HQ0wDM4daEmV1R+8SD2bTXu5TPUn/EhMdNyfQL3G3sU=", + "Signature": `keyId="hmac-key",algorithm="hmac-sha256",signature="JD2+OsbOqw8DBil5n0a8XVIzvMYXLODcnzJ+R7aieT4=",headers="date digest"`, + }, + []byte(`"x", "y"`), + "600a2774d248847509ba27482330d513", "", true, + }, + { + "Wrong Signature", + map[string]interface{}{ + "Date": "Thu, 10 Sep 2020 19:09:14 GMT", + "Digest": "SHA-256=HQ0wDM4daEmV1R+8SD2bTXu5TPUn/EhMdNyfQL3G3sU=", + "Signature": `keyId="hmac-key",algorithm="hmac-sha256",signature="JD2+OsbOqw8DBil5n0a8XVIzvMYXLODcnzJ+R7aieT4",headers="date digest"`, + }, + []byte(`"x", "y"`), + "600a2774d248847509ba27482330d513", "Invalid Signature", false, + }, + { + "Wrong Signature format upstream error", + map[string]interface{}{ + "Date": "Thu, 10 Sep 2020 19:09:14 GMT", + "Digest": "SHA-256=HQ0wDM4daEmV1R+8SD2bTXu5TPUn/EhMdNyfQL3G3sU=", + "Signature": `algorithm="hmac-sha256",signature="JD2+OsbOqw8DBil5n0a8XVIzvMYXLODcnzJ+R7aieT4",headers="date digest"`, + }, + []byte(`"x", "y"`), + "600a2774d248847509ba27482330d513", "httpsignature error", false, + }, + { + "Missing Date header", + map[string]interface{}{ + "Digest": "SHA-256=HQ0wDM4daEmV1R+8SD2bTXu5TPUn/EhMdNyfQL3G3sU=", + "Signature": `keyId="hmac-key",algorithm="hmac-sha256",signature="JD2+OsbOqw8DBil5n0a8XVIzvMYXLODcnzJ+R7aieT4=",headers="date digest"`, + }, + []byte(`"x", "y"`), "600a2774d248847509ba27482330d513", "Missing Date header", false, + }, + { + "Missing Digest header", + map[string]interface{}{ + "Date": "Thu, 10 Sep 2020 19:09:14 GMT", + "Signature": `keyId="hmac-key",algorithm="hmac-sha256",signature="JD2+OsbOqw8DBil5n0a8XVIzvMYXLODcnzJ+R7aieT4=",headers="date digest"`, + }, + []byte(`"x", "y"`), "600a2774d248847509ba27482330d513", "Missing Digest header", false, + }, + { + "Missing Secret", + map[string]interface{}{ + "Date": "Thu, 10 Sep 2020 19:09:14 GMT", + "Digest": "SHA-256=HQ0wDM4daEmV1R+8SD2bTXu5TPUn/EhMdNyfQL3G3sU=", + "Signature": `keyId="hmac-key",algorithm="hmac-sha256",signature="JD2+OsbOqw8DBil5n0a8XVIzvMYXLODcnzJ+R7aieT4=",headers="date digest"`, + }, + []byte(`"x", "y"`), "", "Secret key is required and cannot be empty", false, + }, + { + "Incorrect Secret", + map[string]interface{}{ + "Date": "Thu, 10 Sep 2020 19:09:14 GMT", + "Digest": "SHA-256=HQ0wDM4daEmV1R+8SD2bTXu5TPUn/EhMdNyfQL3G3sU=", + "Signature": `keyId="hmac-key",algorithm="hmac-sha256",signature="JD2+OsbOqw8DBil5n0a8XVIzvMYXLODcnzJ+R7aieT4=",headers="date digest"`, + }, + []byte(`"x", "y"`), "600a2774d248847509ba27482330d51", "Invalid Signature", false, + }, + { + "Incorrect Digest", + map[string]interface{}{ + "Date": "Thu, 10 Sep 2020 19:09:14 GMT", + "Digest": "SHA-256=HQ0wDM4daEmV1R+8SD2bTXu5TPUn/EhMdNyfQL3G3sU", + "Signature": `keyId="hmac-key",algorithm="hmac-sha256",signature="JD2+OsbOqw8DBil5n0a8XVIzvMYXLODcnzJ+R7aieT4=",headers="date digest"`, + }, + []byte(`"x", "y"`), "600a2774d248847509ba27482330d513", "Invalid Signature", false, + }, +} + +func TestCheckHmacSHA256Signature(t *testing.T) { + for _, testCase := range checkHmacSHA256SignatureTests{ + valid, err := CheckHmacSHA256(testCase.headers, testCase.payload, testCase.secret) + if valid != testCase.ok { + t.Errorf("failed to check hmac256 signature for test case: %s\nexpected ok:%#v, got ok:%#v}", + testCase.description, testCase.ok, valid) + } + + if err != nil && err.Error() != testCase.expectedSignature { + t.Errorf("unexpected error message: %s on test case %s", err, testCase.description) + } + } +} + var checkIPWhitelistTests = []struct { addr string ipRange string