# Checking the signature

Each delivery carries `x-mellow-signature`: the HMAC-SHA256 of the raw request body, keyed with the endpoint's secret, written as lowercase hex. The secret starts with `whsec_` and is shown once when the endpoint is made.

Compute the same over the raw bytes you received, before parsing, and compare in constant time.

## The time of the post

The body carries `sentAt`: the time this post was sent, new on each try. It is inside what the signature covers, so nobody can change it and keep the signature. After the signature checks out, refuse a post whose `sentAt` is more than five minutes from your own clock. Then a post that somebody copied cannot be sent to you again later.

`at` is the time of the event, and a retry keeps it. Do not use `at` for this check: a retry after an hour is a real post.

## Node

```js
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(rawBody, header, secret) {
  const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
  const given = Buffer.from(header ?? "", "hex");
  return (
    given.length === expected.length / 2 && timingSafeEqual(given, Buffer.from(expected, "hex"))
  );
}

// After verify, on the parsed body.
export function fresh(body, maxMs = 5 * 60 * 1000) {
  return Math.abs(Date.now() - Date.parse(body.sentAt)) <= maxMs;
}
```

## Python

```python
import hashlib, hmac
from datetime import datetime, timezone

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header or "")

# After verify, on the parsed body.
def fresh(body: dict, max_seconds: int = 300) -> bool:
    sent = datetime.fromisoformat(body["sentAt"].replace("Z", "+00:00"))
    return abs((datetime.now(timezone.utc) - sent).total_seconds()) <= max_seconds
```

## PHP

```php
function verify(string $rawBody, string $header, string $secret): bool {
  return hash_equals(hash_hmac('sha256', $rawBody, $secret), $header);
}

// After verify, on the parsed body.
function fresh(array $body, int $maxSeconds = 300): bool {
  return abs(time() - strtotime($body['sentAt'])) <= $maxSeconds;
}
```

Parse the JSON only after the signature checks out. A body that fails the check is not from us: answer `401` and do nothing with it. Answer a body whose `sentAt` is too old the same way.
