# `Ankusa.Verifier`
[🔗](https://github.com/jamescarr/ankusa/blob/ankusa-v0.7.0/packages/ankusa/lib/ankusa/verifier.ex#L1)

Signature and timestamp verification. HMAC is microseconds, so it runs inline
on the hot path, before the ack.

Adapters implement `verify/2` over the raw envelope (the exact bytes matter).
`opts` carry the per-source secret and any tolerance window
(`Ankusa.Verifier.check_timestamp/2`).

The verifiers in core:

  * `Ankusa.Verifier.None` — accepts everything;
  * `Ankusa.Verifier.Hmac` — a shared-secret signature over the request;
  * `Ankusa.Verifier.Asymmetric` — a public-key (Ed25519, RSA) signature over
    the request; the node holds no secret;
  * `Ankusa.Verifier.Token` — a static bearer, basic, header or query
    credential;
  * `Ankusa.Verifier.ClientCert` — the client certificate of the edge
    listener's mutual-TLS handshake.

The edge puts request context some verifiers need into `opts`, for those
verifiers only: `:peer_cert` (the DER client certificate, or `nil`) for
`ClientCert` and `:query` (the raw query string) for a `Token` in the query.

## Failure vocabulary

Adapters return `{:error, reason}`, which the source's `on_verify_failure`
policy turns into `:reject`, `:quarantine`, or `:accept_flag`. The reasons are
deliberately shared so telemetry and dashboards can count them without
knowing which provider produced them:

  * `:missing_signature` — a required header, credential or certificate is
    absent
  * `:malformed_signature` — present, but not parseable
  * `:no_match` — nothing matched the expected digest, key, token or pin
  * `:timestamp_out_of_tolerance` — outside the replay window
  * `:bad_secret` — the configured secret can't be used as a key
    (`Verifier.Hmac`, `Verifier.Token`)
  * `:bad_key` — a configured public key can't be used (`Verifier.Asymmetric`)
  * `:bad_pin` — a certificate source pins nothing usable
    (`Verifier.ClientCert`)
  * `:bad_scheme` — an unknown or missing scheme name (`Verifier.Hmac`,
    `Verifier.Asymmetric`), or an unknown `:location` (`Verifier.Token`)
  * `:bad_tolerance` — `:tolerance` is not an integer from 0 to
    `max_tolerance_seconds/0` (`Verifier.Hmac`, `Verifier.Asymmetric`)

# `scheme_name`
*optional* 

```elixir
@callback scheme_name(opts :: keyword()) :: String.t() | nil
```

Human name of the scheme for telemetry; `nil` when the verifier has no named
scheme. Optional — verifiers that only implement `verify/2` are attributed by
their module name instead.

# `validate_opts`
*optional* 

```elixir
@callback validate_opts(opts :: keyword()) :: :ok | {:error, atom()}
```

Check `opts` without a request: `:ok`, or the `{:error, reason}` `verify/2`
would return for every hook. Optional — `validate_config!/1` runs it, at boot,
for every source declared in config whose verifier implements it, and the
image's loader runs it for every source an admin API write submits.

# `verify`

```elixir
@callback verify(Ankusa.Envelope.t(), opts :: keyword()) :: :ok | {:error, term()}
```

Verify an envelope. Return `:ok` to accept, or `{:error, reason}` to trigger
the source's `on_verify_failure` policy (`:reject | :quarantine | :accept_flag`).

# `check_shared`

```elixir
@spec check_shared(Ankusa.Config.t(), Ankusa.Source.t()) :: :ok | {:error, String.t()}
```

Whether `source` may run under `config`: `{:error, message}` when it is shared
(`tenant_id` `"default"`), has no verifier (`Ankusa.Verifier.None`), does not
set `trust_url_tenant: true`, and the edge takes the tenant from the request
(any `Ankusa.RouteResolver` but `Ankusa.RouteResolver.Path`). Such a source
would file hooks under whatever tenant the request names, and spend that
tenant's rate limit, with nothing to say the sender may. The verdict depends
on the config alone, never on the node's roles, so every node reading one
config agrees.

# `check_timestamp`

```elixir
@spec check_timestamp(String.t(), keyword()) :: :ok | {:error, atom()}
```

Check a Unix-seconds timestamp from a signature header against a tolerance
window.

Every verifier that carries a signed timestamp needs this, so the window
semantics are defined once instead of per adapter. `opts` may set `:tolerance`
in seconds (default 300, i.e. ±5 minutes), and
`:now` (Unix seconds) to judge the window against a fixed instant instead of
the clock: `Ankusa.Dispatch.Replayer` sets it to the hook's receive time when
it re-verifies a quarantined hook, so a release hours later holds the hook to
the window it arrived in, not one it can no longer meet.

# `check_tolerance`

```elixir
@spec check_tolerance(keyword()) :: :ok | {:error, :bad_tolerance}
```

Whether `opts[:tolerance]` is a usable replay window: absent (the default
window), or an integer from 0 to `max_tolerance_seconds/0`. A window wider
than an hour is not a window, it is the absence of one. Shared by every
verifier that checks a timestamp.

# `max_tolerance_seconds`

```elixir
@spec max_tolerance_seconds() :: pos_integer()
```

The widest replay window, in seconds, a verifier accepts: an hour. Beyond that
a signed hook stays replayable long enough that the window protects nothing.

# `scheme_name`

```elixir
@spec scheme_name(module(), keyword()) :: String.t() | nil
```

The scheme name a verification is attributed to: `mod.scheme_name(opts)`
when the verifier implements it, else the module name.

# `secure_compare`

```elixir
@spec secure_compare(binary(), binary()) :: boolean()
```

Constant-time compare of two binaries of equal length.

# `validate_config!`

```elixir
@spec validate_config!(Ankusa.Config.t()) :: :ok
```

Refuse to boot with a source declared in config whose verifier would fail
every hook, which `on_verify_failure` then quarantines, flags or rejects —
silently, at runtime. Every verifier that implements `validate_opts/1` is
asked: an `Ankusa.Verifier.Hmac` or `Ankusa.Verifier.Token` secret that is
missing, empty or undecodable, an `Ankusa.Verifier.Asymmetric` public key that
is not a key, an `Ankusa.Verifier.ClientCert` that pins nothing, an unknown
scheme, or a `:tolerance` outside `0..max_tolerance_seconds/0`. A
`ClientCert` source also needs `listener.tls` with `verify: :verify_peer`:
no other listener ever receives a client certificate. Sources a writable store
holds are checked when they are written. Raises `ArgumentError` naming the
source. A source that verifies with SHA-1
(`Ankusa.Verifier.Hmac.weak_hash?/1`) boots, with one warning.

# `validate_shared!`

```elixir
@spec validate_shared!(Ankusa.Config.t()) :: :ok
```

Refuse to boot with a source declared in config that `check_shared/2`
refuses. Sources a writable store holds are checked when they are written
(and a stored one written before the check existed is named by
`warn_stored_shared/2`). Raises `ArgumentError` naming the source.

# `warn_stored_shared`

```elixir
@spec warn_stored_shared(Ankusa.Config.t(), [Ankusa.Source.t()]) :: :ok
```

Log one warning per stored (API-managed) source that `check_shared/2`
refuses. Such a source was written before writes were checked: it keeps
serving as it did, and only a write that still lacks `trust_url_tenant: true`
is refused.

# `weak_hash?`

```elixir
@spec weak_hash?(module(), keyword()) :: boolean()
```

Whether the verifier `{mod, opts}` checks signatures with a weak hash (SHA-1).
Only `Ankusa.Verifier.Hmac` can; see `Ankusa.Verifier.Hmac.weak_hash?/1`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
