> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vizochok.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Signature Verification

> Verify VIZOCHOK webhook authenticity using HMAC-SHA256 signatures. Code examples in Python, Node.js, and Go.

Every webhook request from VIZOCHOK includes an HMAC-SHA256 signature that you should verify to ensure the request is authentic and has not been tampered with.

***

## How It Works

VIZOCHOK signs each webhook request using these steps:

1. Get the current Unix timestamp (seconds)
2. Construct the signed payload: `{timestamp}.{request_body}`
3. Compute HMAC-SHA256 of the signed payload using your webhook secret
4. Send the signature and timestamp in request headers

```
Signed Payload = "{timestamp}.{JSON body}"

HMAC-SHA256(webhook_secret, signed_payload) → hex digest

Headers:
  X-VIZOCHOK-Signature: sha256={hex_digest}
  X-VIZOCHOK-Timestamp: {timestamp}
```

***

## Verification Steps

<Steps>
  <Step title="Extract headers">
    Read the `X-VIZOCHOK-Signature` and `X-VIZOCHOK-Timestamp` headers from the incoming request.

    The signature header has the format `sha256={hex_digest}`.
  </Step>

  <Step title="Check timestamp (replay protection)">
    Parse the timestamp and verify it is within 5 minutes of the current time. Reject requests with timestamps outside this window.

    ```
    if abs(current_time - timestamp) > 300:
        reject("Timestamp expired")
    ```
  </Step>

  <Step title="Construct the signed payload">
    Concatenate the timestamp, a dot, and the raw request body:

    ```
    signed_payload = "{timestamp}.{raw_body_bytes}"
    ```

    <Warning>
      Use the raw request body bytes, not a re-serialized version. JSON serialization is not guaranteed to be deterministic, so re-serializing the parsed body may produce a different byte sequence.
    </Warning>
  </Step>

  <Step title="Compute expected signature">
    Calculate HMAC-SHA256 of the signed payload using your webhook secret as the key:

    ```
    expected = HMAC-SHA256(webhook_secret, signed_payload)
    ```
  </Step>

  <Step title="Compare signatures">
    Use a constant-time comparison function to compare the expected signature with the one from the header. This prevents timing attacks.

    ```
    if not constant_time_equal(expected, received):
        reject("Invalid signature")
    ```
  </Step>
</Steps>

***

## Code Examples

<CodeGroup>
  ```python Python theme={null}
  import hashlib
  import hmac
  import time

  WEBHOOK_SECRET = "your_webhook_secret"
  TIMESTAMP_TOLERANCE = 300  # 5 minutes


  def verify_webhook(headers: dict, body: bytes) -> bool:
      """Verify VIZOCHOK webhook signature.

      Args:
          headers: Request headers dict
          body: Raw request body bytes

      Returns:
          True if signature is valid

      Raises:
          ValueError: If signature is missing, expired, or invalid
      """
      sig_header = headers.get("x-vizochok-signature", "")
      timestamp = headers.get("x-vizochok-timestamp", "")

      # 1. Check signature format
      if not sig_header.startswith("sha256="):
          raise ValueError("Missing or malformed signature header")

      received_sig = sig_header[7:]  # Strip "sha256=" prefix

      # 2. Check timestamp (replay protection)
      if not timestamp:
          raise ValueError("Missing timestamp header")

      try:
          ts = int(timestamp)
      except ValueError:
          raise ValueError("Invalid timestamp format")

      if abs(time.time() - ts) > TIMESTAMP_TOLERANCE:
          raise ValueError("Webhook timestamp expired (possible replay attack)")

      # 3. Construct signed payload
      signed_payload = f"{timestamp}.".encode() + body

      # 4. Compute expected signature
      expected_sig = hmac.new(
          WEBHOOK_SECRET.encode(),
          signed_payload,
          hashlib.sha256,
      ).hexdigest()

      # 5. Constant-time comparison
      if not hmac.compare_digest(received_sig, expected_sig):
          raise ValueError("Invalid webhook signature")

      return True


  # Usage with FastAPI
  from fastapi import Request, HTTPException

  async def verify_request(request: Request):
      body = await request.body()
      try:
          verify_webhook(dict(request.headers), body)
      except ValueError as e:
          raise HTTPException(status_code=401, detail=str(e))
  ```

  ```javascript Node.js theme={null}
  const crypto = require('crypto');

  const WEBHOOK_SECRET = 'your_webhook_secret';
  const TIMESTAMP_TOLERANCE = 300; // 5 minutes

  /**
   * Verify VIZOCHOK webhook signature.
   *
   * @param {Object} headers - Request headers
   * @param {Buffer} rawBody - Raw request body
   * @throws {Error} If verification fails
   */
  function verifyWebhook(headers, rawBody) {
    const sigHeader = headers['x-vizochok-signature'] || '';
    const timestamp = headers['x-vizochok-timestamp'] || '';

    // 1. Check signature format
    if (!sigHeader.startsWith('sha256=')) {
      throw new Error('Missing or malformed signature header');
    }

    const receivedSig = sigHeader.slice(7);

    // 2. Check timestamp (replay protection)
    if (!timestamp) {
      throw new Error('Missing timestamp header');
    }

    const ts = parseInt(timestamp, 10);
    if (isNaN(ts)) {
      throw new Error('Invalid timestamp format');
    }

    if (Math.abs(Date.now() / 1000 - ts) > TIMESTAMP_TOLERANCE) {
      throw new Error('Webhook timestamp expired (possible replay attack)');
    }

    // 3. Construct signed payload
    const signedPayload = Buffer.concat([
      Buffer.from(`${timestamp}.`),
      rawBody,
    ]);

    // 4. Compute expected signature
    const expectedSig = crypto
      .createHmac('sha256', WEBHOOK_SECRET)
      .update(signedPayload)
      .digest('hex');

    // 5. Constant-time comparison
    if (!crypto.timingSafeEqual(
      Buffer.from(receivedSig, 'utf8'),
      Buffer.from(expectedSig, 'utf8'),
    )) {
      throw new Error('Invalid webhook signature');
    }
  }

  // Usage with Express
  // Important: capture raw body before JSON parsing
  app.use('/api/vizochok', express.json({
    verify: (req, _res, buf) => {
      req.rawBody = buf;
    },
  }));

  app.post('/api/vizochok/check-products', (req, res) => {
    try {
      verifyWebhook(req.headers, req.rawBody);
    } catch (err) {
      return res.status(401).json({ error: err.message });
    }
    // Handle request...
  });
  ```

  ```go Go theme={null}
  package webhook

  import (
  	"crypto/hmac"
  	"crypto/sha256"
  	"encoding/hex"
  	"fmt"
  	"io"
  	"math"
  	"net/http"
  	"strconv"
  	"time"
  )

  const (
  	WebhookSecret      = "your_webhook_secret"
  	TimestampTolerance = 300 // 5 minutes
  )

  // VerifyWebhook verifies the HMAC-SHA256 signature of a VIZOCHOK webhook.
  func VerifyWebhook(r *http.Request) ([]byte, error) {
  	// Read raw body
  	body, err := io.ReadAll(r.Body)
  	if err != nil {
  		return nil, fmt.Errorf("failed to read body: %w", err)
  	}

  	sigHeader := r.Header.Get("X-VIZOCHOK-Signature")
  	timestamp := r.Header.Get("X-VIZOCHOK-Timestamp")

  	// 1. Check signature format
  	if len(sigHeader) < 8 || sigHeader[:7] != "sha256=" {
  		return nil, fmt.Errorf("missing or malformed signature header")
  	}
  	receivedSig := sigHeader[7:]

  	// 2. Check timestamp (replay protection)
  	if timestamp == "" {
  		return nil, fmt.Errorf("missing timestamp header")
  	}

  	ts, err := strconv.ParseInt(timestamp, 10, 64)
  	if err != nil {
  		return nil, fmt.Errorf("invalid timestamp format")
  	}

  	diff := math.Abs(float64(time.Now().Unix() - ts))
  	if diff > TimestampTolerance {
  		return nil, fmt.Errorf("webhook timestamp expired (possible replay attack)")
  	}

  	// 3. Construct signed payload
  	signedPayload := []byte(fmt.Sprintf("%s.", timestamp))
  	signedPayload = append(signedPayload, body...)

  	// 4. Compute expected signature
  	mac := hmac.New(sha256.New, []byte(WebhookSecret))
  	mac.Write(signedPayload)
  	expectedSig := hex.EncodeToString(mac.Sum(nil))

  	// 5. Constant-time comparison
  	if !hmac.Equal([]byte(receivedSig), []byte(expectedSig)) {
  		return nil, fmt.Errorf("invalid webhook signature")
  	}

  	return body, nil
  }

  // Usage in an HTTP handler
  func HandleWebhook(w http.ResponseWriter, r *http.Request) {
  	body, err := VerifyWebhook(r)
  	if err != nil {
  		http.Error(w, err.Error(), http.StatusUnauthorized)
  		return
  	}
  	// Use body for processing...
  	_ = body
  }
  ```
</CodeGroup>

***

## Replay Protection

The timestamp-based replay protection works as follows:

1. VIZOCHOK includes the current Unix timestamp in the `X-VIZOCHOK-Timestamp` header
2. The timestamp is incorporated into the signed payload (`{timestamp}.{body}`), so it cannot be forged independently of the signature
3. Your server rejects requests where the timestamp is more than 5 minutes old

This prevents an attacker from capturing a valid webhook request and replaying it later.

<Info>
  Requests are accepted if the timestamp is within 5 minutes of the current time (before or after). A request from 2 minutes ago is accepted; a request from 10 minutes ago is rejected.
</Info>

<Info>
  The 5-minute window accounts for clock drift between VIZOCHOK's servers and yours. If your server's clock is significantly skewed, consider using NTP to synchronize it.
</Info>

***

## Troubleshooting

<Accordion title="Signature always fails">
  Common causes:

  * **Re-serialized body**: Make sure you verify against the raw request bytes, not a re-serialized version of the parsed JSON
  * **Wrong secret**: Double-check that you are using the correct webhook secret from your VIZOCHOK admin panel
  * **Middleware interference**: Some frameworks modify the request body before your handler sees it. Capture the raw bytes before JSON parsing
</Accordion>

<Accordion title="Timestamp always expired">
  * Check that your server's clock is accurate (use NTP)
  * The tolerance is 5 minutes -- if your server clock is off by more than that, timestamps will fail
  * In development, you can temporarily increase the tolerance for testing
</Accordion>

<Accordion title="Signature verification optional in development">
  During development, you can skip signature verification by not setting a webhook secret in your VIZOCHOK tenant config. However, **always verify signatures in production**.
</Accordion>
