EOS Signature verification with Elixir 😍

in elixir •  6 years ago 

EOS Signature Verification and ECDSA Tooling with Elixir

kelly-sikkema-1485308-unsplash.jpg
Photo by Kelly Sikkema on Unsplash

Elixir is one of my favorite programming languages. It's a wonderful backend languages and it really lends itself to glueing many of the backend processes that support synchronizing blockchain world to current infrastructures. A recent task I worked on requires verifying using an EOS signature to prove a request on our services. After plenty google-fu and reading through the eosjs-ecc library here's what I came up with:

First, to simplify things for the elixir side we'll grab the EOS signature in hex format vs. the standard SIG_K1_ format. Doing so isn't too difficult with eosjs-ecc:

const sig = ecc.Signature.sign('helloworld', privateKey).toHex();
// or if you already have a signature
const sig = ecc.Signature.fromString('SIG_K1_....');
sig.toHex();
//sample hex:
//'1f529f1b2b1373565975c7cfaf4e1217c5d24bbd26ebab6f9186c6c5cd26ad1b004b4f6d329f4e1ed2e5ee2bbf60d99ce4ef8ee4726699580c45c31ddff227821b'

The signature hex is what we'll use below. Everything else is in the standard form, i.e.:

defmodule EOSClient.KeyTool do
  use Bitwise

  def address_match?(eos_pub, msg, signature_hex) do
    {:ok, recovered_pub} = recover_eos_pub(msg, signature_hex)
    eos_pub == recovered_pub
  end

  def recover_eos_pub(msg, eos_signature) do
    msg_hash = :crypto.hash(:sha256, msg)

    # Here R, S are the same as ETH. V for EOS is found at the beginning of the
    # hash
    {r, s, v} = destructure_sig(eos_signature)
    signature = BitHelper.pad(:binary.encode_unsigned(r), 32) <> BitHelper.pad(:binary.encode_unsigned(s), 32)

    # Calculate i
    # https://github.com/EOSIO/eosjs-ecc/blob/master/src/signature.js#L109
    i = (v - 27) &&& 3
    IO.inspect ["i", i]

    case :libsecp256k1.ecdsa_recover_compact(msg_hash, signature, :compressed, i) do
      {:ok, pub} ->
        {:ok, Base.encode16(pub, case: :lower)
              |> pub_hex_to_eos}
      {:error, err} ->
        {:error, err}
    end
  end

  def destructure_sig(sig) do
    {
      String.slice(sig, 2, 64)
      |> EthClient.Hex.decode_hex
      |> :binary.decode_unsigned,
      String.slice(sig, 66,64)
      |> EthClient.Hex.decode_hex
      |> :binary.decode_unsigned,
      String.slice(sig, 0, 2)
      |> EthClient.Hex.decode_hex
      |> :binary.decode_unsigned
    }
  end

  def eth_to_eos_sig(eth_hex) do
    {sig, pad} =
      eth_hex
      |> String.slice(2..-1)
      |> String.split_at(-2)

    {int, _} = Integer.parse(pad, 16)

    prefix = Integer.to_string(int + 4, 16)
    raw_sig = prefix <> sig
    "SIG_K1_" <> check_encode(raw_sig, "K1")
  end

  def pub_hex_to_eos(pubkey, key_type \\ nil) do
    IO.inspect pubkey
    pub_bin = pubkey
              |> Base.decode16!(case: :lower)


    check = case key_type do
      nil -> [pub_bin]
      key -> [pub_bin, key]
    end

    check_bin =
      check
      |> :erlang.iolist_to_binary

    checksum =
        hash(check_bin, :ripemd160)
        |> :binary.part(0, 4)

    "EOS" <> EthClient.Base58.encode(pub_bin <> checksum)
  end


  def check_encode(key, key_type \\ nil) do
    bin = key
          |> Base.decode16!(case: :lower)

    check = case key_type do
      nil -> [bin]
      key -> [bin, key]
    end

    check_bin =
      check
      |> :erlang.iolist_to_binary

    checksum =
        hash(check_bin, :ripemd160)
        |> :binary.part(0, 4)

    EthClient.Base58.encode(bin <> checksum)
  end

  def hash(data, algorithm) do
    :crypto.hash(algorithm, data)
  end

end

There's some helpers along with this that I had written before for Ethereum sig validation. Since it's the same encryption under the hood, those work here as well. (Yes I should rename these).

Hex helper

defmodule EthClient.Hex do
  @moduledoc """
  Helpers for Compound to encode and decode to Ethereum's
  hex format.
  """

  require Integer

  @spec encode_hex(binary()) :: String.t
  def encode_hex(hex), do: "0x" <> Base.encode16(hex, case: :lower) # TODO: This should be shorten for odd length strings?

  @spec decode_hex(String.t) :: binary()
  def decode_hex("0x" <> hex_data), do: decode_hex(hex_data)
  def decode_hex(hex_data) when Integer.is_odd(byte_size(hex_data)), do: decode_hex("0" <> hex_data)
  def decode_hex(hex_data) do
    Base.decode16!(hex_data, case: :mixed)
  end

  @spec maybe_decode_hex(String.t | nil) :: binary() | nil
  def maybe_decode_hex(nil), do: nil
  def maybe_decode_hex(hex), do: decode_hex(hex)

  @spec maybe_decode_hex_int(String.t | nil) :: integer() | nil
  def maybe_decode_hex_int(hex) do
    case maybe_decode_hex(hex) do
      nil -> nil
      bin -> :binary.decode_unsigned(bin)
    end
  end
end

Base58 helper:

defmodule EthClient.Base58 do
  @alphabet '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'

  def encode(data, hash \\ "")

  def encode(data, hash) when is_binary(data) do
    encode_zeros(data) <> encode(:binary.decode_unsigned(data), hash)
  end

  def encode(0, hash), do: hash

  def encode(data, hash) do
    character = <<Enum.at(@alphabet, rem(data, 58))>>
    encode(div(data, 58), character <> hash)
  end

  defp encode_zeros(data) do
    <<Enum.at(@alphabet, 0)>>
    |> String.duplicate(leading_zeros(data))
  end

  defp leading_zeros(data) do
    :binary.bin_to_list(data)
    |> Enum.find_index(&(&1 != 0))
  end
end

Most of the steps to get here have been derived from reading some excellent blog posts on Bitcoin, and reading through some elixir crypto libraries that deal with either bitcoin or ethereum. I've just managed to glue them together to apply them to EOSIO. 🚀

Some of the posts and/or repos that are helpful (and many more that I lost the chrome tab to):
https://blog.lelonek.me/how-to-calculate-bitcoin-address-in-elixir-68939af4f0e9
https://github.com/KamilLelonek/ex_wallet
http://www.petecorey.com/blog/2018/01/08/bitcoins-base58check-in-pure-elixir/
https://github.com/exthereum/exth_crypto/blob/master/lib/signature/signature.ex

Until next time!

Angel
twitter
sense.chat

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!