Signature Generation and Verification

Signature Generation Algorithm

The signature is computed using HMAC-SHA256 with the following steps:

  1. Construct the message: Concatenate the timestamp and request body with a period (.) separator: message = timestamp + "." + request_body
  2. Decode the secret key: Your webhook secret is Base64 URL-encoded. Decode it to get the raw bytes.
  3. Compute HMAC-SHA256: Use the decoded secret key to compute the HMAC-SHA256 of the message.
  4. Hex encode: Convert the resulting bytes to a lowercase hexadecimal string.

Verification Steps

To validate the signature:

  1. Extract the X-Velora-Timestamp header value
  2. Extract the X-Velora-Signature header value and remove the sha256= prefix
  3. Get the raw request body as a string (preserve exact formatting)
  4. Compute your own signature using the algorithm above
  5. Compare your computed signature with the received signature using a constant-time comparison

Code Examples

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HexFormat;

public class WebhookValidator {

    public static String computeSignature(String base64Secret, String timestamp, String body) {
        try {
            String message = timestamp + "." + body;

            // Decode the Base64 URL-encoded secret
            byte[] keyBytes = Base64.getUrlDecoder().decode(base64Secret);

            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "HmacSHA256");
            mac.init(keySpec);

            byte[] rawHmac = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));

            return HexFormat.of().formatHex(rawHmac);
        } catch (Exception ex) {
            throw new RuntimeException("Unable to compute signature", ex);
        }
    }

    public static boolean verifySignature(String base64Secret, String timestamp,
                                          String body, String receivedSignature) {
        String expectedSignature = computeSignature(base64Secret, timestamp, body);
        // Use constant-time comparison to prevent timing attacks
        return java.security.MessageDigest.isEqual(
            expectedSignature.getBytes(StandardCharsets.UTF_8),
            receivedSignature.getBytes(StandardCharsets.UTF_8)
        );
    }
}
const crypto = require('crypto');

function computeSignature(base64Secret, timestamp, body) {
    const message = `${timestamp}.${body}`;

    // Decode Base64 URL-encoded secret
    const secretBuffer = Buffer.from(base64Secret, 'base64url');

    const hmac = crypto.createHmac('sha256', secretBuffer);
    hmac.update(message, 'utf8');

    return hmac.digest('hex');
}

function verifySignature(base64Secret, timestamp, body, receivedSignature) {
    const expectedSignature = computeSignature(base64Secret, timestamp, body);

    // Use constant-time comparison
    return crypto.timingSafeEqual(
        Buffer.from(expectedSignature, 'utf8'),
        Buffer.from(receivedSignature, 'utf8')
    );
}

// Express.js middleware example
function webhookMiddleware(req, res, next) {
    const timestamp = req.headers['x-velora-timestamp'];
    const signature = req.headers['x-velora-signature']?.replace('sha256=', '');
    const body = req.rawBody; // Ensure you capture raw body

    if (!verifySignature(process.env.WEBHOOK_SECRET, timestamp, body, signature)) {
        return res.status(401).json({ error: 'Invalid signature' });
    }

    next();
}
import hmac
import hashlib
import base64

def compute_signature(base64_secret: str, timestamp: str, body: str) -> str:
    message = f"{timestamp}.{body}"

    # Decode Base64 URL-encoded secret
    secret_bytes = base64.urlsafe_b64decode(base64_secret)

    signature = hmac.new(
        secret_bytes,
        message.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    return signature

def verify_signature(base64_secret: str, timestamp: str, body: str, received_signature: str) -> bool:
    expected_signature = compute_signature(base64_secret, timestamp, body)

    # Use constant-time comparison
    return hmac.compare_digest(expected_signature, received_signature)

# Flask example
from flask import request, abort

@app.before_request
def verify_webhook():
    timestamp = request.headers.get('X-Velora-Timestamp')
    signature = request.headers.get('X-Velora-Signature', '').replace('sha256=', '')
    body = request.get_data(as_text=True)

    if not verify_signature(WEBHOOK_SECRET, timestamp, body, signature):
        abort(401, 'Invalid signature')
<?php

function computeSignature(string $base64Secret, string $timestamp, string $body): string {
    $message = $timestamp . '.' . $body;

    // Decode Base64 URL-encoded secret
    $secretBytes = base64_decode(strtr($base64Secret, '-_', '+/'));

    $signature = hash_hmac('sha256', $message, $secretBytes);

    return $signature;
}

function verifySignature(string $base64Secret, string $timestamp, string $body, string $receivedSignature): bool {
    $expectedSignature = computeSignature($base64Secret, $timestamp, $body);

    // Use constant-time comparison
    return hash_equals($expectedSignature, $receivedSignature);
}

// Usage example
$timestamp = $_SERVER['HTTP_X_VELORA_TIMESTAMP'] ?? '';
$signature = str_replace('sha256=', '', $_SERVER['HTTP_X_VELORA_SIGNATURE'] ?? '');
$body = file_get_contents('php://input');

if (!verifySignature(WEBHOOK_SECRET, $timestamp, $body, $signature)) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}
using System;
using System.Security.Cryptography;
using System.Text;

public class WebhookValidator
{
    public static string ComputeSignature(string base64Secret, string timestamp, string body)
    {
        var message = $"{timestamp}.{body}";

        // Decode Base64 URL-encoded secret
        var secretBytes = Convert.FromBase64String(
            base64Secret.Replace('-', '+').Replace('_', '/'));

        using var hmac = new HMACSHA256(secretBytes);
        var hashBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));

        return Convert.ToHexString(hashBytes).ToLowerInvariant();
    }

    public static bool VerifySignature(string base64Secret, string timestamp,
                                       string body, string receivedSignature)
    {
        var expectedSignature = ComputeSignature(base64Secret, timestamp, body);

        // Use constant-time comparison
        return CryptographicOperations.FixedTimeEquals(
            Encoding.UTF8.GetBytes(expectedSignature),
            Encoding.UTF8.GetBytes(receivedSignature));
    }
}

Did this page help you?