# Verify signatures

Both directions carry the same header, signed with the workspace's one secret:

```text
Hijinks-Signature: t=<unix seconds>,v1=<hex>
```

The hex is HMAC-SHA256 over `<t>.<raw body>` with your secret. Three rules make it safe:

- Sign and verify the **raw bytes**, never a re-serialised parse.
- Compare in **constant time**.
- Refuse timestamps more than **5 minutes** off.

During a secret rotation the header may carry two `v1=` entries; either one matching is enough.

A [poll](/polling) has no body: sign the **query string** exactly as sent (everything after `?`,
or the empty string), so the signed text is `<t>.<query>`. The poll's answer is signed over its
body like a push.

Each snippet has a verify function for events Hijinks sends you (and poll answers), a sign function
for events your bridge sends, and a sign function for polls.

<CodeTabs>

```js title="Node"
import { createHmac, timingSafeEqual } from 'node:crypto';

const TOLERANCE_SECONDS = 300;

/** rawBody: the exact bytes received (e.g. express.raw({ type: 'application/json' })). */
export function verifyHijinks(rawBody, header, secret, now = Date.now()) {
  if (!header) return false;
  const pairs = header.split(',').map((part) => {
    const eq = part.indexOf('=');
    return [part.slice(0, eq).trim(), part.slice(eq + 1).trim()];
  });
  const t = pairs.find(([key]) => key === 't')?.[1];
  if (!t || !/^\d+$/.test(t)) return false;
  if (Math.abs(Math.floor(now / 1000) - Number(t)) > TOLERANCE_SECONDS) return false;

  const expected = createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest();
  return pairs.some(
    ([key, value]) =>
      key === 'v1' &&
      /^[0-9a-f]{64}$/i.test(value) &&
      timingSafeEqual(Buffer.from(value, 'hex'), expected),
  );
}

/** The header for a request your bridge sends; body is the exact string you POST. */
export function signHijinks(body, secret, now = Date.now()) {
  const t = Math.floor(now / 1000);
  const v1 = createHmac('sha256', secret).update(`${t}.${body}`).digest('hex');
  return `t=${t},v1=${v1}`;
}

/** The header for a poll; query is everything after `?` exactly as sent ('' for none). */
export const signPoll = (query, secret, now = Date.now()) => signHijinks(query, secret, now);
```

```python title="Python"
import hashlib
import hmac
import time

TOLERANCE_SECONDS = 300


def verify_hijinks(raw_body: bytes, header: str | None, secret: str) -> bool:
    """raw_body: the exact bytes received (Flask: request.get_data())."""
    if not header:
        return False
    pairs = [
        (key.strip(), value.strip())
        for key, _, value in (part.partition("=") for part in header.split(","))
    ]
    t = next((value for key, value in pairs if key == "t"), "")
    if not (t.isascii() and t.isdigit()):
        return False
    if abs(time.time() - int(t)) > TOLERANCE_SECONDS:
        return False

    expected = hmac.new(
        secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256
    ).hexdigest()
    return any(
        key == "v1" and hmac.compare_digest(value.lower().encode(), expected.encode())
        for key, value in pairs
    )


def sign_hijinks(body: bytes, secret: str) -> str:
    """The header for a request your bridge sends; body is the exact bytes you POST."""
    t = str(int(time.time()))
    v1 = hmac.new(secret.encode(), t.encode() + b"." + body, hashlib.sha256).hexdigest()
    return f"t={t},v1={v1}"


def sign_poll(query: str, secret: str) -> str:
    """The header for a poll; query is everything after "?" exactly as sent ("" for none)."""
    return sign_hijinks(query.encode(), secret)
```

```go title="Go"
package hijinks

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

const tolerance = 5 * time.Minute

// Verify checks a Hijinks-Signature header against the exact bytes received
// (io.ReadAll(r.Body) before any JSON decoding).
func Verify(rawBody []byte, header, secret string, now time.Time) bool {
	var ts string
	var sigs [][]byte
	for _, part := range strings.Split(header, ",") {
		key, value, ok := strings.Cut(strings.TrimSpace(part), "=")
		if !ok {
			continue
		}
		switch key {
		case "t":
			ts = value
		case "v1":
			if sig, err := hex.DecodeString(value); err == nil {
				sigs = append(sigs, sig)
			}
		}
	}
	t, err := strconv.ParseUint(ts, 10, 63)
	if err != nil || len(sigs) == 0 {
		return false
	}
	if skew := now.Sub(time.Unix(int64(t), 0)); skew > tolerance || skew < -tolerance {
		return false
	}

	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(ts + "."))
	mac.Write(rawBody)
	expected := mac.Sum(nil)
	for _, sig := range sigs {
		if hmac.Equal(sig, expected) {
			return true
		}
	}
	return false
}

// Sign returns the header for a request your bridge sends; body is the exact bytes you POST.
func Sign(body []byte, secret string, now time.Time) string {
	t := strconv.FormatInt(now.Unix(), 10)
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(t + "."))
	mac.Write(body)
	return fmt.Sprintf("t=%s,v1=%s", t, hex.EncodeToString(mac.Sum(nil)))
}

// SignPoll returns the header for a poll; query is everything after "?" exactly as sent ("" for none).
func SignPoll(query, secret string, now time.Time) string {
	return Sign([]byte(query), secret, now)
}
```

</CodeTabs>

To check your code end to end without a bridge, sign a request with `openssl` as in the
[Quickstart](/quickstart).

## Rotating the secret

**Rotate secret** in the workspace's settings shows a new secret once. The old one keeps working
for 24 hours, and every event Hijinks sends carries a `v1=` for each, so you can redeploy the
bridge with the new secret at leisure.

If the old secret **leaked**, don't wait: after rotating, press **Expire the old secret now**
beside the new one. From then on only the new secret verifies, in both directions, and a bridge
still holding the old one gets `401` until you redeploy it.
