Last updated

Verifying Webhooks

When Influencer Hero sends a webhook to your endpoint, it includes a cryptographic signature in the request headers. This signature is intended to help you verify that the request genuinely originated from Influencer Hero and has not been tampered with in transit.

Steps to Verify

  1. Retrieve the timestamp from X-InfluencerHero-Timestamp.
  2. Retrieve the signature from X-InfluencerHero-Signature.
  3. Read the raw request body (the JSON payload) as bytes.
  4. Compute the HMAC-SHA256 signature over the raw request body, using the same secret key.
  5. Compare the computed signature with the X-InfluencerHero-Signature header value using a timing-safe comparison.
  6. (Optional) Confirm the timestamp is within an acceptable window (e.g., 5 minutes) to prevent replay attacks.

Example Verification Logic in Various Languages

Below are short examples illustrating how you might perform the verification. You may need to adapt them to specific frameworks or libraries in your environment.

Python

import hmac
import hashlib
import time

def verify_influencerhero_webhook(request_body: bytes, timestamp_header: str, signature_header: str, secret_key: str) -> bool:
    # 1. Compute local HMAC-SHA256 of the raw request body with the secret key
    computed_signature = hmac.new(
        key=secret_key.encode('utf-8'),
        msg=request_body,
        digestmod=hashlib.sha256
    ).hexdigest()

    # 2. Timing-safe comparison to prevent leaks in signature checking
    if not hmac.compare_digest(computed_signature, signature_header):
        return False

    # 3. (Optional) Check if the timestamp is within 5 minutes of current time
    current_timestamp = int(time.time())
    request_timestamp = int(timestamp_header or 0)
    if abs(current_timestamp - request_timestamp) > 300:
        return False

    return True

Node.js (Express)

const crypto = require('crypto');

function verifyInfluencerHeroWebhook(rawBody, timestamp, signature, secretKey) {
  // 1. Compute local HMAC-SHA256
  const computedSignature = crypto
    .createHmac('sha256', secretKey)
    .update(rawBody)
    .digest('hex');

  // 2. Timing-safe comparison
  if (!crypto.timingSafeEqual(Buffer.from(computedSignature), Buffer.from(signature))) {
    return false;
  }

  // 3. (Optional) Check timestamp
  const current = Math.floor(Date.now() / 1000);
  if (Math.abs(current - parseInt(timestamp, 10)) > 300) {
    return false;
  }
  return true;
}

// Usage example:
// Make sure to capture the raw request body in `req.rawBody` (Buffer) before parsing
app.post('/webhook', (req, res) => {
  const timestamp = req.get('X-InfluencerHero-Timestamp');
  const signature = req.get('X-InfluencerHero-Signature');

  const verified = verifyInfluencerHeroWebhook(
    req.rawBody, // Buffer of the raw request body
    timestamp,
    signature,
    process.env.INFLUENCERHERO_SECRET // your stored secret
  );

  if (!verified) {
    return res.status(400).send('Invalid signature');
  }

  // If verified, process the webhook
  res.send('OK');
});

PHP

<?php
function verifyInfluencerHeroWebhook($rawBody, $timestamp, $signature, $secretKey) {
    // 1. Compute expected HMAC-SHA256 signature
    $computedSignature = hash_hmac('sha256', $rawBody, $secretKey);

    // 2. Use a timing-safe comparison
    if (!hash_equals($computedSignature, $signature)) {
        return false;
    }

    // 3. (Optional) Check timestamp (e.g., 5-minute window)
    $currentTime = time();
    if (abs($currentTime - (int)$timestamp) > 300) {
        return false;
    }

    return true;
}

// Usage in a typical endpoint:
$rawBody = file_get_contents('php://input');  // raw POST body
$timestamp = $_SERVER['HTTP_X_INFLUENCERHERO_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_INFLUENCERHERO_SIGNATURE'] ?? '';
$secretKey = 'YOUR_WEBHOOK_SECRET';

if (!verifyInfluencerHeroWebhook($rawBody, $timestamp, $signature, $secretKey)) {
    http_response_code(400);
    echo "Invalid signature";
    exit;
}

// Proceed if verified
http_response_code(200);
echo "OK";

Ruby

require 'openssl'
require 'time'

def verify_influencerhero_webhook(raw_body, timestamp, signature, secret_key)
  # 1. Compute local HMAC-SHA256
  computed_signature = OpenSSL::HMAC.hexdigest('SHA256', secret_key, raw_body)

  # 2. Timing-safe comparison
  return false unless Rack::Utils.secure_compare(computed_signature, signature)

  # 3. (Optional) Check timestamp (5-minute window)
  request_time = Time.at(timestamp.to_i)
  return false if (Time.now.utc - request_time).abs > 300

  true
end

# Usage (e.g., in a Sinatra/Rails controller):
post '/webhook' do
  raw_body = request.body.read
  timestamp = request.env['HTTP_X_INFLUENCERHERO_TIMESTAMP']
  signature = request.env['HTTP_X_INFLUENCERHERO_SIGNATURE']
  secret_key = ENV['INFLUENCERHERO_SECRET']

  unless verify_influencerhero_webhook(raw_body, timestamp, signature, secret_key)
    halt 400, 'Invalid signature'
  end

  status 200
  'OK'
end

Java

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

public class WebhookVerifier {

    public static boolean verifyInfluencerHeroWebhook(
            byte[] rawBody,
            String timestamp,
            String signature,
            String secretKey
    ) {
        String computedSignature;
        try {
            // 1. Compute local HMAC-SHA256
            Mac hmacSha256 = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
            hmacSha256.init(secretKeySpec);
            byte[] hash = hmacSha256.doFinal(rawBody);
            computedSignature = bytesToHex(hash);

            // 2. Timing-safe comparison
            if (!timingSafeEquals(computedSignature, signature)) {
                return false;
            }

            // 3. (Optional) timestamp validation
            long currentTime = System.currentTimeMillis() / 1000L; // seconds
            long requestTime = Long.parseLong(timestamp);
            if (Math.abs(currentTime - requestTime) > 300) {
                return false;
            }

        } catch (NoSuchAlgorithmException | InvalidKeyException | NumberFormatException e) {
            return false;
        }
        return true;
    }

    private static String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }

    // Simple timing-safe comparison
    private static boolean timingSafeEquals(String a, String b) {
        if (a.length() != b.length()) return false;
        int result = 0;
        for (int i = 0; i < a.length(); i++) {
            result |= a.charAt(i) ^ b.charAt(i);
        }
        return result == 0;
    }
}

Go

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "math"
    "net/http"
    "strconv"
    "time"
)

func verifyInfluencerHeroWebhook(rawBody []byte, timestamp, signature, secretKey string) bool {
    // 1. Compute local HMAC-SHA256
    mac := hmac.New(sha256.New, []byte(secretKey))
    mac.Write(rawBody)
    expected := hex.EncodeToString(mac.Sum(nil))

    // 2. Timing-safe comparison
    if !timingSafeCompare(expected, signature) {
        return false
    }

    // 3. (Optional) Check timestamp
    currentSec := time.Now().Unix()
    reqSec, err := strconv.ParseInt(timestamp, 10, 64)
    if err != nil {
        return false
    }
    if math.Abs(float64(currentSec-reqSec)) > 300 {
        return false
    }

    return true
}

// Timing-safe compare for two hex strings of equal length.
func timingSafeCompare(a, b string) bool {
    if len(a) != len(b) {
        return false
    }
    var diff byte
    for i := 0; i < len(a); i++ {
        diff |= a[i] ^ b[i]
    }
    return diff == 0
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    rawBody := make([]byte, r.ContentLength)
    r.Body.Read(rawBody)

    timestamp := r.Header.Get("X-InfluencerHero-Timestamp")
    signature := r.Header.Get("X-InfluencerHero-Signature")
    secretKey := "YOUR_WEBHOOK_SECRET"

    if !verifyInfluencerHeroWebhook(rawBody, timestamp, signature, secretKey) {
        w.WriteHeader(http.StatusBadRequest)
        w.Write([]byte("Invalid signature"))
        return
    }

    w.WriteHeader(http.StatusOK)
    w.Write([]byte("OK"))
}

func main() {
    http.HandleFunc("/webhook", webhookHandler)
    http.ListenAndServe(":8080", nil)
}

C#

using System;
using System.Security.Cryptography;
using System.Text;

public class InfluencerHeroWebhookVerifier
{
    public static bool VerifyInfluencerHeroWebhook(
        byte[] rawBody,
        string timestamp,
        string signature,
        string secretKey)
    {
        // 1. Compute local HMAC-SHA256
        using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secretKey)))
        {
            var computedHash = hmac.ComputeHash(rawBody);
            var computedSignature = BitConverter.ToString(computedHash).Replace("-", "").ToLowerInvariant();

            // 2. Timing-safe comparison
            if (!TimingSafeCompare(computedSignature, signature))
            {
                return false;
            }
        }

        // 3. (Optional) Check timestamp (e.g., 5 minutes)
        if (!long.TryParse(timestamp, out long ts))
        {
            return false;
        }
        var currentTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
        if (Math.Abs(currentTime - ts) > 300)
        {
            return false;
        }

        return true;
    }

    private static bool TimingSafeCompare(string a, string b)
    {
        if (a.Length != b.Length)
            return false;

        int result = 0;
        for (int i = 0; i < a.Length; i++)
        {
            result |= a[i] ^ b[i];
        }
        return (result == 0);
    }
}

Security Recommendations

  1. Always use HTTPS when setting up your webhook endpoint. This ensures payloads and headers are encrypted in transit.
  2. Keep your webhook signing secret as safe as you would a password or API key.
  3. Consider periodically rotating your webhook secret if you suspect it may have been compromised.
  4. Enforce a timeout window by comparing the X-InfluencerHero-Timestamp against the current time.

Implementing these steps allows your application to handle webhooks securely, ensuring that any requests not properly signed (or too old) are rejected.