NervesGithubUpdater.Manifest (nerves_github_updater v0.1.0)

Copy Markdown View Source

Parses and validates release-manifest.json.

Source of truth: docs/manifest-format.md. This module owns schema validation and the two policy checks (counter, expiry) the device runs after signature verification; it does not verify the signature itself (see NervesGithubUpdater.Signature) or touch the filesystem/network.

Library-shape: pure functions, no host dependencies, no processes.

Counter semantics

counter is a monotonic anchor persisted by the caller in Nerves.Runtime.KV (fw_manifest_counter) after a successful flash. check_counter/2 allows the manifest counter to be equal to the stored one (reinstalling the current release), rejects a lower one (rollback), and accepts when nothing is stored yet (first-contact trust — same model as the public key baked into firmware). A stored value that fails to parse as an integer is treated as absent rather than raising: a corrupted KV anchor must not permanently brick the update path.

Expiry is policy-gated

expires_at only matters when the caller opts in via enforce_expiry: true (device-side default off). A dormant project — no CI running, no one rotating expires_at forward — must not have its devices' update path silently brick itself just because a manifest aged out.

Summary

Functions

Rejects a manifest whose counter is lower than the persisted anchor.

Rejects an expired manifest, but only when enforce? is true.

Parses and validates binary as a version-1 release manifest.

Looks up a target entry by Nerves platform name.

Types

t()

@type t() :: %NervesGithubUpdater.Manifest{
  counter: non_neg_integer(),
  deltas: map(),
  expires_at: DateTime.t() | nil,
  signed_at: DateTime.t(),
  targets: %{required(String.t()) => target()},
  version: 1
}

target()

@type target() :: %{asset: String.t(), sha256: String.t(), size: non_neg_integer()}

Functions

check_counter(manifest, stored)

@spec check_counter(t(), String.t() | nil) ::
  :ok | {:error, {:manifest_rollback, non_neg_integer(), integer()}}

Rejects a manifest whose counter is lower than the persisted anchor.

stored is the raw string read from Nerves KV — nil, "", or a string that fails to parse as an integer are all treated as "no anchor yet" and accepted (see moduledoc).

check_expiry(manifest, enforce?, now \\ DateTime.utc_now())

@spec check_expiry(t(), boolean(), DateTime.t()) ::
  :ok | {:error, {:manifest_expired, DateTime.t()}}

Rejects an expired manifest, but only when enforce? is true.

parse(binary)

@spec parse(binary()) :: {:ok, t()} | {:error, {:invalid_manifest, term()}}

Parses and validates binary as a version-1 release manifest.

Returns {:ok, t()} or {:error, {:invalid_manifest, reason}} on any schema violation (bad JSON, unsupported version, missing/invalid fields, empty or malformed targets).

target(manifest, name)

@spec target(t(), String.t()) ::
  {:ok, target()} | {:error, {:target_not_found, String.t()}}

Looks up a target entry by Nerves platform name.