How to Implement SHA256 Webhook Signature Verification

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 signature verification works 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");

// App
const app = express();

const sigHeaderName = "X-Signature-SHA256";
const sigHashAlg = "sha256";
const sigPrefix = ""; //set this to your signature prefix if any
const secret = "my_webhook_api_secret";

//Get the raw body
app.use(
  bodyParser.json({
    verify: (req, res, buf, encoding) => {
      if (buf && buf.length) {
        req.rawBody = buf.toString(encoding || "utf8");
      }
    },
  }),
);

//Validate payload
function 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

<?php

	define('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, abort
import hmac
import hashlib
import base64
app = 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)
  end
end

post '/' 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 webhook

end

Go Example

package verifywebhook

import (
	"crypto/hmac"
	"crypto/sha1"
	"encoding/hex"
	"encoding/json"
	"errors"
	"io/ioutil"
	"net/http"
	"strings"
)

// Hook is an inbound webhook
type Hook struct {

	Signature string

	Payload []byte
}

const signaturePrefix = "" ////set this to your signature prefix if any
const signatureLength =  // Your signature length = len(SignaturePrefix) + len(hex(sha1))

func signBody(secret, body []byte) []byte {
	// Calculate HMAC
	computed := 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 signature
	if hook.Signature = req.Header.Get("X-Signature-SHA256"); len(hook.Signature) == 0 {
		return nil, errors.New("No signature!")
	}

  // Get raw body
	hook.Payload, err = ioutil.ReadAll(req.Body)
	return
}

func Parse(secret []byte, req *http.Request) (hook *Hook, err error) {
	hook, err = New(req)

	//Compare HMACs
	if 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 a secret, use verifywebhook to 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!