2

I am trying to validate facebook's webhook payload using the instruction they have given in their developer docs. The signature I am generating (expectedHash) is not matching the signature that I am receiving from Facebook (signatureHash). I think I am following what they are saying but I am doing something wrong which I cannot pinpoint yet.

Validating Payloads

We sign all Event Notification payloads with a SHA256 signature and include the signature in the request's X-Hub-Signature-256 header, preceded with sha256=. You don't have to validate the payload, but you should.

To validate the payload:

Generate a SHA256 signature using the payload and your app's App Secret.
Compare your signature to the signature in the X-Hub-Signature-256 header (everything after sha256=). 
If the signatures match, the payload is genuine.

Please note that we generate the signature using an escaped unicode version of the payload, with lowercase hex digits. If you just calculate against the decoded bytes, you will end up with a different signature. For example, the string äöå should be escaped to \u00e4\u00f6\u00e5.

Below is my code in lambda

def lambda_handler(event, context):

response = {
    "status": 500,
    "body" : "failed"
}

print("event is")
print(event)

signature = event["headers"]["X-Hub-Signature-256"]
if(not signature):
    return(f"couldn't find {signature} in headers")
else:
    elements = signature.split("=")
    print("elements is")
    print(elements)
    
    signatureHash = elements[1]
    print("signature hash is " + str(signatureHash))
    
    app_secret = os.environ.get('APP_SECRET')
    print("app_secret is " + str(app_secret)) 
    
    
    expectedHash = hmac.new(bytes(app_secret,'utf-8') ,digestmod=hashlib.sha256).hexdigest()
    print("expected hash is " + expectedHash)
    
    if(signatureHash != expectedHash):
        return response
    else:
        response["status"] = 200
        response["body"] = expectedHash
        return response

response I am getting is:

{ "status": 500, "body": "failed" }

expected response:

{ "status": 200, "body": value of expectedHash }

Could you please help me with this?

Edit 1:

Figured out how to do it.

Apparently I was using a wrong content mapping in AWS API Gateway. I needed to use the $input.body to get the raw payload data in the event argument of AWS lambda handler function. My content mapping looks like this:

#set($allParams = $input.params())
{
    "method": "$context.httpMethod",
    "params" : {
        #foreach($type in $allParams.keySet())
        #set($params = $allParams.get($type))
        "$type" : {
              #foreach($paramName in $params.keySet())
              "$paramName" : "$util.escapeJavaScript($params.get($paramName))"
              #if($foreach.hasNext),#end
              #end
        }
        #if($foreach.hasNext),#end
        #end
    },
    
    "body" : $input.body
    
}

Below is my lambda handler function for validating payload:

def lambda_handler(event, context):

response = {
    "status": 500,
    "body" : "failed"
}

print("event is")
print(event)

signature = event["params"]["header"]["X-Hub-Signature-256"]

if(not signature):
    return(f"couldn't find {signature} in headers")
else:
    try:
        elements = signature.split("=")
        print("elements is")
        print(elements)
        
        signatureHash = elements[1]
        #print("signature hash is " + str(signatureHash))
        
        app_secret = os.environ.get('APP_SECRET') 
        
        key = bytes(app_secret, 'UTF-8')
        payload = event['body']
        json_string = json.dumps(payload)
        print("payload json_string is " + json_string)
    
        expectedHash = hmac.new(key, msg=json_string.encode(), digestmod=hashlib.sha256).hexdigest()
        
        print("expected hash is " + expectedHash)
        
        if(signatureHash != expectedHash):
            print(response)
            return response
        else:
            response["status"] = 200
            response["body"] = expectedHash
            print(response)
            return response
    except Exception as e:
        return e

As of 12/14/2022, the above function works for all webhook fields except messages (which is the one I really need). Trying to figure it out.

  • Try this maybe uppercase is require: expectedHash = hmac.new(bytes(app_secret,'utf-8'), digestmod=hashlib.sha256).hexdigest().upper() – MZM Dec 14 '22 at 03:38
  • The signature facebook's webhook sends is lowercase. Upper() would convert it to uppercase, right? my problem is is the ```signatureHash = sha256=abc```, the ```expectedHash = sha256=def```. – Rishabh Tyagi Dec 14 '22 at 16:24
  • @RishabhTyagi, you have to take the raw payload and escape characters with codes over 127. Doing a json.dump() and then an string encode should not work – wrbp Dec 28 '22 at 21:48
  • @wrbp what do you mean escape characters with codes over 127? And would you suggest to remove json.dumps() and string encode? – Rishabh Tyagi Jan 10 '23 at 13:39
  • this is not right ``` json_string = json.dumps(payload)```, you have to use the raw payload and json_string is not. So you have to use an escaped unicode version of event['body']. _Please note that we generate the signature using an escaped unicode version of the payload, with lowercase hex digits. If you just calculate against the decoded bytes, you will end up with a different signature. For example, the string äöå should be escaped to \u00e4\u00f6\u00e5.'_ – wrbp Jan 10 '23 at 21:07
  • @wrbp I did ```json.dumps(payload).encode()``` because the ```msg``` parameter in ```hmac.new(key, msg, digestmod).hexdigest()``` must be a bytes or bytearray objects. I have tried passing raw payload (as in ```hmac.new(key, msg=payload, digestmod=hashlib.sha256).hexdigest()```) but it didn't work for me. I was getting "TypeError: object supporting the buffer API required". – Rishabh Tyagi Jan 11 '23 at 12:34
  • @RishabhTyagi do the same as you do with the key, bytes(payload, 'UTF-8') – wrbp Jan 11 '23 at 15:31
  • @wrbp bro for some reason it's not working you know. I am either getting an error from ```bytes()``` because either payload is not a string or not an integer and it is not working as an iterable even though the ```type(payload)``` returns `````` and dict is iterable. ```bytes(payload, 'UTF-8')``` raises ```TypeError: encoding without a string argument```. – Rishabh Tyagi Jan 12 '23 at 17:54

2 Answers2

2

I ran into this problem a few days ago and have been tearing my hair out trying to figure it out. But I finally have an answer for you!

Initially I thought the problems I was having were related to one of the following:

  • API Gateway automatically decoding the raw bytes and passing the decoded json object to my lambda function. Then not being able to encode back into the correct raw bytes (i.e some spacing data or something had been lost). I thought this because I had seen Javascript and Python(Flask) applications validating the raw bytes before decoding the request body.
  • Something to do with that disclaimer in the documentation you referenced regarding "escaped unicode version of the payload, with lowercase hex digits".
  • Not using the correct "App secret".

One of these was correct - it's the App Secret!

So it turns out there are THREE keys relating to your facebook/whatsapp application. I was trying everything I could with the 2 you usually use, namely:

  • The Verify token (which you set yourself and use when setting up your webhook - in the GET request). Verify Token
  • The Access token (which you use when interacting with messages or downloading media - basically in any call to the messages endpoint) Access Token

But there is a THIRD! The App Secret! Found here: App Secret

Using this "App Secret" - the following code will validate your incoming payload (assuming you have an environment variable on your lambda function called "FB_APP_SECRET" containing that third key shown above):

import hashlib
import hmac
import os

secret = os.environ["FB_APP_SECRET"]

def lambda_function(event, context):
  body = event["body"]
  sig = event["headers"]["X-Hub-Signature-256"]

  if not validate_signature(body, sig):
    print("Could not validate payload!")
    # ... Do whatever you want here.


def validate_signature(data, hmac_header):
  hmac_recieved = str(hmac_header).removeprefix("sha256=")
  digest = hmac.new(secret.encode("utf-8"), data.encode("utf-8"), hashlib.sha256).hexdigest()
  return hmac.compare_digest(hmac_recieved, digest)

Obviously you should still do all the other checks to see if it's a POST or GET, etc... This is just a minimal example.

NOTE: I have confirmed this works with their disclaimer example of "äöå"

Charlie
  • 31
  • 4
  • 1
    for me it was the lambda proxy that resolved my issue. I was using the AWS configurations to get the required request data and it was changing the original payload. When I switched to lambda proxy it worked like a charm. Regardless, thanks for your solution <3 – Rishabh Tyagi Jun 14 '23 at 20:13
  • Glad you got everything working! Appreciate the response. <3 – Charlie Jun 15 '23 at 06:47
1

This is your code but using Lambda Proxy Integration, so event keys are a bit different, event["body"], is a raw string, then you can parse it to get the elements you need from it, i think that is easier than all the mapping stuff without the lambda proxy:

import os
import json
import hmac
import hashlib

def lambda_handler(event, context):

    response = {
        'statusCode': '200',
        'body' : "OK"
    }
    
    
    print("event is")
    print(event)

    
    signature = event["headers"]["X-Hub-Signature-256"]
    
    if(not signature):
        response["body"] =  (f"couldn't find {signature} in headers")
        return response
    else:
        try:
            elements = signature.split("=")
            print("elements is")
            print(elements)
            
            signatureHash = elements[1]
            #print("signature hash is " + str(signatureHash))
            
            app_secret = os.environ.get('APP_SECRET') 
            
            key = bytes(app_secret, 'UTF-8')
            payload = event['body']
            #json_string = json.dumps(payload)
            #print("payload json_string is " + json_string)
        
            expectedHash = hmac.new(key, msg=bytes(payload,'UTF-8'), digestmod=hashlib.sha256).hexdigest()
            
            print("expected hash is " + expectedHash)
            
            if(signatureHash != expectedHash):
                response["body"] = "eh " + expectedHash + " sh " + signatureHash
                print(response)
                return response
            else:
                response["statusCode"] = 200
                response["body"] = "Check ok"
                print(response)
                return response
        except Exception as err:
            response["body"] = f"Unexpected {err=}, {type(err)=}"
            return response
wrbp
  • 870
  • 1
  • 3
  • 9
  • getting this: ```TypeError('encoding without a string argument')``` on ```expectedHash = hmac.new(key, msg=bytes(payload, 'UTF-8'), digestmod=hashlib.sha256).hexdigest()``` – Rishabh Tyagi Feb 10 '23 at 21:15
  • ```{'status': 400, 'body': "Unexpected err = encoding without a string argument\n, type err = "}``` – Rishabh Tyagi Feb 10 '23 at 21:24
  • Also payload is a ```dict```. I do not know if that is what causing the issue – Rishabh Tyagi Feb 10 '23 at 21:42
  • 1
    Remember I'm using Lambda Proxy Integration, the line payload = event["body"] puts a string in payload. Unless you really need to do it without the proxy integration this is much more simple than doing thee mapping. – wrbp Feb 11 '23 at 00:13
  • yeah Lambda Proxy worked for me. Thanks – Rishabh Tyagi Jun 14 '23 at 20:12