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
@spec build( RequestSeal.Message.t(), binary() | RequestSeal.StructuredFields.Value.t(), keyword() ) :: {:ok, binary()} | {:error, RequestSeal.SignatureBase.Error.t()}
Build one ASCII signature base from preserved message facts and ordered parameters.
Parameters
message- aRequestSeal.Message; it is validated first.parameters- one signature's parameterized Inner List: serialized bytes such as~s[("@method" "@path");created=1700000000], or aRequestSeal.StructuredFields.Valueof 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 explicitRequestSeal.StructuredFields.Schemavalues, required forsfandkeycomponents. 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: oneidentifier: valueline 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}}