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 Name | Value Format | Description |
|---|---|---|
x-emalc-timestamp | 1727524800 | UNIX timestamp in seconds when the webhook payload was signed. |
x-emalc-signature | v1=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}` )secretKey: The unique secret string assigned to your endpoint (starts withwhsec_...).timestamp: The string value of thex-emalc-timestampheader.rawBody: The unparsed, raw UTF-8 string payload received in the HTTP request body.
Step-by-Step Verification Algorithm#
- Extract Headers: Read
x-emalc-timestampandx-emalc-signaturefrom the HTTP request headers. - Verify Timestamp Freshness: Compare
x-emalc-timestampagainst current server time. If the difference exceeds 5 minutes (300 seconds), reject the request to prevent Replay Attacks. - Compute HMAC Signature: Generate an HMAC-SHA256 hash using your
secretKeyover${timestamp}.${rawBody}. - 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))
}