Github Webhook Auth and Validation for Fun And Profit

Another day, another dollar… and another problem that wasted hours of my time on an otherwise fine Monday afternoon.

Let’s start with the SEO keyword soup. I’m using aws-go-lambda-proxy to create a Lambda function in Golang that uses gin handlers to implement HTTP endpoints for inbound GitHub webhooks. I’m building this out for several source systems and GitHub is one that I wanted to get a handle on the schema for quickly to determine its viability; to this end I found an example payload in a Gist, wrote some local end-to-end tests without auth, and thought nothing further of it. As it turns out, however, GitHub webhooks’ authentication method isn’t as simple as providing a secret that they send via header or otherwise. They compute a hash based on the body of the payload and a secret that you then have to validate. This didn’t intimidate me four hours ago but a series of gotchas led to me spending more time than I’d anticipated on updating my curl and LocalStack-based test suite to accommodate this validation. They provide test values for this process but it seemed to break down the moment I shoved a full-size webhook payload through API Gateway via curl in my test script. Here are the changes I had to make in no particular order to get things playing nicely:

  • Use ValidateSignature from the go-github library to perform the signature validation. If you’re writing a webhook handler you’re probably using this library already but you might not have realized that this function was built-in given the lack of mention of it in the official docs. The value comes in the X-Hub-Signature-256 header which you can pass to this function along with your secret and the request body. Apropros:
  • Use gin.Context’s GetRawData() to retrieve the request body. Although documentation is sparse it seems as though this function is distinct from doing io.ReadAll on the gin.Context body stream. My experiments determined that the former will give you the request as it came to the Lambda function whereas the latter involves some whitespace massaging (at least of JSON data). Structural JSON equivalence is of no use to us when we’re computing a hash so we need the request body straight from the tap.

These were the two big problems I encountered on the server side of things. Getting my test scripts to properly generate a faux GitHub signature and pass the payload properly were issues unto themselves. Here’s what I discovered on that front:

  • You can use OpenSSL to perform the requisite hashing. OpenSSL might have a reputation akin to a powerful, arcane spellbook but in this case it’s actually pretty simple: cat "$payload_file" | openssl dgst -sha256 -hmac "$secret" | awk '{print $2}' will give you the hash you’re after given $payload_file and $secret.
  • Use --data-binary instead of -d or --data when passing the request body in curl. curl, while being an immensely useful tool, does something funny encoding-wise to the JSON files I was trying to pass to the GitHub webhook handler that totally broke the validation process. The --data-binary option faithfully beams the file in question via HTTP to your request handler. No funny stuff.

After implementing all of the above fixes I was happy to see the GitHub hash validation middleware I’d written cheerfully pass. Happy days! I hope my experience will save you some frustration if you’re doing something similar as Golang Lambda functions running in LocalStack are a bit more complicated to breakpoint debug than regular containers.

Last updated on 2025 Apr 21, 16:39