# `RequestSeal.SignatureBase`
[🔗](https://github.com/baselabs/request_seal/blob/v0.5.2/lib/request_seal/signature_base.ex#L1)

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.

# `build`

```elixir
@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` - 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}}

---

*Consult [api-reference.md](api-reference.md) for complete listing*
