RequestSeal.Plug.Verify (RequestSeal v0.5.2)

Copy Markdown View Source

Verify the immutable captured request under an explicit policy and label.

Verify reads only the message retained by RequestSeal.Plug.Capture. No authenticated identity is implicitly authorized. The RequestSeal reader's invocation is tracked separately from adapter reads. Pass-through parsers that leave the body unread do not impose a replay requirement, even when they fetch empty body params. Wrapper transformations after a read remain caller-controlled and cannot be observed by the replay check. Captured Plug values do not establish exact target evidence, so target-dependent components are refused (see call/2).

Summary

Functions

Verifies the captured request once and records the result in private state.

Validates verification options once, at plug initialization.

Functions

call(conn, opts)

Verifies the captured request once and records the result in private state.

From 0.5.0, a connection is verified at most once. A later Verify with the same :policy and :label values as the first is idempotent: it reuses the first result (assigning it under its own :assign on success) and applies :on_reject again on a rejection. A later Verify with any other policy or label fails closed: private state's :verification becomes {:error, %RequestSeal.Adapter.Error{reason: :already_verified}}, so RequestSeal.Plug.verification/1 returns :error from then on, every assign made by Verify on the connection is removed, and :on_reject applies. The :policy and :label values are compared with ==.

Checks run in this order: a missing capture, the replay checks, policy resolution, target-dependent coverage, then RequestSeal.verify/3 with the label. The replay checks reject a replaced or rewrapped replay adapter, a RequestSeal reader call without a replay read, and a partially drained replay or a handed-out digest that differs from the full captured body. Every check before RequestSeal.verify/3 runs before key resolution, freshness evaluation, or replay callbacks; the policy function or MFA runs only after the capture and replay checks pass.

Parameters

Returns

  • Plug.Conn - on success, private state's :verification is {:ok, %RequestSeal.Verification{}}, readable with RequestSeal.Plug.verification/1, and the result is assigned when :assign is set.
  • Plug.Conn - on rejection, private state's :verification is {:error, %RequestSeal.Adapter.Error{stage: :verify, attempt: 0}}, any :assign key is removed, and with {:halt, status} the connection is sent with that status and an empty body and halted. Reasons: :not_captured (no capture), :parser_order (replay checks above), :invalid_options (a policy function or MFA returned an invalid policy), :unsupported_component (the policy requires, or the selected Signature-Input covers, @request-target, @target-uri, or @query; unrelated labels impose nothing), :request_rejected (the core verifier rejected, with its bounded RequestSeal.Error as source), and :response_rejected (a policy callback raised, threw, or exited, or another unexpected exception, without source details). Meanings: RequestSeal.Adapter.Error.

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> components = ~s[("@method" "@authority" "@path")]
iex> {:ok, message} =
...>   RequestSeal.Message.request("POST", "https://messages.example.com/inbox", [], "hi")
iex> {:ok, signed} =
...>   RequestSeal.sign(message, %{
...>     label: "sig",
...>     algorithm: "ed25519",
...>     components: components,
...>     expires_in: 60
...>   }, handle)
iex> conn =
...>   Enum.reduce(
...>     signed.fields,
...>     Plug.Test.conn(:post, "https://messages.example.com/inbox", "hi"),
...>     fn field, conn ->
...>       Plug.Conn.put_req_header(conn, String.downcase(field.name), field.value)
...>     end
...>   )
iex> {:ok, policy} =
...>   RequestSeal.Policy.new(%{
...>     algorithms: ["ed25519"],
...>     components: components,
...>     key_resolver: fn _ -> {:ok, %{algorithm: "ed25519", key: key}} end,
...>     freshness: :not_evaluated,
...>     content: :not_required,
...>     replay: :not_required
...>   })
iex> capture =
...>   RequestSeal.Plug.Capture.init(origin: :connection, max_body_bytes: 1_024, read_timeout: 5_000)
iex> verify =
...>   RequestSeal.Plug.Verify.init(
...>     policy: policy,
...>     label: "sig",
...>     on_reject: {:halt, 401},
...>     assign: :signature
...>   )
iex> conn = conn |> RequestSeal.Plug.Capture.call(capture) |> RequestSeal.Plug.Verify.call(verify)
iex> {:ok, verification} = RequestSeal.Plug.verification(conn)
iex> {verification.label, verification.authorization, conn.assigns.signature == verification}
{"sig", :not_evaluated, true}
iex> again = RequestSeal.Plug.Verify.call(conn, verify)
iex> {RequestSeal.Plug.verification(again) == {:ok, verification}, again.halted}
{true, false}
iex> other = RequestSeal.Plug.Verify.init(policy: policy, label: "other", on_reject: {:halt, 401})
iex> refused = RequestSeal.Plug.Verify.call(again, other)
iex> {:error, error} = refused.private.request_seal.verification
iex> {error.reason, RequestSeal.Plug.verification(refused), Map.has_key?(refused.assigns, :signature), refused.status}
{:already_verified, :error, false, 401}
iex> RequestSeal.Custody.Local.release(handle)
:ok

init(opts)

Validates verification options once, at plug initialization.

Parameters

  • opts - keyword list of the options below. Unknown or duplicate options reject.

Options

  • :policy (required) - a valid RequestSeal.Policy, a zero-arity function, or an exported {module, function, args}. A function or MFA returns a policy directly and runs once per eligible request, after the capture and parser checks, before key resolution. Resolution runs synchronously; callers own deadlines for external work. No callback diagnostics are retained.
  • :label (required) - the signature label to verify.
  • :on_reject (required) - :continue, or {:halt, status} with a final status in 200..599.
  • :assign - a non-Boolean atom or nil (default nil; Booleans reject from 0.5.0). When set, a successful verification is also assigned under that key.
  • :name - a stable repeat-identity atom (or 1..256-byte binary), default nil. When set, a later Verify on the same connection reuses the first result when its :name and :label are equal, even if the :policy value is a rebuilt-equivalent policy whose function fields make == fail — the name is the caller's explicit assertion that the policy is the same one. A repeat with a different name, no name, or a different label still fails closed :already_verified; a policy-based repeat of a named verification never matches.

Returns

  • keyword list - the validated options.

Raises RequestSeal.Adapter.Error with reason :invalid_options and stage :verify on any invalid option.

Examples

iex> try do
...>   RequestSeal.Plug.Verify.init(policy: fn -> :unused end, label: "sig")
...> rescue
...>   error in RequestSeal.Adapter.Error -> {error.reason, error.stage}
...> end
{:invalid_options, :verify}