From: Carl Worth Date: Mon, 12 Oct 2020 22:04:30 +0000 (-0700) Subject: turbot_lambda: Add Slack signature verification X-Git-Url: https://git.cworth.org/git?p=turbot;a=commitdiff_plain;h=ceeec94475d9d49f1b5d3091bc6e7889f1162bbf turbot_lambda: Add Slack signature verification 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). --- diff --git a/turbot_lambda/turbot_lambda.py b/turbot_lambda/turbot_lambda.py index f52ac4f..b86d918 100644 --- a/turbot_lambda/turbot_lambda.py +++ b/turbot_lambda/turbot_lambda.py @@ -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