From aa37f74c4b9024b53eaf6c57ff8f8673abdb236e Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Fri, 2 Oct 2020 11:41:16 +0300 Subject: [PATCH] feat: generalize wallet/private key + yubihsm2 (#75) * feat: generalize wallet/private key * fix: adjust celo tests * YubiHSM2 Support (#76) * feat: support YubiHSM2 --- Cargo.lock | 503 ++++++++++++++++-- ethers-contract/tests/common/mod.rs | 6 +- ethers-contract/tests/contract.rs | 11 +- ethers-core/Cargo.toml | 3 +- ethers-core/src/lib.rs | 14 +- .../src/types/{chainstate => }/block.rs | 0 .../src/types/{chainstate => }/bytes.rs | 0 ethers-core/src/types/chainstate/mod.rs | 26 - ethers-core/src/types/crypto/keys.rs | 348 ------------ ethers-core/src/types/crypto/mod.rs | 1 + ethers-core/src/types/{chainstate => }/ens.rs | 0 ethers-core/src/types/{chainstate => }/log.rs | 0 ethers-core/src/types/mod.rs | 33 +- .../src/types/{crypto => }/signature.rs | 53 +- .../src/types/{chainstate => }/transaction.rs | 0 ethers-core/src/utils/ganache.rs | 22 +- ethers-core/src/utils/mod.rs | 11 + ethers-middleware/src/client.rs | 17 +- ethers-middleware/tests/gas_oracle.rs | 2 +- ethers-middleware/tests/nonce_manager.rs | 4 +- ethers-middleware/tests/signer.rs | 8 +- ethers-middleware/tests/stack.rs | 4 +- ethers-signers/Cargo.toml | 12 +- ethers-signers/src/ledger/app.rs | 6 +- ethers-signers/src/ledger/types.rs | 19 +- ethers-signers/src/lib.rs | 20 +- ethers-signers/src/wallet.rs | 141 ----- .../src/wallet}/hash.rs | 8 +- ethers-signers/src/wallet/mod.rs | 146 +++++ ethers-signers/src/wallet/private_key.rs | 188 +++++++ ethers-signers/src/wallet/yubi.rs | 113 ++++ ethers/Cargo.toml | 1 + ethers/examples/contract.rs | 2 +- ethers/examples/ens.rs | 4 +- ethers/examples/local_signer.rs | 4 +- ethers/examples/sign.rs | 2 +- ethers/examples/yubi.rs | 31 ++ 37 files changed, 1088 insertions(+), 675 deletions(-) rename ethers-core/src/types/{chainstate => }/block.rs (100%) rename ethers-core/src/types/{chainstate => }/bytes.rs (100%) delete mode 100644 ethers-core/src/types/chainstate/mod.rs delete mode 100644 ethers-core/src/types/crypto/keys.rs rename ethers-core/src/types/{chainstate => }/ens.rs (100%) rename ethers-core/src/types/{chainstate => }/log.rs (100%) rename ethers-core/src/types/{crypto => }/signature.rs (82%) rename ethers-core/src/types/{chainstate => }/transaction.rs (100%) delete mode 100644 ethers-signers/src/wallet.rs rename {ethers-core/src/types/crypto => ethers-signers/src/wallet}/hash.rs (92%) create mode 100644 ethers-signers/src/wallet/mod.rs create mode 100644 ethers-signers/src/wallet/private_key.rs create mode 100644 ethers-signers/src/wallet/yubi.rs create mode 100644 ethers/examples/yubi.rs diff --git a/Cargo.lock b/Cargo.lock index 042921f4..f65871ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,6 +10,68 @@ dependencies = [ "regex", ] +[[package]] +name = "addr2line" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b6a2d3371669ab3ca9797670853d61402b03d0b4b9ebf33d677dfa720203072" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" + +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + +[[package]] +name = "aead" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc95d1bdb8e6666b2b217308eeeb09f2d6728d104be3e31916cc74d15420331" +dependencies = [ + "generic-array", +] + +[[package]] +name = "aes" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2bc6d3f370b5666245ff421e231cba4353df936e26986d2918e61a8fd6aef6" +dependencies = [ + "aes-soft", + "aesni", + "block-cipher", +] + +[[package]] +name = "aes-soft" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63dd91889c49327ad7ef3b500fd1109dbd3c509a03db0d4a9ce413b79f575cb6" +dependencies = [ + "block-cipher", + "byteorder", + "opaque-debug", +] + +[[package]] +name = "aesni" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6fe808308bb07d393e2ea47780043ec47683fcf19cf5efc8ca51c50cc8c68a" +dependencies = [ + "block-cipher", + "opaque-debug", +] + [[package]] name = "aho-corasick" version = "0.7.13" @@ -19,6 +81,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "anomaly" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "550632e31568ae1a5f47998c3aa48563030fc49b9ec91913ca337cf64fbc5ccb" +dependencies = [ + "backtrace", +] + [[package]] name = "anyhow" version = "1.0.32" @@ -64,7 +135,7 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d373d78ded7d0b3fa8039375718cde0aace493f2e34fb60f51cbf567562ca801" dependencies = [ - "async-task 4.0.2", + "async-task", "concurrent-queue", "fastrand", "futures-lite", @@ -74,9 +145,9 @@ dependencies = [ [[package]] name = "async-global-executor" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5586e693d02f9b439742e9d5d68bd64d923c6861954f7d78f91001a0e152d589" +checksum = "fefeb39da249f4c33af940b779a56723ce45809ef5c54dad84bb538d4ffb6d9e" dependencies = [ "async-executor", "async-io", @@ -87,9 +158,9 @@ dependencies = [ [[package]] name = "async-io" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33be191d05a54ec120e4667375e2ad49fe506b846463df384460ab801c7ae5dc" +checksum = "6e727cebd055ab2861a854f79def078c4b99ea722d54c6800a0e274389882d4c" dependencies = [ "concurrent-queue", "fastrand", @@ -114,15 +185,14 @@ dependencies = [ [[package]] name = "async-std" -version = "1.6.4" +version = "1.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c92085acfce8b32e5b261d0b59b8f3309aee69fea421ea3f271f8b93225754f" +checksum = "a9fa76751505e8df1c7a77762f60486f60c71bbd9b8557f4da6ad47d083732ed" dependencies = [ "async-attributes", "async-global-executor", "async-io", "async-mutex", - "async-task 3.0.0", "blocking", "crossbeam-utils", "futures-channel", @@ -141,12 +211,6 @@ dependencies = [ "wasm-bindgen-futures", ] -[[package]] -name = "async-task" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17772156ef2829aadc587461c7753af20b7e8db1529bc66855add962a3b35d3" - [[package]] name = "async-task" version = "4.0.2" @@ -167,9 +231,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "687c230d85c0a52504709705fc8a53e4a692b83a2184f03dae73e38e1e93a783" +checksum = "b246867b8b3b6ae56035f1eb1ed557c1d8eae97f0d53696138a50fa0e3a3b8c0" dependencies = [ "proc-macro2", "quote", @@ -206,6 +270,20 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "backtrace" +version = "0.3.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1931848a574faa8f7c71a12ea00453ff5effbb5f51afe7f77d7a48cace6ac1" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "base64" version = "0.11.0" @@ -276,6 +354,25 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-cipher" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f337a3e6da609650eb74e02bc9fac7b735049f7623ab12f2e4c719316fcc7e80" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-modes" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9b14fd8a4739e6548d4b6018696cf991dcf8c6effd9ef9eb33b29b8a650972" +dependencies = [ + "block-cipher", + "block-padding", +] + [[package]] name = "block-padding" version = "0.2.1" @@ -332,6 +429,17 @@ version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef611cc68ff783f18535d77ddd080185275713d852c4f5cbb6122c462a7a825c" +[[package]] +name = "ccm" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbba800a6a55058ecb75c7a42e3d16a715a2b1f1afa9acf07365d6ab30d62ce1" +dependencies = [ + "aead", + "block-cipher", + "subtle", +] + [[package]] name = "cfg-if" version = "0.1.10" @@ -340,17 +448,28 @@ checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" [[package]] name = "chrono" -version = "0.4.18" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d021fddb7bd3e734370acfa4a83f34095571d8570c039f1420d77540f68d5772" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" dependencies = [ "libc", "num-integer", "num-traits", + "serde", "time", "winapi 0.3.9", ] +[[package]] +name = "cmac" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5220604fe5c112e2851b00da795c72cbb71bf112f2cbd532bdcfb4106eeb320b" +dependencies = [ + "crypto-mac", + "dbl", +] + [[package]] name = "coins-ledger" version = "0.1.0" @@ -417,6 +536,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" +[[package]] +name = "crc32fast" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-utils" version = "0.7.2" @@ -440,6 +568,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58bcd97a54c7ca5ce2f6eb16f6bede5b0ab5f0055fedc17d2f0b4466e21671ca" dependencies = [ + "block-cipher", "generic-array", "subtle", ] @@ -474,6 +603,28 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "curve25519-dalek" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8492de420e9e60bc9a1d66e2dbb91825390b738a388606600663fc529b4b307" +dependencies = [ + "byteorder", + "digest", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "dbl" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2735145c3b9ba15f2d7a3ae8cdafcbc8c98a7bef7f62afe9d08bd99fbf7130de" +dependencies = [ + "generic-array", +] + [[package]] name = "digest" version = "0.9.0" @@ -491,15 +642,38 @@ checksum = "134951f4028bdadb9b84baf4232681efbf277da25144b9b0ad65df75946c422b" [[package]] name = "ecdsa" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a7c278cc833033b4322122e05b99dbc5a2a2a9bb2c51534ff1f4899c7802d66" +checksum = "765a6f9c0938fef990bb7af306fe1e534721bc52a1eb918cc938c5212d6e07eb" dependencies = [ "elliptic-curve", "hmac", "signature", ] +[[package]] +name = "ed25519" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07dfc993ea376e864fe29a4099a61ca0bb994c6d7745a61bf60ddb3d64e05237" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand", + "serde", + "sha2", + "zeroize", +] + [[package]] name = "either" version = "1.6.1" @@ -657,7 +831,6 @@ dependencies = [ "rustc-hex", "serde", "serde_json", - "sha2", "thiserror", "tiny-keccak 2.0.2", ] @@ -712,14 +885,18 @@ version = "0.1.3" dependencies = [ "async-trait", "coins-ledger", + "elliptic-curve", "ethers", "ethers-core", "futures-util", + "rand", "rustc-hex", "serde", "serde_json", + "sha2", "thiserror", "tokio", + "yubihsm", ] [[package]] @@ -745,6 +922,18 @@ dependencies = [ "subtle", ] +[[package]] +name = "filetime" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed85775dcc68644b5c950ac06a2b23768d3bc9390464151aaf27136998dcf9e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "winapi 0.3.9", +] + [[package]] name = "fixed-hash" version = "0.6.1" @@ -937,6 +1126,12 @@ dependencies = [ "wasi 0.9.0+wasi-snapshot-preview1", ] +[[package]] +name = "gimli" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf91faf136cb47367fa430cd46e37a788775e7fa104f8b4bcb3861dc389b724" + [[package]] name = "glob" version = "0.3.0" @@ -987,10 +1182,16 @@ dependencies = [ ] [[package]] -name = "hashbrown" -version = "0.9.0" +name = "harp" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00d63df3d41950fb462ed38308eea019113ad1508da725bbedcd0fa5a85ef5f7" +checksum = "60bf12c625ed5e96f81609ae4377c34e9fa3e4d1fada392404322daeace511ab" + +[[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" [[package]] name = "hermit-abi" @@ -1184,12 +1385,14 @@ dependencies = [ [[package]] name = "k256" -version = "0.5.5" -source = "git+https://github.com/RustCrypto/elliptic-curves#8845cc09dc55ba3fa41ffa2750d799cf6a23d405" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be18b15cf00d98162fc37081a5098647f41168604d96174726fb1dff59c18a22" dependencies = [ "cfg-if", "ecdsa", "elliptic-curve", + "sha2", "sha3", ] @@ -1226,9 +1429,41 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.77" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f96b10ec2560088a8e76961b00d47107b3a625fecb76dedb29ee7ccbf98235" +checksum = "aa7087f49d294270db4e1928fc110c976cd4b9e5a16348e0a1df09afa99e6c98" + +[[package]] +name = "libflate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9bac9023e1db29c084f9f8cd9d3852e5e8fddf98fb47c4964a0ea4663d95949" +dependencies = [ + "adler32", + "crc32fast", + "libflate_lz77", + "rle-decode-fast", +] + +[[package]] +name = "libflate_lz77" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3286f09f7d4926fc486334f28d8d2e6ebe4f7f9994494b6dab27ddfad2c9b11b" + +[[package]] +name = "libusb1-sys" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f02e930161703cc97c0aab3a905feb9740db03a80910f31ab0f8fa309223f39" +dependencies = [ + "cc", + "libc", + "libflate", + "pkg-config", + "tar", + "vcpkg", +] [[package]] name = "libz-sys" @@ -1279,6 +1514,16 @@ dependencies = [ "unicase", ] +[[package]] +name = "miniz_oxide" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c60c0dfe32c10b43a144bad8fc83538c52f58302c92300ea7ec7bf7b38d5a7b9" +dependencies = [ + "adler", + "autocfg", +] + [[package]] name = "mio" version = "0.6.22" @@ -1330,9 +1575,9 @@ dependencies = [ [[package]] name = "nb-connect" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e847c76b390f44529c2071ef06d0b52fbb4bdb04cc8987a5cfa63954c000abca" +checksum = "701f47aeb98466d0a7fea67e2c2f667c33efa1f2e4fd7f76743aac1153196f72" dependencies = [ "libc", "winapi 0.3.9", @@ -1391,6 +1636,12 @@ dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ab52be62400ca80aa00285d25253d7f7c437b7375c4de678f5405d3afe82ca5" + [[package]] name = "once_cell" version = "1.4.1" @@ -1436,6 +1687,27 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "p256" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1909a5a44a469f7d21c8dec2c634721ba15379b6cad80040d16abfaa8609b37c" +dependencies = [ + "ecdsa", + "elliptic-curve", + "sha2", +] + +[[package]] +name = "p384" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51ce2b1182d022bfbe2af57aeca44fe7832356dd2d998e98e423cd9b94fae4f7" +dependencies = [ + "ecdsa", + "elliptic-curve", +] + [[package]] name = "parity-scale-codec" version = "1.3.5" @@ -1454,6 +1726,15 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" +[[package]] +name = "pbkdf2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7170d73bf11f39b4ce1809aabc95bf5c33564cdc16fc3200ddda17a5f6e5e48b" +dependencies = [ + "crypto-mac", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -1462,18 +1743,18 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] name = "pin-project" -version = "0.4.24" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f48fad7cfbff853437be7cf54d7b993af21f53be7f0988cbfe4a51535aa77205" +checksum = "2b9e280448854bd91559252582173b3bd1f8e094a0e644791c0628ca9b1f144f" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "0.4.24" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24c6d293bdd3ca5a1697997854c6cf7855e43fb6a0ba1c47af57a5bcafd158ae" +checksum = "c8c8b352676bc6a4c3d71970560b913cea444a7a921cc2e2d920225e4b91edaa" dependencies = [ "proc-macro2", "quote", @@ -1482,9 +1763,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.1.8" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71f349a4f0e70676ffb2dbafe16d0c992382d02f0a952e3ddf584fc289dac6b3" +checksum = "e555d9e657502182ac97b539fb3dae8b79cda19e3e4f8ffb5e8de4f18df93c95" [[package]] name = "pin-utils" @@ -1544,9 +1825,9 @@ checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a" [[package]] name = "proc-macro2" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51ef7cd2518ead700af67bf9d1a658d90b6037d77110fd9c0445429d0ba1c6c9" +checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" dependencies = [ "unicode-xid", ] @@ -1699,14 +1980,36 @@ dependencies = [ ] [[package]] -name = "rlp" -version = "0.4.5" +name = "rle-decode-fast" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a7d3f9bed94764eac15b8f14af59fac420c236adaff743b7bcc88e265cb4345" +checksum = "cabe4fa914dec5870285fa7f71f602645da47c486e68486d2b4ceb4a343e90ac" + +[[package]] +name = "rlp" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1190dcc8c3a512f1eef5d09bb8c84c7f39e1054e174d1795482e18f5272f2e73" dependencies = [ "rustc-hex", ] +[[package]] +name = "rusb" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67fa037368ee577fca9ef237c5ec129084c18e7e3e5987cc611fb8b2d78cf84a" +dependencies = [ + "libc", + "libusb1-sys", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" + [[package]] name = "rustc-hex" version = "2.1.0" @@ -1822,9 +2125,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "164eacbdb13512ec2745fb09d51fd5b22b0d65ed294a1dcf7285a360c80a675c" +checksum = "a230ea9107ca2220eea9d46de97eddcb04cd00e92d13dda78e478dd33fa82bd4" dependencies = [ "itoa", "ryu", @@ -1889,6 +2192,19 @@ checksum = "29f060a7d147e33490ec10da418795238fd7545bba241504d6b31a409f2e6210" dependencies = [ "digest", "rand_core", + "signature_derive", +] + +[[package]] +name = "signature_derive" +version = "1.0.0-pre.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0656c180103507cca4b82519908e813225b2f6b90b2bd59ee119f46155ae872" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", ] [[package]] @@ -1938,6 +2254,30 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "synstructure" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "tar" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "489997b7557e9a43e192c527face4feacc78bfbe6eed67fd55c4c9e381cba290" +dependencies = [ + "filetime", + "libc", + "redox_syscall", + "xattr", +] + [[package]] name = "tempfile" version = "3.1.0" @@ -2089,20 +2429,21 @@ checksum = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" [[package]] name = "tracing" -version = "0.1.19" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d79ca061b032d6ce30c660fded31189ca0b9922bf483cd70759f13a2d86786c" +checksum = "b0987850db3733619253fe60e17cb59b82d37c7e6c0236bb81e4d6b87c879f27" dependencies = [ "cfg-if", "log", + "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bcf46c1f1f06aeea2d6b81f3c863d0930a596c86ad1920d4e5bad6dd1d7119a" +checksum = "f50de3927f93d202783f4513cda820ab47ef17f624b03c096e86ef00c67e6b5f" dependencies = [ "lazy_static", ] @@ -2207,6 +2548,15 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05e42f7c18b8f902290b009cde6d651262f956c98bc51bca4cd1d511c9cd85c7" +[[package]] +name = "uuid" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11" +dependencies = [ + "serde", +] + [[package]] name = "vcpkg" version = "0.2.10" @@ -2424,8 +2774,67 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" +[[package]] +name = "xattr" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c" +dependencies = [ + "libc", +] + +[[package]] +name = "yubihsm" +version = "0.35.0-rc" +source = "git+https://github.com/iqlusioninc/yubihsm.rs.git?branch=develop#a8fa868fbc50b8a326c46ab2b178f42e3e7a4e88" +dependencies = [ + "aes", + "anomaly", + "bitflags", + "block-modes", + "ccm", + "chrono", + "cmac", + "digest", + "ecdsa", + "ed25519", + "ed25519-dalek", + "harp", + "hmac", + "k256", + "log", + "p256", + "p384", + "pbkdf2", + "rand_core", + "rusb", + "serde", + "serde_json", + "sha2", + "signature", + "subtle", + "thiserror", + "uuid", + "zeroize", +] + [[package]] name = "zeroize" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f33972566adbd2d3588b0491eb94b98b43695c4ef897903470ede4f3f5a28a" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f369ddb18862aba61aa49bf31e74d29f0f162dec753063200e1dc084345d16" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] diff --git a/ethers-contract/tests/common/mod.rs b/ethers-contract/tests/common/mod.rs index 6697a240..582268cc 100644 --- a/ethers-contract/tests/common/mod.rs +++ b/ethers-contract/tests/common/mod.rs @@ -7,7 +7,7 @@ use ethers_contract::{Contract, ContractFactory}; use ethers_core::utils::{GanacheInstance, Solc}; use ethers_middleware::Client; use ethers_providers::{Http, Middleware, Provider}; -use ethers_signers::Wallet; +use ethers_signers::LocalWallet; use std::{convert::TryFrom, sync::Arc, time::Duration}; // Note: We also provide the `abigen` macro for generating these bindings automatically @@ -44,14 +44,14 @@ pub fn compile_contract(name: &str, filename: &str) -> (Abi, Bytes) { (contract.abi.clone(), contract.bytecode.clone()) } -type HttpWallet = Client, Wallet>; +type HttpWallet = Client, LocalWallet>; /// connects the private key to http://localhost:8545 pub fn connect(ganache: &GanacheInstance, idx: usize) -> Arc { let provider = Provider::::try_from(ganache.endpoint()) .unwrap() .interval(Duration::from_millis(10u64)); - let wallet: Wallet = ganache.keys()[idx].clone().into(); + let wallet: LocalWallet = ganache.keys()[idx].clone().into(); Arc::new(Client::new(provider, wallet)) } diff --git a/ethers-contract/tests/contract.rs b/ethers-contract/tests/contract.rs index a030b8df..11625ebf 100644 --- a/ethers-contract/tests/contract.rs +++ b/ethers-contract/tests/contract.rs @@ -322,14 +322,15 @@ mod eth_tests { assert_eq!(return_data.2, multicall_contract.address()); assert_eq!(return_data.3, multicall_contract.address()); + let addrs = ganache.addresses(); // query ETH balances of multiple addresses // these keys haven't been used to do any tx // so should have 100 ETH multicall .clear_calls() - .eth_balance_of(Address::from(&ganache.keys()[4])) - .eth_balance_of(Address::from(&ganache.keys()[5])) - .eth_balance_of(Address::from(&ganache.keys()[6])); + .eth_balance_of(addrs[4]) + .eth_balance_of(addrs[5]) + .eth_balance_of(addrs[6]); let balances: (U256, U256, U256) = multicall.call().await.unwrap(); assert_eq!(balances.0, U256::from(100000000000000000000u128)); assert_eq!(balances.1, U256::from(100000000000000000000u128)); @@ -343,7 +344,7 @@ mod celo_tests { use ethers::{ middleware::Client, providers::{Http, Provider}, - signers::Wallet, + signers::LocalWallet, types::BlockNumber, }; use std::{convert::TryFrom, sync::Arc, time::Duration}; @@ -359,7 +360,7 @@ mod celo_tests { // Funded with https://celo.org/developers/faucet let wallet = "d652abb81e8c686edba621a895531b1f291289b63b5ef09a94f686a5ecdd5db1" - .parse::() + .parse::() .unwrap(); let client = Client::new(provider, wallet); diff --git a/ethers-core/Cargo.toml b/ethers-core/Cargo.toml index 818a74e2..6b3d5caa 100644 --- a/ethers-core/Cargo.toml +++ b/ethers-core/Cargo.toml @@ -20,10 +20,9 @@ arrayvec = { version = "0.5.1", default-features = false } ecdsa = { version = "0.8.0", features = ["std"] } elliptic-curve = { version = "0.6.1", features = ["arithmetic"] } generic-array = "0.14.4" -k256 = { git = "https://github.com/RustCrypto/elliptic-curves", version = "0.5.2", features = ["keccak256", "ecdsa"] } +k256 = { version = "0.5.2", features = ["keccak256", "ecdsa"] } rand = "0.7.2" tiny-keccak = { version = "2.0.2", default-features = false } -sha2 = { version = "0.9.1" } # misc serde = { version = "1.0.110", default-features = false, features = ["derive"] } diff --git a/ethers-core/src/lib.rs b/ethers-core/src/lib.rs index 42415671..3a4d3ff7 100644 --- a/ethers-core/src/lib.rs +++ b/ethers-core/src/lib.rs @@ -13,19 +13,21 @@ //! signing the hash of the result. //! //! ```rust -//! use ethers::core::types::{PrivateKey, Address}; +//! # async fn foo() -> Result<(), Box> { +//! use ethers::signers::{Signer, LocalWallet}; //! //! let message = "Some data"; -//! let key = PrivateKey::new(&mut rand::thread_rng()); -//! let address = Address::from(&key); +//! let wallet = LocalWallet::new(&mut rand::thread_rng()); //! //! // Sign the message -//! let signature = key.sign(message); +//! let signature = wallet.sign_message(message).await?; //! //! // Recover the signer from the message -//! let recovered = signature.recover(message).unwrap(); +//! let recovered = signature.recover(message)?; //! -//! assert_eq!(recovered, address); +//! assert_eq!(recovered, wallet.address()); +//! # Ok(()) +//! # } //! ``` //! //! ## Utilities diff --git a/ethers-core/src/types/chainstate/block.rs b/ethers-core/src/types/block.rs similarity index 100% rename from ethers-core/src/types/chainstate/block.rs rename to ethers-core/src/types/block.rs diff --git a/ethers-core/src/types/chainstate/bytes.rs b/ethers-core/src/types/bytes.rs similarity index 100% rename from ethers-core/src/types/chainstate/bytes.rs rename to ethers-core/src/types/bytes.rs diff --git a/ethers-core/src/types/chainstate/mod.rs b/ethers-core/src/types/chainstate/mod.rs deleted file mode 100644 index 1dc25793..00000000 --- a/ethers-core/src/types/chainstate/mod.rs +++ /dev/null @@ -1,26 +0,0 @@ -pub type Selector = [u8; 4]; - -// Re-export common ethereum datatypes with more specific names - -/// A transaction Hash -pub use ethereum_types::H256 as TxHash; - -pub use ethereum_types::{Address, Bloom, H160, H256, U128, U256, U64}; - -mod transaction; -pub use transaction::{Transaction, TransactionReceipt, TransactionRequest}; - -mod bytes; -pub use bytes::Bytes; - -mod block; -pub use block::{Block, BlockId, BlockNumber}; - -#[cfg(feature = "celo")] -pub use block::Randomness; - -mod log; -pub use log::{Filter, Log, ValueOrArray}; - -mod ens; -pub use ens::NameOrAddress; diff --git a/ethers-core/src/types/crypto/keys.rs b/ethers-core/src/types/crypto/keys.rs deleted file mode 100644 index 2f4a78b5..00000000 --- a/ethers-core/src/types/crypto/keys.rs +++ /dev/null @@ -1,348 +0,0 @@ -use super::hash::Sha256Proxy; -use crate::{ - types::{Address, Signature, TransactionRequest, H256}, - utils::{hash_message, keccak256}, -}; - -use rand::{CryptoRng, Rng}; -use rustc_hex::FromHex; -use serde::{ - de::Error as DeserializeError, - de::{SeqAccess, Visitor}, - ser::SerializeTuple, - Deserialize, Deserializer, Serialize, Serializer, -}; -use std::{fmt, ops::Deref, str::FromStr}; - -use k256::{ - ecdsa::{ - recoverable::{Id as RecoveryId, Signature as RecoverableSignature}, - signature::DigestSigner, - SigningKey, - }, - elliptic_curve::{error::Error as EllipticCurveError, FieldBytes}, - EncodedPoint as K256PublicKey, Secp256k1, SecretKey as K256SecretKey, -}; - -const SECRET_KEY_SIZE: usize = 32; -const COMPRESSED_PUBLIC_KEY_SIZE: usize = 33; - -/// A private key on Secp256k1 -#[derive(Clone, Debug)] -pub struct PrivateKey(pub(super) K256SecretKey); - -impl PartialEq for PrivateKey { - fn eq(&self, other: &Self) -> bool { - self.0.to_bytes().eq(&other.0.to_bytes()) - } -} - -impl Serialize for PrivateKey { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let mut seq = serializer.serialize_tuple(SECRET_KEY_SIZE)?; - for e in self.0.to_bytes() { - seq.serialize_element(&e)?; - } - seq.end() - } -} - -impl<'de> Deserialize<'de> for PrivateKey { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let bytes = <[u8; SECRET_KEY_SIZE]>::deserialize(deserializer)?; - Ok(PrivateKey( - K256SecretKey::from_bytes(&bytes).map_err(DeserializeError::custom)?, - )) - } -} - -impl FromStr for PrivateKey { - type Err = EllipticCurveError; - - fn from_str(src: &str) -> Result { - let src = src - .from_hex::>() - .expect("invalid hex when reading PrivateKey"); - let sk = K256SecretKey::from_bytes(&src)?; - Ok(PrivateKey(sk)) - } -} - -impl PrivateKey { - pub fn new(rng: &mut R) -> Self { - PrivateKey(K256SecretKey::random(rng)) - } - - /// Sign arbitrary string data. - /// - /// The data is UTF-8 encoded and enveloped the same way as with - /// `hash_message`. The returned signed data's signature is in 'Electrum' - /// notation, that is the recovery value `v` is either `27` or `28` (as - /// opposed to the standard notation where `v` is either `0` or `1`). This - /// is important to consider when using this signature with other crates. - pub fn sign(&self, message: S) -> Signature - where - S: AsRef<[u8]>, - { - let message = message.as_ref(); - let message_hash = hash_message(message); - - self.sign_hash_with_eip155(message_hash, None) - } - - /// RLP encodes and then signs the stransaction. - /// - /// If no chain_id is provided, then EIP-155 is not used. - /// - /// This will return an error if called if any of the `nonce`, `gas_price` or `gas` - /// fields are not populated. - /// - /// # Panics - /// - /// If `tx.to` is an ENS name. The caller MUST take care of name resolution before - /// calling this function. - pub fn sign_transaction(&self, tx: &TransactionRequest, chain_id: Option) -> Signature { - let sighash = tx.sighash(chain_id); - self.sign_hash_with_eip155(sighash, chain_id) - } - - fn sign_hash_with_eip155(&self, hash: H256, chain_id: Option) -> Signature { - let signing_key = SigningKey::new(&self.0.to_bytes()).expect("invalid secret key"); - - let recoverable_sig: RecoverableSignature = - signing_key.sign_digest(Sha256Proxy::from(hash)); - - let v = to_eip155_v(recoverable_sig.recovery_id(), chain_id); - - let r_bytes: FieldBytes = recoverable_sig.r().into(); - let s_bytes: FieldBytes = recoverable_sig.s().into(); - let r = H256::from_slice(&r_bytes.as_slice()); - let s = H256::from_slice(&s_bytes.as_slice()); - - Signature { r, s, v } - } -} - -/// Applies [EIP155](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md) -fn to_eip155_v(recovery_id: RecoveryId, chain_id: Option) -> u64 { - let standard_v: u8 = recovery_id.into(); - if let Some(chain_id) = chain_id { - // When signing with a chain ID, add chain replay protection. - (standard_v as u64) + 35 + chain_id * 2 - } else { - // Otherwise, convert to 'Electrum' notation. - (standard_v as u64) + 27 - } -} - -impl Deref for PrivateKey { - type Target = K256SecretKey; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -/// A secp256k1 Public Key -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct PublicKey(pub(super) K256PublicKey); - -impl From for PublicKey { - /// Gets the public address of a private key. - fn from(src: K256PublicKey) -> PublicKey { - PublicKey(src) - } -} - -impl From<&PrivateKey> for PublicKey { - /// Gets the public address of a private key. - fn from(src: &PrivateKey) -> PublicKey { - let public_key = K256PublicKey::from_secret_key(src, false); - PublicKey(public_key) - } -} - -/// Gets the address of a public key. -/// -/// The public address is defined as the low 20 bytes of the keccak hash of -/// the public key. Note that the public key returned from the `secp256k1` -/// crate is 65 bytes long, that is because it is prefixed by `0x04` to -/// indicate an uncompressed public key; this first byte is ignored when -/// computing the hash. -impl From<&PublicKey> for Address { - fn from(src: &PublicKey) -> Address { - let public_key = src.0.as_bytes(); - - debug_assert_eq!(public_key[0], 0x04); - let hash = keccak256(&public_key[1..]); - - Address::from_slice(&hash[12..]) - } -} - -impl From for Address { - fn from(src: PublicKey) -> Address { - Address::from(&src) - } -} - -impl From<&PrivateKey> for Address { - fn from(src: &PrivateKey) -> Address { - let public_key = PublicKey::from(src); - Address::from(&public_key) - } -} - -impl From for Address { - fn from(src: PrivateKey) -> Address { - Address::from(&src) - } -} - -impl Serialize for PublicKey { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let mut seq = serializer.serialize_tuple(COMPRESSED_PUBLIC_KEY_SIZE)?; - - for e in self.0.compress().as_bytes().iter() { - seq.serialize_element(e)?; - } - seq.end() - } -} - -impl<'de> Deserialize<'de> for PublicKey { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct ArrayVisitor; - - impl<'de> Visitor<'de> for ArrayVisitor { - type Value = PublicKey; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a valid proof") - } - - fn visit_seq(self, mut seq: S) -> Result - where - S: SeqAccess<'de>, - { - let mut bytes = [0u8; COMPRESSED_PUBLIC_KEY_SIZE]; - for b in &mut bytes[..] { - *b = seq - .next_element()? - .ok_or_else(|| DeserializeError::custom("could not read bytes"))?; - } - - let pub_key = K256PublicKey::from_bytes(&bytes[..]) - .map_err(|_| DeserializeError::custom("parse pub key"))?; - - let uncompressed_pub_key = pub_key.decompress(); - if uncompressed_pub_key.is_some().into() { - Ok(PublicKey(uncompressed_pub_key.unwrap())) - } else { - Err(DeserializeError::custom("parse pub key")) - } - } - } - - deserializer.deserialize_tuple(COMPRESSED_PUBLIC_KEY_SIZE, ArrayVisitor) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn serde() { - for _ in 0..10 { - let key = PrivateKey::new(&mut rand::thread_rng()); - let serialized = bincode::serialize(&key).unwrap(); - assert_eq!(serialized.as_slice(), key.0.to_bytes().as_slice()); - let de: PrivateKey = bincode::deserialize(&serialized).unwrap(); - assert_eq!(key, de); - - let public = PublicKey::from(&key); - println!("public = {:?}", public); - - let serialized = bincode::serialize(&public).unwrap(); - let de: PublicKey = bincode::deserialize(&serialized).unwrap(); - assert_eq!(public, de); - } - } - - #[test] - #[cfg(not(feature = "celo"))] - fn signs_tx() { - use crate::types::Address; - // retrieved test vector from: - // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction - let tx = TransactionRequest { - from: None, - to: Some( - "F0109fC8DF283027b6285cc889F5aA624EaC1F55" - .parse::
() - .unwrap() - .into(), - ), - value: Some(1_000_000_000.into()), - gas: Some(2_000_000.into()), - nonce: Some(0.into()), - gas_price: Some(21_000_000_000u128.into()), - data: None, - }; - let chain_id = 1; - - let key: PrivateKey = "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318" - .parse() - .unwrap(); - - let sig = key.sign_transaction(&tx, Some(chain_id)); - let sighash = tx.sighash(Some(chain_id)); - assert!(sig.verify(sighash, Address::from(key)).is_ok()); - } - - #[test] - fn key_to_address() { - let priv_key: PrivateKey = - "0000000000000000000000000000000000000000000000000000000000000001" - .parse() - .unwrap(); - let addr: Address = priv_key.into(); - assert_eq!( - addr, - Address::from_str("7E5F4552091A69125d5DfCb7b8C2659029395Bdf").expect("Decoding failed") - ); - - let priv_key: PrivateKey = - "0000000000000000000000000000000000000000000000000000000000000002" - .parse() - .unwrap(); - let addr: Address = priv_key.into(); - assert_eq!( - addr, - Address::from_str("2B5AD5c4795c026514f8317c7a215E218DcCD6cF").expect("Decoding failed") - ); - - let priv_key: PrivateKey = - "0000000000000000000000000000000000000000000000000000000000000003" - .parse() - .unwrap(); - let addr: Address = priv_key.into(); - assert_eq!( - addr, - Address::from_str("6813Eb9362372EEF6200f3b1dbC3f819671cBA69").expect("Decoding failed") - ); - } -} diff --git a/ethers-core/src/types/crypto/mod.rs b/ethers-core/src/types/crypto/mod.rs index d1bd89fa..9264e81d 100644 --- a/ethers-core/src/types/crypto/mod.rs +++ b/ethers-core/src/types/crypto/mod.rs @@ -5,3 +5,4 @@ mod signature; pub use signature::Signature; mod hash; +pub use hash::Sha256Proxy; diff --git a/ethers-core/src/types/chainstate/ens.rs b/ethers-core/src/types/ens.rs similarity index 100% rename from ethers-core/src/types/chainstate/ens.rs rename to ethers-core/src/types/ens.rs diff --git a/ethers-core/src/types/chainstate/log.rs b/ethers-core/src/types/log.rs similarity index 100% rename from ethers-core/src/types/chainstate/log.rs rename to ethers-core/src/types/log.rs diff --git a/ethers-core/src/types/mod.rs b/ethers-core/src/types/mod.rs index 06a83666..3e2dcd4d 100644 --- a/ethers-core/src/types/mod.rs +++ b/ethers-core/src/types/mod.rs @@ -1,6 +1,29 @@ -//! Ethereum related datatypes -mod crypto; -pub use crypto::*; +pub type Selector = [u8; 4]; -mod chainstate; -pub use chainstate::*; +// Re-export common ethereum datatypes with more specific names + +/// A transaction Hash +pub use ethereum_types::H256 as TxHash; + +pub use ethereum_types::{Address, Bloom, H160, H256, U128, U256, U64}; + +mod transaction; +pub use transaction::{Transaction, TransactionReceipt, TransactionRequest}; + +mod bytes; +pub use bytes::Bytes; + +mod block; +pub use block::{Block, BlockId, BlockNumber}; + +#[cfg(feature = "celo")] +pub use block::Randomness; + +mod log; +pub use log::{Filter, Log, ValueOrArray}; + +mod ens; +pub use ens::NameOrAddress; + +mod signature; +pub use signature::*; diff --git a/ethers-core/src/types/crypto/signature.rs b/ethers-core/src/types/signature.rs similarity index 82% rename from ethers-core/src/types/crypto/signature.rs rename to ethers-core/src/types/signature.rs index 0de3cb43..43e7bb1d 100644 --- a/ethers-core/src/types/crypto/signature.rs +++ b/ethers-core/src/types/signature.rs @@ -1,6 +1,6 @@ // Code adapted from: https://github.com/tomusdrw/rust-web3/blob/master/src/api/accounts.rs use crate::{ - types::{Address, PublicKey, H256}, + types::{Address, H256}, utils::hash_message, }; @@ -106,7 +106,10 @@ impl Signature { let uncompressed_pub_key = K256PublicKey::from(&verify_key).decompress(); if uncompressed_pub_key.is_some().into() { - Ok(PublicKey::from(uncompressed_pub_key.unwrap()).into()) + let public_key = uncompressed_pub_key.unwrap().to_bytes(); + debug_assert_eq!(public_key[0], 0x04); + let hash = crate::utils::keccak256(&public_key[1..]); + Ok(Address::from_slice(&hash[12..])) } else { Err(SignatureError::RecoveryError) } @@ -245,55 +248,17 @@ impl From for RecoveryMessage { #[cfg(test)] mod tests { use super::*; - use crate::types::PrivateKey; - - #[test] - fn recover_signature_from_message() { - let message = "Some data"; - let hash = hash_message(message); - let key = PrivateKey::new(&mut rand::thread_rng()); - let address = Address::from(&key); - - // sign a message - let signature = key.sign(message); - - // ecrecover via the message will hash internally - let recovered = signature.recover(message).unwrap(); - - // if provided with a hash, it will skip hashing - let recovered2 = signature.recover(hash).unwrap(); - - // verifies the signature is produced by `address` - signature.verify(message, address).unwrap(); - - assert_eq!(recovered, address); - assert_eq!(recovered2, address); - } #[test] fn recover_web3_signature() { // test vector taken from: // https://web3js.readthedocs.io/en/v1.2.2/web3-eth-accounts.html#sign - let key: PrivateKey = "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318" - .parse() - .unwrap(); - let address = Address::from(&key); - let our_signature = key.sign("Some data"); let signature = Signature::from_str( "b91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a0291c" ).expect("could not parse signature"); - assert_eq!(our_signature.recover("Some data").unwrap(), address,); - assert_eq!(signature.recover("Some data").unwrap(), address); - assert_eq!(our_signature, signature); - } - - #[test] - fn to_vec() { - let message = "Some data"; - let key = PrivateKey::new(&mut rand::thread_rng()); - let signature = key.sign(message); - let serialized = signature.to_vec(); - let de = Signature::try_from(&serialized[..]).unwrap(); - assert_eq!(signature, de); + assert_eq!( + signature.recover("Some data").unwrap(), + Address::from_str("2c7536E3605D9C16a7a3D7b1898e529396a65c23").unwrap() + ); } } diff --git a/ethers-core/src/types/chainstate/transaction.rs b/ethers-core/src/types/transaction.rs similarity index 100% rename from ethers-core/src/types/chainstate/transaction.rs rename to ethers-core/src/types/transaction.rs diff --git a/ethers-core/src/utils/ganache.rs b/ethers-core/src/utils/ganache.rs index 32926ee6..9af063c4 100644 --- a/ethers-core/src/utils/ganache.rs +++ b/ethers-core/src/utils/ganache.rs @@ -1,4 +1,6 @@ -use crate::types::PrivateKey; +use crate::{types::Address, utils::secret_key_to_address}; +use k256::{ecdsa::SigningKey, SecretKey as K256SecretKey}; +use rustc_hex::FromHex; use std::{ io::{BufRead, BufReader}, net::TcpListener, @@ -14,16 +16,22 @@ const GANACHE_STARTUP_TIMEOUT_MILLIS: u64 = 10_000; /// Construct this using [`Ganache`](crate::utils::Ganache) pub struct GanacheInstance { pid: Child, - private_keys: Vec, + private_keys: Vec, + addresses: Vec
, port: u16, } impl GanacheInstance { /// Returns the private keys used to instantiate this instance - pub fn keys(&self) -> &[PrivateKey] { + pub fn keys(&self) -> &[K256SecretKey] { &self.private_keys } + /// Returns the addresses used to instantiate this instance + pub fn addresses(&self) -> &[Address] { + &self.addresses + } + /// Returns the port of this instance pub fn port(&self) -> u16 { self.port @@ -130,6 +138,7 @@ impl Ganache { let mut reader = BufReader::new(stdout); let mut private_keys = Vec::new(); + let mut addresses = Vec::new(); let mut is_private_key = false; loop { if start + Duration::from_millis(GANACHE_STARTUP_TIMEOUT_MILLIS) <= Instant::now() { @@ -150,7 +159,11 @@ impl Ganache { if is_private_key && line.starts_with('(') { let key_str = &line[6..line.len() - 1]; - let key: PrivateKey = key_str.parse().expect("did not get private key"); + let key_hex = key_str + .from_hex::>() + .expect("could not parse as hex"); + let key = K256SecretKey::from_bytes(&key_hex).expect("did not get private key"); + addresses.push(secret_key_to_address(&SigningKey::from(&key))); private_keys.push(key); } } @@ -160,6 +173,7 @@ impl Ganache { GanacheInstance { pid: child, private_keys, + addresses, port, } } diff --git a/ethers-core/src/utils/mod.rs b/ethers-core/src/utils/mod.rs index dbb540ef..4a2404c9 100644 --- a/ethers-core/src/utils/mod.rs +++ b/ethers-core/src/utils/mod.rs @@ -18,6 +18,7 @@ pub use hash::{hash_message, id, keccak256, serialize}; pub use rlp; use crate::types::{Address, Bytes, U256}; +use k256::{ecdsa::SigningKey, EncodedPoint as K256PublicKey}; use std::convert::TryInto; /// 1 Ether = 1e18 Wei == 0x0de0b6b3a7640000 Wei @@ -100,6 +101,16 @@ pub fn get_create2_address( Address::from(bytes) } +/// Converts a K256 SigningKey to an Ethereum Address +pub fn secret_key_to_address(secret_key: &SigningKey) -> Address { + // TODO: Can we do this in a better way? + let uncompressed_pub_key = K256PublicKey::from(&secret_key.verify_key()).decompress(); + let public_key = uncompressed_pub_key.unwrap().to_bytes(); + debug_assert_eq!(public_key[0], 0x04); + let hash = keccak256(&public_key[1..]); + Address::from_slice(&hash[12..]) +} + #[cfg(test)] mod tests { use super::*; diff --git a/ethers-middleware/src/client.rs b/ethers-middleware/src/client.rs index 1ff27042..669463dc 100644 --- a/ethers-middleware/src/client.rs +++ b/ethers-middleware/src/client.rs @@ -1,5 +1,3 @@ -use ethers_signers::Signer; - use ethers_core::{ types::{ Address, BlockNumber, Bytes, NameOrAddress, Signature, Transaction, TransactionRequest, @@ -7,7 +5,8 @@ use ethers_core::{ }, utils::keccak256, }; -use ethers_providers::Middleware; +use ethers_providers::{FromErr, Middleware}; +use ethers_signers::Signer; use async_trait::async_trait; use futures_util::{future::ok, join}; @@ -23,7 +22,7 @@ use thiserror::Error; /// ```no_run /// use ethers::{ /// providers::{Middleware, Provider, Http}, -/// signers::Wallet, +/// signers::LocalWallet, /// middleware::Client, /// types::{Address, TransactionRequest}, /// }; @@ -35,7 +34,7 @@ use thiserror::Error; /// /// // Transactions will be signed with the private key below and will be broadcast /// // via the eth_sendRawTransaction API) -/// let wallet: Wallet = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc" +/// let wallet: LocalWallet = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc" /// .parse()?; /// /// let mut client = Client::new(provider, wallet); @@ -55,7 +54,7 @@ use thiserror::Error; /// let receipt = client.pending_transaction(tx_hash).confirmations(6).await?; /// /// // You can connect with other wallets at runtime via the `with_signer` function -/// let wallet2: Wallet = "cd8c407233c0560f6de24bb2dc60a8b02335c959a1a17f749ce6c1ccf63d74a7" +/// let wallet2: LocalWallet = "cd8c407233c0560f6de24bb2dc60a8b02335c959a1a17f749ce6c1ccf63d74a7" /// .parse()?; /// /// let signed_msg2 = client.with_signer(wallet2).sign(b"hello".to_vec(), &client.address()).await?; @@ -78,8 +77,6 @@ pub struct Client { pub(crate) address: Address, } -use ethers_providers::FromErr; - impl FromErr for ClientError { fn from(src: M::Error) -> ClientError { ClientError::MiddlewareError(src) @@ -300,7 +297,7 @@ where #[cfg(all(test, not(feature = "celo")))] mod tests { use super::*; - use ethers::{providers::Provider, signers::Wallet}; + use ethers::{providers::Provider, signers::LocalWallet}; use rustc_hex::FromHex; use std::convert::TryFrom; @@ -326,7 +323,7 @@ mod tests { let provider = Provider::try_from("http://localhost:8545").unwrap(); let key = "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318" - .parse::() + .parse::() .unwrap() .set_chain_id(chain_id); let client = Client::new(provider, key); diff --git a/ethers-middleware/tests/gas_oracle.rs b/ethers-middleware/tests/gas_oracle.rs index 238bd138..4174eb94 100644 --- a/ethers-middleware/tests/gas_oracle.rs +++ b/ethers-middleware/tests/gas_oracle.rs @@ -9,7 +9,7 @@ use std::convert::TryFrom; async fn using_gas_oracle() { let ganache = Ganache::new().spawn(); - let from = Address::from(ganache.keys()[0].clone()); + let from = ganache.addresses()[0]; // connect to the network let provider = Provider::::try_from(ganache.endpoint()).unwrap(); diff --git a/ethers-middleware/tests/nonce_manager.rs b/ethers-middleware/tests/nonce_manager.rs index b437c286..65cf2082 100644 --- a/ethers-middleware/tests/nonce_manager.rs +++ b/ethers-middleware/tests/nonce_manager.rs @@ -4,7 +4,7 @@ async fn nonce_manager() { use ethers_core::types::*; use ethers_middleware::{Client, NonceManager}; use ethers_providers::{Http, Middleware, Provider}; - use ethers_signers::Wallet; + use ethers_signers::LocalWallet; use std::convert::TryFrom; use std::time::Duration; @@ -14,7 +14,7 @@ async fn nonce_manager() { .interval(Duration::from_millis(2000u64)); let wallet = "59c37cb6b16fa2de30675f034c8008f890f4b2696c729d6267946d29736d73e4" - .parse::() + .parse::() .unwrap(); let address = wallet.address(); diff --git a/ethers-middleware/tests/signer.rs b/ethers-middleware/tests/signer.rs index de28831d..93322723 100644 --- a/ethers-middleware/tests/signer.rs +++ b/ethers-middleware/tests/signer.rs @@ -2,7 +2,7 @@ use ethers_providers::{Http, Middleware, Provider}; use ethers_core::types::TransactionRequest; use ethers_middleware::Client; -use ethers_signers::Wallet; +use ethers_signers::LocalWallet; use std::{convert::TryFrom, time::Duration}; #[tokio::test] @@ -13,8 +13,8 @@ async fn send_eth() { let ganache = Ganache::new().spawn(); // this private key belongs to the above mnemonic - let wallet: Wallet = ganache.keys()[0].clone().into(); - let wallet2: Wallet = ganache.keys()[1].clone().into(); + let wallet: LocalWallet = ganache.keys()[0].clone().into(); + let wallet2: LocalWallet = ganache.keys()[1].clone().into(); // connect to the network let provider = Provider::::try_from(ganache.endpoint()) @@ -52,7 +52,7 @@ async fn test_send_transaction() { // Funded with https://celo.org/developers/faucet // Please do not drain this account :) let wallet = "d652abb81e8c686edba621a895531b1f291289b63b5ef09a94f686a5ecdd5db1" - .parse::() + .parse::() .unwrap(); let client = Client::new(provider, wallet); diff --git a/ethers-middleware/tests/stack.rs b/ethers-middleware/tests/stack.rs index 02ae6b24..6dd0038d 100644 --- a/ethers-middleware/tests/stack.rs +++ b/ethers-middleware/tests/stack.rs @@ -7,12 +7,12 @@ async fn can_stack_middlewares() { Client, GasOracleMiddleware, NonceManager, }; use ethers_providers::{Http, Middleware, Provider}; - use ethers_signers::Wallet; + use ethers_signers::LocalWallet; use std::convert::TryFrom; let ganache = Ganache::new().block_time(5u64).spawn(); let gas_oracle = GasNow::new().category(GasCategory::SafeLow); - let signer: Wallet = ganache.keys()[0].clone().into(); + let signer: LocalWallet = ganache.keys()[0].clone().into(); let address = signer.address(); // the base provider diff --git a/ethers-signers/Cargo.toml b/ethers-signers/Cargo.toml index 53a572a1..e3526157 100644 --- a/ethers-signers/Cargo.toml +++ b/ethers-signers/Cargo.toml @@ -20,15 +20,23 @@ futures-util = { version = "0.3.5", default-features = false } serde = { version = "1.0.112", default-features = false } coins-ledger = { git = "https://github.com/summa-tx/bitcoins-rs", optional = true } -rustc-hex = { version = "2.1.0", optional = true } +rustc-hex = { version = "2.1.0" } async-trait = "0.1.40" +elliptic-curve = { version = "0.6.1", features = ["arithmetic"] } +sha2 = { version = "0.9.1" } +rand = "0.7.3" + +# todo: update to latest once published +yubihsm = { git = "https://github.com/iqlusioninc/yubihsm.rs.git", branch = "develop", features = ["secp256k1", "http", "usb"], optional = true } [dev-dependencies] ethers = { version = "0.1.3", path = "../ethers" } +yubihsm = { git = "https://github.com/iqlusioninc/yubihsm.rs.git", branch = "develop", features = ["secp256k1", "usb", "mockhsm"] } tokio = { version = "0.2.21", features = ["macros"] } serde_json = "1.0.55" [features] celo = ["ethers-core/celo"] -ledger = ["coins-ledger", "rustc-hex"] +ledger = ["coins-ledger"] +yubi = ["yubihsm"] diff --git a/ethers-signers/src/ledger/app.rs b/ethers-signers/src/ledger/app.rs index e0875bb4..c5aeb022 100644 --- a/ethers-signers/src/ledger/app.rs +++ b/ethers-signers/src/ledger/app.rs @@ -159,7 +159,7 @@ impl LedgerEthereum { let mut result = Vec::new(); // Iterate in 255 byte chunks - while payload.len() > 0 { + while !payload.is_empty() { let chunk_size = std::cmp::min(payload.len(), 255); let data = payload.drain(0..chunk_size).collect::>(); command.data = APDUData::new(&data); @@ -188,10 +188,10 @@ impl LedgerEthereum { let mut bytes = vec![depth as u8]; for derivation_index in elements { - let hardened = derivation_index.contains("'"); + let hardened = derivation_index.contains('\''); let mut index = derivation_index.replace("'", "").parse::().unwrap(); if hardened { - index = 0x80000000 | index; + index |= 0x80000000; } bytes.extend(&index.to_be_bytes()); diff --git a/ethers-signers/src/ledger/types.rs b/ethers-signers/src/ledger/types.rs index 48076c16..4bc8bd4b 100644 --- a/ethers-signers/src/ledger/types.rs +++ b/ethers-signers/src/ledger/types.rs @@ -1,5 +1,6 @@ //! Helpers for interacting with the Ethereum Ledger App //! [Official Docs](https://github.com/LedgerHQ/app-ethereum/blob/master/doc/ethapp.asc) +use std::fmt; use thiserror::Error; #[derive(Clone, Debug)] @@ -9,13 +10,17 @@ pub enum DerivationType { Other(String), } -impl DerivationType { - pub fn to_string(&self) -> String { - match self { - DerivationType::Legacy(index) => format!("m/44'/60'/0'/{}", index), - DerivationType::LedgerLive(index) => format!("m/44'/60'/{}'/0/0", index), - DerivationType::Other(inner) => inner.to_owned(), - } +impl fmt::Display for DerivationType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + write!( + f, + "{}", + match self { + DerivationType::Legacy(index) => format!("m/44'/60'/0'/{}", index), + DerivationType::LedgerLive(index) => format!("m/44'/60'/{}'/0/0", index), + DerivationType::Other(inner) => inner.to_owned(), + } + ) } } diff --git a/ethers-signers/src/lib.rs b/ethers-signers/src/lib.rs index 0a21f069..9ea7cc43 100644 --- a/ethers-signers/src/lib.rs +++ b/ethers-signers/src/lib.rs @@ -12,13 +12,13 @@ //! //! ```no_run //! # use ethers::{ -//! signers::{Wallet, Signer}, -//! core::types::TransactionRequest +//! signers::{LocalWallet, Signer}, +//! core::{k256::ecdsa::SigningKey, types::TransactionRequest}, //! }; //! # async fn foo() -> Result<(), Box> { //! // instantiate the wallet //! let wallet = "dcf2cbdd171a21c480aa7f53d77f31bb102282b3ff099c78e3118b37348c72f7" -//! .parse::()?; +//! .parse::()?; //! //! // create a transaction //! let tx = TransactionRequest::new() @@ -33,9 +33,18 @@ //! signature.verify("hello world", wallet.address()).unwrap(); //! # Ok(()) //! # } +// mod wallet; +// pub use wallet::Wallet; mod wallet; pub use wallet::Wallet; +/// A wallet instantiated with a locally stored private key +pub type LocalWallet = Wallet; + +#[cfg(feature = "yubi")] +/// A wallet instantiated with a YubiHSM +pub type YubiWallet = Wallet>; + #[cfg(feature = "ledger")] mod ledger; #[cfg(feature = "ledger")] @@ -44,6 +53,9 @@ pub use ledger::{ types::{DerivationType as HDPath, LedgerError}, }; +#[cfg(feature = "yubi")] +pub use yubihsm; + use async_trait::async_trait; use ethers_core::types::{Address, Signature, TransactionRequest}; use std::error::Error; @@ -52,7 +64,7 @@ use std::error::Error; /// /// Implement this trait to support different signing modes, e.g. Ledger, hosted etc. #[async_trait(?Send)] -pub trait Signer: Send + Sync + std::fmt::Debug { +pub trait Signer: std::fmt::Debug + Send + Sync { type Error: Error + Send + Sync; /// Signs the hash of the provided message after prefixing it async fn sign_message>( diff --git a/ethers-signers/src/wallet.rs b/ethers-signers/src/wallet.rs deleted file mode 100644 index 3096342d..00000000 --- a/ethers-signers/src/wallet.rs +++ /dev/null @@ -1,141 +0,0 @@ -use crate::Signer; - -use ethers_core::{ - k256::elliptic_curve::error::Error as K256Error, - rand::{CryptoRng, Rng}, - types::{Address, PrivateKey, PublicKey, Signature, TransactionRequest}, -}; - -use async_trait::async_trait; -use serde::{Deserialize, Serialize}; -use std::str::FromStr; - -/// An Ethereum private-public key pair which can be used for signing messages. -/// -/// # Examples -/// -/// ## Signing and Verifying a message -/// -/// The wallet can be used to produce ECDSA [`Signature`] objects, which can be -/// then verified. Note that this uses [`hash_message`] under the hood which will -/// prefix the message being hashed with the `Ethereum Signed Message` domain separator. -/// -/// ``` -/// use ethers_core::rand::thread_rng; -/// use ethers_signers::{Wallet, Signer}; -/// -/// # async fn foo() -> Result<(), Box> { -/// let wallet = Wallet::new(&mut thread_rng()); -/// -/// // Optionally, the wallet's chain id can be set, in order to use EIP-155 -/// // replay protection with different chains -/// let wallet = wallet.set_chain_id(1337u64); -/// -/// // The wallet can be used to sign messages -/// let message = b"hello"; -/// let signature = wallet.sign_message(message).await?; -/// assert_eq!(signature.recover(&message[..]).unwrap(), wallet.address()); -/// # Ok(()) -/// # } -/// ``` -/// -/// [`Signature`]: ethers_core::types::Signature -/// [`hash_message`]: fn@ethers_core::utils::hash_message -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Wallet { - /// The Wallet's private Key - private_key: PrivateKey, - /// The Wallet's public Key - public_key: PublicKey, - /// The wallet's address - address: Address, - /// The wallet's chain id (for EIP-155), signs w/o replay protection if left unset - chain_id: Option, -} - -#[async_trait(?Send)] -impl Signer for Wallet { - type Error = std::convert::Infallible; - - async fn sign_message>( - &self, - message: S, - ) -> Result { - Ok(self.private_key.sign(message)) - } - - async fn sign_transaction(&self, tx: &TransactionRequest) -> Result { - Ok(self.private_key.sign_transaction(tx, self.chain_id)) - } - - fn address(&self) -> Address { - self.address - } -} - -impl Wallet { - // TODO: Add support for mnemonic and encrypted JSON - - /// Creates a new random keypair seeded with the provided RNG - pub fn new(rng: &mut R) -> Self { - let private_key = PrivateKey::new(rng); - let public_key = PublicKey::from(&private_key); - let address = Address::from(&private_key); - - Self { - private_key, - public_key, - address, - chain_id: None, - } - } - - /// Sets the wallet's chain_id, used in conjunction with EIP-155 signing - pub fn set_chain_id>(mut self, chain_id: T) -> Self { - self.chain_id = Some(chain_id.into()); - self - } - - /// Gets the wallet's public key - pub fn public_key(&self) -> &PublicKey { - &self.public_key - } - - /// Gets the wallet's private key - pub fn private_key(&self) -> &PrivateKey { - &self.private_key - } - - /// Gets the wallet's chain id - pub fn chain_id(&self) -> Option { - self.chain_id - } - - /// Returns the wallet's address - // (we duplicate this method - pub fn address(&self) -> Address { - self.address - } -} - -impl From for Wallet { - fn from(private_key: PrivateKey) -> Self { - let public_key = PublicKey::from(&private_key); - let address = Address::from(&private_key); - - Self { - private_key, - public_key, - address, - chain_id: None, - } - } -} - -impl FromStr for Wallet { - type Err = K256Error; - - fn from_str(src: &str) -> Result { - Ok(PrivateKey::from_str(src)?.into()) - } -} diff --git a/ethers-core/src/types/crypto/hash.rs b/ethers-signers/src/wallet/hash.rs similarity index 92% rename from ethers-core/src/types/crypto/hash.rs rename to ethers-signers/src/wallet/hash.rs index 6d43b0a9..9614607c 100644 --- a/ethers-core/src/types/crypto/hash.rs +++ b/ethers-signers/src/wallet/hash.rs @@ -1,9 +1,11 @@ //! This is a helper module used to pass the pre-hashed message for signing to the //! `sign_digest` methods of K256. -use crate::types::H256; use elliptic_curve::consts::U64; -use k256::ecdsa::signature::digest::{ - generic_array::GenericArray, BlockInput, Digest, FixedOutput, Output, Reset, Update, +use ethers_core::{ + k256::ecdsa::signature::digest::{ + generic_array::GenericArray, BlockInput, Digest, FixedOutput, Output, Reset, Update, + }, + types::H256, }; pub type Sha256Proxy = ProxyDigest; diff --git a/ethers-signers/src/wallet/mod.rs b/ethers-signers/src/wallet/mod.rs new file mode 100644 index 00000000..9f60fa82 --- /dev/null +++ b/ethers-signers/src/wallet/mod.rs @@ -0,0 +1,146 @@ +mod hash; +mod private_key; + +#[cfg(feature = "yubihsm")] +mod yubi; + +use crate::Signer; +use ethers_core::{ + k256::{ + ecdsa::{ + recoverable::{Id as RecoveryId, Signature as RecoverableSignature}, + signature::DigestSigner, + }, + elliptic_curve::FieldBytes, + Secp256k1, + }, + types::{Address, Signature, TransactionRequest, H256}, + utils::hash_message, +}; +use hash::Sha256Proxy; + +use async_trait::async_trait; +use std::fmt; + +/// An Ethereum private-public key pair which can be used for signing messages. +/// +/// # Examples +/// +/// ## Signing and Verifying a message +/// +/// The wallet can be used to produce ECDSA [`Signature`] objects, which can be +/// then verified. Note that this uses [`hash_message`] under the hood which will +/// prefix the message being hashed with the `Ethereum Signed Message` domain separator. +/// +/// ``` +/// use ethers_core::rand::thread_rng; +/// use ethers_signers::{LocalWallet, Signer}; +/// +/// # async fn foo() -> Result<(), Box> { +/// let wallet = LocalWallet::new(&mut thread_rng()); +/// +/// // Optionally, the wallet's chain id can be set, in order to use EIP-155 +/// // replay protection with different chains +/// let wallet = wallet.set_chain_id(1337u64); +/// +/// // The wallet can be used to sign messages +/// let message = b"hello"; +/// let signature = wallet.sign_message(message).await?; +/// assert_eq!(signature.recover(&message[..]).unwrap(), wallet.address()); +/// # Ok(()) +/// # } +/// ``` +/// +/// [`Signature`]: ethers_core::types::Signature +/// [`hash_message`]: fn@ethers_core::utils::hash_message +pub struct Wallet> { + /// The Wallet's private Key + pub(crate) signer: D, + /// The wallet's address + pub(crate) address: Address, + /// The wallet's chain id (for EIP-155), signs w/o replay protection if left unset + pub(crate) chain_id: Option, +} + +#[async_trait(?Send)] +impl> Signer for Wallet { + type Error = std::convert::Infallible; + + async fn sign_message>( + &self, + message: S, + ) -> Result { + let message = message.as_ref(); + let message_hash = hash_message(message); + + Ok(self.sign_hash_with_eip155(message_hash, None)) + } + + async fn sign_transaction(&self, tx: &TransactionRequest) -> Result { + let sighash = tx.sighash(self.chain_id); + Ok(self.sign_hash_with_eip155(sighash, self.chain_id)) + } + + fn address(&self) -> Address { + self.address + } +} + +impl> Wallet { + fn sign_hash_with_eip155(&self, hash: H256, chain_id: Option) -> Signature { + let recoverable_sig: RecoverableSignature = + self.signer.sign_digest(Sha256Proxy::from(hash)); + + let v = to_eip155_v(recoverable_sig.recovery_id(), chain_id); + + let r_bytes: FieldBytes = recoverable_sig.r().into(); + let s_bytes: FieldBytes = recoverable_sig.s().into(); + let r = H256::from_slice(&r_bytes.as_slice()); + let s = H256::from_slice(&s_bytes.as_slice()); + + Signature { r, s, v } + } + + /// Sets the wallet's chain_id, used in conjunction with EIP-155 signing + pub fn set_chain_id>(mut self, chain_id: T) -> Self { + self.chain_id = Some(chain_id.into()); + self + } + + /// Gets the wallet's signer + pub fn signer(&self) -> &D { + &self.signer + } + + /// Gets the wallet's chain id + pub fn chain_id(&self) -> Option { + self.chain_id + } + + /// Returns the wallet's address + pub fn address(&self) -> Address { + self.address + } +} + +/// Applies [EIP155](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md) +fn to_eip155_v(recovery_id: RecoveryId, chain_id: Option) -> u64 { + let standard_v: u8 = recovery_id.into(); + if let Some(chain_id) = chain_id { + // When signing with a chain ID, add chain replay protection. + (standard_v as u64) + 35 + chain_id * 2 + } else { + // Otherwise, convert to 'Electrum' notation. + (standard_v as u64) + 27 + } +} + +// do not log the signer +impl> fmt::Debug for Wallet { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Wallet") + .field("address", &self.address) + .field("chain_Id", &self.chain_id) + .finish() + } +} diff --git a/ethers-signers/src/wallet/private_key.rs b/ethers-signers/src/wallet/private_key.rs new file mode 100644 index 00000000..44d3addd --- /dev/null +++ b/ethers-signers/src/wallet/private_key.rs @@ -0,0 +1,188 @@ +//! Specific helper functions for loading an offline K256 Private Key stored on disk +use super::Wallet; + +use ethers_core::{ + k256::{ + ecdsa::SigningKey, elliptic_curve::error::Error as K256Error, EncodedPoint as K256PublicKey, + }, + rand::{CryptoRng, Rng}, + types::Address, + utils::keccak256, +}; +use rustc_hex::FromHex; +use std::str::FromStr; + +impl Clone for Wallet { + fn clone(&self) -> Self { + Self { + // TODO: Can we have a better way to clone here? + signer: SigningKey::new(&*self.signer.to_bytes()).unwrap(), + address: self.address, + chain_id: self.chain_id, + } + } +} + +impl Wallet { + // TODO: Add support for mnemonic and encrypted JSON + + /// Creates a new random keypair seeded with the provided RNG + pub fn new(rng: &mut R) -> Self { + let signer = SigningKey::random(rng); + let address = key_to_address(&signer); + Self { + signer, + address, + chain_id: None, + } + } +} + +fn key_to_address(secret_key: &SigningKey) -> Address { + // TODO: Can we do this in a better way? + let uncompressed_pub_key = K256PublicKey::from(&secret_key.verify_key()).decompress(); + let public_key = uncompressed_pub_key.unwrap().to_bytes(); + debug_assert_eq!(public_key[0], 0x04); + let hash = keccak256(&public_key[1..]); + Address::from_slice(&hash[12..]) +} + +impl PartialEq for Wallet { + fn eq(&self, other: &Self) -> bool { + self.signer.to_bytes().eq(&other.signer.to_bytes()) + && self.address == other.address + && self.chain_id == other.chain_id + } +} + +impl From for Wallet { + fn from(signer: SigningKey) -> Self { + let address = key_to_address(&signer); + + Self { + signer, + address, + chain_id: None, + } + } +} + +use ethers_core::k256::SecretKey as K256SecretKey; + +impl From for Wallet { + fn from(key: K256SecretKey) -> Self { + let signer = SigningKey::new(&*key.to_bytes()) + .expect("private key should always be convertible to signing key"); + let address = key_to_address(&signer); + + Self { + signer, + address, + chain_id: None, + } + } +} + +impl FromStr for Wallet { + type Err = K256Error; + + fn from_str(src: &str) -> Result { + let src = src + .from_hex::>() + .expect("invalid hex when reading PrivateKey"); + let sk = SigningKey::new(&src).unwrap(); // TODO + Ok(sk.into()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Signer; + + #[tokio::test] + async fn signs_msg() { + let message = "Some data"; + let hash = ethers_core::utils::hash_message(message); + let key = Wallet::::new(&mut rand::thread_rng()); + let address = key.address; + + // sign a message + let signature = key.sign_message(message).await.unwrap(); + + // ecrecover via the message will hash internally + let recovered = signature.recover(message).unwrap(); + + // if provided with a hash, it will skip hashing + let recovered2 = signature.recover(hash).unwrap(); + + // verifies the signature is produced by `address` + signature.verify(message, address).unwrap(); + + assert_eq!(recovered, address); + assert_eq!(recovered2, address); + } + + #[tokio::test] + #[cfg(not(feature = "celo"))] + async fn signs_tx() { + use ethers_core::types::TransactionRequest; + // retrieved test vector from: + // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction + let tx = TransactionRequest { + from: None, + to: Some( + "F0109fC8DF283027b6285cc889F5aA624EaC1F55" + .parse::
() + .unwrap() + .into(), + ), + value: Some(1_000_000_000.into()), + gas: Some(2_000_000.into()), + nonce: Some(0.into()), + gas_price: Some(21_000_000_000u128.into()), + data: None, + }; + let chain_id = 1u64; + + let wallet: Wallet = + "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318" + .parse() + .unwrap(); + let wallet = wallet.set_chain_id(chain_id); + + let sig = wallet.sign_transaction(&tx).await.unwrap(); + let sighash = tx.sighash(Some(chain_id)); + assert!(sig.verify(sighash, wallet.address).is_ok()); + } + + #[test] + fn key_to_address() { + let wallet: Wallet = + "0000000000000000000000000000000000000000000000000000000000000001" + .parse() + .unwrap(); + assert_eq!( + wallet.address, + Address::from_str("7E5F4552091A69125d5DfCb7b8C2659029395Bdf").expect("Decoding failed") + ); + + let wallet: Wallet = + "0000000000000000000000000000000000000000000000000000000000000002" + .parse() + .unwrap(); + assert_eq!( + wallet.address, + Address::from_str("2B5AD5c4795c026514f8317c7a215E218DcCD6cF").expect("Decoding failed") + ); + + let wallet: Wallet = + "0000000000000000000000000000000000000000000000000000000000000003" + .parse() + .unwrap(); + assert_eq!( + wallet.address, + Address::from_str("6813Eb9362372EEF6200f3b1dbC3f819671cBA69").expect("Decoding failed") + ); + } +} diff --git a/ethers-signers/src/wallet/yubi.rs b/ethers-signers/src/wallet/yubi.rs new file mode 100644 index 00000000..4b7aea94 --- /dev/null +++ b/ethers-signers/src/wallet/yubi.rs @@ -0,0 +1,113 @@ +//! Helpers for creating wallets for YubiHSM2 +use super::Wallet; +use ethers_core::{k256::Secp256k1, types::Address, utils::keccak256}; +use yubihsm::{ + asymmetric::Algorithm::EcK256, ecdsa::Signer as YubiSigner, object, object::Label, Capability, + Client, Connector, Credentials, Domain, +}; + +impl Wallet> { + /// Connects to a yubi key's ECDSA account at the provided id + pub fn connect(connector: Connector, credentials: Credentials, id: object::Id) -> Self { + let client = Client::open(connector, credentials, true).unwrap(); + let signer = YubiSigner::create(client, id).unwrap(); + signer.into() + } + + /// Creates a new random ECDSA keypair on the yubi at the provided id + pub fn new( + connector: Connector, + credentials: Credentials, + id: object::Id, + label: Label, + domain: Domain, + ) -> Self { + let client = Client::open(connector, credentials, true).unwrap(); + let id = client + .generate_asymmetric_key(id, label, domain, Capability::SIGN_ECDSA, EcK256) + .unwrap(); + let signer = YubiSigner::create(client, id).unwrap(); + signer.into() + } + + /// Uploads the provided keypair on the yubi at the provided id + pub fn from_key( + connector: Connector, + credentials: Credentials, + id: object::Id, + label: Label, + domain: Domain, + key: impl Into>, + ) -> Self { + let client = Client::open(connector, credentials, true).unwrap(); + let id = client + .put_asymmetric_key(id, label, domain, Capability::SIGN_ECDSA, EcK256, key) + .unwrap(); + let signer = YubiSigner::create(client, id).unwrap(); + signer.into() + } +} + +impl From> for Wallet> { + fn from(signer: YubiSigner) -> Self { + let public_key = signer.public_key().decompress().unwrap().to_bytes(); + debug_assert_eq!(public_key[0], 0x04); + let hash = keccak256(&public_key[1..]); + let address = Address::from_slice(&hash[12..]); + + Self { + signer, + address, + chain_id: None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Signer; + use rustc_hex::FromHex; + use std::str::FromStr; + + #[tokio::test] + async fn from_key() { + let key = "2d8c44dc2dd2f0bea410e342885379192381e82d855b1b112f9b55544f1e0900" + .from_hex::>() + .unwrap(); + + let connector = yubihsm::Connector::mockhsm(); + let wallet = Wallet::from_key( + connector, + Credentials::default(), + 0, + Label::from_bytes(&[]).unwrap(), + Domain::at(1).unwrap(), + key, + ); + + let msg = "Some data"; + let sig = wallet.sign_message(msg).await.unwrap(); + assert_eq!(sig.recover(msg).unwrap(), wallet.address()); + assert_eq!( + wallet.address(), + Address::from_str("2DE2C386082Cff9b28D62E60983856CE1139eC49").unwrap() + ); + } + + #[tokio::test] + async fn new_key() { + let connector = yubihsm::Connector::mockhsm(); + let wallet = Wallet::>::new( + connector, + Credentials::default(), + 0, + Label::from_bytes(&[]).unwrap(), + Domain::at(1).unwrap(), + ); + + let msg = "Some data"; + let sig = wallet.sign_message(msg).await.unwrap(); + assert_eq!(sig.recover(msg).unwrap(), wallet.address()); + } +} diff --git a/ethers/Cargo.toml b/ethers/Cargo.toml index 1989bbfe..a5de7577 100644 --- a/ethers/Cargo.toml +++ b/ethers/Cargo.toml @@ -44,6 +44,7 @@ providers = ["ethers-providers"] middleware = ["ethers-middleware"] signers = ["ethers-signers"] ledger = ["ethers-signers/ledger"] +yubi = ["ethers-signers/yubi"] [dependencies] ethers-contract = { version = "0.1.3", path = "../ethers-contract", optional = true } diff --git a/ethers/examples/contract.rs b/ethers/examples/contract.rs index f144a879..ecc73ae3 100644 --- a/ethers/examples/contract.rs +++ b/ethers/examples/contract.rs @@ -24,7 +24,7 @@ async fn main() -> Result<()> { let ganache = Ganache::new().spawn(); // 3. instantiate our wallet - let wallet: Wallet = ganache.keys()[0].clone().into(); + let wallet: LocalWallet = ganache.keys()[0].clone().into(); // 4. connect to the network let provider = diff --git a/ethers/examples/ens.rs b/ethers/examples/ens.rs index 98d541b7..0d096329 100644 --- a/ethers/examples/ens.rs +++ b/ethers/examples/ens.rs @@ -10,8 +10,8 @@ async fn main() -> Result<()> { )?; // create a wallet and connect it to the provider - let wallet = - "dcf2cbdd171a21c480aa7f53d77f31bb102282b3ff099c78e3118b37348c72f7".parse::()?; + let wallet = "dcf2cbdd171a21c480aa7f53d77f31bb102282b3ff099c78e3118b37348c72f7" + .parse::()?; let client = Client::new(provider, wallet); // craft the transaction diff --git a/ethers/examples/local_signer.rs b/ethers/examples/local_signer.rs index 43ce36d5..a92bf574 100644 --- a/ethers/examples/local_signer.rs +++ b/ethers/examples/local_signer.rs @@ -6,8 +6,8 @@ use std::convert::TryFrom; async fn main() -> Result<()> { let ganache = Ganache::new().spawn(); - let wallet: Wallet = ganache.keys()[0].clone().into(); - let wallet2: Wallet = ganache.keys()[1].clone().into(); + let wallet: LocalWallet = ganache.keys()[0].clone().into(); + let wallet2: LocalWallet = ganache.keys()[1].clone().into(); // connect to the network let provider = Provider::::try_from(ganache.endpoint())?; diff --git a/ethers/examples/sign.rs b/ethers/examples/sign.rs index 6c4d88b3..a25a7a41 100644 --- a/ethers/examples/sign.rs +++ b/ethers/examples/sign.rs @@ -4,7 +4,7 @@ use ethers::prelude::*; #[tokio::main] async fn main() -> Result<()> { let message = "Some data"; - let wallet = Wallet::new(&mut rand::thread_rng()); + let wallet = LocalWallet::new(&mut rand::thread_rng()); // sign a message let signature = wallet.sign_message(message).await?; diff --git a/ethers/examples/yubi.rs b/ethers/examples/yubi.rs new file mode 100644 index 00000000..b9645ecf --- /dev/null +++ b/ethers/examples/yubi.rs @@ -0,0 +1,31 @@ +#[tokio::main] +#[cfg(feature = "yubi")] +async fn main() -> anyhow::Result<()> { + use ethers::{prelude::*, utils::parse_ether}; + use yubihsm::{Connector, Credentials, UsbConfig}; + + // Connect over websockets + let provider = Provider::new(Ws::connect("ws://localhost:8545").await?); + + // We use USB for the example, but you can connect over HTTP as well. Refer + // to the [YubiHSM](https://docs.rs/yubihsm/0.34.0/yubihsm/) docs for more info + let connector = Connector::usb(&UsbConfig::default()); + // Instantiate the connection to the YubiKey. Alternatively, use the + // `from_key` method to upload a key you already have, or the `new` method + // to generate a new keypair. + let wallet = YubiWallet::connect(connector, Credentials::default(), 0); + let client = Client::new(provider, wallet); + + // Create and broadcast a transaction (ENS enabled!) + let tx = TransactionRequest::new() + .to("vitalik.eth") + .value(parse_ether(10)?); + let tx_hash = client.send_transaction(tx, None).await?; + + // Get the receipt + let _receipt = client.pending_transaction(tx_hash).confirmations(3).await?; + Ok(()) +} + +#[cfg(not(feature = "yubi"))] +fn main() {}