X-Genesys-Signature HMAC verification failing on EventBridge webhook consumer

{“error”: “signature_verification_failed”, “timestamp”: “2024-05-12T09:14:22Z”, “source_ip”: “13.214.18.44”}
building a custom EventBridge consumer for GC webhooks and trying to validate the X-Genesys-Signature header to stop replay attacks. the platform docs claim it’s an HMAC-SHA256 of the raw payload plus the shared secret, but the verification keeps failing. here’s the verification block: const sig = crypto.createHmac('sha256', process.env.GC_SECRET).update(rawBody).digest('hex');

  • checked that express isn’t parsing the request body before the hash comparison
  • switched to timestamp-based nonce tracking in Redis to catch duplicate deliveries anyway
    weird thing is the signature matches fine when the handler logs it directly, but the actual comparison throws a mismatch. could be an encoding issue or maybe the service is base64 encoding the hash before transmission. a custom DogStatsD metric tracks every 403 failure rate, and it’s spiking hard after the last deployment. the timestamp header looks valid too. just not sure what’s breaking the raw body string comparison.

import hmac
import hashlib

def verify_signature(payload: bytes, signature: str, secret: str) → bool:
expected = hmac.new(
secret.encode(‘utf-8’),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)


The documentation actually says the signature is hex-encoded, not base64, so your `digest('hex')` part is fine. but you're likely missing the timestamp concatenation or dealing with whitespace in the raw body. Genesys sends the signature as `HMAC(secret, timestamp + payload)`. check if your event bridge consumer is stripping newlines or if the `rawBody` includes the BOM. i've seen this fail because of hidden characters in the webhook payload. also, make sure the secret isn't url-encoded. use `hmac.compare_digest` to avoid timing attacks, though that's probably not your current problem. just log the exact bytes you're signing versus what you expect. usually it's a simple string encoding mismatch.