Image.Plug.Signing (image_plug v0.1.0)

Copy Markdown View Source

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

Summary

Functions

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

Verifies the signature on path_with_query.

Types

key()

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

keys()

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

Functions

sign(path, keys, options \\ [])

@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_atDateTime 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(path_with_query, keys, options \\ [])

@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.