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:
- The cryptographic strength of the underlying hashing function.
- The size of the hash output (224, 256, 384, or 512 digest size bits).
- The size and quality (key length and characters) of the key.
The image above shows how signature verification is achieved through the following steps:
- A secret key is known by both the webhook producer and consumer. This secret is often referred to as the webhook signing key.
- 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.
- The signature is sent in a custom header along with the webhook request. Sometimes the type of algorithm used is also sent.
- 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.
- 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!
Gain control over your webhooks
Try Hookdeck to handle your webhook security, observability, queuing, routing, and error recovery.