How to Implement SHA256 Webhook Signature Verification (2024)

A major part of securing webhooks involves the verification of the webhook source and destination, as well as the validation of the webhook payload. In a previous article, we looked at different webhook authentication strategies, including their strengths and their limitations. After analyzing the different webhook authentication strategies available, signature verification stands out as the strongest form of protection for securing webhooks.

In this article, I will talk about the theory of signature verification and how it works, demonstrate how we can use signature verification to secure our webhooks. We will also look at code examples to implement SHA256 signature verification for the most popular coding languages.

How signature verification works

Before we begin, let’s take some time to discuss the process of webhook verification using cryptographic signatures.

Signature verification makes use of the Hash-based Message Authentication Code (HMAC) for authenticating and validating webhooks. An HMAC is calculated using a secret key and a cryptographic hash function like SHA-2 or SHA-3. The resulting HMAC, which becomes the signature of the webhook, is then used to authenticate the webhook and validate its payload.

The strength of the security provided by an HMAC depends on 3 things:

  1. The cryptographic strength of the underlying hashing function.
  2. The size of the hash output (224, 256, 384, or 512 digest size bits).
  3. The size and quality (key length and characters) of the key.

How to Implement SHA256 Webhook Signature Verification (1) The image above shows how signature verification is achieved through the following steps:

  1. A secret key is known by both the webhook producer and consumer. This secret is often referred to as the webhook signing key.
  2. When sending a webhook, the producer uses this key and an HMAC hashing algorithm (for example SHA256) to create a cryptographic hash of the webhook payload. This cryptographic hash is the webhook’s unique signature.
  3. The signature is sent in a custom header along with the webhook request. Sometimes the type of algorithm used is also sent.
  4. When the webhook arrives at the webhook URL, the receiving application takes the webhook payload and uses the secret key and the cryptographic algorithm to calculate the signature.
  5. The calculated signature is then compared with the one sent by the producer in the custom header. If there is a match then the request is valid, and if not the webhook is rejected.

Choosing a cryptographic algorithm

We have already learned that to create a HMAC signature for webhook verification, we need a secret key, a hashing algorithm, and the webhook payload.

The secret key is a value shared between the webhook producer and consumer. Oftentimes, this is an API key/secret that can be gotten from the webhook provider’s dashboard (Stripe for example), or alternatively you’re allowed to enter a random string as the secret when setting up the webhook (as is the case with GitHub).

As for the hashing algorithm, there are several options, but HMACs are mostly calculated using hashing functions contained in the SHA-2 or SHA-3 series. This is because older cryptographic hash functions in the MD5 and SHA-1 series are now cryptographically broken. Also, other functions in the MD series (MD2, MD4, etc.) are either weak, highly compromised, or cryptographically broken.

Today, almost all major webhook providers I have come across use the SHA-256 hash function in the SHA-2 series for signature verification of their webhooks. These webhook providers include Stripe, GitHub, CircleCI, Zendesk, Shopify, and Okta. For added security, webhook providers like Stripe forbid using a lesser quality hash function in order to prevent downgrade attacks.

Using HMAC signature verification to authenticate and validate webhooks

Now that we fully understand how signature verification works, let’s see how it is implemented.

To authenticate our webhooks using signature verification, we’ll take some required steps which I’ve implemented in different languages below. Let’s assume that the custom header that carries the webhook signature is X-Signature-SHA256. The steps required are:

  • Get the raw body of the request;
  • Extract the signature header value;
  • Calculate the HMAC of the raw body using the SHA-256 hash function and the secret; and
  • Compare the calculated HMAC with the one sent in the X-Signature-SHA256 signature header, making sure that both values use the same encoding.

Note that the name of the signature header is different for each webhook provider, so consult your provider’s documentation for the actual name of this header.

Finally, let’s take a look at what authenticating our webhooks using signature verification looks like for some of the most popular languages. Contact us if you need a code example for another language.

Node.js example

const express = require("express");const routes = require("./routes");const bodyParser = require("body-parser");const crypto = require("crypto");// Appconst app = express();const sigHeaderName = "X-Signature-SHA256";const sigHashAlg = "sha256";const sigPrefix = ""; //set this to your signature prefix if anyconst secret = "my_webhook_api_secret";//Get the raw bodyapp.use( bodyParser.json({ verify: (req, res, buf, encoding) => { if (buf && buf.length) { req.rawBody = buf.toString(encoding || "utf8"); } }, }),);//Validate payloadfunction validatePayload(req, res, next) { if (req.get(sigHeaderName)) { //Extract Signature header const sig = Buffer.from(req.get(sigHeaderName) || "", "utf8"); //Calculate HMAC const hmac = crypto.createHmac(sigHashAlg, secret); const digest = Buffer.from( sigPrefix + hmac.update(req.rawBody).digest("hex"), "utf8", ); //Compare HMACs if (sig.length !== digest.length || !crypto.timingSafeEqual(digest, sig)) { return res.status(401).send({ message: `Request body digest (${digest}) did not match ${sigHeaderName} (${sig})`, }); } } return next();}app.use(validatePayload);app.use("/", routes);const port = process.env.PORT || "1337";app.set("port", port);app.listen(port, () => console.log(`Server running on localhost:${port}`));

PHP example

<?phpdefine('API_SECRET_KEY', 'my_webhook_api_secret');function verify_webhook($data, $hmac_header){# Calculate HMAC $calculated_hmac = base64_encode(hash_hmac('sha256', $data, API_SECRET_KEY, true)); return hash_equals($hmac_header, $calculated_hmac);}# Extract the signature header$hmac_header = $_SERVER['X-Signature-SHA256'];# Get the raw body$data = file_get_contents('php://input');# Compare HMACs$verified = verify_webhook($data, $hmac_header);error_log('Webhook verified: '.var_export($verified, true));if ($verified) { # Do something with the webhook} else { http_response_code(401);}?>

Python (Flask) example

from flask import Flask, request, abortimport hmacimport hashlibimport base64app = Flask(__name__)API_SECRET_KEY = 'my_webhook_api_secret'def verify_webhook(data, hmac_header):# Calculate HMAC digest = hmac.new(API_SECRET_KEY.encode('utf-8'), data, digestmod=hashlib.sha256).digest() computed_hmac = base64.b64encode(digest) return hmac.compare_digest(computed_hmac, hmac_header.encode('utf-8'))@app.route('/webhook', methods=['POST'])def handle_webhook():# Get raw body data = request.get_data()# Compare HMACs verified = verify_webhook(data, request.headers.get('X-Signature-SHA256')) if not verified: abort(401) # Do something with the webhook return ('', 200)

Ruby example

require 'rubygems'require 'base64'require 'openssl'require 'sinatra'require 'active_support/security_utils'API_SECRET_KEY = 'my_webhook_api_secret'helpers do def verify_webhook(data, hmac_header)# Calculate HMAC calculated_hmac = Base64.strict_encode64(OpenSSL::HMAC.digest('sha256', API_SECRET_KEY, data)) ActiveSupport::SecurityUtils.secure_compare(calculated_hmac, hmac_header) endendpost '/' do request.body.rewind # Get raw body data = request.body.read# Compare HMACs verified = verify_webhook(data, env["X-Signature-SHA256"]) halt 401 unless verified # Do something with the webhookend

Go Example

package verifywebhookimport ("crypto/hmac""crypto/sha1""encoding/hex""encoding/json""errors""io/ioutil""net/http""strings")// Hook is an inbound webhooktype Hook struct {Signature stringPayload []byte}const signaturePrefix = "" ////set this to your signature prefix if anyconst signatureLength = // Your signature length = len(SignaturePrefix) + len(hex(sha1))func signBody(secret, body []byte) []byte {// Calculate HMACcomputed := hmac.New(sha1.New, secret)computed.Write(body)return []byte(computed.Sum(nil))}func (h *Hook) SignedBy(secret []byte) bool {if len(h.Signature) != signatureLength || !strings.HasPrefix(h.Signature, signaturePrefix) {return false}actual := make([]byte, 20)hex.Decode(actual, []byte(h.Signature[5:]))return hmac.Equal(signBody(secret, h.Payload), actual)}func (h *Hook) Extract(dst interface{}) error {return json.Unmarshal(h.Payload, dst)}func New(req *http.Request) (hook *Hook, err error) {hook = new(Hook)if !strings.EqualFold(req.Method, "POST") {return nil, errors.New("Unknown method!")} // Extract signatureif hook.Signature = req.Header.Get("X-Signature-SHA256"); len(hook.Signature) == 0 {return nil, errors.New("No signature!")} // Get raw bodyhook.Payload, err = ioutil.ReadAll(req.Body)return}func Parse(secret []byte, req *http.Request) (hook *Hook, err error) {hook, err = New(req)//Compare HMACsif err == nil && !hook.SignedBy(secret) {err = errors.New("Invalid signature")}return}

Using the Go package for signature verification

For an incoming*http.Request representing a webhook signed with asecret, useverifywebhookto validate and parse its content, as shown below.

secret := []byte("my_webhook_api_secret")webhook, err := verifywebhook.Parse(secret, req)

Java Example

import com.sun.net.httpserver.Headers;import com.sun.net.httpserver.HttpExchange;import com.sun.net.httpserver.HttpHandler;import java.io.*;import javax.crypto.Mac;import javax.crypto.spec.SecretKeySpec;import java.nio.charset.StandardCharsets;import java.security.InvalidKeyException;import java.security.NoSuchAlgorithmException;import java.util.Base64;class SignatureVerificationHandler implements HttpHandler { private String encodingAlgorithm = "HmacSHA256"; private String secretKey = "someSecretKeyThatShouldBeSecure"; private String headerThatContainsSignature = "X-Signature-SHA256"; private boolean verifySignature(String payload, String signature) throws NoSuchAlgorithmException, InvalidKeyException { var sha256_HMAC = Mac.getInstance(encodingAlgorithm); SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(), encodingAlgorithm); sha256_HMAC.init(secretKeySpec); byte[] hash = sha256_HMAC.doFinal(payload.getBytes()); String message = Base64.getEncoder().encodeToString(hash); System.out.println("Payload : "+ payload); System.out.println("Message : "+ message); System.out.println("Signature : "+ signature); return message.equals(signature); } @Override public void handle(HttpExchange httpExchange) throws IOException { Headers headers = httpExchange.getRequestHeaders(); String signature = headers.getFirst(headerThatContainsSignature); String payload = readBody(httpExchange); try { boolean isValidMessage = verifySignature(payload, signature); if (isValidMessage){ System.out.println("Got valid signature, returning 200"); returnWithStatus(httpExchange, 200); return; } } catch (Exception e) { System.out.println("Exception encountered, return 500 server error"); returnWithStatus(httpExchange, 500); return; } System.out.println("Invalid signature, returning 401 Unauthorized"); returnWithStatus(httpExchange, 401); } private String readBody(HttpExchange httpExchange) throws IOException { BufferedInputStream stream = new BufferedInputStream(httpExchange.getRequestBody()); ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream(); for (int result = stream.read(); result != -1; result = stream.read()) { byteBuffer.write((byte) result); } return byteBuffer.toString(StandardCharsets.UTF_8); } private void returnWithStatus(HttpExchange httpExchange, int httpStatusCode) throws IOException { httpExchange.sendResponseHeaders(httpStatusCode, 0); httpExchange.getResponseBody().close(); }}

Conclusion

In this article, we dove deep into the details of signature verification and what makes it so powerful and effective at securing our webhooks. We also looked at different code samples in the most used languages on the web for processing webhooks to see a practical implementation of the verification process.

Signature verification can be combined with other security controls in our checklist to ensure you’re getting optimal protection for your webhooks. One of these controls is SSL encryption; it ensures that all of your communication is encrypted, because even though your data is encoded, signature verification does not encrypt your webhook data.

Happy coding!

How to Implement SHA256 Webhook Signature Verification (2024)
Top Articles
The Top 10 Skills You Need for a Job in DeFi
What is Privacy Protection in Blockchain | Shardeum
Www.paystubportal.com/7-11 Login
Cottonwood Vet Ottawa Ks
Coverage of the introduction of the Water (Special Measures) Bill
Air Canada bullish about its prospects as recovery gains steam
Red Wing Care Guide | Fat Buddha Store
What is IXL and How Does it Work?
The Wicked Lady | Rotten Tomatoes
Alaska: Lockruf der Wildnis
735 Reeds Avenue 737 & 739 Reeds Ave., Red Bluff, CA 96080 - MLS# 20240686 | CENTURY 21
Diesel Mechanic Jobs Near Me Hiring
Tcgplayer Store
iOS 18 Hadir, Tapi Mana Fitur AI Apple?
Interactive Maps: States where guns are sold online most
Odfl4Us Driver Login
Walgreens Tanque Verde And Catalina Hwy
Ac-15 Gungeon
Surplus property Definition: 397 Samples | Law Insider
How Taraswrld Leaks Exposed the Dark Side of TikTok Fame
Makemv Splunk
Bj타리
Gen 50 Kjv
Infinite Campus Asd20
Jamielizzz Leaked
Fastpitch Softball Pitching Tips for Beginners Part 1 | STACK
Kristen Hanby Sister Name
Nicole Wallace Mother Of Pearl Necklace
Smartfind Express Henrico
Tas Restaurant Fall River Ma
Aliciabibs
Boggle BrainBusters: Find 7 States | BOOMER Magazine
Maxpreps Field Hockey
Nancy Pazelt Obituary
Registrar Lls
Letter of Credit: What It Is, Examples, and How One Is Used
Craigslist en Santa Cruz, California: Tu Guía Definitiva para Comprar, Vender e Intercambiar - First Republic Craigslist
Achieving and Maintaining 10% Body Fat
Unblocked Games Gun Games
Gregory (Five Nights at Freddy's)
R: Getting Help with R
Rescare Training Online
Frequently Asked Questions
Gt500 Forums
Phone Store On 91St Brown Deer
Minecraft: Piglin Trade List (What Can You Get & How)
Rovert Wrestling
Sams La Habra Gas Price
When Is The First Cold Front In Florida 2022
Island Vibes Cafe Exeter Nh
Latest Posts
Article information

Author: Sen. Ignacio Ratke

Last Updated:

Views: 6489

Rating: 4.6 / 5 (56 voted)

Reviews: 87% of readers found this page helpful

Author information

Name: Sen. Ignacio Ratke

Birthday: 1999-05-27

Address: Apt. 171 8116 Bailey Via, Roberthaven, GA 58289

Phone: +2585395768220

Job: Lead Liaison

Hobby: Lockpicking, LARPing, Lego building, Lapidary, Macrame, Book restoration, Bodybuilding

Introduction: My name is Sen. Ignacio Ratke, I am a adventurous, zealous, outstanding, agreeable, precious, excited, gifted person who loves writing and wants to share my knowledge and understanding with you.