Optional ash_hooks inbound RFC 9421 verification bridge (Elixir 1.20).
A consumer's AshHooks.Provider.verify_signature/3 delegates to
verify_signature/4 with an explicit policy and label. RequestSeal never
interprets the provider's secret/key reference as key material or
authorizes an application action.
The provider's headers map has already lost duplicate lines and ordering;
the helper cannot recover them and returns only the provider's :ok/error
contract.
Prefer RequestSeal.Plug.Capture and RequestSeal.Plug.Verify before
ash_hooks ingress to retain ordered headers, exact body bytes, and facts.
The RequestSeal reader replays unchanged bytes to AshHooks.BodyReader.
Provider errors never expose key references, body bytes, or callback errors.
Summary
Functions
Verifies a provider's exact body under an explicit policy and signature label.
Functions
@spec verify_signature( binary(), AshHooks.Provider.verify_context(), binary(), keyword() ) :: :ok | {:error, :invalid_signature | :no_webhook_secret}
Verifies a provider's exact body under an explicit policy and signature label.
The request is rebuilt with RequestSeal.Message.request/5 from the
context's method, URI, and header map, with body as retained bytes, then
checked with RequestSeal.verify/3.
Parameters
body- the exact raw request body bytes.context- anAshHooks.Provider.verify_context/0map. Its:method, absolute:request_uri,:headersmap, and:tenantare used; other keys are ignored.reference- the provider's nonempty binary secret/key reference. It is passed only to a policy function, never used as key material.opts- keyword list of the options below. Unknown or duplicate options return{:error, :invalid_signature}.
Options
:policy- aRequestSeal.Policy, or an arity-two function called asfun.(context.tenant, reference)that returns one.:label- the signature label to verify.
Returns
:ok- the signature verified. This grants no authorization.{:error, :no_webhook_secret}-referenceis not a nonempty binary.{:error, :invalid_signature}- any other failure: invalid options, a policy function that raised, a context the message cannot be built from, or a verification rejection. No key reference, body bytes, or callback error is exposed.
Examples
iex> {_public, seed} = :crypto.generate_key(:eddsa, :ed25519)
iex> {:ok, handle} = RequestSeal.Custody.Local.new("ed25519", {:ed25519, seed})
iex> {:ok, key} = RequestSeal.Custody.public_key(handle)
iex> body = ~s({"build":"1842","state":"passed"})
iex> url = "https://hooks.example.com/builds"
iex> spec = %{
...> label: "sig",
...> algorithm: "ed25519",
...> components: ~s[("@method" "@authority" "@path" "content-digest" "content-type")],
...> expires_in: 300,
...> digest: ["sha-256"]
...> }
iex> {:ok, message} =
...> RequestSeal.Message.request("POST", url, [{"content-type", "application/json"}], body)
iex> {:ok, signed} = RequestSeal.sign(message, spec, handle)
iex> headers = Map.new(signed.fields, &{String.downcase(&1.name), &1.value})
iex> context = %{
...> method: "POST",
...> request_uri: url,
...> headers: headers,
...> signature: headers["signature"],
...> tenant: nil
...> }
iex> {:ok, policy} =
...> RequestSeal.Policy.new(%{
...> algorithms: ["ed25519"],
...> components: spec.components,
...> key_resolver: fn _ -> {:ok, %{algorithm: "ed25519", key: key}} end,
...> freshness: :not_evaluated,
...> content: %{kind: :content, algorithms: ["sha-256"], section: :headers},
...> replay: :not_required
...> })
iex> opts = [policy: fn nil, "builds-key" -> policy end, label: "sig"]
iex> RequestSeal.AshHooks.verify_signature(body, context, "builds-key", opts)
:ok
iex> RequestSeal.AshHooks.verify_signature(body <> " ", context, "builds-key", opts)
{:error, :invalid_signature}
iex> RequestSeal.AshHooks.verify_signature(body, context, "", opts)
{:error, :no_webhook_secret}
iex> RequestSeal.Custody.Local.release(handle)
:ok