RequestSeal.AshHooks (RequestSeal v0.5.2)

Copy Markdown View Source

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

verify_signature(body, context, reference, opts)

@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 - an AshHooks.Provider.verify_context/0 map. Its :method, absolute :request_uri, :headers map, and :tenant are 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 - a RequestSeal.Policy, or an arity-two function called as fun.(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} - reference is 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