MobDev.Plugin.Sign (mob_dev v0.7.2)

Copy Markdown View Source

Author-side signing workflow for mob plugins.

Produces priv/mob_plugin.sig for a plugin directory by:

  1. Loading the manifest (priv/mob_plugin.exs).
  2. Computing SHA-256 hashes for every file the manifest references (Swift sources, Android bridge/JNI sources, NIF native_dir contents, and the manifest bytes themselves).
  3. Building the canonical payload (sorted file hashes + envelope version).
  4. Signing the canonical encoding of the payload via Crypto.sign/2.
  5. Writing a binary priv/mob_plugin.sig containing the signature and the signed file_hashes list, so verifiers can check integrity without needing to Code.eval_file the manifest first (see MOB-74).

Pure helpers are exposed for tests: compute_file_hashes/2 and build_payload/1 are deterministic given their inputs.

Envelope versions

  • v1 (deprecated, MOB-74) — payload was %{manifest: <map>, file_hashes: [...]} and the envelope on disk carried only the signature. Verifiers needed the eval'd manifest map to rebuild the payload, so the eval had to run before verification could — letting a malicious priv/mob_plugin.exs execute arbitrary code at build time. Refused by MobDev.Plugin.Verify since mob_dev 0.7.2, except for Hex plugins accepted under the MOB-287 transition rule (MobDev.Plugin.V1Transition). Never produced any more.
  • v2 (current) — payload is %{file_hashes: [...], envelope_version: 2}; file_hashes includes priv/mob_plugin.exs; envelope on disk embeds file_hashes alongside the signature. Verifiers can check every on-disk file against the signed hashes without touching the manifest map, so verification is safe to run before eval.

Summary

Types

SHA-256 digest of a single file (raw 32-byte binary).

Sorted list of {relative_path, sha256} tuples.

Relative path inside the plugin directory.

Functions

Builds the canonical payload term that gets signed.

Returns the relative-path-sorted list of {relative_path, sha256} tuples for every file the manifest references.

Current signing envelope version.

Relative path inside a plugin dir where the manifest lives.

Signs plugin_dir and writes priv/mob_plugin.sig.

Relative path inside a plugin dir where the signature lives.

Types

file_hash()

@type file_hash() :: binary()

SHA-256 digest of a single file (raw 32-byte binary).

file_hashes()

@type file_hashes() :: [{rel_path(), file_hash()}]

Sorted list of {relative_path, sha256} tuples.

rel_path()

@type rel_path() :: String.t()

Relative path inside the plugin directory.

Functions

build_payload(file_hashes)

@spec build_payload(file_hashes()) :: map()

Builds the canonical payload term that gets signed.

Shape:

%{
  file_hashes: [{rel_path, sha256}, ...],
  envelope_version: 2
}

Authoritative for what's inside the signature — any new field added here needs both author and host updates.

Note that the payload no longer includes the manifest term itself (see MOB-74). The manifest is one of the files hashed in file_hashes, so its bytes are covered — and dropping the map from the payload lets Verify recompute the payload without eval'ing the manifest first.

compute_file_hashes(plugin_dir, manifest)

@spec compute_file_hashes(Path.t(), map() | nil) :: file_hashes()

Returns the relative-path-sorted list of {relative_path, sha256} tuples for every file the manifest references.

Pure given the plugin dir + manifest. The set covers:

  • manifest.ios.swift_files — single files (list of paths).
  • manifest.android.bridge_kt and manifest.android.jni_source — single paths each.
  • manifest.android.res_files — the resource files copied verbatim into the app res/ tree (list of paths).
  • manifest.nifs[].native_dir — recursive over .c, .h, .cpp, .zig files inside. This is the only case where a directory is expanded.

Other manifest fields are either name-only (component atoms, swift_struct) or pure data (plist keys, permission strings, framework names) and are covered by the manifest term itself being part of the signed payload.

Missing files are skipped silently — Validator.validate_plugin/3 is responsible for refusing to publish a plugin with missing declared paths, so the signing surface assumes paths that exist.

envelope_version()

@spec envelope_version() :: integer()

Current signing envelope version.

manifest_path(plugin_dir)

@spec manifest_path(Path.t()) :: Path.t()

Relative path inside a plugin dir where the manifest lives.

sign_plugin(plugin_dir, priv_key)

@spec sign_plugin(Path.t(), MobDev.Plugin.Crypto.priv_key()) :: :ok | {:error, term()}

Signs plugin_dir and writes priv/mob_plugin.sig.

Orchestrates the full author workflow: loads the manifest (to know which files it references), computes file hashes (including the manifest bytes themselves), builds the v2 payload, signs it, wraps the signature and the file_hashes list in the envelope binary, and writes the file. Returns :ok on success or {:error, reason} if the manifest is missing/invalid.

The envelope carries file_hashes on disk so Verify.verify_plugin/1 can check tampering without needing to Code.eval_file the manifest first — see MOB-74.

signature_path(plugin_dir)

@spec signature_path(Path.t()) :: Path.t()

Relative path inside a plugin dir where the signature lives.