RequestSeal.Policy (RequestSeal v0.2.0)

Copy Markdown View Source

Explicit generic RFC 9421 acceptance policy; no implicit profile or required choices.

RFC 9421 Section 2.2.3 lowercases the authority host and omits default ports (HTTP 80 and HTTPS 443). Coverage of @authority alone can accept the same signature across http://example.com, http://example.com:80, https://example.com, and https://example.com:443 when all other covered values match. Require @scheme (or @target-uri) whenever scheme matters. Plug supports @scheme; its unavailable exact target evidence prevents coverage of @target-uri.

new/1 requires all six keys:

  • :algorithms — nonempty unique list of exact RequestSeal.Crypto HTTP tokens or supported {:jws, name} selections. The resolver's algorithm is authoritative; sender alg never selects it. JWS requires sender alg to be absent.
  • :components — serialized Structured Fields Inner List, at most 256 required component identifiers, with exact parameter identity and no metadata parameters. "()" explicitly permits no required coverage. Duplicate identifiers reject.
  • :key_resolver — arity-one function receiving keyid (binary or nil), label (binary), and tag (binary or nil) in a map. Returns {:ok, %{algorithm: algorithm, key: public_key_or_verification_function}} or :error. HMAC secret custody stays inside an arity-three function (algorithm, base, signature), returning :ok or {:error, term}. Optional :identity carries a trusted RequestSeal.KeyIdentity. Public keys derive their canonical public identity and must agree with a supplied identity. Verification functions without one have unknown key equivalence. Identity values remain internal; no trusted principal association is inferred.
  • :freshness — :not_evaluated or a map containing all of :clock (arity zero, integer Unix seconds), :max_age (positive integer or nil), :skew (0..86,400), :require_expires (Boolean). Max-age requires created; expiration is exclusive, max-age inclusive. All checks must pass for some integer clock within [now - skew, now + skew]. Clock and max-age magnitudes are at most 999,999,999,999,999; RFC integer bounds apply to signature timestamps.
  • :content — :not_required or a map with :kind (:content or :representation), nonempty unique :algorithms ("sha-256", "sha-512"), and :section (:headers or :trailers). Require full coverage of precisely that digest field, recompute it, and require every supported present algorithm to be selected. Unknown algorithms cannot establish integrity.
  • :replay — :not_required or a map with exactly :identifier (:nonce), :namespace (caller tenant/trust context, 1..256 bytes), :commitment (arity-one function returning {:ok, key} with 1..256 bytes or :error), :store (RequestSeal.Replay.Store), and :timeout (1..300,000 ms). Required replay requires bounded freshness: require_expires: true or max-age. After validation the function receives exactly identifier, algorithm, keyid, tag, created, expires, and profile: %{name: :rfc9421} or a package-namespaced extension stamp; never a signature label. Its result is claimed once under the caller namespace. RequestSeal supplies no application commitment recipe. Nonce is an authenticated RFC 9421 signature parameter, limited to 1,024 bytes; absent/empty nonce rejects. Retention ends at the first rejected second: minimum of expires + skew and created + max_age + skew + 1, over present bounds.

Optional keys: :field_schemas defaults to %{} (lowercase field names to validated RequestSeal.StructuredFields.Schema values; at most 1,024), :extra_components defaults to :allow (or :reject for exact coverage), :max_signatures defaults to 16 (1..64, counts dictionary encounters, including repeats, before duplicate-label rejection). Unknown keys or invalid direct structs also reject. new/1 returns {:error, RequestSeal.Error} with :invalid_policy, layer :input, retryable false. No callback is invoked during construction.

Summary

Functions

Construct a fully explicit policy or return a bounded invalid-policy error.

Types

signer()

@type signer() :: (RequestSeal.Crypto.algorithm(), binary() ->
               {:ok, binary()} | {:error, term()})

t()

@type t() :: %RequestSeal.Policy{
  algorithms: [RequestSeal.Crypto.algorithm()],
  components: binary(),
  content: :not_required | map(),
  extra_components: :allow | :reject,
  field_schemas: map(),
  freshness: :not_evaluated | map(),
  key_resolver: (map() ->
                   {:ok,
                    %{
                      optional(:identity) => RequestSeal.KeyIdentity.t(),
                      algorithm: RequestSeal.Crypto.algorithm(),
                      key: RequestSeal.PublicKey.t() | verify_fun()
                    }}
                   | :error),
  max_signatures: 1..64,
  replay: :not_required | map()
}

verify_fun()

@type verify_fun() :: (RequestSeal.Crypto.algorithm(), binary(), binary() ->
                   :ok | {:error, term()})

Functions

new(attrs)

@spec new(map()) :: {:ok, t()} | {:error, RequestSeal.Error.t()}

Construct a fully explicit policy or return a bounded invalid-policy error.