Wehbook validation, in phython

Hi!

I’ve been using this post as a guide, but I’m still running into a mis-match when trying to validate the X-Ghost-Signature.

Here’s my code thus far:

    def post(self, request, *args, **kwargs):
        ghost_webhook_secret = SiteConfiguration.get_solo().ghost_webhook_secret
        if ghost_webhook_secret is None or ghost_webhook_secret == "":
            logger.error("No ghost_webhook_secret configured")
            return HttpResponse(status=401)

        ghost_signature = request.headers.get("X_GHOST_SIGNATURE")
        if ghost_signature is None:
            logger.error("No X_GHOST_SIGNATURE header")
            return HttpResponse(status=401)

        # Of form sha256=c992ed22cb8a7eae6b925acf97c707be55a15c65155b1f8c0b8749549620f044, t=1724537500613
        try:
            parts = ghost_signature.split(", ")
            signature = parts[0].split("=")[1]
            timestamp = parts[1].split("=")[1]
        except ValueError:
            logger.error("Invalid X_GHOST_SIGNATURE header, expected two")
            return HttpResponse(status=401)

        body = request.body
        # Make sure body is bytes
        if not isinstance(body, bytes):
            body = body.encode('utf-8')

        # Make sure the key is bytes
        if not isinstance(ghost_webhook_secret, bytes):
            ghost_webhook_secret = ghost_webhook_secret.encode('utf-8')

        # Calculate the HMAC
        calculated_signature = hmac.new(ghost_webhook_secret, body, hashlib.sha256).hexdigest()

        if not hmac.compare_digest(calculated_signature, signature):
            logger.error("Signatures do not match - webhook not processed")
            return HttpResponse(status=401)

        payload = request.data
        print("Headers: {}".format(request.headers))
        print(f"Payload: {payload}")

        return HttpResponse(status=200)

I’ve been staring at this for a while now, so figured it’s time to post and see if others with less glazed eyes can shed any light!

btw: if you’re curious, this is what it’s like from the debugger (the shared key is correct, and now, also not what’s in that pic :):

what im trying to show is:

  • inputs to hmac.new are ‘bytes’
  • correct parsing/splitting of the signature parts
  • my sanity

not sure about the third one…

any ideas?

Urk! I might be going mad.

I just changed it to:

        # Make sure timestamp is bytes
        if not isinstance(timestamp, bytes):
            timestamp = timestamp.encode('utf-8')

        # Make sure the key is bytes
        if not isinstance(ghost_webhook_secret, bytes):
            ghost_webhook_secret = ghost_webhook_secret.encode('utf-8')

        # Calculate the HMAC (not documented from what I can tell, but timestamp has to be included)
        sign_this_please = body + timestamp
        calculated_signature = hmac.new(ghost_webhook_secret, sign_this_please, hashlib.sha256).hexdigest()

i,e: include timestamp after the body (I was using just body, previously).

Um.
Now it passes.

(btw: Im using ghost 5.89.1 - which might be relevant)

The webhook generation logic was updated back in July to avoid replay attacks, you’re correct to append the timestamp to the body now :slight_smile:

1 Like