diff --git a/eqc/ecu_crypto_eqc.erl b/eqc/ecu_crypto_eqc.erl new file mode 100644 index 0000000..64c3290 --- /dev/null +++ b/eqc/ecu_crypto_eqc.erl @@ -0,0 +1,25 @@ +%%% File : ecu_crypto_eqc.erl +%%% Author : Hans Svensson +%%% Description : +%%% Created : 7 Jan 2023 by Hans Svensson +-module(ecu_crypto_eqc). + +-compile([export_all, nowarn_export_all]). + +-include_lib("eqc/include/eqc.hrl"). + +gen_ecdsa_secp256k1_privkey() -> + <> = crypto:strong_rand_bytes(32), + P = (P0 rem (ecu_secp256k1:n() - 1)) + 1, + return(<>). + +prop_recover() -> + ?FORALL(PK, gen_ecdsa_secp256k1_privkey(), + begin + MsgHash = sha3:hash(256, PK), + Sig = ecu_crypto:eth_sign(MsgHash, PK), + Pub1 = ecrecover:recover(MsgHash, Sig), + Pub2 = ecu_crypto:ec_recover(MsgHash, Sig), + equals(Pub1, Pub2) + end). + diff --git a/rebar.config b/rebar.config index 456ff2a..8f01fce 100644 --- a/rebar.config +++ b/rebar.config @@ -7,7 +7,8 @@ {deps, [{sha3, {git, "https://github.com/aeternity/erlang-sha3", {ref, "b5f27a2"}}}]}. -{profiles, [{test, [{deps, [{enacl, {git, "https://github.com/aeternity/enacl.git", {ref, "01dd0c2"}}}, +{profiles, [{test, [{deps, [{enacl, {git, "https://github.com/aeternity/enacl.git", {ref, "5bae41c"}}}, {ecrecover, {git, "https://github.com/aeternity/ecrecover.git", {ref, "74b7816"}}}]}]}, - {eqc, [{deps, [{enacl, {git, "https://github.com/aeternity/enacl.git", {ref, "38ffc76"}}}]}]} + {eqc, [{deps, [{enacl, {git, "https://github.com/aeternity/enacl.git", {ref, "38ffc76"}}}, + {ecrecover, {git, "https://github.com/aeternity/ecrecover.git", {ref, "74b7816"}}}]}]} ]}. diff --git a/src/ecu_crypto.erl b/src/ecu_crypto.erl index 6f1c8ef..4034846 100644 --- a/src/ecu_crypto.erl +++ b/src/ecu_crypto.erl @@ -6,7 +6,9 @@ -vsn("1.0.0"). -export([private_to_short/2, public_to_short/2, - eth_sign/2, eth_recover/2, eth_verify/3, eth_msg_hash/1, + ec_recover/2, + eth_sign/2, eth_recover/2, eth_verify/3, + eth_msg_sign/2, eth_msg_recover/2, eth_msg_verify/3, eth_msg_hash/1, keccak256/1]). private_to_short(bitcoin, PrivateKey) -> @@ -27,15 +29,33 @@ public_to_short(ethereum, PubKey) -> ShortPub end. +eth_msg_sign(Msg, PrivateKey = <<_:32/bytes>>) -> + eth_sign(eth_msg_hash(Msg), PrivateKey). + eth_sign(Msg, PrivateKey = <<_:32/bytes>>) -> - {BaseSig, YVal} = ecu_ecdsa:sign_secp256k1(eth_msg_hash(Msg), PrivateKey), + {BaseSig, YVal} = ecu_ecdsa:sign_secp256k1(Msg, PrivateKey), V = if YVal rem 2 == 0 -> 27; true -> 28 end, <>. -eth_recover(Msg, Sig = <<_:65/bytes>>) -> - MsgHash = eth_msg_hash(Msg), +eth_msg_recover(Msg, Sig = <<_:65/bytes>>) -> + eth_recover(eth_msg_hash(Msg), Sig). + +%% This is the mythical Ethereum ECRECOVERY operation +ec_recover(MsgHash = <<_:32/bytes>>, Sig = <<_:65/bytes>>) -> + <> = Sig, + case (V == 27 orelse V == 28) andalso + (R >= 1 andalso R =< ecu_secp256k1:n()) andalso + (S >= 1 andalso S =< ecu_secp256k1:n()) of + true -> + ShortPub = eth_recover(MsgHash, Sig), + <<0:96, ShortPub/binary>>; + false -> + <<0:256>> + end. + +eth_recover(MsgHash = <<_:32/bytes>>, Sig = <<_:65/bytes>>) -> <> = MsgHash, <> = Sig, Z = E rem ecu_secp256k1:n(), @@ -50,8 +70,11 @@ eth_recover(Msg, Sig = <<_:65/bytes>>) -> <<_:12/bytes, RPub:20/bytes>> = keccak256(<>), RPub. -eth_verify(Msg, PublicKey, Sig) -> - PublicKey == eth_recover(Msg, Sig). +eth_msg_verify(Msg, PublicKey, Sig) -> + eth_verify(eth_msg_hash(Msg), PublicKey, Sig). + +eth_verify(Msg, PublickKey, Sig) -> + PublickKey == eth_recover(Msg, Sig). eth_msg_hash(Msg0) -> Msg = ["\x19Ethereum Signed Message:\n", integer_to_list(byte_size(Msg0)), Msg0], diff --git a/src/ecu_ed25519.erl b/src/ecu_ed25519.erl index 8122fc6..8fc4bf9 100644 --- a/src/ecu_ed25519.erl +++ b/src/ecu_ed25519.erl @@ -12,12 +12,12 @@ -type pt_affine() :: {non_neg_integer(), non_neg_integer()}. %% {X, Y} -type pt_hom_ext() :: {non_neg_integer(), non_neg_integer(), non_neg_integer(), non_neg_integer()}. %% {X, Y, Z, T} --type pt_compressed() :: <<_:32>>. %% Y coord + odd/even X. +-type pt_compressed() :: <<_:256>>. %% Y coord + odd/even X. -type pt() :: pt_affine() | pt_hom_ext() | pt_compressed(). %% -type fld_elem() :: 0..(?P-1). --type scalar() :: 0..(?N-1). +-type scalar() :: 0..(?N-1). -define(D, 16#52036CEE2B6FFE738CC740797779E89800700A4D4141D8AB75EB4DCA135978A3). -define(X, 16#216936D3CD6E53FEC0A4E231FDD6DC5C692CC7609525A7B2C9562D608F25D51A). @@ -34,6 +34,8 @@ -define(SUB(A, B), ((A - B + ?P) rem ?P)). -define(DIV(A, B), f_div(A, B)). +-export_type([pt/0, scalar/0]). + -export([on_curve/1, p/0, n/0, pt_eq/2, scalar_mul/2, scalar_mul_base/1, scalar_mul_noclamp/2, scalar_mul_base_noclamp/1, @@ -77,14 +79,14 @@ n() -> ?N. -define(TWO_POW_255_MINUS_1, 16#7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF). --spec compress(P :: pt()) -> <<_:32>>. +-spec compress(P :: pt()) -> pt_compressed(). compress(<<_:32/binary>> = P) -> P; compress({_, _, _, _} = P) -> compress(to_affine(P)); compress({X, Y}) -> V = (Y band ?TWO_POW_255_MINUS_1) bor ((X band 1) bsl 255), <>. --spec decompress(<<_:32>>) -> pt_hom_ext(). +-spec decompress(pt_compressed()) -> pt_hom_ext(). decompress(<>) -> X0 = Y0 bsr 255, Y = Y0 band ?TWO_POW_255_MINUS_1, diff --git a/src/ecu_eddsa.erl b/src/ecu_eddsa.erl index aea16dc..c45304b 100644 --- a/src/ecu_eddsa.erl +++ b/src/ecu_eddsa.erl @@ -17,23 +17,22 @@ %% %% The keypair is returned as a map with keys 'public' and 'secret'. %% @end --spec sign_keypair() -> #{ atom() => binary() }. +-spec sign_keypair() -> #{ public => binary(), secret => binary() }. sign_keypair() -> Secret = crypto:strong_rand_bytes(32), - <> = crypto:hash(sha512, Secret), + <> = crypto:hash(sha512, Secret), - Pub = ecu_ed25519:scalar_mul_base(Seed), + Pub = ecu_ed25519:compress(ecu_ed25519:scalar_mul_base(Seed)), #{public => Pub, secret => <>}. %% @doc sign_seed_keypair/1 computes the signing keypair from a seed. %% %% The keypair is returned as a map with keys 'public' and 'secret'. %% @end --spec sign_seed_keypair(Seed :: <<_:32>>) -> #{ atom() => binary() }. +-spec sign_seed_keypair(Secret :: <<_:256>>) -> #{ public => binary(), secret => binary() }. sign_seed_keypair(Secret) -> - <> = crypto:hash(sha512, Secret), + <> = crypto:hash(sha512, Secret), Pub = ecu_ed25519:compress(ecu_ed25519:scalar_mul_base(Seed)), -%% Pub = enacl:crypto_ed25519_scalarmult_base(Seed), #{public => Pub, secret => <>}. @@ -42,7 +41,7 @@ sign_seed_keypair(Secret) -> %% Given a message `Msg' and a secret key `SK' the function will sign the %% message and return a signed message `SM'. %% @end --spec sign(Msg :: iodata(), SK :: <<_:32>> | <<_:64>>) -> SM :: binary(). +-spec sign(Msg :: iodata(), SK :: <<_:256>> | <<_:512>>) -> SM :: binary(). sign(Msg, SK) -> BinMsg = iolist_to_binary(Msg), Sig = sign_detached(Msg, SK), @@ -55,12 +54,12 @@ sign(Msg, SK) -> %% `{error, failed_verification}' depending on the correctness of the %% signature. %% @end --spec sign_open(SMsg :: binary(), PK :: <<_:32>>) -> +-spec sign_open(SMsg :: binary(), PK :: <<_:256>>) -> {ok, Msg :: binary()} | {error, failed_verification}. sign_open(<>, PK) -> - <> = Sig, + <> = Sig, - Ks0 = crypto:hash(sha512, <>), + Ks0 = crypto:hash(sha512, <>), Ks = ecu_ed25519:scalar_reduce(Ks0), LHS = ecu_ed25519:scalar_mul_base_noclamp(Ss), @@ -78,7 +77,7 @@ sign_open(<>, PK) -> %% Given a message `Msg' and a secret key `SK' the function will compute the %% digital signature `Sig'. %% @end --spec sign_detached(Msg :: iodata(), SK :: <<_:32>>) -> Sig :: binary(). +-spec sign_detached(Msg :: iodata(), SK :: <<_:256>> | <<_:512>>) -> Sig :: binary(). sign_detached(Msg, SK) -> BinMsg = iolist_to_binary(Msg), <> = SK, @@ -120,12 +119,12 @@ sign_detached(Msg, SK) -> %% function computes true iff the `Sig' is valid for `Msg' and `PK'; and, %% false otherwise. %% @end --spec sign_verify_detached(Sig :: <<_:64>>, Msg :: iodata(), PK :: <<_:32>>) -> boolean(). +-spec sign_verify_detached(Sig :: <<_:512>>, Msg :: iodata(), PK :: <<_:256>>) -> boolean(). sign_verify_detached(Sig, Msg, PK) -> BinMsg = iolist_to_binary(Msg), - <> = Sig, + <> = Sig, - Ks0 = crypto:hash(sha512, <>), + Ks0 = crypto:hash(sha512, <>), Ks = ecu_ed25519:scalar_reduce(Ks0), LHS = ecu_ed25519:scalar_mul_base_noclamp(Ss), @@ -137,5 +136,5 @@ sign_verify_detached(Sig, Msg, PK) -> %% Clamp a 32-byte little-endian integer - i.e clear the lowest three bits %% of the first byte and clear the highest and set the second highest of %% the last byte (i.e. making it divisible by 8 and -clamp(<>) -> - <<(B0 band 16#f8):8, B1_30/bytes, ((B31 band 16#7f) bor 16#40):8>>. +clamp(<>) -> + <<(B0 band 16#f8):8, B1_30/binary, ((B31 band 16#7f) bor 16#40):8>>. diff --git a/test/crypto_tests.erl b/test/crypto_tests.erl index 0561901..0970a3d 100644 --- a/test/crypto_tests.erl +++ b/test/crypto_tests.erl @@ -20,16 +20,16 @@ eth_sign_verify_test() -> 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)) + Sig = ecu_crypto:eth_msg_sign(MsgHash, PrivK), + ?assertEqual(PubK, ecu_crypto:eth_msg_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)]). + ?debugFmt("Average time for eth_msg_sign+eth_msg_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>>)). + ?assertEqual(ShortPub, ecu_crypto:eth_msg_recover(<<"test">>, <<28:8, R:256, S:256>>)).