---
title: Verification
description: Verify a Sigil-Signature header, enforce a tolerance window, and decode the payload.
url: https://pr-1-10d18e06d3bf.thally.app/verification
---

# Verification

Verify a Sigil-Signature header, enforce a tolerance window, and decode the payload.

## Verify a signature

`verify` checks the header against the body and returns the header's Unix
timestamp on success. It throws `SigilError` on any rejection.

```ts
import { verify } from '@sigil/core';

const timestamp = verify(rawBody, header, secret);
```

### Non-throwing alternative

`verifyResult` returns a discriminated union instead of throwing:

```ts
import { verifyResult } from '@sigil/core';

const result = verifyResult(rawBody, header, secret);
if (!result.ok) {
  console.warn(result.code, result.message);
}
```

## Order of checks

1. The secrets are non-empty.
2. The header parses.
3. The timestamp is inside the tolerance window.
4. At least one signature matches at least one secret.

The timestamp is checked **before** any HMAC is computed. A flood of stale
requests costs one integer comparison each rather than one hash each. The
observable consequence: a request that is both expired and wrongly signed
reports `timestamp_out_of_tolerance`, not `signature_mismatch`.

## The tolerance window

`DEFAULT_TOLERANCE_SECONDS` is **300** (five minutes). A timestamp is accepted
when `abs(now - t) <= tolerance`, so the window is symmetric — a sender whose
clock runs fast is treated exactly like one that runs slow.

Exactly 300 seconds of drift is accepted; 301 is not.

```ts
verify(body, header, secret, { toleranceSeconds: 60 });  // stricter
verify(body, header, secret, { toleranceSeconds: 0 });   // no window at all
verify(body, header, secret, { now: 1767225600 });       // fixed clock, for tests
```

`toleranceSeconds: 0` disables the check entirely rather than demanding an
exact match. Use it only where replay is prevented some other way, such as an
idempotency table.

## Verification options

| Option             | Type     | Default                          | Description                                   |
| ------------------ | -------- | -------------------------------- | --------------------------------------------- |
| `toleranceSeconds` | `number` | `300`                            | Maximum absolute difference, in seconds, between the header's timestamp and `now`. `0` disables the window entirely. |
| `now`              | `number` | `Math.floor(Date.now() / 1000)` | Unix seconds to check the timestamp against. Pass it explicitly to make a test deterministic. |

## Constant-time comparison

Digests are compared with `crypto.timingSafeEqual`. A length mismatch would
make that function throw (and the throw would itself be a timing signal), so
unequal lengths are turned into a constant-time `false`.

Every secret is checked against every signature even after a match is found,
so the work done does not reveal which pair matched.

## Multiple secrets

Pass an array of secrets to accept any one of them. This is the receiver side
of [key rotation](/key-rotation):

```ts
verify(body, header, [process.env.SECRET_OLD!, process.env.SECRET_NEW!]);
```

## Verify and decode together

Most receivers verify a request and then parse its JSON body.
`constructEvent` does both in that order:

```ts
import { constructEvent } from '@sigil/core';

const event = constructEvent<{ id: string; type: string }>(
  rawBody,
  header,
  secret,
);
console.log(event.payload.type, 'signed at', event.timestamp);
```

Verification runs first, so a body that fails its signature never reaches
`JSON.parse`. A body that verifies but is not valid JSON throws
`payload_not_json` — distinct from `signature_mismatch`, because the two mean
different things: one is an attacker or a misconfigured secret, the other is a
sender emitting something unexpected.

The type parameter `T` is a convenience for the caller. Nothing validates that
the payload actually matches it.

`constructEvent` accepts the same `VerifyOptions` as `verify`:

```ts
const event = constructEvent(rawBody, header, secret, {
  toleranceSeconds: 60,
  now: 1767225600,
});
```