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
Functions
Signs path with the first key in keys, returning the path with
?sig=<hex> (and optionally ?exp=<unix-seconds>&sig=<hex>)
appended.
Arguments
pathis 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.keysis 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—DateTimeor 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
@spec verify(String.t(), keys(), keyword()) :: :ok | {:error, Image.Plug.Error.t()}
Verifies the signature on path_with_query.
Arguments
path_with_queryis the request path as it appeared on the wire, including any query string.keysis a non-empty list of secret-key strings. Verification accepts a signature produced with any key in the list (for rotation).
Options
:required?— whentrue, a missing?sigparameter causes a:signature_requirederror. Whenfalse(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 toSystem.system_time(:second). Test-only override.
Returns
:okwhen the signature is valid (or absent and not required).{:error, %Image.Plug.Error{tag: :signature_required}}when no?sigis present and signing is required.{:error, %Image.Plug.Error{tag: :invalid_signature}}when the?sigvalue does not match any key.{:error, %Image.Plug.Error{tag: :signature_expired}}when an?expparameter is present and has passed.