From 43a10ec3c20274c37772b675293d770c110dd8b8 Mon Sep 17 00:00:00 2001 From: Emil Henry Flakk Date: Sun, 21 Feb 2021 14:43:05 +0100 Subject: [PATCH] Add support for validating MS Teams outgoing webhooks --- internal/hook/hook.go | 57 +++++++++++++++++++++++++++------ internal/hook/hook_test.go | 64 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 10 deletions(-) diff --git a/internal/hook/hook.go b/internal/hook/hook.go index 0510095..8566fec 100644 --- a/internal/hook/hook.go +++ b/internal/hook/hook.go @@ -234,6 +234,39 @@ func CheckPayloadSignature512(payload []byte, secret, signature string) (string, return ValidateMAC(payload, hmac.New(sha512.New, []byte(secret)), signatures) } +func CheckMSTeamsSignature(r *Request, signingKey string) (bool, error) { + if r.Headers == nil { + return false, nil + } + + // Check if the signing key is valid + if signingKey == "" { + return false, errors.New("signature validation key can not be empty") + } + secret, err := base64.StdEncoding.DecodeString(signingKey) + if err != nil { + return false, errors.New("signature validation key must be valid base64") + } + // Check if a valid HMAC header was provided + if _, ok := r.Headers["Authorization"]; !ok { + return false, nil + } + headerParts := strings.SplitN(r.Headers["Authorization"].(string), " ", 2) + if len(headerParts) != 2 || headerParts[0] != "HMAC" { + return false, errors.New("malformed 'Authorization' header") + } + providedSignature := headerParts[1] + + mac := hmac.New(sha256.New, secret) + mac.Write(r.Body) + expectedSignature := base64.StdEncoding.EncodeToString(mac.Sum(nil)) + if !hmac.Equal([]byte(providedSignature), []byte(expectedSignature)) { + return false, &SignatureError{Signature: providedSignature} + } + return true, nil + +} + func CheckScalrSignature(r *Request, signingKey string, checkDate bool) (bool, error) { if r.Headers == nil { return false, nil @@ -896,16 +929,17 @@ type MatchRule struct { // Constants for the MatchRule type const ( - MatchValue string = "value" - MatchRegex string = "regex" - 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" + MatchValue string = "value" + MatchRegex string = "regex" + 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" + MSTeamsSignature string = "msteams-signature" ) // Evaluate MatchRule will return based on the type @@ -916,6 +950,9 @@ func (r MatchRule) Evaluate(req *Request) (bool, error) { if r.Type == ScalrSignature { return CheckScalrSignature(req, r.Secret, true) } + if r.Type == MSTeamsSignature { + return CheckMSTeamsSignature(req, r.Secret) + } arg, err := r.Parameter.Get(req) if err == nil { diff --git a/internal/hook/hook_test.go b/internal/hook/hook_test.go index e9f29e4..1c3560d 100644 --- a/internal/hook/hook_test.go +++ b/internal/hook/hook_test.go @@ -192,6 +192,70 @@ func TestCheckScalrSignature(t *testing.T) { } } +var checkMSTeamsSignatureTests = []struct { + description string + headers map[string]interface{} + body []byte + secret string + expectedSignature string + ok bool +}{ + { + "Valid signature", + map[string]interface{}{"Authorization": "HMAC gpjdTlOlaReTBLRFdwqdXhLqG7hFXVYTBorGDpaW5UE="}, + []byte(`{"a": "b"}`), "bmV2ZXJnb25uYWdpdmV5b3V1cA==", + "gpjdTlOlaReTBLRFdwqdXhLqG7hFXVYTBorGDpaW5UE=", true, + }, + { + "Wrong signature", + map[string]interface{}{"Authorization": "HMAC 1337TlOlaReTBLRFdwqdXhLqG7hFXVYTBorGDpaW5UE="}, + []byte(`{"a": "b"}`), "bmV2ZXJnb25uYWdpdmV5b3V1cA==", + "gpjdTlOlaReTBLRFdwqdXhLqG7hFXVYTBorGDpaW5UE=", false, + }, + { + "Missing Authorization header", + map[string]interface{}{"Different-Header": "HMAC wrong"}, + []byte(`{"a": "b"}`), "bmV2ZXJnb25uYWdpdmV5b3V1cA==", + "gpjdTlOlaReTBLRFdwqdXhLqG7hFXVYTBorGDpaW5UE=", false, + }, + { + "Malformed Authorization header", + map[string]interface{}{"Authorization": "HMAC 123---"}, + []byte(`{"a": "b"}`), "bmV2ZXJnb25uYWdpdmV5b3V1cA==", + "gpjdTlOlaReTBLRFdwqdXhLqG7hFXVYTBorGDpaW5UE=", false, + }, + { + "Missing signing key", + map[string]interface{}{"Authorization": "HMAC gpjdTlOlaReTBLRFdwqdXhLqG7hFXVYTBorGDpaW5UE="}, + []byte(`{"a": "b"}`), "", + "gpjdTlOlaReTBLRFdwqdXhLqG7hFXVYTBorGDpaW5UE=", false, + }, + { + "Malformed signing key", + map[string]interface{}{"Authorization": "HMAC gpjdTlOlaReTBLRFdwqdXhLqG7hFXVYTBorGDpaW5UE="}, + []byte(`{"a": "b"}`), "---2ZXJnb25uYWdpdmV5b3V1cA==", + "gpjdTlOlaReTBLRFdwqdXhLqG7hFXVYTBorGDpaW5UE=", false, + }, +} + +func TestCheckMSTeamsSignature(t *testing.T) { + for _, testCase := range checkMSTeamsSignatureTests { + r := &Request{ + Headers: testCase.headers, + Body: testCase.body, + } + valid, err := CheckMSTeamsSignature(r, testCase.secret) + if valid != testCase.ok { + t.Errorf("failed to check MS Teams signature fot test case: %s\nexpected ok:%#v, got ok:%#v}", + testCase.description, testCase.ok, valid) + } + + if err != nil && testCase.secret != "" && strings.Contains(err.Error(), testCase.expectedSignature) { + t.Errorf("error message should not disclose expected mac: %s on test case %s", err, testCase.description) + } + } +} + var checkIPWhitelistTests = []struct { addr string ipRange string