lib/baobab/entry/validator.ex

defmodule Baobab.Entry.Validator do
  alias Baobab.Persistence

  @moduledoc """
  Validation of `Baobab.Entry` structs
  """
  @doc """
  Validate a `Baobab.Entry` struct relative to the provided clump_id

  Includes validation of its available certificate pool
  """
  @spec validate(String.t(), %Baobab.Entry{}) :: %Baobab.Entry{} | {:error, String.t()}
  def validate(clump_id, %Baobab.Entry{seqnum: seq, author: author, log_id: log_id} = entry) do
    case validate_entry(clump_id, entry) do
      :ok ->
        case verify_chain(
               Baobab.certificate_pool(author, seq, log_id, clump_id),
               {clump_id, author, log_id},
               :ok
             ) do
          :ok -> entry
          error -> error
        end

      error ->
        error
    end
  end

  def validate(_, _), do: {:error, "Input is not a Baobab.Entry"}

  defp verify_chain([], _log, answer), do: answer
  defp verify_chain(_links, _log, answer) when is_tuple(answer), do: answer

  defp verify_chain([seq | rest], {clump_id, author, log_id} = which, _answer) do
    new_answer =
      case Persistence.retrieve(author, seq, {:entry, log_id, false, clump_id}) do
        :error ->
          {:error, "Could not retrieve certificate chain seqnum: " <> Integer.to_string(seq)}

        link ->
          validate_link(clump_id, link)
      end

    verify_chain(rest, which, new_answer)
  end

  defp validate_link(clump_id, entry) do
    with :ok <- validate_sig(entry),
         :ok <- validate_backlink(clump_id, entry),
         :ok <- validate_lipmaalink(clump_id, entry, true) do
      :ok
    else
      error -> error
    end
  end

  @doc """
  Validate a `Baobab.Entry` without full certificate pool verification.

  Confirms:
    - Signature
    - Payload hash
    - Backlink
    - Lipmaalink

    Relative to the provided `clump_id`
  """
  @spec validate_entry(String.t(), %Baobab.Entry{}) :: :ok | {:error, String.t()}
  def validate_entry(clump_id, entry) do
    with :ok <- validate_sig(entry),
         :ok <- validate_payload_hash(entry),
         :ok <- validate_backlink(clump_id, entry),
         :ok <- validate_lipmaalink(clump_id, entry) do
      :ok
    else
      error -> error
    end
  end

  @doc """
  Validate the `sig` field of a `Baobab.Entry`
  """
  @spec validate_sig(%Baobab.Entry{}) :: :ok | {:error, String.t()}
  def validate_sig(%Baobab.Entry{
        tag: tag,
        sig: sig,
        author: author,
        seqnum: seq,
        size: size,
        payload_hash: payload_hash,
        log_id: log_id,
        lipmaalink: lipmaa,
        backlink: back
      }) do
    head = tag <> author <> Varu64.encode(log_id) <> Varu64.encode(seq)

    ll =
      case lipmaa do
        nil -> <<>>
        val -> val
      end

    bl =
      case back do
        nil -> <<>>
        val -> val
      end

    tail = Varu64.encode(size) <> payload_hash

    case :enacl.sign_verify_detached(sig, head <> ll <> bl <> tail, author) do
      true -> :ok
      false -> {:error, "Invalid signature"}
    end
  end

  @doc """
  Validate the `payload_hash` field of a `Baobab.Entry`
  """
  @spec validate_payload_hash(%Baobab.Entry{}) :: :ok | {:error, String.t()}
  def validate_payload_hash(%Baobab.Entry{payload: payload, payload_hash: hash}) do
    case YAMFhash.verify(hash, payload) do
      <<>> -> :ok
      _ -> {:error, "Invalid payload hash"}
    end
  end

  @doc """
  Validate the `lipmaalink` field of a `Baobab.Entry` relative to the provided clump_id
  """
  @spec validate_lipmaalink(String.t(), %Baobab.Entry{}, boolean) :: :ok | {:error, String.t()}
  def validate_lipmaalink(clump_id, entry, missing_ok \\ false)
  def validate_lipmaalink(_clump_id, %Baobab.Entry{seqnum: 1, lipmaalink: nil}, _), do: :ok

  def validate_lipmaalink(
        clump_id,
        %Baobab.Entry{
          author: author,
          log_id: log_id,
          seqnum: seq,
          lipmaalink: ll
        },
        missing_ok
      ) do
    case {seq - 1, Lipmaa.linkseq(seq), ll} do
      {n, n, nil} ->
        :ok

      {n, n, _} ->
        {:error, "Invalid lipmaa link when matches backlink"}

      {_, n, ll} ->
        case Persistence.content(:entry, :contents, {author, log_id, n}, clump_id) do
          :error ->
            case missing_ok do
              false -> {:error, "Missing lipmaalink entry for verificaton"}
              true -> :ok
            end

          fll ->
            case YAMFhash.verify(ll, fll) do
              <<>> -> :ok
              _ -> {:error, "Invalid lipmaalink hash"}
            end
        end
    end
  end

  @doc """
  Validate the `backlink` field of a `Baobab.Entry` relative to the provided clump
  """
  @spec validate_backlink(String.t(), %Baobab.Entry{}) :: :ok | {:error, String.t()}
  def validate_backlink(_, %Baobab.Entry{seqnum: 1, backlink: nil}), do: :ok

  def validate_backlink(_, %Baobab.Entry{backlink: nil}),
    do: {:error, "Missing required backlink"}

  def validate_backlink(clump_id, %Baobab.Entry{
        author: author,
        log_id: log_id,
        seqnum: seq,
        backlink: bl
      }) do
    case Persistence.content(:entry, :contents, {author, log_id, seq - 1}, clump_id) do
      # We don't have it so we cannot check it.  We'll say it's OK
      # This is required for partial replication to be meaningful.
      # I am sure I will come to regret this post-haste
      :error ->
        :ok

      back_entry ->
        case YAMFhash.verify(bl, back_entry) do
          <<>> -> :ok
          _ -> {:error, "Invalid backlink hash"}
        end
    end
  end
end