]> git.cworth.org Git - turbot/commitdiff
turbot_lambda: Add Slack signature verification
authorCarl Worth <cworth@cworth.org>
Mon, 12 Oct 2020 22:04:30 +0000 (15:04 -0700)
committerCarl Worth <cworth@cworth.org>
Mon, 12 Oct 2020 22:07:13 +0000 (15:07 -0700)
This will reject any request that does not come from Slack itself,
(as verified by the Slack-specified HMAC algorithm and the
SLACK_SIGNING_SECRET that Slack made available to us and that we
have registered as an encrypted SSM parameter).

turbot_lambda/turbot_lambda.py

index f52ac4fa73ad257221cef0e7cf4a5260bace4c0d..b86d9184921c9a61fb67a4b1b89a97d4f308dd0e 100644 (file)
@@ -3,17 +3,67 @@ from turbot.rot import rot
 from slack import WebClient
 import boto3
 import requests
+import hashlib
+import hmac
 
 ssm = boto3.client('ssm')
+
+response = ssm.get_parameter(Name='SLACK_SIGNING_SECRET', WithDecryption=True)
+slack_signing_secret = bytes(response['Parameter']['Value'], 'utf-8')
+
 response = ssm.get_parameter(Name='SLACK_BOT_TOKEN', WithDecryption=True)
 slack_bot_token = response['Parameter']['Value']
 slack_client = WebClient(slack_bot_token)
 
+def error(message):
+    """Generate an error response for a Slack request
+
+    This will print the error message (so that it appears in CloudWatch
+    logs) and will then return a dictionary suitable for returning
+    as an error response."""
+
+    print("Error: {}.".format(message))
+
+    return {
+        'statusCode': 400,
+        'body': ''
+    }
+
+def slack_is_valid_request(slack_signature, timestamp, body):
+    """Returns True if the timestamp and body correspond to signature.
+
+    This implements the Slack signature verification using the slack
+    signing secret (obtained via an SSM parameter in code above)."""
+
+    content = "v0:{}:{}".format(timestamp,body).encode('utf-8')
+
+    signature = 'v0=' + hmac.new(slack_signing_secret,
+                                 content,
+                                 hashlib.sha256).hexdigest()
+
+    if hmac.compare_digest(signature, slack_signature):
+        return True
+    else:
+        print("Bad signature: {} != {}".format(signature, slack_signature))
+        return False
+
 def turbot_lambda(event, context):
     """Top-level entry point for our lambda function.
 
-    This parses the request and arguments and farms out to supporting
-    functions to implement all supported slash commands."""
+    This function first verifies that the request actually came from
+    Slack, (by means of the SLACK_SIGNING_SECRET SSM parameter), and
+    refuses to do anything if not.
+
+    Then this parses the request and arguments and farms out to
+    supporting functions to implement all supported slash commands.
+
+    """
+
+    signature = event['headers']['X-Slack-Signature']
+    timestamp = event['headers']['X-Slack-Request-Timestamp']
+
+    if not slack_is_valid_request(signature, timestamp, event['body']):
+        return error("Invalid Slack signature")
 
     body = parse_qs(event['body'])
     command = body['command'][0]
@@ -22,14 +72,7 @@ def turbot_lambda(event, context):
     if (command == "/rotlambda" or command == "/rot"):
         return rot_slash_command(body, args)
 
-    error = "Command {} not implemented.".format(command)
-
-    print("Error: {}".format(error))
-
-    return {
-        'statusCode': 404,
-        'body': error
-    }
+    return error("Command {} not implemented".format(command))
 
 def rot_slash_command(body, args):
     """Implementation of the /rot command