RequestSeal.Policy (RequestSeal v0.3.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]. Caller clocks are integers from 0 through 253,402,300,799 Unix seconds. Max-age is at most 253,402,300,799; larger values reject with :invalid_policy. 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. A derived retention end above 253,402,300,799 rejects with :retention_exceeded at :replay before commitment or storage, even if the authenticated wire timestamp is valid. 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.