Explicit generic RFC 9421 acceptance policy; no implicit profile or required choices.
new/1 takes six required choices (algorithms, required components, key
resolver, freshness, content, replay) plus optional bounds, validates them
without invoking any callback, and returns an opaque policy for
RequestSeal.verify/3, RequestSeal.Quorum slots, and RequestSeal.Profile.
Verification revalidates directly built structs.
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.
The resolver's algorithm is authoritative; sender alg never selects it.
Key identity values remain internal; no trusted principal association is
inferred.
Summary
Functions
Construct a fully explicit policy or return a bounded invalid-policy error.
Types
@type signer() :: (RequestSeal.Crypto.algorithm(), binary() -> {:ok, binary()} | {:error, term()})
@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() }
@type verify_fun() :: (RequestSeal.Crypto.algorithm(), binary(), binary() -> :ok | {:error, term()})
Functions
@spec new(map()) :: {:ok, t()} | {:error, RequestSeal.Error.t()}
Construct a fully explicit policy or return a bounded invalid-policy error.
Parameters
attrs- map with all six required keys and any of the three optional keys. Unknown keys reject.:algorithms- nonempty unique list of at most 64 exactRequestSeal.CryptoHTTP tokens or supported{:jws, name}selections. JWS requires senderalgto be absent.:components- serialized Structured Fields Inner List of 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. At verification it receives a map ofkeyid(binary or nil),label(binary), andtag(binary or nil), and returns{:ok, %{algorithm: algorithm, key: key}}or:error.keyis aRequestSeal.PublicKeyor an arity-three verification function(algorithm, base, signature)returning:okor{:error, term}. HMAC ("hmac-sha256",{:jws, "HS256"}) secret custody must stay inside such a function. Optional:identitycarries a trustedRequestSeal.KeyIdentity. Public keys derive their canonical public identity and must agree with a supplied identity. Verification functions without one have unknown key equivalence.:freshness-:not_evaluatedor a map containing exactly:clock(arity zero, integer Unix seconds),:max_age(1 to 253,402,300,799, or nil),:skew(0 to 86,400), and: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]. At verification, clocks must return integers from 0 through 253,402,300,799. RFC integer bounds apply to signature timestamps.:content-:not_requiredor a map of exactly:kind(:contentor:representation), nonempty unique:algorithms("sha-256","sha-512"), and:section(:headersor:trailers). Verification requires full coverage of precisely that digest field, recomputes it, and requires every supported present algorithm to be selected. Unknown algorithms cannot establish integrity.:replay-:not_requiredor 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: trueor max-age. After validation the function receives exactlyidentifier,algorithm,keyid,tag,created,expires, andprofile: %{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_exceededat:replaybefore 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 ofexpires + skewandcreated + max_age + skew + 1, over present bounds.:field_schemas- optional, default%{}. At most 1,024 lowercase field names mapped to validatedRequestSeal.StructuredFields.Schemavalues.:extra_components- optional, default:allow;:rejectrequires exact coverage.:max_signatures- optional, default 16 (1 to 64). Counts dictionary encounters, including repeats, before duplicate-label rejection.
Returns
{:ok, %RequestSeal.Policy{}}- inspection shows onlyalgorithms,extra_components, andmax_signatures.{:error, %RequestSeal.Error{}}- always reason:invalid_policy, layer:input,retryable: false, for a missing, unknown, or invalid key, or a non-map argument. Meanings:RequestSeal.Error.
Examples
iex> {public, _secret} = :crypto.generate_key(:eddsa, :ed25519)
iex> {:ok, key} = RequestSeal.PublicKey.import({:ed25519, public}, :raw)
iex> attrs = %{
...> algorithms: ["ed25519"],
...> components: ~s[("@method" "@scheme" "@authority" "@path" "content-digest")],
...> key_resolver: fn
...> %{keyid: "telemetry-agent"} -> {:ok, %{algorithm: "ed25519", key: key}}
...> _ -> :error
...> end,
...> freshness: %{clock: fn -> System.system_time(:second) end, max_age: 300, skew: 5, require_expires: false},
...> content: %{kind: :content, algorithms: ["sha-256"], section: :headers},
...> replay: :not_required
...> }
iex> {:ok, policy} = RequestSeal.Policy.new(attrs)
iex> {policy.extra_components, policy.max_signatures}
{:allow, 16}
iex> {:error, error} = RequestSeal.Policy.new(Map.delete(attrs, :replay))
iex> {error.reason, error.layer}
{:invalid_policy, :input}
iex> {:error, error} = RequestSeal.Policy.new(%{attrs | freshness: %{attrs.freshness | skew: 86_401}})
iex> error.reason
:invalid_policy