defmodule Minisign do
@moduledoc """
Elixir implementation of the Minisign signature verification format.
see https://jedisct1.github.io/minisign/
"""
alias Minisign.PublicKey
alias Minisign.Signature
@type public_key_input :: String.t() | {:file, Path.t()} | PublicKey.t()
@type signature_input :: String.t() | {:file, Path.t()} | Signature.t()
@type message_input :: binary | {:file, Path.t()}
@type reason :: :key_id_match | :signature | :global_signature
@spec verify(message_input, signature_input, public_key_input) ::
:ok | {:invalid, reason} | {:error, any}
@doc """
verifies a signature, returning `:ok` if the signature is valid.
if the signature is invalid, returns an `:invalid` tuple indicating the
reason why the signature failed to validate.
if any of the inputs have failed, returns an `:error` tuple.
For the key or signature inputs you may provide Base64 encoded strings, or a
`{:file, path}` tuple to read from a file or a preparsed datastructure.
For the message input, you may either provide the raw binary or, a `{:file, path}`
tuple to read the message from the file.
"""
def verify(message, signature, public_key) do
with {:ok, public_key} <- marshal(public_key, PublicKey),
{:ok, signature} <- marshal(signature, Signature),
{:ok, message} <- marshal(message, nil) do
do_verify(public_key, signature, message)
end
end
@spec verify!(message_input, signature_input, public_key_input) :: boolean
@doc """
verifies a signature, and raises if any of the inputs are invalid.
"""
def verify!(message, signature, public_key) do
case verify(message, signature, public_key) do
:ok -> true
:invalid -> false
{:error, error} -> raise error
end
end
defp marshal({:file, file}, module), do: marshal(File.read(file), module)
defp marshal(string, nil) when is_binary(string), do: {:ok, string}
defp marshal(string, module) when is_binary(string), do: module.parse(string)
defp marshal(%module{} = datatype, module), do: {:ok, datatype}
# everything else raises argument error.
defp do_verify(pk, sg, msg) do
digest = :crypto.hash(:blake2b, msg)
cond do
sg.key_id != pk.key_id ->
{:invalid, :key_id_match}
!:crypto.verify(:eddsa, :none, {:digest, digest}, sg.signature, [pk.key, :ed25519]) ->
{:invalid, :signature}
!:crypto.verify(
:eddsa,
:none,
{:digest, sg.signature <> sg.trusted_comment},
sg.global_signature,
[pk.key, :ed25519]
) ->
{:invalid, :global_signature}
true ->
:ok
end
rescue
a in ArgumentError -> {:error, a}
end
end