RequestSeal.SignatureBase (RequestSeal v0.5.2)

Copy Markdown View Source

Exact ordered RFC 9421 Section 2 signature-base construction.

build/3 turns a validated RequestSeal.Message and one signature's parameterized Inner List into the exact signature base, never a partial base. Components and all parameters retain their order; equivalent component identifiers with reordered parameters reject. The final "@signature-params" line has no trailing newline. Construction performs no signing, verification, key operation, body read, network request, or authorization.

Two component rules are deliberately stricter than a plain RFC 9421 reading, and cross-implementation peers should know them before relying on the edges (RFC 9421 Section 2.5): duplicate component identity is exact — the same component name with the same parameters in a different order rejects as :duplicate_component rather than coalescing — and duplicate identifiers that differ only in parameter order never merge.

All nine derived components are supported. @request-target, @path, and @query preserve percent-encoded octets. @target-uri assembles the preserved target URI without normalizing it: percent-escape hex case and IPvFuture literal case pass through exactly, unlike @authority, which lowercases host and IPvFuture case — signers targeting both components from one target must send the bytes they signed for each. Only @authority and @scheme normalize host/scheme case and the HTTP(S) default port. Origin-form requests need the caller's explicit :scheme/:authority for these three origin components; absolute-form requests provide them in the raw target. Host and Forwarded fields never supply missing origin facts. Authority/asterisk-form target URIs have empty paths (RFC 9110 Section 7.1); @path normalizes an empty path to /. req requires a response with its validated original :related_request.

@query-param requires the canonical encoded name, rejects duplicate decoded names, and preserves empty values. Parsing uses UTF-8 form semantics; encoding uses RFC 9421 Section 2.2.8's percent-encode-after-encoding operation and its published vectors (%20 for spaces, uppercase hex, no double decoding), under the January 2024 URL Standard referenced by that RFC. Invalid UTF-8 rejects.

Field values trim only HTTP OWS, preserve internal bytes, and combine ordered occurrences with comma-space. Capture must already have removed HTTP/1.1 obsolete folding: Message rejects CR/LF. bs wraps each trimmed occurrence separately, including non-ASCII octets. Without bs, non-ASCII bytes reject. tr selects only trailers; unavailable or pending trailers reject. For fields whose unquoted commas make occurrence boundaries significant, callers must select bs (RFC 9421 Sections 2.1.3 and 7.5.6).

Structured Fields (sf) and dictionary member (key) components need an explicit schema for the field; key requires a Dictionary schema and strictly serializes only the selected member. Schemas apply equally to headers, trailers, and the related request; the field's public specification selects its revision, never the input's syntax. The Structured Fields parser's documented ceilings also apply to signature parameters and sf/key fields (including 65,536 serialized bytes). Extension signature metadata retains RFC 8941 types; registered created and expires require Integers, and nonce, alg, keyid, and tag require Strings. Successful construction establishes bytes only, not authentication.

Summary

Functions

Build one ASCII signature base from preserved message facts and ordered parameters.

Functions

build(message, parameters, opts \\ [])

Build one ASCII signature base from preserved message facts and ordered parameters.

Parameters

  • message - a RequestSeal.Message; it is validated first.
  • parameters - one signature's parameterized Inner List: serialized bytes such as ~s[("@method" "@path");created=1700000000], or a RequestSeal.StructuredFields.Value of type :inner_list.
  • opts - keyword list of the options below. Unknown and duplicate options reject with :invalid_options.

Options

  • :field_schemas - map of at most 1,024 lowercase field names to explicit RequestSeal.StructuredFields.Schema values, required for sf and key components. Default %{}.
  • :max_components - component ceiling, 1 to 256. Default 256.
  • :max_bytes - ceiling for the final base, 1 to 1,048,576. Default 1,048,576.

Returns

  • {:ok, bytes} - the signature base: one identifier: value line per component in order, then the "@signature-params" line.
  • {:error, %RequestSeal.SignatureBase.Error{}} - reasons this function can return: :invalid_message, :invalid_options, :invalid_signature_parameters, :limit; component reasons :invalid_component, :unknown_component, :duplicate_component, :invalid_component_parameters, :incompatible_parameters; context reasons :wrong_message_kind, :missing_request_context, :unavailable_origin, :unavailable_trailers, :missing_field; field reasons :non_ascii, :unknown_field_schema, :invalid_field_schema, :invalid_structured_field, :missing_dictionary_key; query reasons :noncanonical_query_name, :invalid_query_encoding, :missing_query_parameter, :ambiguous_query_parameter. Meanings: RequestSeal.SignatureBase.Error.

Examples

iex> {:ok, message} =
...>   RequestSeal.Message.request(
...>     "POST",
...>     "https://Telemetry.Example:443/v1/metrics?batch=7",
...>     [{"Content-Type", " application/json "}],
...>     "{}"
...>   )
iex> {:ok, base} =
...>   RequestSeal.SignatureBase.build(
...>     message,
...>     ~s[("@method" "@authority" "@path" "@query-param";name="batch" "content-type");created=1700000000]
...>   )
iex> String.split(base, "\n")
[
  ~s["@method": POST],
  ~s["@authority": telemetry.example],
  ~s["@path": /v1/metrics],
  ~s["@query-param";name="batch": 7],
  ~s["content-type": application/json],
  ~s["@signature-params": ("@method" "@authority" "@path" "@query-param";name="batch" "content-type");created=1700000000]
]
iex> RequestSeal.SignatureBase.build(message, ~s[("@status")])
{:error, %RequestSeal.SignatureBase.Error{reason: :wrong_message_kind}}
iex> RequestSeal.SignatureBase.build(message, ~s[("x-sensor-id")])
{:error, %RequestSeal.SignatureBase.Error{reason: :missing_field}}