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
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
conn- aPlug.Connthat passedRequestSeal.Plug.Capture.opts- options returned byinit/1.
Returns
Plug.Conn- on success, private state's:verificationis{:ok, %RequestSeal.Verification{}}, readable withRequestSeal.Plug.verification/1, and the result is assigned when:assignis set.Plug.Conn- on rejection, private state's:verificationis{:error, %RequestSeal.Adapter.Error{stage: :verify, attempt: 0}}, any:assignkey 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 boundedRequestSeal.Errorassource), 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
Validates verification options once, at plug initialization.
Parameters
opts- keyword list of the options below. Unknown or duplicate options reject.
Options
:policy(required) - a validRequestSeal.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:nameand:labelare equal, even if the:policyvalue 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}