Host-side signature verification for activated mob plugins.
Given a plugin directory, this module:
- Loads
priv/mob_plugin.sig(the signed envelope). - Loads
priv/mob_plugin.pub(the plugin author's public key). - Rebuilds the canonical payload from the envelope's embedded
file_hasheslist and runsCrypto.verify/3— proving the envelope on disk came from the author. - 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.
Because the envelope carries file_hashes on disk (v2, see
Sign moduledoc for the version history), verification does not
require the eval'd manifest map — so the safe order is
verify → then eval. 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— nopriv/mob_plugin.sig.:missing_pubkey— nopriv/mob_plugin.pub.:invalid_signature— sig present but doesn't verify, or on-disk files no longer match the signed hashes (tamper detected), 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.verify_plugin/1always reports it;load_verified/2hands it toMobDev.Plugin.V1Transition, which accepts it only under the MOB-287 transition rule and otherwise keeps this refusal. Authors re-sign withmix mob.plugin.signon 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.
Summary
Types
A decoded v2 envelope.
Errors load_envelope/1 can return.
Errors load_pubkey/1 can return.
Errors verify_plugin/1 can return.
Functions
Loads and decodes the signature envelope from priv/mob_plugin.sig.
Loads the raw 32-byte public key from priv/mob_plugin.pub.
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.
Verifies the plugin, then loads and evaluates the manifest.
Verifies that the plugin in plugin_dir has a valid v2 signature and
that the on-disk files match what the author signed.
Types
@type envelope() :: %{ signature: MobDev.Plugin.Crypto.signature(), file_hashes: MobDev.Plugin.Sign.file_hashes(), envelope_version: 2 }
A decoded v2 envelope.
@type envelope_error() :: :missing | :corrupt | :envelope_v1_unsupported
Errors load_envelope/1 can return.
@type pubkey_error() :: :missing | :malformed
Errors load_pubkey/1 can return.
@type verify_error() ::
:missing_signature
| :missing_pubkey
| :invalid_signature
| :envelope_v1_unsupported
Errors verify_plugin/1 can return.
Functions
@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 a re-sign hint — v1 required the eval'd manifest to
verify, which is the bug MOB-74 closes.
@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.
@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.
@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(defaultfalse) — whentrue, an unsigned plugin ({:error, :missing_signature}) is loaded anyway. This is the documented escape hatch for:acknowledge_unsafe_plugins(seeSignatureGate.check_plugin/4) — the user has explicitly opted into running an unsigned plugin's manifest, and theSignatureGatebanner already warns them. Every OTHER failure (:invalid_signature,:missing_pubkey,:envelope_v1_unsupported) still refuses the eval — those are the tamper / mis-key cases, not the "unsigned during dev" case.:scms,:lock,:deps_path,:trust_map— provenance inputs for the transitional v1 rule (seeMobDev.Plugin.V1Transition); default to the current Mix project's values.
A v1 envelope is handed to MobDev.Plugin.V1Transition.load/2, which
evaluates the manifest only for a hexpm Hex dependency whose public key
is trusted, then verifies the v1 signature (MOB-287). Every other v1
envelope is refused unevaluated with :envelope_v1_unsupported.
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).
@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.
The check runs 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 now safe to run before eval, closing the
RCE window where a malicious priv/mob_plugin.exs could execute
arbitrary code during plugin activation.
Returns :ok on success or one of the distinguished error reasons
(see 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.