RequestSeal.AcceptSignature (RequestSeal v0.5.2)

Copy Markdown View Source

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

request()

@type request() :: %{
  label: binary(),
  components: RequestSeal.StructuredFields.Value.t(),
  parameters: %{optional(binary()) => true | binary()}
}

Functions

fulfill(message, requests, chooser, signer, opts)

@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 valid RequestSeal.Message to sign; its kind is the target every request must apply to.
  • requests - list of at most 16 request/0 maps 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 to RequestSeal.sign/4.

Returns

  • {:ok, %RequestSeal.Message{}} - the message with one Signature-Input and one Signature occurrence appended per request.
  • {:error, %RequestSeal.Error{}} - reasons this function can return: :invalid_options and :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's detail. 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

parse(wire, opts)

@spec parse(binary() | [binary()], keyword()) ::
  {:ok, [request()]} | {:error, RequestSeal.Error.t()}

Parse ordered signature requests for an explicit target message kind.

Parameters

  • wire - one Accept-Signature field 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/0 maps in wire order, each with :label, :components (a metadata-free Structured Fields Inner List), and :parameters (a map of "created"/"expires" to true and "nonce", "keyid", "tag", "alg" to strings). An empty field value or list returns {:ok, []}.
  • {:error, %RequestSeal.Error{}} - reasons this function can return: :invalid_options and :limit (layer :input); :duplicate_label and :duplicate_parameter (layer :fields); :invalid_accept_signature and :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

serialize(requests)

@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

  • requests - list of at most 16 request/0 maps with unique labels, shaped as parse/2 returns them.

Returns

  • {:ok, binary} - the Accept-Signature field 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}