The trust chain's top: a root document shipped with this library names the keys allowed to sign batches; a batch names every manifest by hash. Both travel in an envelope signed over raw bytes. Sequence numbers only go up.
Every entry point here parses attacker-reachable input (a shipped file, a
cached file, or whatever a server hands back), so every helper is written
to return an error tuple instead of raising, and the public functions that
touch the network or disk also carry a function-level rescue as a second
line of defense.
A batch read from a bundle (bundle: true) is bounded by its own expiry
instead of the 24 h staleness rule. A bundle's offline.json
(verify_offline/3) has no expiry at all, and on that path the root's
expiry is not checked either (offline: true).
Summary
Functions
The current batch, asking the source only whether it changed: the last
verified batch and its ETag are kept in the state dir and the ETag is sent
as If-None-Match. A 304 says only that the source still serves those
bytes: they go through the same signature, freshness, clock and sequence
checks as fresh ones, every run, so a batch older than a day is refused
however often the source answers 304. New bytes replace the cache only
once they verify. Offline, the cached batch is checked the same way and
no request is made.
The root in use: the shipped root, advanced along the adopted chain in
roots.json. The shipped root must verify under its own root role; each
chain entry must also verify under the previous root's root role and have
a higher version, and the first one that fails ends the chain. As in TUF,
only the root in use (the last one) must be unexpired: an expired
intermediate root does not end the chain, and a release keeps working
past its shipped root's expiry once a newer root has been adopted.
Whether the served root should be fetched this run: never stamped, the
stamp :root_check_seconds (default an hour) old or older, or a stamp in
the future (a clock set back). A batch that fails to verify fetches the
root regardless (see Precompiled.Check).
The keys and threshold for a role, or {%{}, nil} when root doesn't name
that role with a usable shape. Total: never raises, whatever root is.
Where the batch sequence and adopted roots live for this source: under
config.home, named by the source URL and the shipped root's first root
key, so two sources (or two trust anchors) never share replay state.
Always keyed by the SHIPPED root, so a rotation keeps the same dir.
config[:state_key], when set, names it instead of the source URL: the
sources that share one key (a device's network source and its bundles)
share one sequence floor.
Adopt the source's served root when it chains from root (the root in
use), appending it to roots.json and recording its version as the
floor load_root/1 will not go back below. Anything else keeps root.
A bundle's offline document (offline.json): manifest maps signed by the
root's offline role, with no expiry, for a device that stays offline
(PRECOMPILED_API.md, "Offline bundles"). The caller reads it only from
a file:// source the user named.
Types
Functions
@spec fetch_batch_cached(map(), root(), conditional_fetch()) :: {:ok, batch()} | {:error, String.t()}
The current batch, asking the source only whether it changed: the last
verified batch and its ETag are kept in the state dir and the ETag is sent
as If-None-Match. A 304 says only that the source still serves those
bytes: they go through the same signature, freshness, clock and sequence
checks as fresh ones, every run, so a batch older than a day is refused
however often the source answers 304. New bytes replace the cache only
once they verify. Offline, the cached batch is checked the same way and
no request is made.
When the source cannot be reached ({:error, {:unreachable, why}} from
fetch: a transport error, timeout, 5xx or the run deadline), the cached
batch is used under the same rule as offline and one line says so. A
response the source did give and that is refused (bad signature, an
older sequence, malformed bytes, a 4xx) never falls back to the cache.
The root in use: the shipped root, advanced along the adopted chain in
roots.json. The shipped root must verify under its own root role; each
chain entry must also verify under the previous root's root role and have
a higher version, and the first one that fails ends the chain. As in TUF,
only the root in use (the last one) must be unexpired: an expired
intermediate root does not end the chain, and a release keeps working
past its shipped root's expiry once a newer root has been adopted.
With offline: true, for verifying a bundle's offline.json and nothing
else, the root in use is returned even when it has expired: an offline
document is valid indefinitely, so the clock must not end it. The chain
and every signature are checked as always.
The version this ends on can never drop below one update_root/3 already
recorded: a stored root_version higher than the folded root's version
means roots.json lost the entries that led there (deleted, truncated, or
rolled back), and that is reported rather than silently accepted.
Whether the served root should be fetched this run: never stamped, the
stamp :root_check_seconds (default an hour) old or older, or a stamp in
the future (a clock set back). A batch that fails to verify fetches the
root regardless (see Precompiled.Check).
@spec root_keys(root(), String.t()) :: {%{required(String.t()) => binary()}, pos_integer() | nil}
The keys and threshold for a role, or {%{}, nil} when root doesn't name
that role with a usable shape. Total: never raises, whatever root is.
Where the batch sequence and adopted roots live for this source: under
config.home, named by the source URL and the shipped root's first root
key, so two sources (or two trust anchors) never share replay state.
Always keyed by the SHIPPED root, so a rotation keeps the same dir.
config[:state_key], when set, names it instead of the source URL: the
sources that share one key (a device's network source and its bundles)
share one sequence floor.
@spec update_root( map(), root(), (String.t() -> {:ok, binary()} | {:error, term()}), keyword() ) :: {:ok, root()}
Adopt the source's served root when it chains from root (the root in
use), appending it to roots.json and recording its version as the
floor load_root/1 will not go back below. Anything else keeps root.
With offline: true, for a bundle's root.json read to verify its
offline.json, a root that chains is adopted even when it has expired
(see load_root/2). The online path then chains from it to a newer,
unexpired root before it judges expiry (Precompiled.Check).
A bundle's offline document (offline.json): manifest maps signed by the
root's offline role, with no expiry, for a device that stays offline
(PRECOMPILED_API.md, "Offline bundles"). The caller reads it only from
a file:// source the user named.
Its keys come from the offline role of root alone, with a threshold
above zero and each keyid counted once. A key that any other role of the
root also names (the online root and batch key) never counts for it, so
a batch signature can never pass as an offline one. No clock is read: not
for the document, which has no expiry, and not for the root. The document
is held to its own sequence floor, offline_state in the state dir, apart
from the batch sequence, and the floor is raised to its sequence.
Returns the batch shape, with content: nil and offline: true.
Refused outright when config.source is not a file:// URL: a bundle's
offline.json is honoured only from a source the user named as a bundle,
never from a network server that could otherwise use it to dodge the 24 h
batch staleness check.