Emalc

Search Documentation

Quickly jump to any guide, document, or section...

Security & Signature Verification

To ensure that incoming HTTP requests originate from Emalc and were not forged or altered by malicious third parties, every webhook delivery includes cryptographic headers signed with your endpoint's Secret Key (whsec_...).


Webhook Signing Headers#

Each HTTP POST request dispatched to your webhook URL includes two security headers:

Header NameValue FormatDescription
x-emalc-timestamp1727524800UNIX timestamp in seconds when the webhook payload was signed.
x-emalc-signaturev1=8f93a021b47e...HMAC-SHA256 hex digest of the timestamp + raw JSON body.

Signature Verification Formula#

Emalc calculates signatures using HMAC-SHA256:

text
signature = HMAC-SHA256( secretKey, `${x-emalc-timestamp}.${rawBody}` )
  1. secretKey: The unique secret string assigned to your endpoint (starts with whsec_...).
  2. timestamp: The string value of the x-emalc-timestamp header.
  3. rawBody: The unparsed, raw UTF-8 string payload received in the HTTP request body.

Step-by-Step Verification Algorithm#

  1. Extract Headers: Read x-emalc-timestamp and x-emalc-signature from the HTTP request headers.
  2. Verify Timestamp Freshness: Compare x-emalc-timestamp against current server time. If the difference exceeds 5 minutes (300 seconds), reject the request to prevent Replay Attacks.
  3. Compute HMAC Signature: Generate an HMAC-SHA256 hash using your secretKey over ${timestamp}.${rawBody}.
  4. Time-Safe String Comparison: Compare your computed hash against the value extracted from v1=<signature>. Use a constant-time comparison function (e.g. crypto.timingSafeEqual) to prevent timing attacks.

Code Verification Implementations#

Node.js / TypeScript (Express / Next.js)#

typescript
import crypto from 'crypto';
import { Request, Response } from 'express';

export function verifyEmalcWebhook(
  req: Request,
  secretKey: string
): boolean {
  const timestamp = req.headers['x-emalc-timestamp'] as string;
  const signatureHeader = req.headers['x-emalc-signature'] as string;

  if (!timestamp || !signatureHeader) {
    return false;
  }

  // 1. Replay attack prevention (5 minute tolerance)
  const currentTime = Math.floor(Date.now() / 1000);
  if (Math.abs(currentTime - parseInt(timestamp, 10)) > 300) {
    console.error('Webhook timestamp outside acceptable tolerance window');
    return false;
  }

  // 2. Extract v1 signature
  const expectedSignature = signatureHeader.replace('v1=', '');

  // 3. Compute HMAC-SHA256 over timestamp + raw JSON string body
  const rawBody = typeof req.body === 'string' ? req.body : JSON.stringify(req.body);
  const computedHash = crypto
    .createHmac('sha256', secretKey)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');

  // 4. Timing-safe comparison
  return crypto.timingSafeEqual(
    Buffer.from(computedHash, 'utf-8'),
    Buffer.from(expectedSignature, 'utf-8')
  );
}

Python (Flask / FastAPI)#

python
import hmac
import hashlib
import time

def verify_emalc_signature(raw_body: str, timestamp_header: str, signature_header: str, secret_key: str) -> bool:
    if not timestamp_header or not signature_header:
        return False

    # 1. Prevent replay attacks (5 minute window)
    current_time = int(time.time())
    if abs(current_time - int(timestamp_header)) > 300:
        return False

    # 2. Compute expected HMAC-SHA256
    expected_sig = signature_header.replace("v1=", "")
    signed_payload = f"{timestamp_header}.{raw_body}".encode('utf-8')
    computed_sig = hmac.new(
        secret_key.encode('utf-8'),
        signed_payload,
        hashlib.sha256
    ).hexdigest()

    # 3. Constant-time comparison
    return hmac.compare_digest(computed_sig, expected_sig)

PHP#

php
<?php

function verifyEmalcWebhook($rawBody, $timestamp, $signatureHeader, $secretKey) {
    if (!$timestamp || !$signatureHeader) {
        return false;
    }

    // 1. Replay attack check (5 minute window)
    if (abs(time() - (int)$timestamp) > 300) {
        return false;
    }

    // 2. Compute HMAC-SHA256
    $expectedSig = str_replace('v1=', '', $signatureHeader);
    $signedPayload = $timestamp . '.' . $rawBody;
    $computedSig = hash_hmac('sha256', $signedPayload, $secretKey);

    // 3. Timing-safe comparison
    return hash_equals($computedSig, $expectedSig);
}

Go#

go
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"math"
	"strconv"
	"strings"
	"time"
)

func VerifyEmalcWebhook(rawBody string, timestampStr string, sigHeader string, secretKey string) bool {
	timestamp, err := strconv.ParseInt(timestampStr, 10, 64)
	if err != nil {
		return false
	}

	// 1. Prevent replay attack
	if math.Abs(float64(time.Now().Unix()-timestamp)) > 300 {
		return false
	}

	// 2. Compute HMAC-SHA256
	expectedSig := strings.TrimPrefix(sigHeader, "v1=")
	mac := hmac.New(sha256.New, []byte(secretKey))
	mac.Write([]byte(fmt.Sprintf("%s.%s", timestampStr, rawBody)))
	computedSig := hex.EncodeToString(mac.Sum(nil))

	// 3. Constant time check
	return hmac.Equal([]byte(computedSig), []byte(expectedSig))
}