lib/minisign/signature.ex

defmodule Minisign.Signature do
  @moduledoc """
  Datastructure representing a minisign signature.

  fields: 

  - `:algorithm`: always `:ED` (note the capitalization)
  - `:trusted_comment`: (string) the trusted comment
  - `:key_id`: (binary) 8 byte key id
  - `:signature`: (binary) 64 byte signature
  - `:global_signature`: (binary) 64 byte global signature

  see https://jedisct1.github.io/minisign/#signature-format
  """

  defstruct [:algorithm, :trusted_comment, :key_id, :signature, :global_signature]

  alias Minisign.ParseError

  @type t :: %__MODULE__{
          algorithm: :Ed,
          trusted_comment: String.t(),
          key_id: <<_::8>>,
          signature: <<_::256>>,
          global_signature: <<_::256>>
        }

  @spec parse(String.t()) :: {:ok, t} | {:error, ParseError.t()}
  @doc """
  parses a signature string, and returns the resulting struct if it's valid.
  """
  def parse(string) do
    string
    |> String.split("\n")
    |> get_parts
  end

  @spec parse!(String.t()) :: t
  @doc """
  parses a signature string, or raises if the string is not a signature representation

  see `parse/1`
  """
  def parse!(string) do
    case parse(string) do
      {:ok, Signature} -> Signature
      {:error, reason} -> raise reason
    end
  end

  defp get_parts([
         "untrusted comment: " <> _,
         encoded_signature,
         "trusted comment: " <> trusted_comment,
         encoded_global_signature | _
       ]) do
    with {{:ok, <<"ED", key_id::binary-size(8), signature::binary-size(64)>>}, :s} <-
           {Base.decode64(encoded_signature), :s},
         {{:ok, <<global_signature::binary-size(64)>>}, :g} <-
           {Base.decode64(encoded_global_signature), :g} do
      {:ok,
       %__MODULE__{
         algorithm: :ED,
         trusted_comment: trusted_comment,
         key_id: key_id,
         signature: signature,
         global_signature: global_signature
       }}
    else
      {:error, :s} ->
        {:error, %ParseError{comment: "Invalid base64 encoding of signature", parser: __MODULE__}}

      {:error, :g} ->
        {:error,
         %ParseError{comment: "Invalid base64 encoding of global signature", parser: __MODULE__}}

      {{:ok, _}, :s} ->
        {:error, %ParseError{comment: "Invalid signature", ref: true, parser: __MODULE__}}

      {{:ok, _}, :g} ->
        {:error, %ParseError{comment: "Invalid global signature", ref: true, parser: __MODULE__}}
    end
  end

  defp get_parts(_), do: {:error, %ParseError{comment: "Invalid format", parser: __MODULE__}}
end