Verifying Signatures

Swivell includes a X-Webhook-Signature header on each webhook call containing a hex-encoded HMAC-SHA256 signature of the POST body. To verify the authenticity of the request:

  1. Compute a local copy of the signature using the signing_key returned when creating the webhook.
  2. Compare the local signature to X-Webhook-Signature; if they match, the request is authenticated.

Please note that an authenticating payload does not prevent replay attacks. To make sure an event is processed once, please keep track of the event id in the payload and discard subsequent calls with the same ID. Another approach is to discard events with a time older than 2 minutes.

HMAC-SHA256 Signature Verification

Below are some code examples for verify HMAC-SHA256 signatures in your webhook endpoints.

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "crypto/subtle"
    "encoding/hex"
)

func verifyHmacSignature(payload, signature, signingKey string) bool {
    // Decode hex secret
    secretBytes, err := hex.DecodeString(signingKeyHex)
    if err != nil {
        return false
    }
    
    // Create HMAC hash
    mac := hmac.New(sha256.New, secretBytes)
    mac.Write([]byte(payload))
    expectedSignature := hex.EncodeToString(mac.Sum(nil))
    
    // Use ConstantTimeCompare to prevent timing attacks
    return subtle.ConstantTimeCompare(
        []byte(signature),
        []byte(expectedSignature),
    ) == 1
}
import hmac
import hashlib

def verify_hmac_signature(payload: str, signature: str, signing_key: str) -> bool:
    """
    Verify HMAC-SHA256 signature
    
    Args:
        payload: The raw payload string
        signature: The hex signature to verify
        secret_hex: The hex-encoded secret key
        
    Returns:
        True if signature is valid, False otherwise
    """
    # Convert hex secret to bytes
    secret_bytes = bytes.fromhex(signing_key)
    
    # Create expected signature
    expected_signature = hmac.new(
        secret_bytes,
        payload.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()
    
    # Use compare_digest to prevent timing attacks
    return hmac.compare_digest(signature, expected_signature)
import * as crypto from 'crypto';

function verifyHmacSignature(
  payload: string,
  signature: `0x{string}`,
  signingKey: `0x{string}`
): boolean {
  // Convert hex secret to buffer
  const secretBuffer = Buffer.from(signingKey.slice(2), 'hex');
  
  // Create HMAC hash of the payload
  const expectedSignature = crypto
    .createHmac('sha256', secretBuffer)
    .update(payload, 'utf8')
    .digest('hex');
  
  // Use timingSafeEqual to prevent timing attacks
  const sigBuffer = Buffer.from(signature.slice(2), 'hex');
  const expectedBuffer = Buffer.from(expectedSignature, 'hex');
  
  if (sigBuffer.length !== expectedBuffer.length) {
    return false;
  }
  
  return crypto.timingSafeEqual(sigBuffer, expectedBuffer);
}