The library ships the portal's root of trust (priv/precompiled/root.json, Ed25519, expires 2027-10-22). Every batch index is signed by a key the root names; every manifest is fetched by the hash the batch names; every file by the hash the manifest names. Nothing unsigned or unhashed reaches _build. Plain http sources are refused except on loopback.

The batch may name content origins (a CDN) that files and manifests are fetched from before the source; the project can override them with :content. Everything fetched from them is checked against the hashes in the signed batch and manifests. A bad mirror can therefore only cause a fallback to the source or a miss, never a wrong install. Redirects are followed only within the request's origin or to a listed content origin.

The verified batch is kept in the state dir and revalidated with its ETag. A cached batch passes the same signature, freshness and sequence checks as a fresh one. The served root is checked at most once an hour (and whenever a batch fails to verify), so a root rotation reaches a warm client within that hour. When the source cannot be reached (a transport error, a 5xx, the run deadline), a cached batch that still passes every check is used and one line says so; a response the source did give and that is refused (bad signature, an older sequence, an expired batch) never falls back to the cache.

A batch is also checked for freshness, on top of its signature, expiry and sequence: it is refused when its issued timestamp is more than 24 hours ago ("batch stale"), more than 5 minutes in the future ("clock"), when it stays valid for more than 30 days past issued ("batch lifetime"), or when issued does not parse ("batch malformed"). A local clock reading earlier than the moment this library was built is refused outright ("clock"), since none of the above can be trusted on it. A batch the source did not deliver (a network error, a 4xx or 5xx, a refused redirect, no usable cached batch to stand in) is reported as batch unavailable: <why>; a batch that was delivered and fails a check is reported by the check alone: batch signature invalid, batch stale: ..., batch expired at ..., batch sequence N is older than M, batch lifetime over 30 days ..., batch malformed, clock: ... or clock is before .... Every refusal falls back to compiling. clock_offset_seconds moves the client's notion of now for these checks; it is test-only, read only from project config, and never from an environment variable.

Every run, Precompiled.Host.install/2 included, also rechecks builds an earlier run installed (into a Mix project's _build or into the Mix.install/2 project) against the current batch: one the batch no longer names is removed and compiled from source; one whose build still passes every rule of What matches with exactly the files on disk is kept as it is, even when a newer portal run made the same build again; any other is replaced or removed. One newly listed in :except is removed and compiled from source whether or not a batch is fetched. When no batch is available (offline, or refused), installed builds are left as they are and the run prints one warning, precompiled: no fresh batch (<reason>); N installed builds kept, not rechecked, rather than a FAIL line per dependency; dependencies Mix is about to compile still get their own FAIL line with the reason. report.json lists every kept build as fail with a detail ending (installed build kept).

An install is checked before it is placed: every path must be a plain relative path inside the dependency's own build dir, with a safe mode (unsafe path or mode (...)); no two paths may collide, including case and Unicode-normalisation variants (path collision: ...); ebin/ may hold only .beam files, <dep>.app and <dep>.appup (anything else, such as another app's .app, is foreign path: ...); and the .app must list exactly the beams shipped. Cache entries are never followed through symlinks.

Root rotation follows TUF: each adopted root must be signed by the previous root's root keys (to its threshold) and carry a higher version, and only the root in use must be unexpired, so a root that expires after a newer one was adopted does not end the chain.

Once a root is adopted, its version is kept as a floor that a later run will not go back below; roots.json losing the entries that reached it (deleted, truncated, rolled back) is reported as an error rather than silently accepted (root version N is older than M); deleting the root_version file the error names, under the trust state dir in PRECOMPILED_HOME, accepts the older root. A state file that exists but will not parse (the batch sequence or the root version) is logged as a warning and treated as 0.

Today the root and batch roles are one Ed25519 key, threshold 1, held online by the portal. Whoever controls the portal host can therefore rotate the root and ship code to every opted-in host and device, until a library release ships a new root. The proposals for splitting the roles are in the compatibility repository's docs/PRECOMPILED_SIGNING_PROPOSALS.md.

Trust on the device

The device checks the same chain as the host: the root this library ships (or the firmware's root), the signed batch, each manifest by the hash the batch names, and each file by the hash its manifest names, size included. A file is staged, verified and only then moved into the store; the loader hashes the .app, every beam and every priv file once more just before anything loads, and loads the beams from those verified bytes. Nothing unsigned is ever loaded, nothing is compiled, and there is no fallback that skips a check. An online batch must pass the freshness checks above, which is why the clock must be set.

CA certificates

An https source is verified with, in this order: cacerts_path, the PRECOMPILED_CACERTS_PATH or HEX_CACERTS_PATH file, the system's own store (:public_key.cacerts_get/0), then the castore package's bundle when it is in the firmware. With none of them, the call fails (no CA certificates). Many Nerves systems ship a store of their own (nerves_system_qemu_aarch64 does); add castore for one whose store is empty, or to be independent of it. mix igniter.install precompiled --device adds castore unless the project already depends on it.