Packages
mob_dev
0.6.2
0.6.23
0.6.22
0.6.21
0.6.20
0.6.19
0.6.18
0.6.17
0.6.16
0.6.15
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.17
0.5.16
0.5.15
0.5.14
0.5.13
0.5.12
0.5.11
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.0
0.3.37
0.3.35
0.3.34
0.3.33
0.3.28
0.3.26
0.3.23
0.3.21
0.3.19
0.3.18
0.3.17
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.18
0.2.17
0.2.15
0.2.14
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
Development tooling for the Mob mobile framework
Current section
Files
Jump to
Current section
Files
lib/mob_dev/plugin/crypto.ex
defmodule MobDev.Plugin.Crypto do
@moduledoc """
Ed25519 sign/verify primitives + canonical-term encoding for plugin signing.
The signing scheme (see `MOB_PLUGIN_SECURITY.md`, Phase 2):
- Plugin authors generate a per-plugin Ed25519 keypair; the public key
ships in `priv/mob_plugin.pub` and the manifest+sources are signed
with the private key into `priv/mob_plugin.sig`.
- Hosts verify the signature against the public key at activation time
and refuse to build untrusted/unsigned plugins.
This module is the single place the Ed25519 + canonical-encoding
decisions are encoded. Keep it crypto-only — workflow (sign/verify
orchestration, fingerprint storage) lives in `Sign`, `Verify`, and
`TrustStore`.
"""
@typedoc "Raw 32-byte Ed25519 private key."
@type priv_key :: <<_::256>>
@typedoc "Raw 32-byte Ed25519 public key."
@type pub_key :: <<_::256>>
@typedoc "Raw 64-byte Ed25519 signature."
@type signature :: <<_::512>>
@typedoc "Host-visible trust identifier; base64-encoded SHA-256 of the public key."
@type fingerprint :: String.t()
@doc """
Generates a fresh Ed25519 keypair.
Returns `{priv_bin, pub_bin}` as raw 32-byte binaries. The format
matches what `:crypto.sign/4` and `:crypto.verify/5` accept directly
with the `:eddsa`/`:ed25519` options.
"""
@spec generate_keypair() :: {priv_key(), pub_key()}
def generate_keypair do
{pub, priv} = :crypto.generate_key(:eddsa, :ed25519)
{priv, pub}
end
@doc """
Signs `payload_term` with `priv_bin`.
The term is canonically encoded via `canonical_encode/1` before signing,
so the signature is over the deterministic binary form — the same map
with the same contents produces the same signature regardless of map
insertion order.
"""
@spec sign(term(), priv_key()) :: signature()
def sign(payload_term, priv_bin) when is_binary(priv_bin) do
payload = canonical_encode(payload_term)
:crypto.sign(:eddsa, :sha512, payload, [priv_bin, :ed25519])
end
@doc """
Verifies `signature_bin` against `payload_term` and `pub_bin`.
Returns `:ok` on valid signature, `{:error, :invalid_signature}`
otherwise. Mirrors `sign/2` — the same canonical encoding is applied
before verifying.
A wrong-*size* signature or public key (not a 64-byte sig / 32-byte
Ed25519 key) is treated as an invalid signature rather than crashing:
`:crypto.verify/5` raises `:badarg` from OpenSSL when handed an
ill-sized key, and these bytes originate from attacker-controlled
plugin files (`priv/mob_plugin.sig` / `priv/mob_plugin.pub`). The
documented contract (`:ok | {:error, :invalid_signature}`) must hold
for every binary input, so the raise is caught here.
"""
@spec verify(term(), signature(), pub_key()) :: :ok | {:error, :invalid_signature}
def verify(payload_term, signature_bin, pub_bin)
when is_binary(signature_bin) and is_binary(pub_bin) do
payload = canonical_encode(payload_term)
if :crypto.verify(:eddsa, :sha512, payload, signature_bin, [pub_bin, :ed25519]) do
:ok
else
{:error, :invalid_signature}
end
rescue
ArgumentError -> {:error, :invalid_signature}
ErlangError -> {:error, :invalid_signature}
end
@doc """
Computes the host-visible trust identifier for a public key.
Format: `"ed25519:<base64>"` where `<base64>` is the standard base64
encoding (with `=` padding) of the SHA-256 digest of the raw 32-byte
public key. Suitable for storing in `mob.exs` under
`:trusted_plugins` and for comparing two keys for equality without
printing the key itself.
"""
@spec fingerprint(pub_key()) :: fingerprint()
def fingerprint(pub_bin) when is_binary(pub_bin) do
digest = :crypto.hash(:sha256, pub_bin)
"ed25519:" <> Base.encode64(digest)
end
@doc """
Canonical encoding of an arbitrary Erlang term to a binary.
Uses `:erlang.term_to_binary/2` with `:deterministic` so the same logical
value produces the same bytes regardless of map iteration order.
Centralised here so the determinism flag is in one place: `sign/2`,
`verify/3`, and any future hash-of-payload helper all agree.
"""
@spec canonical_encode(term()) :: binary()
def canonical_encode(term) do
:erlang.term_to_binary(term, [:deterministic, minor_version: 2])
end
end