# `Image.Plug.Signing`
[🔗](https://github.com/elixir-image/image_plug/blob/v0.1.0/lib/image/plug/signing.ex#L1)

HMAC signing and verification for `Image.Plug` request URLs.

When configured, every request URL must carry a `?sig=<hex>` query
parameter whose value is `HMAC-SHA256(secret, path)`, where `path`
is the request path including any query string with the `sig`
parameter removed. Optionally a `?exp=<unix-seconds>` parameter
carries an expiry that the verifier checks against system time.

### Why query-parameter signing?

A query parameter survives URL-rewriting intermediaries that may
strip or reorder path segments, and it composes cleanly with the
Cloudflare Images URL grammar (which uses `,` and `/` as
significant path separators). Signed URLs work as drop-in
replacements for unsigned ones — the canonical pipeline is
unchanged.

### Why HMAC-SHA256?

HMAC-SHA256 is the same primitive Cloudflare's signed-URL feature
uses for its R2 and other URL-signing endpoints. Standard, fast,
no key-distribution surprises. The verifier supports a list of
keys for rotation: signing always uses the first key, verification
accepts any.

### Example

    iex> keys = ["secret-1"]
    iex> path = "/cdn-cgi/image/width=200/photo.jpg"
    iex> signed = Image.Plug.Signing.sign(path, keys)
    iex> Image.Plug.Signing.verify(signed, keys, required?: true)
    :ok

# `key`

```elixir
@type key() :: String.t()
```

# `keys`

```elixir
@type keys() :: [key(), ...]
```

# `sign`

```elixir
@spec sign(String.t(), keys(), keyword()) :: String.t()
```

Signs `path` with the first key in `keys`, returning the path with
`?sig=<hex>` (and optionally `?exp=<unix-seconds>&sig=<hex>`)
appended.

### Arguments

* `path` is the request path string (with or without an existing
  query string). If a query string is present, the signature
  covers the full path including that query string.

* `keys` is a non-empty list of secret-key strings. The first key
  is used for signing; the rest are accepted at verification time
  to support rotation.

### Options

* `:expires_at` — `DateTime` or unix-seconds integer. When set,
  appends `?exp=<unix-seconds>` to the path and signs the result.
  Verification rejects the URL after this time.

### Returns

* The path with the appropriate query parameters appended.

### Examples

    iex> Image.Plug.Signing.sign("/foo.jpg", ["secret"]) |> String.starts_with?("/foo.jpg?sig=")
    true

# `verify`

```elixir
@spec verify(String.t(), keys(), keyword()) :: :ok | {:error, Image.Plug.Error.t()}
```

Verifies the signature on `path_with_query`.

### Arguments

* `path_with_query` is the request path as it appeared on the
  wire, including any query string.

* `keys` is a non-empty list of secret-key strings. Verification
  accepts a signature produced with any key in the list (for
  rotation).

### Options

* `:required?` — when `true`, a missing `?sig` parameter causes a
  `:signature_required` error. When `false` (default), an
  unsigned URL passes verification — useful for gradual roll-out
  where some clients haven't been updated yet.

* `:now` — current unix-seconds timestamp for expiry comparison.
  Defaults to `System.system_time(:second)`. Test-only override.

### Returns

* `:ok` when the signature is valid (or absent and not required).

* `{:error, %Image.Plug.Error{tag: :signature_required}}` when no
  `?sig` is present and signing is required.

* `{:error, %Image.Plug.Error{tag: :invalid_signature}}` when the
  `?sig` value does not match any key.

* `{:error, %Image.Plug.Error{tag: :signature_expired}}` when an
  `?exp` parameter is present and has passed.

---

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