diff --git a/src/ar_bundles.erl b/src/ar_bundles.erl index 3f070b54a..94f25cae3 100644 --- a/src/ar_bundles.erl +++ b/src/ar_bundles.erl @@ -129,7 +129,7 @@ enforce_valid_tx(TX) -> {invalid_field, anchor, TX#tx.anchor} ), hb_util:ok_or_throw(TX, - hb_util:check_size(TX#tx.owner, [0, byte_size(?DEFAULT_OWNER)]), + hb_util:check_size(TX#tx.owner, [0, 32, byte_size(?DEFAULT_OWNER)]), {invalid_field, owner, TX#tx.owner} ), hb_util:ok_or_throw(TX, @@ -137,7 +137,7 @@ enforce_valid_tx(TX) -> {invalid_field, target, TX#tx.target} ), hb_util:ok_or_throw(TX, - hb_util:check_size(TX#tx.signature, [0, 65, byte_size(?DEFAULT_SIG)]), + hb_util:check_size(TX#tx.signature, [0, 64, 65, byte_size(?DEFAULT_SIG)]), {invalid_field, signature, TX#tx.signature} ), hb_util:ok_or_throw(TX, @@ -184,8 +184,7 @@ data_item_signature_data(RawItem) -> ar_deep_hash:hash([ utf8_encoded("dataitem"), utf8_encoded("1"), - %% Only SignatureType 1 is supported for now (RSA 4096) - utf8_encoded("1"), + utf8_encoded(get_signature_type(Item#tx.signature_type)), <<(Item#tx.owner)/binary>>, <<(Item#tx.target)/binary>>, <<(Item#tx.anchor)/binary>>, @@ -193,6 +192,9 @@ data_item_signature_data(RawItem) -> <<(Item#tx.data)/binary>> ]). +get_signature_type({rsa, 65537}) -> "1"; +get_signature_type({eddsa, ed25519}) -> "2". + %% @doc Verify the data item's ID matches the signature. verify_data_item_id(DataItem) -> ExpectedID = crypto:hash(sha256, DataItem#tx.signature), @@ -316,6 +318,8 @@ to_serialized_pair(Item, false, Signed) -> %% little-endian format which is why we encode to `<<1, 0>>'. encode_signature_type({rsa, 65537}) -> <<1, 0>>; +encode_signature_type({eddsa, ed25519}) -> + <<2, 0>>; encode_signature_type(_) -> unsupported_tx_format. @@ -498,6 +502,8 @@ decode_bundle_header(Count, <>'. decode_signature(<<1, 0, Signature:512/binary, Owner:512/binary, Rest/binary>>) -> {{rsa, 65537}, Signature, Owner, Rest}; +decode_signature(<<2, 0, Signature:64/binary, Owner:32/binary, Rest/binary>>) -> + {{eddsa, ed25519}, Signature, Owner, Rest}; decode_signature(Other) -> ?event({error_decoding_signature, {sig_type, {explicit, binary:part(Other, 0, 2)}}, @@ -749,6 +755,32 @@ bundle_map_test() -> ?assertEqual(Item1#tx.data, (maps:get(<<"key1">>, BundleItem#tx.data))#tx.data), ?assert(verify_item(BundleItem)). +eddsa_cases_test() -> + Key = ar_wallet:new(?EDDSA_KEY_TYPE), + %% Owner and SignatureType defined during signing process. + Item1 = sign_item(#tx{ + format = ans104, + target = crypto:strong_rand_bytes(32), + anchor = crypto:strong_rand_bytes(32), + tags = [{<<"tag1">>, <<"value1">>}, {<<"tag2">>, <<"value2">>}], + data = <<"item1_data">> + }, Key), + Bundle = serialize(dev_arweave_common:normalize(Item1)), + BundleItem = deserialize(Bundle), + %% Sign a valid transaction and verify it + ?assert(verify_item(BundleItem)), + %% Missing Anchor should fail + ?assertNot(verify_item(BundleItem#tx{anchor = <<>>})), + %% Missing Tags should fail + ?assertNot(verify_item(BundleItem#tx{tags = []})), + %% Missing Owner should fail + ?assertNot(verify_item(BundleItem#tx{owner = crypto:strong_rand_bytes(32)})), + %% Missing Target should fail + ?assertNot(verify_item(BundleItem#tx{target = <<>>})), + %% Missing Data should fail + ?assertNot(verify_item(BundleItem#tx{data = <<>>})), + ok. + extremely_large_bundle_test() -> W = ar_wallet:new(), Data = crypto:strong_rand_bytes(100_000_000), @@ -991,4 +1023,14 @@ generate_and_write_map_bundle_test_disabled() -> ?event(debug_test, {deserialized, {explicit, Deserialized}}), ?assert(verify_item(Deserialized)), ok = file:write_file( - <<"test/arbundles.js/ans104-map-bundle-erlang.bundle">>, Serialized). \ No newline at end of file + <<"test/arbundles.js/ans104-map-bundle-erlang.bundle">>, Serialized). + +deserialize_ed25519_transaction_test() -> + % ans104-item-ed25519.bin is dataitem 1rTy7gQuK9lJydlKqCEhtGLp2WWG-GOrVo5JdiCmaxs + {ok, Serialized} = file:read_file(<<"test/arbundles.js/ans104-item-ed25519.bin">>), + Deserialized = deserialize(Serialized), + ?assertEqual([{<<"Content-Type">>,<<"image/png">>}], Deserialized#tx.tags), + ?assertEqual(<<"ZbExyvGrJKOJTJcHMtKzoOZVCQBkjZ+5">>, Deserialized#tx.anchor), + ?assertEqual(<<"ejhYD9Cw9VCsVik6yGLoclo3CLRvAITHTZamLY_6ro4">>, + hb_util:human_id(ar_wallet:to_address(Deserialized#tx.owner, Deserialized#tx.signature_type))), + ?assert(verify_item(Deserialized)). \ No newline at end of file diff --git a/src/ar_wallet.erl b/src/ar_wallet.erl index 4bff3d6f7..d5670e9b9 100644 --- a/src/ar_wallet.erl +++ b/src/ar_wallet.erl @@ -17,9 +17,11 @@ new() -> new(KeyType = {KeyAlg, PublicExpnt}) when KeyType =:= {rsa, 65537} -> {[_, Pub], [_, Pub, Priv|_]} = {[_, Pub], [_, Pub, Priv|_]} = crypto:generate_key(KeyAlg, {4096, PublicExpnt}), + {{KeyType, Priv, Pub}, {KeyType, Pub}}; +new(KeyType = {KeyAlg, Curve}) when KeyType =:= {?EDDSA_SIGN_ALG, ed25519} -> + {Pub, Priv} = crypto:generate_key(KeyAlg, Curve), {{KeyType, Priv, Pub}, {KeyType, Pub}}. - %% @doc Sign some data with a private key. sign(Key, Data) -> sign(Key, Data, sha256). @@ -36,6 +38,8 @@ sign({{rsa, PublicExpnt}, Priv, Pub}, Data, DigestType) when PublicExpnt =:= 655 privateExponent = binary:decode_unsigned(Priv) } ); +sign({KeyType = {KeyAlg, Curve}, Priv, _Pub}, Data, _DigestType) when KeyType =:= {?EDDSA_SIGN_ALG, ed25519} -> + crypto:sign(KeyAlg, none, Data, [Priv, Curve]); sign({{KeyType, Priv, Pub}, {KeyType, Pub}}, Data, DigestType) -> sign({KeyType, Priv, Pub}, Data, DigestType). @@ -57,7 +61,11 @@ verify({{rsa, PublicExpnt}, Pub}, Data, Sig, DigestType) when PublicExpnt =:= 65 publicExponent = PublicExpnt, modulus = binary:decode_unsigned(Pub) } - ). + ); +verify({{eddsa, Curve}, Pub}, Data, Sig, _DigestType) when + byte_size(Pub) == 32 andalso byte_size(Sig) == 64 andalso Curve =:= ed25519 -> + crypto:verify(eddsa, none, Data, Sig, [Pub, Curve]). + %% @doc Find a public key from a wallet. to_pubkey(Pubkey) -> @@ -81,7 +89,9 @@ to_address({{_, _, PubKey}, {_, PubKey}}, _) -> to_address(PubKey, {rsa, 65537}) -> to_rsa_address(PubKey); to_address(PubKey, {ecdsa, 256}) -> - to_ecdsa_address(PubKey). + to_ecdsa_address(PubKey); +to_address(PubKey, {eddsa, ed25519}) -> + to_eddsa_address(PubKey). %% @doc Generate a new wallet public and private key, with a corresponding keyfile. %% The provided key is used as part of the file name. @@ -230,6 +240,9 @@ hash_address(PubKey) -> to_ecdsa_address(PubKey) -> hb_keccak:key_to_ethereum_address(PubKey). +to_eddsa_address(PubKey) -> + hash_address(PubKey). + %%%=================================================================== %%% Private functions. %%%=================================================================== diff --git a/src/dev_codec_ans104_from.erl b/src/dev_codec_ans104_from.erl index a60e90505..973745104 100644 --- a/src/dev_codec_ans104_from.erl +++ b/src/dev_codec_ans104_from.erl @@ -203,13 +203,17 @@ with_unsigned_commitment( with_signed_commitment( Item, Device, FieldCommitments, Tags, UncommittedMessage, CommittedKeys, Opts) -> - Address = hb_util:human_id(ar_wallet:to_address(Item#tx.owner)), + Address = hb_util:human_id(ar_wallet:to_address(Item#tx.owner, Item#tx.signature_type)), ID = hb_util:human_id(Item#tx.id), ExtraCommitments = hb_maps:merge( FieldCommitments, hb_maps:with(?BUNDLE_KEYS, Tags), Opts ), + Type = case Item#tx.signature_type of + ?RSA_KEY_TYPE -> <<"rsa-pss-sha256">>; + ?EDDSA_KEY_TYPE -> <<"ed25519">> + end, Commitment = filter_unset( hb_maps:merge( @@ -221,7 +225,7 @@ with_signed_commitment( <<"signature">> => hb_util:encode(Item#tx.signature), <<"keyid">> => <<"publickey:", (hb_util:encode(Item#tx.owner))/binary>>, - <<"type">> => <<"rsa-pss-sha256">>, + <<"type">> => Type, <<"bundle">> => bundle_commitment_key(Tags, Opts), <<"original-tags">> => original_tags(Item, Opts) }, diff --git a/src/dev_codec_tx.erl b/src/dev_codec_tx.erl index 3363969d2..7f9ee2f9a 100644 --- a/src/dev_codec_tx.erl +++ b/src/dev_codec_tx.erl @@ -305,7 +305,6 @@ enforce_valid_tx_test() -> SigInvalidSize66 = crypto:strong_rand_bytes(66), SigInvalidSize511 = crypto:strong_rand_bytes(511), SigTooLong513 = crypto:strong_rand_bytes(byte_size(?DEFAULT_SIG)+1), - FailureCases = [ {not_a_tx_record, not_a_tx_record_atom, {invalid_tx, not_a_tx_record_atom}}, diff --git a/src/dev_message.erl b/src/dev_message.erl index 239307147..ddd3d9705 100644 --- a/src/dev_message.erl +++ b/src/dev_message.erl @@ -937,9 +937,16 @@ set_ignore_undefined_test() -> ?assertEqual(#{ <<"test-key">> => <<"Value1">> }, hb_private:reset(hb_util:ok(set(Base, Req, #{ hashpath => ignore })))). -verify_test() -> +verify_test_() -> + {foreach, fun () -> ok end, fun (_) -> ok end, [ + {"RSA", fun () -> test_verify(?RSA_KEY_TYPE) end}, + {"EDSSA", fun () -> test_verify(?EDDSA_KEY_TYPE) end} + ]}. + +test_verify(KeyType) -> Unsigned = #{ <<"a">> => <<"b">> }, - Signed = hb_message:commit(Unsigned, #{ priv_wallet => hb:wallet() }), + Wallet = ar_wallet:new(KeyType), + Signed = hb_message:commit(Unsigned, #{ priv_wallet => Wallet }), ?event({signed, Signed}), BadSigned = Signed#{ <<"a">> => <<"c">> }, ?event({bad_signed, BadSigned}), diff --git a/src/hb_gateway_client.erl b/src/hb_gateway_client.erl index 7f4edf061..b5eb78b1e 100644 --- a/src/hb_gateway_client.erl +++ b/src/hb_gateway_client.erl @@ -257,6 +257,7 @@ result_to_message(ExpectedID, Item, Opts) -> ), SignatureType = case byte_size(Signature) of + 64 -> {eddsa, ed25519}; 65 -> {ecdsa, 256}; 512 -> {rsa, 65537}; _ -> unsupported_tx_signature_type @@ -424,11 +425,44 @@ l1_transaction_test() -> %% @doc Test l2 message from graphql l2_dataitem_test() -> _Node = hb_http_server:start_node(#{}), - {ok, Res} = read(<<"oyo3_hCczcU7uYhfByFZ3h0ELfeMMzNacT-KpRoJK6g">>, #{}), + {ok, Res} = read(ID = <<"oyo3_hCczcU7uYhfByFZ3h0ELfeMMzNacT-KpRoJK6g">>, #{}), ?event(gateway, {l2_dataitem, Res}), + Opts = #{}, + CommitmentType = hb_util:deep_get( + [<<"commitments">>, ID, <<"type">>], + Res, + not_found, + Opts + ), + ?assertEqual(<<"rsa-pss-sha256">>, CommitmentType), Data = maps:get(<<"data">>, Res), ?assertEqual(<<"Hello World">>, Data). +%% @doc ed25519 L2 Transaction test +l2_dataitem_ed25519_test() -> + _Node = hb_http_server:start_node(#{}), + ID = <<"AwrAs-HaBlc8xeI8sw6Wpbi7A0weQWeXYwW20CpX5oM">>, + {ok, Res} = read(ID, #{}), + ?event(gateway, {l2_dataitem, Res}), + Opts = #{}, + CommitmentType = hb_util:deep_get( + [<<"commitments">>, ID, <<"type">>], + Res, + not_found, + Opts + ), + ?assertEqual(<<"ed25519">>, CommitmentType), + CommitmentCommitter = hb_util:deep_get( + [<<"commitments">>, ID, <<"committer">>], + Res, + not_found, + Opts + ), + ?assertEqual(<<"ejhYD9Cw9VCsVik6yGLoclo3CLRvAITHTZamLY_6ro4">>, CommitmentCommitter), + %% Check Data + Data = maps:get(<<"data">>, Res), + ?assertEqual(<<"{\"displayName\":\"Test Hub\",\"description\":\"This is a test hub created in the test suite\",\"externalurl\":\"\",\"image\":\"\"}">>, Data). + %% @doc Test optimistic index ao_dataitem_test() -> _Node = hb_http_server:start_node(#{}), diff --git a/src/include/ar.hrl b/src/include/ar.hrl index 751acef95..636479292 100644 --- a/src/include/ar.hrl +++ b/src/include/ar.hrl @@ -97,10 +97,11 @@ -define(ECDSA_SIGN_ALG, ecdsa). -define(ECDSA_TYPE_BYTE, <<2>>). +-define(ECDSA_KEY_TYPE, {?ECDSA_SIGN_ALG, secp256k1}). -define(EDDSA_SIGN_ALG, eddsa). -define(EDDSA_TYPE_BYTE, <<3>>). --define(ECDSA_KEY_TYPE, {?ECDSA_SIGN_ALG, secp256k1}). +-define(EDDSA_KEY_TYPE, {?EDDSA_SIGN_ALG, ed25519}). %% The default key type used by transactions that do not specify a signature type. -define(DEFAULT_KEY_TYPE, ?RSA_KEY_TYPE).