RFC 9421 Section 5 signature requests and nonce challenges.
A requester serializes ordered requests with serialize/1 into an
Accept-Signature field. The other party parses them with parse/2 for its
own message kind and signs them with fulfill/5, whose chooser and signer
own key selection and custody. Created/expires requests are bare true;
nonce, keyid, tag, and alg are strings. Algorithms are exact HTTP registry
tokens, never JWS selections. Unknown parameters reject. Components include
their exact parameters; request targets cannot cover @status or related
requests. Response targets require req for request-derived components.
Passing requests as accept_signature: to RequestSeal.verify_quorum/3
requires each exact label in the verified, selector-eligible pool after key-unit
filtering, with the same covered component identity set and every requested
parameter. The label need not be assigned to a counted slot; it appears in
qualifying and signatures only when assigned. Extra metadata and separate
signatures are allowed.
A challenge match is established; replay storage and authorization are not.
Limits: 16 requests, 65,536 wire bytes, and the signature-base component bounds.
Summary
Functions
Fulfill requests in order through caller-owned key selection and signing.
Parse ordered signature requests for an explicit target message kind.
Serialize ordered requests with deterministic signature metadata ordering.
Types
@type request() :: %{ label: binary(), components: RequestSeal.StructuredFields.Value.t(), parameters: %{optional(binary()) => true | binary()} }
Functions
@spec fulfill( RequestSeal.Message.t(), [request()], (request() -> {:ok, map()} | :error), RequestSeal.Policy.signer(), keyword() ) :: {:ok, RequestSeal.Message.t()} | {:error, RequestSeal.Error.t()}
Fulfill requests in order through caller-owned key selection and signing.
For each request the chooser returns the algorithm and times to use.
A requested alg must equal the chosen algorithm; keyid, nonce, tag, and
alg are copied verbatim. Requested created and expires take the
chooser's values, which must then be integers; times that were not
requested are not written. Signatures are appended in request order through
the explicit form of RequestSeal.sign/4. Any failure returns an error and
no partially signed message.
Parameters
message- the validRequestSeal.Messageto sign; its kind is the target every request must apply to.requests- list of at most 16request/0maps with unique labels.chooser- arity-one function receiving each request and returning{:ok, %{algorithm: algorithm, created: integer_or_nil, expires: integer_or_nil}}(an HTTP registry algorithm; times from -999,999,999,999,999 through 999,999,999,999,999) or:error.signer- arity-two function(algorithm, base)returning{:ok, signature_bytes}or{:error, term}.opts-[]or[field_schemas: schemas], passed toRequestSeal.sign/4.
Returns
{:ok, %RequestSeal.Message{}}- the message with oneSignature-Inputand oneSignatureoccurrence appended per request.{:error, %RequestSeal.Error{}}- reasons this function can return::invalid_optionsand:invalid_message(layer:input);:duplicate_label(layer:fields);:invalid_accept_signature,:inapplicable_component, and:negotiation_unfulfillable(layer:negotiation; unavailable component, incompatible chooser, unfulfillable parameter, callback fault, or signing failure). When an underlying signing step fails, its bounded reason appears in the error'sdetail. Meanings:RequestSeal.Error.
Examples
iex> {_public, seed} = :crypto.generate_key(:eddsa, :ed25519)
iex> {:ok, handle} = RequestSeal.Custody.Local.new("ed25519", {:ed25519, seed})
iex> {:ok, message} =
...> RequestSeal.Message.request("POST", "https://chat.example/rooms/7/messages", [], "hello")
iex> {:ok, requests} =
...> RequestSeal.AcceptSignature.parse(~s[reply=("@method" "@path");created;nonce="chat-7f3a"], target: :request)
iex> chooser = fn _request -> {:ok, %{algorithm: "ed25519", created: 1_700_000_000, expires: nil}} end
iex> signer = fn "ed25519", base -> RequestSeal.Custody.sign(handle, base) end
iex> {:ok, signed} = RequestSeal.AcceptSignature.fulfill(message, requests, chooser, signer, [])
iex> signed.fields |> hd() |> Map.fetch!(:value)
~s[reply=("@method" "@path");created=1700000000;nonce="chat-7f3a"]
iex> {:error, error} = RequestSeal.AcceptSignature.fulfill(message, requests, fn _ -> :error end, signer, [])
iex> {error.reason, error.layer}
{:negotiation_unfulfillable, :negotiation}
iex> RequestSeal.Custody.Local.release(handle)
:ok
@spec parse(binary() | [binary()], keyword()) :: {:ok, [request()]} | {:error, RequestSeal.Error.t()}
Parse ordered signature requests for an explicit target message kind.
Parameters
wire- oneAccept-Signaturefield value, or a list of at most 1,024 ordered field occurrences (joined with", "). At most 65,536 bytes in total, at most 16 dictionary members.opts- exactly[target: :request]or[target: :response]: the kind of message that will carry the requested signatures.
Returns
{:ok, [request]}-request/0maps in wire order, each with:label,:components(a metadata-free Structured Fields Inner List), and:parameters(a map of"created"/"expires"totrueand"nonce","keyid","tag","alg"to strings). An empty field value or list returns{:ok, []}.{:error, %RequestSeal.Error{}}- reasons this function can return::invalid_optionsand:limit(layer:input);:duplicate_labeland:duplicate_parameter(layer:fields);:invalid_accept_signatureand:inapplicable_component(layer:negotiation). Meanings:RequestSeal.Error.
Examples
iex> {:ok, [request]} =
...> RequestSeal.AcceptSignature.parse(
...> ~s[reply=("@method" "@path" "content-digest");created;nonce="chat-7f3a";keyid="relay"],
...> target: :request
...> )
iex> {request.label, request.parameters}
{"reply", %{"created" => true, "keyid" => "relay", "nonce" => "chat-7f3a"}}
iex> {:error, error} = RequestSeal.AcceptSignature.parse(~s[reply=("@status")], target: :request)
iex> {error.reason, error.layer}
{:inapplicable_component, :negotiation}
iex> {:error, error} = RequestSeal.AcceptSignature.parse(~s[reply=("@method");alg="RS256"], target: :request)
iex> error.reason
:invalid_accept_signature
@spec serialize([request()]) :: {:ok, binary()} | {:error, RequestSeal.Error.t()}
Serialize ordered requests with deterministic signature metadata ordering.
Requests keep their order; parameters are written in the order keyid, alg, created, expires, nonce, tag. No target applicability check is made.
Parameters
Returns
{:ok, binary}- theAccept-Signaturefield value.{:error, %RequestSeal.Error{}}- reasons this function can return::invalid_accept_signature(layer:negotiation),:duplicate_label(layer:fields),:limit(layer:input). Meanings:RequestSeal.Error.
Examples
iex> {:ok, requests} =
...> RequestSeal.AcceptSignature.parse(
...> ~s[reply=("@method" "@path");nonce="chat-7f3a";created;keyid="relay"],
...> target: :request
...> )
iex> RequestSeal.AcceptSignature.serialize(requests)
{:ok, ~s[reply=("@method" "@path");keyid="relay";created;nonce="chat-7f3a"]}
iex> {:error, error} = RequestSeal.AcceptSignature.serialize(requests ++ requests)
iex> {error.reason, error.layer}
{:duplicate_label, :fields}