EOS Signature Verification and ECDSA Tooling with Elixir
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