What you will build: An HTTPS JWKS fetcher, an explicit verification policy using its keys, and a caller-owned cache with key removal. Install RequestSeal and choose a trusted HTTPS publisher. Set JWKS_URL in your environment to your trusted publisher's HTTPS endpoint. System CA roots and public addresses are the defaults; for an approved private publisher, set JWKS_CA_FILE to a PEM CA file and JWKS_PERMITTED_ADDRESSES to comma-separated IP addresses. Select the source independently of message-supplied key IDs. Sources: RFC 7517, RFC 7638, Web Bot Auth protocol-00, and CIMD-02.
1. Fetch only your configured source
jwks_url = System.fetch_env!("JWKS_URL")
trust_roots =
case System.get_env("JWKS_CA_FILE") do
nil ->
:os
path ->
for {:Certificate, der, :not_encrypted} <- :public_key.pem_decode(File.read!(path)), do: der
end
permitted_addresses =
for address <- String.split(System.get_env("JWKS_PERMITTED_ADDRESSES", ""), ",", trim: true) do
{:ok, ip} = :inet.parse_address(String.to_charlist(String.trim(address)))
ip
end
{:ok, _apps} = Application.ensure_all_started(:ssl)
{:ok, source} =
RequestSeal.Discovery.Source.new(%{
type: :jwks_uri,
location: jwks_url,
cacerts: trust_roots,
permitted_addresses: permitted_addresses,
timeout: 5_000
})
{:ok, key_set} = RequestSeal.Discovery.fetch(source)Use the fetched keys in a resolver and select the rest of your acceptance policy explicitly:
{thumbprint, _entry} = Enum.at(key_set.keys, 0)
resolver = RequestSeal.Discovery.resolver(key_set, algorithms: ["ed25519"])
{:ok, %{key: discovered_key}} = resolver.(%{keyid: thumbprint})
{:ok, discovered_policy} =
RequestSeal.Policy.new(%{
algorithms: ["ed25519"],
components: ~s[("@method" "@scheme" "@authority" "@path" "content-digest")],
key_resolver: resolver,
freshness: %{
clock: fn -> System.system_time(:second) end,
max_age: 60,
skew: 5,
require_expires: true
},
content: %{kind: :content, algorithms: ["sha-256"], section: :headers},
replay: :not_required
})This example uses an Ed25519 JWKS publisher. Selecting a fetched key demonstrates resolution; application policy chooses which publisher and keys to trust. In your app, configure the source and accepted key IDs independently. No message-supplied ID selects a URL. fetch/2 performs one bounded operation; you start SSL and choose addresses, roots, redirects, and byte/key/time limits.
2. Cache explicitly and remove a key
{:ok, cache} = RequestSeal.Discovery.Cache.start_link(max_sources: 8)
{:ok, _snapshot} = RequestSeal.Discovery.Cache.refresh(cache, source)
{:ok, resolution} =
RequestSeal.Discovery.Cache.resolve(cache, source, thumbprint, algorithms: ["ed25519"])
:ok = RequestSeal.Discovery.Cache.remove(cache, source, thumbprint)
:ok = RequestSeal.Discovery.Cache.invalidate(cache, source)
RequestSeal.Discovery.Cache.resolve(cache, source, thumbprint, algorithms: ["ed25519"])
# => {:error, %RequestSeal.Discovery.Error{reason: :revoked_key, ...}}Use Discovery.resolver({cache, source}, algorithms: [...]) for a generic policy backed by the cache. Cache misses/expiry may fetch under your source configuration; refresh, removal, and invalidation are explicit. Removal is a persistent thumbprint denial, including after refresh or invalidation. The cache has no background timer and never serves expired keys after a failed refresh.
Source and key-ID options
Choose :directory, :jwks_uri, or :cimd; each requires explicit HTTPS. A directory origin expands to its well-known path. Signed directory possession proofs are required by default and establish possession; your profile's trust policy owns principal attribution. CIMD requires the exact document client_id spelling and may resolve a same-origin JWKS subresource.
Default key IDs are SHA-256 RFC 7638/8037 thumbprints. JWKS and CIMD may explicitly choose key_id: :directory for unique directory-assigned IDs; thumbprints still govern identity, revocation, and removal. Directory sources always require thumbprint IDs. Private, malformed, revoked, expired, not-yet-valid, or verification-forbidden keys cannot establish a resolver success. From 0.5.0, a JWKS key's alg may use its standard RFC 7517 JOSE name ("EdDSA") or the HTTP registry identifier ("ed25519"); either spelling normalizes once at import, and either allowlist spelling selects the key. From 0.5.1, a key whose alg names an algorithm RequestSeal does not verify with — an encryption key, or a name outside both registries — is skipped and the rest of the set still resolves; a malformed non-string alg still rejects the whole set.
HTTP freshness honors Cache-Control max-age before Expires, adjusted by Date/Age and source bounds. Without either field, fallback freshness defaults to 300 seconds. No-cache/no-store is not retained, including their qualified spellings such as no-cache="content-type", which RFC 9111 publishers use to demand revalidation. Interim 1xx responses (for example 103 Early Hints) are skipped, at most four, before the final status. From 0.5.0, a source that serves an ETag makes cache revalidation conditional: refetches send If-None-Match (RFC 9111 Section 13.1.1), and an unchanged source's 304 keeps the stored key set and extends freshness under the 304's own directives (the stored etag is visible on RequestSeal.Discovery.KeySet). Negative caching is at most 300 seconds; CIMD failures are not cached. Configure trusted clocks and do not silently reuse a stale snapshot. Discovery and custody workers terminate on deadlines or caller death.
Errors
RequestSeal.Discovery.Error separates :address_denied and :redirect_denied (source policy) from TLS/transport errors, :invalid_key_set (document/key shape), :unknown_key (no eligible selected ID), :revoked_key, :source_unavailable, and :cache_overloaded. :unexpected_status is retryable for 5xx and nonretryable for 4xx; your application owns retry scheduling. No exception text or URL is retained in the bounded error. :transport_unavailable means the :ssl application is not started. RequestSeal.Discovery.Source.new/1 returns :invalid_source for a malformed source and :invalid_options for out-of-range options such as a negative_ttl above 300.
Module docs: RequestSeal.Discovery, RequestSeal.Discovery.Source, RequestSeal.Discovery.KeySet, RequestSeal.Discovery.Resolution, RequestSeal.Discovery.Cache, RequestSeal.Discovery.Error.