0BitDeveloper Docs
0Bit public documentation

Verifying signatures

Verify Gate-Signature against the raw webhook request body.

Every delivery includes:

Gate-Signature: t=1716729600,v1=HEX_HMAC_SHA256
X-0bit-Timestamp: 1716729600
X-0bit-Event-Id: EVENT_UUID
X-0bit-Event-Type: gate_session.completed

The signed bytes are:

<timestamp>.<raw request body>

Verification order:

  1. read the raw body bytes before JSON parsing;
  2. parse t and v1 from Gate-Signature;
  3. reject malformed values or a timestamp more than 300 seconds from your clock;
  4. compute HMAC-SHA256(webhook_secret, timestamp + "." + rawBody) as lowercase hex;
  5. compare the supplied and expected digests in constant time;
  6. parse JSON and deduplicate by event id.

Node

import crypto from 'node:crypto';

export function verifyGateWebhook(rawBody, signatureHeader, secret, now = Date.now()) {
  const parts = Object.fromEntries(
    signatureHeader.split(',').map(part => {
      const index = part.indexOf('=');
      return [part.slice(0, index).trim(), part.slice(index + 1).trim()];
    }),
  );

  const timestamp = Number(parts.t);
  if (!Number.isInteger(timestamp)) return false;
  if (Math.abs(Math.floor(now / 1000) - timestamp) > 300) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');

  const supplied = Buffer.from(parts.v1 ?? '', 'hex');
  const calculated = Buffer.from(expected, 'hex');
  return supplied.length === calculated.length && crypto.timingSafeEqual(supplied, calculated);
}

Python

import hashlib
import hmac
import time

def verify_gate_webhook(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(part.strip().split("=", 1) for part in header.split(",") if "=" in part)
    try:
        timestamp = int(parts["t"])
        supplied = parts["v1"]
    except (KeyError, ValueError):
        return False
    if abs(int(time.time()) - timestamp) > 300:
        return False
    payload = str(timestamp).encode() + b"." + raw_body
    expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
    return hmac.compare_digest(supplied, expected)

Do not parse and reserialize JSON before verification. Whitespace and property order are part of the signed body. Rotate the secret through POST /dashboard/webhook-secret/rotate and store the replacement immediately; the response is the only time the raw replacement is returned.