From 68b4456469afc1a914f13d6ec876a15b1c8b04ad Mon Sep 17 00:00:00 2001 From: Hans Svensson Date: Thu, 13 Jan 2022 16:26:43 +0100 Subject: [PATCH] Add some Ethereum crypto functionality --- CHANGELOG.md | 1 + rebar.config | 3 ++- rebar.lock | 12 +++++++++ src/ecu_crypto.erl | 60 +++++++++++++++++++++++++++++++++++++++++++ src/ecu_misc.erl | 18 ++++++++++++- test/crypto_tests.erl | 35 +++++++++++++++++++++++++ 6 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 rebar.lock create mode 100644 src/ecu_crypto.erl create mode 100644 test/crypto_tests.erl diff --git a/CHANGELOG.md b/CHANGELOG.md index 2954334..869af17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Basic operations (scalar arithmetics, point addition, scalar multiplication) for secp256k1 in pure Erlang code - ecdsa sign/verify +- Some crypto (Ethereum) specific operations ecrecovery/ecverify, etc. [Unreleased]: https://github.com/hanssv/ec_utils/compare/v0.1.0...HEAD diff --git a/rebar.config b/rebar.config index f618f3e..8eef024 100644 --- a/rebar.config +++ b/rebar.config @@ -1,2 +1,3 @@ {erl_opts, [debug_info]}. -{deps, []}. \ No newline at end of file +{deps, + [{sha3, {git, "https://github.com/aeternity/erlang-sha3", {ref, "b5f27a2"}}}]}. diff --git a/rebar.lock b/rebar.lock new file mode 100644 index 0000000..2ca880d --- /dev/null +++ b/rebar.lock @@ -0,0 +1,12 @@ +{"1.2.0", +[{<<"hex2bin">>,{pkg,<<"hex2bin">>,<<"1.0.0">>},1}, + {<<"sha3">>, + {git,"https://github.com/aeternity/erlang-sha3", + {ref,"b5f27a29ba1179e5907c50d7ec7aa79b2857e981"}}, + 0}]}. +[ +{pkg_hash,[ + {<<"hex2bin">>, <<"AAC26EAB998AE80EACEE1C7607C629AB503EBF77A62B9242BAE2B94D47DCB71E">>}]}, +{pkg_hash_ext,[ + {<<"hex2bin">>, <<"E7012D1D9AADD26E680F0983D26FB8923707F05FAC9688F19F530FA3795E716F">>}]} +]. diff --git a/src/ecu_crypto.erl b/src/ecu_crypto.erl new file mode 100644 index 0000000..c2a166e --- /dev/null +++ b/src/ecu_crypto.erl @@ -0,0 +1,60 @@ +%%% File : ecu_crypto.erl +%%% Author : Hans Svensson +%%% Description : +%%% Created : 13 Jan 2022 by Hans Svensson +-module(ecu_crypto). + +-export([private_to_short/2, public_to_short/2, + eth_sign/2, eth_recover/2, eth_verify/3, eth_msg_hash/1, + keccak256/1]). + +private_to_short(bitcoin, PrivateKey) -> + public_to_short(bitcoin, aeu_ecdsa:private_to_public(secp256k1, PrivateKey)); +private_to_short(ethereum, <>) -> + public_to_short(ethereum, ecu_secp256k1:scalar_mul_base(PrivateKey)). + +public_to_short(bitcoin, PubKey = <<_:33/bytes>>) -> + crypto:hash(ripemd160, crypto:hash(sha256, PubKey)); +public_to_short(bitcoin, PubKey) -> + crypto:hash(ripemd160, crypto:hash(sha256, ecu_secp256k1:compress(PubKey))); +public_to_short(ethereum, PubKey) -> + case PubKey of + <<_:33/bytes>> -> public_to_short(ethereum, ecu_secp256k1:decompress(PubKey)); + <<4:8, X:256, Y:256>> -> public_to_short(ethereum, {X, Y}); + {X, Y} -> + <<_:12/bytes, ShortPub:20/bytes>> = keccak256(<>), + ShortPub + end. + +eth_sign(Msg, PrivateKey = <<_:32/bytes>>) -> + {BaseSig, YVal} = ecu_ecdsa:sign_secp256k1(eth_msg_hash(Msg), PrivateKey), + V = if YVal rem 2 == 0 -> 27; + true -> 28 + end, + <>. + +eth_recover(Msg, Sig = <<_:65/bytes>>) -> + MsgHash = eth_msg_hash(Msg), + <> = MsgHash, + <> = Sig, + Z = E rem ecu_secp256k1:n(), + RInv = ecu_secp256k1:s_inv(R), + Rd = ecu_secp256k1:decompress(<<(V - 27 + 2):8, R:256>>), + [P1, P2] = + ecu_misc:pcomp( + [fun() -> ecu_secp256k1:scalar_mul(ecu_secp256k1:s_mul(RInv, S), Rd) end, + fun() -> ecu_secp256k1:scalar_mul_base(ecu_secp256k1:s_mul(RInv,Z)) end]), + + {X, Y} = ecu_secp256k1:p_add(P1, ecu_secp256k1:p_neg(P2)), + <<_:12/bytes, RPub:20/bytes>> = keccak256(<>), + RPub. + +eth_verify(Msg, PublicKey, Sig) -> + PublicKey == eth_recover(Msg, Sig). + +eth_msg_hash(Msg0) -> + Msg = ["\x19Ethereum Signed Message:\n", integer_to_list(byte_size(Msg0)), Msg0], + keccak256(iolist_to_binary(Msg)). + +keccak256(Bin) -> + sha3:hash(256, Bin). diff --git a/src/ecu_misc.erl b/src/ecu_misc.erl index 7acdbd6..41565e6 100644 --- a/src/ecu_misc.erl +++ b/src/ecu_misc.erl @@ -4,7 +4,9 @@ %%% Created : 13 Jan 2022 by Hans Svensson -module(ecu_misc). --export([eea/2, pcomp/1]). +-export([eea/2, + hex_to_bin/1, bin_to_hex/1, + pcomp/1]). %% Extended Euclidean Algorithm eea(A, B) when ((A < 1) or (B < 1)) -> @@ -23,3 +25,17 @@ pcomp(Fs) -> Parent = self(), Pids = [ spawn(fun() -> Parent ! {self(), F()} end) || F <- Fs ], [ receive {Pid, X} -> X after 500 -> error(timeout) end || Pid <- Pids ]. + +%% Hex encode/decode +-spec hex_to_bin(Input :: string()) -> binary(). +hex_to_bin(S) -> + hex_to_bin(S, []). +hex_to_bin([], Acc) -> + list_to_binary(lists:reverse(Acc)); +hex_to_bin([X,Y|T], Acc) -> + {ok, [V], []} = io_lib:fread("~16u", [X,Y]), + hex_to_bin(T, [V | Acc]). + +-spec bin_to_hex(Input :: binary()) -> string(). +bin_to_hex(Bin) -> + lists:flatten([io_lib:format("~2.16.0B", [X]) || X <- binary_to_list(Bin)]). diff --git a/test/crypto_tests.erl b/test/crypto_tests.erl new file mode 100644 index 0000000..0561901 --- /dev/null +++ b/test/crypto_tests.erl @@ -0,0 +1,35 @@ +%%% File : crypto_tests.erl +%%% Author : Hans Svensson +%%% Description : +%%% Created : 12 Jan 2022 by Hans Svensson +-module(crypto_tests). + +-compile([export_all, nowarn_export_all]). + +-include_lib("eunit/include/eunit.hrl"). + + +gen_ecdsa_secp256k1_privkey() -> + <> = crypto:strong_rand_bytes(32), + P = (P0 rem (ecu_secp256k1:n() - 1)) + 1, + <>. + +eth_sign_verify_test() -> + Data0 = [ {gen_ecdsa_secp256k1_privkey(), crypto:strong_rand_bytes(32)} || _ <- lists:seq(1, 10) ], + + Data = [{Pr, ecu_crypto:private_to_short(ethereum, Pr), M} || {Pr, M} <- Data0], + + Test = fun(PrivK, PubK, MsgHash) -> + Sig = ecu_crypto:eth_sign(MsgHash, PrivK), + ?assertEqual(PubK, ecu_crypto:eth_recover(MsgHash, Sig)) + end, + + {T, _} = timer:tc(fun() -> [ Test(Pr, Pu, M) || {Pr, Pu, M} <- Data ] end), + ?debugFmt("Average time for eth_sign+eth_recovery: ~.3f ms", [(T / 1000) / length(Data)]). + +recover_test() -> + <> = ecu_misc:hex_to_bin("a5f270865420c8595128cf7132dcedb1221abf89286f926d067dff2fa59347c07a0fd06e8b4a567b0628a01d5398480a49c540c0cbd9980abd08cf3818f25e2e"), +%% <> = hex_to_bin("9a4a5c038e7ce00f0ad216894afc00de6b41bbca1d4d7742104cb9f078c6d2df"), +%% <> = hex_to_bin("4a5c5d454721bbbb25540c3317521e71c373ae36458f960d2ad46ef088110e95"), %% MsgHash + ShortPub = ecu_misc:hex_to_bin("E53e2125F377D5c62a1FfbfEEB89A0826E9dE54C"), + ?assertEqual(ShortPub, ecu_crypto:eth_recover(<<"test">>, <<28:8, R:256, S:256>>)).