# `MobDev.Plugin.Verify`
[🔗](https://github.com/genericjam/mob_dev/blob/master/lib/mob_dev/plugin/verify.ex#L1)

Host-side signature verification for activated mob plugins.

Given a plugin directory, this module:

1. Loads `priv/mob_plugin.sig` (the signed envelope).
2. Loads `priv/mob_plugin.pub` (the plugin author's public key).
3. Rebuilds the canonical payload from the envelope's embedded
   `file_hashes` list and runs `Crypto.verify/3` — proving the
   envelope on disk came from the author.
4. Re-hashes each file the envelope declares and compares against the
   signed hash — proving the on-disk state has not been tampered with
   since the author signed it.
5. When the signed list carries the build-input coverage marker
   (MOB-297, see `Sign`), evaluates the now-verified manifest and
   requires every `Sign.build_inputs/2` file to be listed — proving no
   file the build reads was added outside the signature. Envelopes
   without the marker (signed before MOB-297) skip this step.

Because the envelope carries `file_hashes` on disk (v2, see
`Sign` moduledoc for the version history), steps 1–4 do not need the
eval'd manifest map — so the safe order is **verify → then eval**, and
step 5's eval runs only on manifest bytes that already matched their
signed hash. This closes the MOB-74 class of bug where a malicious
`priv/mob_plugin.exs` could execute arbitrary code during plugin
activation because the eval ran before the signature check.

Failure modes are distinguished:

- `:missing_signature` — no `priv/mob_plugin.sig`.
- `:missing_pubkey` — no `priv/mob_plugin.pub`.
- `:invalid_signature` — sig present but doesn't verify, or on-disk
  files no longer match the signed hashes (tamper detected), or a
  build input is missing from a coverage-marked signature (or its
  manifest cannot be evaluated to check), or the envelope is malformed.
- `:envelope_v1_unsupported` — a legacy v1 envelope was found. v1
  verification required the eval'd manifest to rebuild the payload,
  which is the very bug we are closing, so every v1 envelope is refused
  without evaluating anything (MOB-301 removed the MOB-287 transition
  that accepted some). Consumers `mix deps.update <plugin>` to a
  v2-signed release; authors re-sign with `mix mob.plugin.sign` on
  mob_dev 0.7.2 or later.

Trust (mapping a verified public key to "the host operator approved
it") lives in `TrustStore` and is layered on top of this module.

# `envelope`

```elixir
@type envelope() :: %{
  signature: MobDev.Plugin.Crypto.signature(),
  file_hashes: MobDev.Plugin.Sign.file_hashes(),
  envelope_version: 2
}
```

A decoded v2 envelope.

# `envelope_error`

```elixir
@type envelope_error() :: :missing | :corrupt | :envelope_v1_unsupported
```

Errors `load_envelope/1` can return.

# `pubkey_error`

```elixir
@type pubkey_error() :: :missing | :malformed
```

Errors `load_pubkey/1` can return.

# `verify_error`

```elixir
@type verify_error() ::
  :missing_signature
  | :missing_pubkey
  | :invalid_signature
  | :envelope_v1_unsupported
```

Errors `verify_plugin/1` can return.

# `load_envelope`

```elixir
@spec load_envelope(Path.t()) :: {:ok, envelope()} | {:error, envelope_error()}
```

Loads and decodes the signature envelope from `priv/mob_plugin.sig`.

Returns the full envelope map (v2 shape) on success, or a distinguished
error. A v1 envelope on disk is reported as `:envelope_v1_unsupported` so
the caller can print an update hint — v1 required the eval'd manifest to
verify, which is the bug MOB-74 closes.

# `load_pubkey`

```elixir
@spec load_pubkey(Path.t()) ::
  {:ok, MobDev.Plugin.Crypto.pub_key()} | {:error, pubkey_error()}
```

Loads the raw 32-byte public key from `priv/mob_plugin.pub`.

Format: a single line of base64 (with `=` padding) of the raw 32-byte
Ed25519 public key, optionally followed by a trailing newline. Plain
text so plugin authors can `cat` it or paste it into a release note.

# `load_signature`

```elixir
@spec load_signature(Path.t()) ::
  {:ok, MobDev.Plugin.Crypto.signature()} | {:error, envelope_error()}
```

Back-compat shim for `mob 0.8.x` and the `SignatureGate.maybe_print_unsafe_banner/1`
path, both of which used the v1 helper that returned just the raw
signature. Now returns the same 64-byte signature but extracted from a v2
envelope. Callers that need the full envelope should use `load_envelope/1`.

# `load_verified`

```elixir
@spec load_verified(
  Path.t(),
  keyword()
) :: {:ok, map() | nil} | {:error, verify_error() | String.t()}
```

Verifies the plugin, then loads and evaluates the manifest.

This is the safe consumer-side path: if `verify_plugin/1` refuses,
`Manifest.load/1` is never called and `Code.eval_file/1` on the
potentially-malicious `priv/mob_plugin.exs` never runs.

## Options

- `:acknowledged_unsafe` (default `false`) — when `true`, an
  unsigned plugin (`{:error, :missing_signature}`) is loaded anyway.
  This is the documented escape hatch for
  `:acknowledge_unsafe_plugins` (see `SignatureGate.check_plugin/4`)
  — the user has explicitly opted into running an unsigned plugin's
  manifest, and the `SignatureGate` banner already warns them. Every
  OTHER failure (`:invalid_signature`, `:missing_pubkey`,
  `:envelope_v1_unsupported`) still refuses the eval — those are
  the tamper / mis-key / legacy-envelope cases, not the "unsigned during
  dev" case.

For plugins with no `priv/mob_plugin.exs` at all (tier-0 plugins),
returns `{:ok, nil}` without requiring a signature — the
`acknowledged_unsafe` flag has no effect here (no manifest to load).

# `verify_plugin`

```elixir
@spec verify_plugin(Path.t()) :: :ok | {:error, verify_error()}
```

Verifies that the plugin in `plugin_dir` has a valid v2 signature and
that the on-disk files match what the author signed.

Steps 1–4 (see the moduledoc) run entirely off the envelope's embedded
`file_hashes` list — the manifest is one of those files, and rehashing
its bytes on disk detects tampering without any `Code.eval_file` call.
That is the MOB-74 fix: verification is safe to run *before* eval,
closing the RCE window where a malicious `priv/mob_plugin.exs` could
execute arbitrary code during plugin activation.

For a signature carrying the build-input coverage marker (MOB-297),
step 5 then evaluates the manifest — only after its bytes matched the
signed hash and the signature verified — to recompute
`Sign.build_inputs/2` and refuse any build input the signature omits.

Returns `:ok` on success or one of the distinguished error reasons
(see `t:verify_error/0`). The caller is responsible for any trust
decision; this function only proves that the bytes on disk match
what the plugin author signed.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
